diff --git a/internal/db/sqlite/db_local_test.go b/internal/db/sqlite/db_local_test.go index ed0349867..1487f8266 100644 --- a/internal/db/sqlite/db_local_test.go +++ b/internal/db/sqlite/db_local_test.go @@ -9,6 +9,7 @@ package sqlite import ( "testing" + "github.com/syncthing/syncthing/internal/db" "github.com/syncthing/syncthing/internal/itererr" "github.com/syncthing/syncthing/lib/protocol" ) @@ -138,6 +139,211 @@ func TestBlocksDeleted(t *testing.T) { } } +func TestDropBlockIndex(t *testing.T) { + t.Parallel() + + sdb, err := Open(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { sdb.Close() }) + + // Insert files with blocks + files := []protocol.FileInfo{ + genFile("a", 3, 0), + genFile("b", 2, 0), + } + if err := sdb.Update(folderID, protocol.LocalDeviceID, files); err != nil { + t.Fatal(err) + } + + // Verify blocks exist + hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, files[0].Blocks[0].Hash)) + if err != nil { + t.Fatal(err) + } + if len(hits) == 0 { + t.Fatal("expected block hits before drop") + } + + // Drop the block index + if err := sdb.DropBlockIndex(folderID); err != nil { + t.Fatal(err) + } + + // Verify blocks are gone + hits, err = itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, files[0].Blocks[0].Hash)) + if err != nil { + t.Fatal(err) + } + if len(hits) != 0 { + t.Fatal("expected no block hits after drop") + } + + // Dropping again should be a no-op (already empty) + if err := sdb.DropBlockIndex(folderID); err != nil { + t.Fatal(err) + } + + // Dropping a nonexistent folder should be fine + if err := sdb.DropBlockIndex("nonexistent"); err != nil { + t.Fatal(err) + } +} + +func TestPopulateBlockIndex(t *testing.T) { + t.Parallel() + + sdb, err := Open(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { sdb.Close() }) + + // Insert files with blocks + files := []protocol.FileInfo{ + genFile("a", 3, 0), + genFile("b", 2, 0), + } + if err := sdb.Update(folderID, protocol.LocalDeviceID, files); err != nil { + t.Fatal(err) + } + + // Collect the original block entries for comparison + origHitsA, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, files[0].Blocks[0].Hash)) + if err != nil { + t.Fatal(err) + } + if len(origHitsA) != 1 { + t.Fatal("expected one hit for block a[0]") + } + + // Drop the block index + if err := sdb.DropBlockIndex(folderID); err != nil { + t.Fatal(err) + } + + // Populate it back from existing blocklists + if err := sdb.PopulateBlockIndex(folderID); err != nil { + t.Fatal(err) + } + + // Verify all blocks are back + for i, f := range files { + for j, b := range f.Blocks { + hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, b.Hash)) + if err != nil { + t.Fatal(err) + } + if len(hits) == 0 { + t.Errorf("file %d block %d: expected hits after populate", i, j) + } + } + } + + // Populating again should be a no-op (not empty) + if err := sdb.PopulateBlockIndex(folderID); err != nil { + t.Fatal(err) + } +} + +func TestPopulateBlockIndexSkipsRemoteFiles(t *testing.T) { + t.Parallel() + + sdb, err := Open(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { sdb.Close() }) + + // Insert a local file (blocks indexed) and a remote file (blocks not indexed) + localFile := genFile("local", 2, 0) + if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{localFile}); err != nil { + t.Fatal(err) + } + remoteFile := genFile("remote", 2, 1) + if err := sdb.Update(folderID, protocol.DeviceID{42}, []protocol.FileInfo{remoteFile}); err != nil { + t.Fatal(err) + } + + // Drop and repopulate + if err := sdb.DropBlockIndex(folderID); err != nil { + t.Fatal(err) + } + if err := sdb.PopulateBlockIndex(folderID); err != nil { + t.Fatal(err) + } + + // Local file blocks should be present + hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, localFile.Blocks[0].Hash)) + if err != nil { + t.Fatal(err) + } + if len(hits) == 0 { + t.Error("expected hits for local file blocks") + } + + // Remote file blocks should not be present (blocks are only + // indexed for local files) + hits, err = itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, remoteFile.Blocks[0].Hash)) + if err != nil { + t.Fatal(err) + } + if len(hits) != 0 { + t.Error("expected no hits for remote file blocks") + } +} + +func TestSkipBlockIndexOnUpdate(t *testing.T) { + t.Parallel() + + sdb, err := Open(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { sdb.Close() }) + + // Insert a file with SkipBlockIndex + file := genFile("a", 3, 0) + if err := sdb.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}, db.WithSkipBlockIndex()); err != nil { + t.Fatal(err) + } + + // Blocks should not be indexed + hits, err := itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, file.Blocks[0].Hash)) + if err != nil { + t.Fatal(err) + } + if len(hits) != 0 { + t.Fatal("expected no block hits with SkipBlockIndex") + } + + // The blocklist should still be stored (file info is retrievable with blocks) + fi, ok, err := sdb.GetDeviceFile(folderID, protocol.LocalDeviceID, "a") + if err != nil { + t.Fatal(err) + } + if !ok { + t.Fatal("file not found") + } + if len(fi.Blocks) != 3 { + t.Fatalf("expected 3 blocks in file info, got %d", len(fi.Blocks)) + } + + // Populate should fill in the blocks + if err := sdb.PopulateBlockIndex(folderID); err != nil { + t.Fatal(err) + } + + hits, err = itererr.Collect(sdb.AllLocalBlocksWithHash(folderID, file.Blocks[0].Hash)) + if err != nil { + t.Fatal(err) + } + if len(hits) != 1 { + t.Fatal("expected one hit after populate") + } +} + func TestRemoteSequence(t *testing.T) { t.Parallel()