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 looks up the most recent completed snapshot of the same name and counts only the files it lists that the walk did not find and that are not excluded. The snapshots row stores no deleted figure; only the backup's output changes. Model: opus-5-5
150 lines
4.6 KiB
Go
150 lines
4.6 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"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 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))
|
|
}
|