package vaultik_test import ( "bytes" "context" "maps" "path/filepath" "testing" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/chunker" "sneak.berlin/go/vaultik/internal/database" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/storage" "sneak.berlin/go/vaultik/internal/vaultik" ) // TestNewChunkSizeOfChangedFileStaysFixed covers // https://git.eeqj.de/sneak/vaultik/issues/311. A backup whose only change // is a modified file lists the size of the chunks that change added as its // NEW CHUNK SIZE, and a later backup that changes the file again leaves // every earlier snapshot's figure as it was. The file's row in the local // index is shared by all three snapshots and rewritten by each backup. func TestNewChunkSizeOfChangedFileStaysFixed(t *testing.T) { log.Initialize(log.Config{}) t.Parallel() fs := afero.NewOsFs() tempDir := t.TempDir() dataDir := filepath.Join(tempDir, "src") dbPath := filepath.Join(tempDir, "index.sqlite") changedPath := filepath.Join(dataDir, "changed.bin") ctx := context.Background() files := writeFaultSourceTree(t, fs, dataDir) cfg := changedFileConfig(dataDir, dbPath) appendRandomBytes(t, fs, files, changedPath, appendedFileSize) store, err := storage.NewFileStorer(filepath.Join(tempDir, "remote")) require.NoError(t, err) db, err := database.New(ctx, dbPath) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() }) repos := database.NewRepositories(db) v := newBackupVaultik(ctx, cfg, store, repos, db, fs) require.NoError(t, backUp(v, "first")) firstFiles := maps.Clone(files) firstID := localSnapshotID(ctx, t, repos, "first") appendRandomBytes(t, fs, files, changedPath, faultChunkSize) require.NoError(t, backUp(v, "second")) secondID := localSnapshotID(ctx, t, repos, "second") secondAdded := sizeOfNewChunks(t, files, firstFiles) require.Positive(t, secondAdded) assert.Equal(t, secondAdded, listedNewChunkSizes(t, v)[secondID], "the second snapshot must count the chunks the change added") appendRandomBytes(t, fs, files, changedPath, faultChunkSize) require.NoError(t, backUp(v, "third")) sizes := listedNewChunkSizes(t, v) assert.Equal(t, sizeOfNewChunks(t, firstFiles, nil), sizes[firstID], "a later backup must not change the first snapshot's figure") assert.Equal(t, secondAdded, sizes[secondID], "a later backup must not change the second snapshot's figure") } // sizeOfNewChunks returns the total size of the distinct chunks of the // files in after that no file in before has, cut the way the backup cuts // them. func sizeOfNewChunks(t *testing.T, after, before map[string][]byte) int64 { t.Helper() known := make(map[string]bool) for _, chunk := range chunksOf(t, before) { known[chunk.Hash] = true } var total int64 for _, chunk := range chunksOf(t, after) { if !known[chunk.Hash] { known[chunk.Hash] = true total += chunk.Size } } return total } // chunksOf returns the chunks of every file in files. func chunksOf(t *testing.T, files map[string][]byte) []chunker.Chunk { t.Helper() c := chunker.NewChunker(faultChunkSize) var all []chunker.Chunk for _, content := range files { chunks, err := c.ChunkReader(bytes.NewReader(content)) require.NoError(t, err) all = append(all, chunks...) } return all } // listedNewChunkSizes runs `snapshot list --json` and returns each listed // snapshot's new_chunk_size by snapshot ID. func listedNewChunkSizes(t *testing.T, v *vaultik.Vaultik) map[string]int64 { t.Helper() var out bytes.Buffer v.Stdout = &out require.NoError(t, v.ListSnapshots(true)) sizes := make(map[string]int64) for _, row := range decodeListJSON(t, out.String()) { sizes[row.ID] = row.NewChunkSize } return sizes }