Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffaa1f160a |
@@ -10,6 +10,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/dustin/go-humanize"
|
||||||
)
|
)
|
||||||
|
|
||||||
// groupSeparator joins an open group path to an attribute key. This
|
// groupSeparator joins an open group path to an attribute key. This
|
||||||
@@ -304,18 +306,13 @@ func formatDuration(d time.Duration) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatBytes formats bytes in a human-readable way
|
// formatBytes formats a byte count with humanize.Bytes, as every command
|
||||||
|
// prints one. humanize.Bytes takes no negative count, so one is printed
|
||||||
|
// as it is.
|
||||||
func formatBytes(b int64) string {
|
func formatBytes(b int64) string {
|
||||||
const unit = 1024
|
if b < 0 {
|
||||||
if b < unit {
|
|
||||||
return fmt.Sprintf("%d B", b)
|
return fmt.Sprintf("%d B", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
div, exp := int64(unit), 0
|
return humanize.Bytes(uint64(b))
|
||||||
for n := b / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,7 +200,8 @@ func TestTTYHandlerWithGroupQualifiesKeys(t *testing.T) {
|
|||||||
func TestTTYHandlerByteFormattingSurvivesGrouping(t *testing.T) {
|
func TestTTYHandlerByteFormattingSurvivesGrouping(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const oneAndAHalfKiB = 1536
|
// Printed as "1.0 MB", in powers of 1000 like every other byte count.
|
||||||
|
const oneMegabyte = 1_000_000
|
||||||
|
|
||||||
for name, testCase := range map[string]struct {
|
for name, testCase := range map[string]struct {
|
||||||
derive func(*slog.Logger) *slog.Logger
|
derive func(*slog.Logger) *slog.Logger
|
||||||
@@ -223,13 +224,13 @@ func TestTTYHandlerByteFormattingSurvivesGrouping(t *testing.T) {
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
logger := slog.New(log.NewTTYHandler(&buf, debugHandlerOptions()))
|
logger := slog.New(log.NewTTYHandler(&buf, debugHandlerOptions()))
|
||||||
testCase.derive(logger).Info("uploaded", "bytes", oneAndAHalfKiB)
|
testCase.derive(logger).Info("uploaded", "bytes", oneMegabyte)
|
||||||
|
|
||||||
line := ansiEscape.ReplaceAllString(buf.String(), "")
|
line := ansiEscape.ReplaceAllString(buf.String(), "")
|
||||||
|
|
||||||
assert.Contains(t, line, testCase.key+"=1.5 KB",
|
assert.Contains(t, line, testCase.key+"=1.0 MB",
|
||||||
"a byte count must be human-readable however it is qualified")
|
"a byte count must be human-readable however it is qualified")
|
||||||
assert.NotContains(t, line, strconv.Itoa(oneAndAHalfKiB),
|
assert.NotContains(t, line, strconv.Itoa(oneMegabyte),
|
||||||
"the raw number must not survive the formatting")
|
"the raw number must not survive the formatting")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,22 +89,6 @@ type SnapshotInfo struct {
|
|||||||
RemotePresent *bool `json:"remote_present"`
|
RemotePresent *bool `json:"remote_present"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatBytes formats bytes in a human-readable format
|
|
||||||
func formatBytes(bytes int64) string {
|
|
||||||
const unit = 1024
|
|
||||||
if bytes < unit {
|
|
||||||
return fmt.Sprintf("%d B", bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
div, exp := int64(unit), 0
|
|
||||||
for n := bytes / unit; n >= unit; n /= unit {
|
|
||||||
div *= unit
|
|
||||||
exp++
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSnapshotTimestamp extracts the timestamp from a snapshot ID
|
// parseSnapshotTimestamp extracts the timestamp from a snapshot ID
|
||||||
// Format: hostname_snapshotname_2026-01-12T14:41:15Z
|
// Format: hostname_snapshotname_2026-01-12T14:41:15Z
|
||||||
func parseSnapshotTimestamp(snapshotID string) (time.Time, error) {
|
func parseSnapshotTimestamp(snapshotID string) (time.Time, error) {
|
||||||
|
|||||||
@@ -633,7 +633,7 @@ func (v *Vaultik) confirmAndExecutePurge(
|
|||||||
v.stdoutf(" %s (%s, %s)\n",
|
v.stdoutf(" %s (%s, %s)\n",
|
||||||
snap.ID,
|
snap.ID,
|
||||||
snap.Timestamp.Format("2006-01-02 15:04:05"),
|
snap.Timestamp.Format("2006-01-02 15:04:05"),
|
||||||
formatBytes(snap.CompressedSize))
|
ubytes(snap.CompressedSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -553,8 +553,8 @@ func (v *Vaultik) printSnapshotTable(snapshots []SnapshotInfo) error {
|
|||||||
|
|
||||||
if snap.LocallyTracked {
|
if snap.LocallyTracked {
|
||||||
id = snap.ID.String()
|
id = snap.ID.String()
|
||||||
uncompressed = formatBytes(snap.UncompressedSize)
|
uncompressed = ubytes(snap.UncompressedSize)
|
||||||
newChunks = formatBytes(snap.NewChunkSize)
|
newChunks = ubytes(snap.NewChunkSize)
|
||||||
} else {
|
} else {
|
||||||
id = formatRemoteOnlyID(snap.RemoteKey)
|
id = formatRemoteOnlyID(snap.RemoteKey)
|
||||||
uncompressed = remoteOnlyCell
|
uncompressed = remoteOnlyCell
|
||||||
@@ -564,7 +564,7 @@ func (v *Vaultik) printSnapshotTable(snapshots []SnapshotInfo) error {
|
|||||||
_, err = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
_, err = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n",
|
||||||
id,
|
id,
|
||||||
snap.Timestamp.Format("2006-01-02 15:04:05"),
|
snap.Timestamp.Format("2006-01-02 15:04:05"),
|
||||||
formatBytes(snap.CompressedSize),
|
ubytes(snap.CompressedSize),
|
||||||
uncompressed,
|
uncompressed,
|
||||||
newChunks)
|
newChunks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -212,8 +212,8 @@ const (
|
|||||||
listConfiguredName = "home"
|
listConfiguredName = "home"
|
||||||
listLocalID = "testhost_home_2026-03-01T10:00:00Z"
|
listLocalID = "testhost_home_2026-03-01T10:00:00Z"
|
||||||
listRemoteID = "otherhost_media_2026-03-02T11:22:33Z"
|
listRemoteID = "otherhost_media_2026-03-02T11:22:33Z"
|
||||||
// fiveMegabytes formats as "5.0 MB" through formatBytes.
|
// fiveMegabytes is printed as "5.0 MB" by every command.
|
||||||
fiveMegabytes = 5 * 1024 * 1024
|
fiveMegabytes = 5_000_000
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestListSnapshots_RemoteWithoutSecretKey is the regression guard for
|
// TestListSnapshots_RemoteWithoutSecretKey is the regression guard for
|
||||||
@@ -306,6 +306,48 @@ func TestListSnapshots_RemoteOnlyRowRendering(t *testing.T) {
|
|||||||
"are not in the local index")
|
"are not in the local index")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSnapshotListAndPurgePrintSizesLikeRemoteInfo checks that the
|
||||||
|
// `snapshot list` table and the purge listing print a byte count the way
|
||||||
|
// `remote info` does: in powers of 1000, as the MB label says.
|
||||||
|
func TestSnapshotListAndPurgePrintSizesLikeRemoteInfo(t *testing.T) {
|
||||||
|
log.Initialize(log.Config{})
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const wantSize = "5.0 MB" // fiveMegabytes
|
||||||
|
|
||||||
|
env := newListEnv(t)
|
||||||
|
|
||||||
|
// A remote-only snapshot: `snapshot list` and `remote info` both take
|
||||||
|
// its size from the manifest.
|
||||||
|
remoteKey := env.addRemote(t, listRemoteID,
|
||||||
|
time.Date(2026, 3, 2, 11, 22, 33, 0, time.UTC))
|
||||||
|
|
||||||
|
// A locally tracked snapshot of the same size for the purge to list.
|
||||||
|
// The purge prints the snapshot's blob size, the last argument here.
|
||||||
|
start := time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC)
|
||||||
|
env.addLocal(t, listLocalID, start)
|
||||||
|
env.addRemote(t, listLocalID, start)
|
||||||
|
require.NoError(t, env.v.Repositories.Snapshots.UpdateCounts(
|
||||||
|
context.Background(), nil, listLocalID, 0, 0, 0, 0, fiveMegabytes))
|
||||||
|
|
||||||
|
require.NoError(t, env.v.RemoteInfo(false))
|
||||||
|
assert.Contains(t, findTableRow(t, env.stdout.String(), remoteKey[:16]),
|
||||||
|
wantSize, "remote info")
|
||||||
|
|
||||||
|
env.stdout.Reset()
|
||||||
|
require.NoError(t, env.v.ListSnapshots(false))
|
||||||
|
assert.Contains(t, findTableRow(t, env.stdout.String(),
|
||||||
|
"<remote only:"+remoteKey[:12]+">"), wantSize, "snapshot list")
|
||||||
|
|
||||||
|
// Without --force and with nothing on stdin the purge lists what it
|
||||||
|
// would delete, reads no confirmation, and deletes nothing.
|
||||||
|
env.stdout.Reset()
|
||||||
|
require.NoError(t, env.v.PurgeSnapshotsWithOptions(
|
||||||
|
&vaultik.SnapshotPurgeOptions{OlderThan: "1d"}))
|
||||||
|
assert.Contains(t, findTableRow(t, env.stdout.String(), listLocalID),
|
||||||
|
wantSize, "snapshot purge")
|
||||||
|
}
|
||||||
|
|
||||||
// findTableRow returns the single output line containing needle.
|
// findTableRow returns the single output line containing needle.
|
||||||
func findTableRow(t *testing.T, out, needle string) string {
|
func findTableRow(t *testing.T, out, needle string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
Reference in New Issue
Block a user