Insert the blank line wsl_v5 requires above `defer` and `go` statements that share no variables with the statement above them. Applied mechanically via `make lint-fix`; the diff is 60 added blank lines and nothing else.
232 lines
6.6 KiB
Go
232 lines
6.6 KiB
Go
package snapshot_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
"sneak.berlin/go/vaultik/internal/types"
|
|
)
|
|
|
|
// verifyChunkChange checks that after a content change the file references
|
|
// the new chunk, the old chunk still exists, and the old chunk no longer
|
|
// maps to the modified file.
|
|
func verifyChunkChange(
|
|
ctx context.Context,
|
|
t *testing.T,
|
|
repos *database.Repositories,
|
|
oldChunkHash, newChunkHash types.ChunkHash,
|
|
) {
|
|
t.Helper()
|
|
|
|
// Verify the chunk hashes are different
|
|
assert.NotEqual(t, oldChunkHash, newChunkHash,
|
|
"Chunk hash should change when content changes")
|
|
|
|
// Get chunk files from second scan
|
|
chunkFiles2, err := repos.ChunkFiles.GetByFilePath(ctx, "/test.txt")
|
|
require.NoError(t, err)
|
|
assert.Len(t, chunkFiles2, 1)
|
|
assert.Equal(t, newChunkHash, chunkFiles2[0].ChunkHash)
|
|
|
|
// Verify old chunk still exists (it's still valid data)
|
|
oldChunk, err := repos.Chunks.GetByHash(ctx, oldChunkHash.String())
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, oldChunk)
|
|
|
|
// Verify new chunk exists
|
|
newChunk, err := repos.Chunks.GetByHash(ctx, newChunkHash.String())
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, newChunk)
|
|
|
|
// Verify that chunk_files for old chunk no longer references this file
|
|
oldChunkFiles, err := repos.ChunkFiles.GetByChunkHash(ctx, oldChunkHash)
|
|
require.NoError(t, err)
|
|
|
|
for _, cf := range oldChunkFiles {
|
|
file, err := repos.Files.GetByID(ctx, cf.FileID)
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, "/data/test.txt", file.Path,
|
|
"Old chunk should not be associated with the modified file")
|
|
}
|
|
}
|
|
|
|
// TestFileContentChange verifies that when a file's content changes,
|
|
// the old chunks are properly disassociated
|
|
func TestFileContentChange(t *testing.T) {
|
|
// Initialize logger for tests
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
// Create in-memory filesystem
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create initial file
|
|
err := afero.WriteFile(fs, "/test.txt", []byte("Initial content"), 0644)
|
|
require.NoError(t, err)
|
|
|
|
// Create test database
|
|
db, err := database.NewTestDB()
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer func() {
|
|
err := db.Close()
|
|
if err != nil {
|
|
t.Errorf("failed to close database: %v", err)
|
|
}
|
|
}()
|
|
|
|
repos := database.NewRepositories(db)
|
|
|
|
// Create scanner
|
|
scanner := snapshot.NewScanner(snapshot.ScannerConfig{
|
|
FS: fs,
|
|
ChunkSize: int64(1024 * 16), // 16KB chunks for testing
|
|
Repositories: repos,
|
|
MaxBlobSize: int64(1024 * 1024), // 1MB blobs
|
|
CompressionLevel: 3,
|
|
AgeRecipients: []string{testAgePublicKey},
|
|
})
|
|
|
|
// Create first snapshot
|
|
ctx := context.Background()
|
|
snapshotID1 := "snapshot1"
|
|
createSnapshotRecord(ctx, t, repos, snapshotID1)
|
|
|
|
// First scan - should create chunks for initial content
|
|
result1, err := scanner.Scan(ctx, "/", snapshotID1)
|
|
require.NoError(t, err)
|
|
t.Logf("First scan: %d files scanned", result1.FilesScanned)
|
|
|
|
// Get file chunks from first scan
|
|
fileChunks1, err := repos.FileChunks.GetByPath(ctx, "/test.txt")
|
|
require.NoError(t, err)
|
|
assert.Len(t, fileChunks1, 1) // Small file = 1 chunk
|
|
oldChunkHash := fileChunks1[0].ChunkHash
|
|
|
|
// Get chunk files from first scan
|
|
chunkFiles1, err := repos.ChunkFiles.GetByFilePath(ctx, "/test.txt")
|
|
require.NoError(t, err)
|
|
assert.Len(t, chunkFiles1, 1)
|
|
|
|
// Modify the file
|
|
time.Sleep(10 * time.Millisecond) // Ensure mtime changes
|
|
|
|
err = afero.WriteFile(fs, "/test.txt",
|
|
[]byte("Modified content with different data"), 0644)
|
|
require.NoError(t, err)
|
|
|
|
// Create second snapshot
|
|
snapshotID2 := "snapshot2"
|
|
createSnapshotRecord(ctx, t, repos, snapshotID2)
|
|
|
|
// Second scan - should create new chunks and remove old associations
|
|
result2, err := scanner.Scan(ctx, "/", snapshotID2)
|
|
require.NoError(t, err)
|
|
t.Logf("Second scan: %d files scanned", result2.FilesScanned)
|
|
|
|
// Get file chunks from second scan
|
|
fileChunks2, err := repos.FileChunks.GetByPath(ctx, "/test.txt")
|
|
require.NoError(t, err)
|
|
assert.Len(t, fileChunks2, 1) // Still 1 chunk but different hash
|
|
newChunkHash := fileChunks2[0].ChunkHash
|
|
|
|
verifyChunkChange(ctx, t, repos, oldChunkHash, newChunkHash)
|
|
}
|
|
|
|
// TestMultipleFileChanges verifies handling of multiple file changes in one scan
|
|
func TestMultipleFileChanges(t *testing.T) {
|
|
// Initialize logger for tests
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
// Create in-memory filesystem
|
|
fs := afero.NewMemMapFs()
|
|
|
|
// Create initial files
|
|
files := map[string]string{
|
|
"/file1.txt": "Content 1",
|
|
"/file2.txt": "Content 2",
|
|
"/file3.txt": "Content 3",
|
|
}
|
|
|
|
for path, content := range files {
|
|
err := afero.WriteFile(fs, path, []byte(content), 0644)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Create test database
|
|
db, err := database.NewTestDB()
|
|
|
|
require.NoError(t, err)
|
|
|
|
defer func() {
|
|
err := db.Close()
|
|
if err != nil {
|
|
t.Errorf("failed to close database: %v", err)
|
|
}
|
|
}()
|
|
|
|
repos := database.NewRepositories(db)
|
|
|
|
// Create scanner
|
|
scanner := snapshot.NewScanner(snapshot.ScannerConfig{
|
|
FS: fs,
|
|
ChunkSize: int64(1024 * 16), // 16KB chunks for testing
|
|
Repositories: repos,
|
|
MaxBlobSize: int64(1024 * 1024), // 1MB blobs
|
|
CompressionLevel: 3,
|
|
AgeRecipients: []string{testAgePublicKey},
|
|
})
|
|
|
|
// Create first snapshot
|
|
ctx := context.Background()
|
|
snapshotID1 := "snapshot1"
|
|
createSnapshotRecord(ctx, t, repos, snapshotID1)
|
|
|
|
// First scan
|
|
result1, err := scanner.Scan(ctx, "/", snapshotID1)
|
|
require.NoError(t, err)
|
|
// Only regular files are counted, not directories
|
|
assert.Equal(t, 3, result1.FilesScanned)
|
|
|
|
// Modify two files
|
|
time.Sleep(10 * time.Millisecond) // Ensure mtime changes
|
|
|
|
err = afero.WriteFile(fs, "/file1.txt", []byte("Modified content 1"), 0644)
|
|
require.NoError(t, err)
|
|
err = afero.WriteFile(fs, "/file3.txt", []byte("Modified content 3"), 0644)
|
|
require.NoError(t, err)
|
|
|
|
// Create second snapshot
|
|
snapshotID2 := "snapshot2"
|
|
createSnapshotRecord(ctx, t, repos, snapshotID2)
|
|
|
|
// Second scan
|
|
result2, err := scanner.Scan(ctx, "/", snapshotID2)
|
|
require.NoError(t, err)
|
|
|
|
// Only regular files are counted, not directories
|
|
assert.Equal(t, 3, result2.FilesScanned)
|
|
|
|
// Verify each file has exactly one set of chunks
|
|
for path := range files {
|
|
fileChunks, err := repos.FileChunks.GetByPath(ctx, path)
|
|
require.NoError(t, err)
|
|
assert.Len(t, fileChunks, 1,
|
|
"File %s should have exactly 1 chunk association", path)
|
|
|
|
chunkFiles, err := repos.ChunkFiles.GetByFilePath(ctx, path)
|
|
require.NoError(t, err)
|
|
assert.Len(t, chunkFiles, 1,
|
|
"File %s should have exactly 1 chunk-file association", path)
|
|
}
|
|
}
|