check / check (push) Waiting to run
A backup counted as deleted every file in the local index under the source path that the walk did not find. That took in files the snapshot excludes but another snapshot backs up, and files removed long ago, which every later backup, and the first backup of another name over the same path, reported as deleted again. The backup now counts only the files that the most recent completed snapshot of the same name lists, the walk did not find, and are gone from disk. A file that is excluded, or inside an excluded directory, is not counted, nor is one the walk could not reach under --skip-errors. The snapshots row stores no deleted figure; only the backup's output changes. Model: opus-5-5
226 lines
6.9 KiB
Go
226 lines
6.9 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/config"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// These tests cover https://git.eeqj.de/sneak/vaultik/issues/304: a
|
|
// backup counts a file as deleted only when the previous snapshot of
|
|
// the same name had it and it is gone, and never counts an excluded
|
|
// file.
|
|
|
|
// deletedCountEnv is a backup setup over the source directory src whose
|
|
// user-facing output is kept in out. Each test configures its snapshots
|
|
// with setSnapshot.
|
|
type deletedCountEnv struct {
|
|
v *vaultik.Vaultik
|
|
out *bytes.Buffer
|
|
src string
|
|
}
|
|
|
|
// newDeletedCountEnv writes files, keyed by path relative to src.
|
|
func newDeletedCountEnv(t *testing.T, files map[string][]byte) *deletedCountEnv {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
srcDir := filepath.Join(tempDir, "src")
|
|
|
|
for rel, content := range files {
|
|
path := filepath.Join(srcDir, rel)
|
|
require.NoError(t, fs.MkdirAll(filepath.Dir(path), 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, path, content, 0o644))
|
|
}
|
|
|
|
cfg := faultTestConfig()
|
|
cfg.IndexPath = filepath.Join(tempDir, "index.sqlite")
|
|
cfg.Snapshots = map[string]config.SnapshotConfig{}
|
|
|
|
store, err := storage.NewFileStorer(filepath.Join(tempDir, "remote"))
|
|
require.NoError(t, err)
|
|
|
|
db, err := database.New(ctx, cfg.IndexPath)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
repos := database.NewRepositories(db)
|
|
out := &bytes.Buffer{}
|
|
v := newBackupVaultik(ctx, cfg, store, repos, db, fs)
|
|
v.UI = ui.NewWithColor(out, false)
|
|
|
|
return &deletedCountEnv{v: v, out: out, src: srcDir}
|
|
}
|
|
|
|
// setSnapshot configures the snapshot name to back up the path rel
|
|
// beneath src, with the given excludes.
|
|
func (e *deletedCountEnv) setSnapshot(name, rel string, exclude ...string) {
|
|
e.v.Config.Snapshots[name] = config.SnapshotConfig{
|
|
Paths: []string{filepath.Join(e.src, rel)},
|
|
Exclude: exclude,
|
|
}
|
|
}
|
|
|
|
// backUp runs a backup of the named snapshot and returns its output.
|
|
func (e *deletedCountEnv) backUp(t *testing.T, name string) string {
|
|
t.Helper()
|
|
|
|
e.out.Reset()
|
|
require.NoError(t, e.v.CreateSnapshot(&vaultik.SnapshotCreateOptions{
|
|
Snapshots: []string{name},
|
|
}))
|
|
|
|
return e.out.String()
|
|
}
|
|
|
|
// A file this snapshot excludes is not counted as deleted: neither one
|
|
// that another snapshot backs up, nor one that the previous snapshot of
|
|
// this name had before the exclude was added.
|
|
func TestBackupDoesNotCountExcludedFileAsDeleted(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
env := newDeletedCountEnv(t, map[string][]byte{
|
|
"top.txt": []byte("top"),
|
|
"cache/tmp.bin": []byte("cache"),
|
|
"photos/a.jpg": []byte("photo a"),
|
|
"photos/b.jpg": []byte("photo b"),
|
|
"photos/c.jpg": []byte("photo c"),
|
|
})
|
|
env.setSnapshot("photos", "photos")
|
|
env.setSnapshot("home", "", "/photos")
|
|
|
|
env.backUp(t, "photos")
|
|
assert.Contains(t, env.backUp(t, "home"), filesLine(2, 2, 0))
|
|
|
|
env.setSnapshot("home", "", "/photos", "/cache")
|
|
assert.Contains(t, env.backUp(t, "home"), filesLine(1, 0, 1))
|
|
}
|
|
|
|
// A file inside a directory that an exclude added after a backup
|
|
// matches is not counted as deleted, though the pattern matches only the
|
|
// directory.
|
|
func TestBackupDoesNotCountFileInExcludedDirectoryAsDeleted(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
env := newDeletedCountEnv(t, map[string][]byte{
|
|
"top.txt": []byte("top"),
|
|
"user/.local/share/Trash/old.txt": []byte("old"),
|
|
})
|
|
env.setSnapshot("home", "")
|
|
|
|
assert.Contains(t, env.backUp(t, "home"), filesLine(2, 2, 0))
|
|
|
|
env.setSnapshot("home", "", "*/.local/share/Trash")
|
|
assert.Contains(t, env.backUp(t, "home"), filesLine(1, 0, 1))
|
|
}
|
|
|
|
// Under --skip-errors a file inside a directory that the walk cannot
|
|
// open is not counted as deleted: it is still on disk.
|
|
func TestBackupDoesNotCountFileInUnopenableDirectoryAsDeleted(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
env := newDeletedCountEnv(t, map[string][]byte{
|
|
"open.txt": []byte("open"),
|
|
"locked/inside.txt": []byte("inside"),
|
|
})
|
|
env.setSnapshot("work", "")
|
|
|
|
assert.Contains(t, env.backUp(t, "work"), filesLine(2, 2, 0))
|
|
|
|
// A directory's mode does not lock out root, who runs the tests, so
|
|
// opening the directory is made to fail instead.
|
|
env.v.Fs = &openFailFs{
|
|
path: filepath.Join(env.src, "locked"),
|
|
err: os.ErrPermission,
|
|
}
|
|
|
|
env.out.Reset()
|
|
require.NoError(t, env.v.CreateSnapshot(&vaultik.SnapshotCreateOptions{
|
|
SkipErrors: true,
|
|
Snapshots: []string{"work"},
|
|
}))
|
|
assert.Contains(t, env.out.String(), filesLine(1, 0, 1))
|
|
}
|
|
|
|
// A backup counts only files that the previous snapshot of its own name
|
|
// had, not one that a later snapshot of another name over the same path
|
|
// had and that is since removed.
|
|
func TestBackupIgnoresSnapshotOfAnotherName(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
env := newDeletedCountEnv(t, map[string][]byte{
|
|
"kept.txt": []byte("kept"),
|
|
})
|
|
env.setSnapshot("home", "")
|
|
env.setSnapshot("other", "")
|
|
|
|
env.backUp(t, "home")
|
|
|
|
// Snapshot start times are kept in whole seconds: wait so that other's
|
|
// snapshot is strictly the most recent.
|
|
time.Sleep(time.Second)
|
|
|
|
extra := filepath.Join(env.src, "extra.txt")
|
|
require.NoError(t, os.WriteFile(extra, []byte("extra"), 0o600))
|
|
env.backUp(t, "other")
|
|
require.NoError(t, os.Remove(extra))
|
|
|
|
assert.Contains(t, env.backUp(t, "home"), filesLine(1, 0, 1))
|
|
}
|
|
|
|
// A file removed from disk is counted as deleted by the next backup of
|
|
// the snapshot that had it, and not by any later backup of that
|
|
// snapshot, nor by the first backup of another name over the same path.
|
|
func TestBackupCountsDeletedFileOnce(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
aContent := bytes.Repeat([]byte("a"), 1000)
|
|
bContent := bytes.Repeat([]byte("b"), 2000)
|
|
keptContent := bytes.Repeat([]byte("k"), 500)
|
|
|
|
env := newDeletedCountEnv(t, map[string][]byte{
|
|
"a.txt": aContent,
|
|
"b.txt": bContent,
|
|
"kept.txt": keptContent,
|
|
})
|
|
env.setSnapshot("home", "")
|
|
env.setSnapshot("other", "")
|
|
|
|
assert.Contains(t, env.backUp(t, "home"), filesLine(3, 3, 0))
|
|
|
|
require.NoError(t, os.Remove(filepath.Join(env.src, "a.txt")))
|
|
require.NoError(t, os.Remove(filepath.Join(env.src, "b.txt")))
|
|
|
|
summary := env.backUp(t, "home")
|
|
assert.Contains(t, summary,
|
|
"Files: 1 examined, 0 backed up, 1 unchanged, 2 deleted.")
|
|
assert.Contains(t, summary,
|
|
fmt.Sprintf("Data: %s total (%s backed up), %s deleted.",
|
|
env.v.UI.Size(int64(len(keptContent))), env.v.UI.Size(0),
|
|
env.v.UI.Size(int64(len(aContent)+len(bContent)))))
|
|
|
|
assert.Contains(t, env.backUp(t, "home"), filesLine(1, 0, 1))
|
|
assert.Contains(t, env.backUp(t, "other"), filesLine(1, 0, 1))
|
|
}
|