// Copyright (C) 2014 The Syncthing Authors. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at https://mozilla.org/MPL/2.0/. package db import ( "bytes" "context" "fmt" "os" "testing" "github.com/syncthing/syncthing/lib/db/backend" "github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/protocol" ) func genBlocks(n int) []protocol.BlockInfo { b := make([]protocol.BlockInfo, n) for i := range b { h := make([]byte, 32) for j := range h { h[j] = byte(i + j) } b[i].Size = i b[i].Hash = h } return b } func TestIgnoredFiles(t *testing.T) { ldb, err := openJSONS("testdata/v0.14.48-ignoredfiles.db.jsons") if err != nil { t.Fatal(err) } db := NewLowlevel(ldb) defer db.Close() if err := UpdateSchema(db); err != nil { t.Fatal(err) } fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db) // The contents of the database are like this: // // fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db) // fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{ // { // invalid (ignored) file // Name: "foo", // Type: protocol.FileInfoTypeFile, // Invalid: true, // Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1000}}}, // }, // { // regular file // Name: "bar", // Type: protocol.FileInfoTypeFile, // Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1001}}}, // }, // }) // fs.Update(protocol.DeviceID{42}, []protocol.FileInfo{ // { // invalid file // Name: "baz", // Type: protocol.FileInfoTypeFile, // Invalid: true, // Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1000}}}, // }, // { // regular file // Name: "quux", // Type: protocol.FileInfoTypeFile, // Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1002}}}, // }, // }) // Local files should have the "ignored" bit in addition to just being // generally invalid if we want to look at the simulation of that bit. snap := fs.Snapshot() defer snap.Release() fi, ok := snap.Get(protocol.LocalDeviceID, "foo") if !ok { t.Fatal("foo should exist") } if !fi.IsInvalid() { t.Error("foo should be invalid") } if !fi.IsIgnored() { t.Error("foo should be ignored") } fi, ok = snap.Get(protocol.LocalDeviceID, "bar") if !ok { t.Fatal("bar should exist") } if fi.IsInvalid() { t.Error("bar should not be invalid") } if fi.IsIgnored() { t.Error("bar should not be ignored") } // Remote files have the invalid bit as usual, and the IsInvalid() method // should pick this up too. fi, ok = snap.Get(protocol.DeviceID{42}, "baz") if !ok { t.Fatal("baz should exist") } if !fi.IsInvalid() { t.Error("baz should be invalid") } if !fi.IsInvalid() { t.Error("baz should be invalid") } fi, ok = snap.Get(protocol.DeviceID{42}, "quux") if !ok { t.Fatal("quux should exist") } if fi.IsInvalid() { t.Error("quux should not be invalid") } if fi.IsInvalid() { t.Error("quux should not be invalid") } } const myID = 1 var ( remoteDevice0, remoteDevice1 protocol.DeviceID update0to3Folder = "UpdateSchema0to3" invalid = "invalid" slashPrefixed = "/notgood" haveUpdate0to3 map[protocol.DeviceID]fileList ) func init() { remoteDevice0, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR") remoteDevice1, _ = protocol.DeviceIDFromString("I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU") haveUpdate0to3 = map[protocol.DeviceID]fileList{ protocol.LocalDeviceID: { protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)}, protocol.FileInfo{Name: slashPrefixed, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)}, }, remoteDevice0: { protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)}, protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(5), RawInvalid: true}, protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(7)}, }, remoteDevice1: { protocol.FileInfo{Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(7)}, protocol.FileInfo{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}, Blocks: genBlocks(5), RawInvalid: true}, protocol.FileInfo{Name: invalid, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1004}}}, Blocks: genBlocks(5), RawInvalid: true}, }, } } func TestUpdate0to3(t *testing.T) { ldb, err := openJSONS("testdata/v0.14.45-update0to3.db.jsons") if err != nil { t.Fatal(err) } db := NewLowlevel(ldb) defer db.Close() updater := schemaUpdater{db} folder := []byte(update0to3Folder) if err := updater.updateSchema0to1(0); err != nil { t.Fatal(err) } trans, err := db.newReadOnlyTransaction() if err != nil { t.Fatal(err) } defer trans.Release() if _, ok, err := trans.getFile(folder, protocol.LocalDeviceID[:], []byte(slashPrefixed)); err != nil { t.Fatal(err) } else if ok { t.Error("File prefixed by '/' was not removed during transition to schema 1") } var key []byte key, err = db.keyer.GenerateGlobalVersionKey(nil, folder, []byte(invalid)) if err != nil { t.Fatal(err) } if _, err := db.Get(key); err != nil { t.Error("Invalid file wasn't added to global list") } if err := updater.updateSchema1to2(1); err != nil { t.Fatal(err) } found := false trans, err = db.newReadOnlyTransaction() if err != nil { t.Fatal(err) } defer trans.Release() _ = trans.withHaveSequence(folder, 0, func(fi protocol.FileIntf) bool { f := fi.(protocol.FileInfo) l.Infoln(f) if found { t.Error("Unexpected additional file via sequence", f.FileName()) return true } if e := haveUpdate0to3[protocol.LocalDeviceID][0]; f.IsEquivalentOptional(e, 0, true, true, 0) { found = true } else { t.Errorf("Wrong file via sequence, got %v, expected %v", f, e) } return true }) if !found { t.Error("Local file wasn't added to sequence bucket", err) } if err := updater.updateSchema2to3(2); err != nil { t.Fatal(err) } need := map[string]protocol.FileInfo{ haveUpdate0to3[remoteDevice0][0].Name: haveUpdate0to3[remoteDevice0][0], haveUpdate0to3[remoteDevice1][0].Name: haveUpdate0to3[remoteDevice1][0], haveUpdate0to3[remoteDevice0][2].Name: haveUpdate0to3[remoteDevice0][2], } trans, err = db.newReadOnlyTransaction() if err != nil { t.Fatal(err) } defer trans.Release() key, err = trans.keyer.GenerateNeedFileKey(nil, folder, nil) if err != nil { t.Fatal(err) } dbi, err := trans.NewPrefixIterator(key) if err != nil { t.Fatal(err) } defer dbi.Release() for dbi.Next() { name := trans.keyer.NameFromGlobalVersionKey(dbi.Key()) key, err = trans.keyer.GenerateGlobalVersionKey(key, folder, name) bs, err := trans.Get(key) if err != nil { t.Fatal(err) } var vl VersionListDeprecated if err := vl.Unmarshal(bs); err != nil { t.Fatal(err) } key, err = trans.keyer.GenerateDeviceFileKey(key, folder, vl.Versions[0].Device, name) fi, ok, err := trans.getFileTrunc(key, false) if err != nil { t.Fatal(err) } if !ok { device := "" if dev, err := protocol.DeviceIDFromBytes(vl.Versions[0].Device); err != nil { device = dev.String() } t.Fatal("surprise missing global file", string(name), device) } e, ok := need[fi.FileName()] if !ok { t.Error("Got unexpected needed file:", fi.FileName()) } f := fi.(protocol.FileInfo) delete(need, f.Name) if !f.IsEquivalentOptional(e, 0, true, true, 0) { t.Errorf("Wrong needed file, got %v, expected %v", f, e) } } if dbi.Error() != nil { t.Fatal(err) } for n := range need { t.Errorf(`Missing needed file "%v"`, n) } } // TestRepairSequence checks that a few hand-crafted messed-up sequence entries get fixed. func TestRepairSequence(t *testing.T) { db := NewLowlevel(backend.OpenMemory()) defer db.Close() folderStr := "test" folder := []byte(folderStr) id := protocol.LocalDeviceID short := protocol.LocalDeviceID.Short() files := []protocol.FileInfo{ {Name: "fine", Blocks: genBlocks(1)}, {Name: "duplicate", Blocks: genBlocks(2)}, {Name: "missing", Blocks: genBlocks(3)}, {Name: "overwriting", Blocks: genBlocks(4)}, {Name: "inconsistent", Blocks: genBlocks(5)}, {Name: "inconsistentNotIndirected", Blocks: genBlocks(2)}, } for i, f := range files { files[i].Version = f.Version.Update(short) } trans, err := db.newReadWriteTransaction() if err != nil { t.Fatal(err) } defer trans.close() addFile := func(f protocol.FileInfo, seq int64) { dk, err := trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte(f.Name)) if err != nil { t.Fatal(err) } if err := trans.putFile(dk, f); err != nil { t.Fatal(err) } sk, err := trans.keyer.GenerateSequenceKey(nil, folder, seq) if err != nil { t.Fatal(err) } if err := trans.Put(sk, dk); err != nil { t.Fatal(err) } } // Plain normal entry var seq int64 = 1 files[0].Sequence = 1 addFile(files[0], seq) // Second entry once updated with original sequence still in place f := files[1] f.Sequence = int64(len(files) + 1) addFile(f, f.Sequence) // Original sequence entry seq++ sk, err := trans.keyer.GenerateSequenceKey(nil, folder, seq) if err != nil { t.Fatal(err) } dk, err := trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte(f.Name)) if err != nil { t.Fatal(err) } if err := trans.Put(sk, dk); err != nil { t.Fatal(err) } // File later overwritten thus missing sequence entry seq++ files[2].Sequence = seq addFile(files[2], seq) // File overwriting previous sequence entry (no seq bump) seq++ files[3].Sequence = seq addFile(files[3], seq) // Inconistent files seq++ files[4].Sequence = 101 addFile(files[4], seq) seq++ files[5].Sequence = 102 addFile(files[5], seq) // And a sequence entry pointing at nothing because why not sk, err = trans.keyer.GenerateSequenceKey(nil, folder, 100001) if err != nil { t.Fatal(err) } dk, err = trans.keyer.GenerateDeviceFileKey(nil, folder, id[:], []byte("nonexisting")) if err != nil { t.Fatal(err) } if err := trans.Put(sk, dk); err != nil { t.Fatal(err) } if err := trans.Commit(); err != nil { t.Fatal(err) } // Loading the metadata for the first time means a "re"calculation happens, // along which the sequences get repaired too. db.gcMut.RLock() _ = db.loadMetadataTracker(folderStr) db.gcMut.RUnlock() if err != nil { t.Fatal(err) } // Check the db ro, err := db.newReadOnlyTransaction() if err != nil { t.Fatal(err) } defer ro.close() it, err := ro.NewPrefixIterator([]byte{KeyTypeDevice}) if err != nil { t.Fatal(err) } defer it.Release() for it.Next() { fi, err := ro.unmarshalTrunc(it.Value(), true) if err != nil { t.Fatal(err) } if sk, err = ro.keyer.GenerateSequenceKey(sk, folder, fi.SequenceNo()); err != nil { t.Fatal(err) } dk, err := ro.Get(sk) if backend.IsNotFound(err) { t.Error("Missing sequence entry for", fi.FileName()) } else if err != nil { t.Fatal(err) } if !bytes.Equal(it.Key(), dk) { t.Errorf("Wrong key for %v, expected %s, got %s", f.FileName(), it.Key(), dk) } } if err := it.Error(); err != nil { t.Fatal(err) } it.Release() it, err = ro.NewPrefixIterator([]byte{KeyTypeSequence}) if err != nil { t.Fatal(err) } defer it.Release() for it.Next() { intf, ok, err := ro.getFileTrunc(it.Value(), false) if err != nil { t.Fatal(err) } fi := intf.(protocol.FileInfo) seq := ro.keyer.SequenceFromSequenceKey(it.Key()) if !ok { t.Errorf("Sequence entry %v points at nothing", seq) } else if fi.SequenceNo() != seq { t.Errorf("Inconsistent sequence entry for %v: %v != %v", fi.FileName(), fi.SequenceNo(), seq) } if len(fi.Blocks) == 0 { t.Error("Missing blocks in", fi.FileName()) } } if err := it.Error(); err != nil { t.Fatal(err) } it.Release() } func TestDowngrade(t *testing.T) { db := NewLowlevel(backend.OpenMemory()) defer db.Close() // sets the min version etc if err := UpdateSchema(db); err != nil { t.Fatal(err) } // Bump the database version to something newer than we actually support miscDB := NewMiscDataNamespace(db) if err := miscDB.PutInt64("dbVersion", dbVersion+1); err != nil { t.Fatal(err) } l.Infoln(dbVersion) // Pretend we just opened the DB and attempt to update it again err := UpdateSchema(db) if err, ok := err.(*databaseDowngradeError); !ok { t.Fatal("Expected error due to database downgrade, got", err) } else if err.minSyncthingVersion != dbMinSyncthingVersion { t.Fatalf("Error has %v as min Syncthing version, expected %v", err.minSyncthingVersion, dbMinSyncthingVersion) } } func TestCheckGlobals(t *testing.T) { db := NewLowlevel(backend.OpenMemory()) defer db.Close() fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), db) // Add any file name := "foo" fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{ { Name: name, Type: protocol.FileInfoTypeFile, Version: protocol.Vector{Counters: []protocol.Counter{{ID: 1, Value: 1001}}}, }, }) // Remove just the file entry if err := db.dropPrefix([]byte{KeyTypeDevice}); err != nil { t.Fatal(err) } // Clean up global entry of the now missing file if err := db.checkGlobals([]byte(fs.folder)); err != nil { t.Fatal(err) } // Check that the global entry is gone gk, err := db.keyer.GenerateGlobalVersionKey(nil, []byte(fs.folder), []byte(name)) if err != nil { t.Fatal(err) } _, err = db.Get(gk) if !backend.IsNotFound(err) { t.Error("Expected key missing error, got", err) } } func TestUpdateTo10(t *testing.T) { ldb, err := openJSONS("./testdata/v1.4.0-updateTo10.json") if err != nil { t.Fatal(err) } db := NewLowlevel(ldb) defer db.Close() UpdateSchema(db) folder := "test" meta := db.getMetaAndCheck(folder) empty := Counts{} c := meta.Counts(protocol.LocalDeviceID, needFlag) if c.Files != 1 { t.Error("Expected 1 needed file locally, got", c.Files) } c.Files = 0 if c.Deleted != 1 { t.Error("Expected 1 needed deletion locally, got", c.Deleted) } c.Deleted = 0 if !c.Equal(empty) { t.Error("Expected all counts to be zero, got", c) } c = meta.Counts(remoteDevice0, needFlag) if !c.Equal(empty) { t.Error("Expected all counts to be zero, got", c) } trans, err := db.newReadOnlyTransaction() if err != nil { t.Fatal(err) } defer trans.Release() // a vl, err := trans.getGlobalVersions(nil, []byte(folder), []byte("a")) if err != nil { t.Fatal(err) } for _, v := range vl.RawVersions { if !v.Deleted { t.Error("Unexpected undeleted global version for a") } } // b vl, err = trans.getGlobalVersions(nil, []byte(folder), []byte("b")) if err != nil { t.Fatal(err) } if !vl.RawVersions[0].Deleted { t.Error("vl.Versions[0] not deleted for b") } if vl.RawVersions[1].Deleted { t.Error("vl.Versions[1] deleted for b") } // c vl, err = trans.getGlobalVersions(nil, []byte(folder), []byte("c")) if err != nil { t.Fatal(err) } if vl.RawVersions[0].Deleted { t.Error("vl.Versions[0] deleted for c") } if !vl.RawVersions[1].Deleted { t.Error("vl.Versions[1] not deleted for c") } } func TestDropDuplicates(t *testing.T) { names := []string{ "foo", "bar", "dcxvoijnds", "3d/dsfase/4/ss2", } tcs := []struct{ in, out []int }{ {[]int{0}, []int{0}}, {[]int{0, 1}, []int{0, 1}}, {[]int{0, 1, 0, 1}, []int{0, 1}}, {[]int{0, 1, 1, 1, 1}, []int{0, 1}}, {[]int{0, 0, 0, 1}, []int{0, 1}}, {[]int{0, 1, 2, 3}, []int{0, 1, 2, 3}}, {[]int{3, 2, 1, 0, 0, 1, 2, 3}, []int{0, 1, 2, 3}}, {[]int{0, 1, 1, 3, 0, 1, 0, 1, 2, 3}, []int{0, 1, 2, 3}}, } for tci, tc := range tcs { inp := make([]protocol.FileInfo, len(tc.in)) expSeq := make(map[string]int) for i, j := range tc.in { inp[i] = protocol.FileInfo{Name: names[j], Sequence: int64(i)} expSeq[names[j]] = i } outp := normalizeFilenamesAndDropDuplicates(inp) if len(outp) != len(tc.out) { t.Errorf("tc %v: Expected %v entries, got %v", tci, len(tc.out), len(outp)) continue } for i, f := range outp { if exp := names[tc.out[i]]; exp != f.Name { t.Errorf("tc %v: Got file %v at pos %v, expected %v", tci, f.Name, i, exp) } if exp := int64(expSeq[outp[i].Name]); exp != f.Sequence { t.Errorf("tc %v: Got sequence %v at pos %v, expected %v", tci, f.Sequence, i, exp) } } } } func TestGCIndirect(t *testing.T) { // Verify that the gcIndirect run actually removes block lists. db := NewLowlevel(backend.OpenMemory()) defer db.Close() meta := newMetadataTracker(db.keyer) // Add three files with different block lists files := []protocol.FileInfo{ {Name: "a", Blocks: genBlocks(100)}, {Name: "b", Blocks: genBlocks(200)}, {Name: "c", Blocks: genBlocks(300)}, } db.updateLocalFiles([]byte("folder"), files, meta) // Run a GC pass db.gcIndirect(context.Background()) // Verify that we have three different block lists n, err := numBlockLists(db) if err != nil { t.Fatal(err) } if n != len(files) { t.Fatal("expected each file to have a block list") } // Change the block lists for each file for i := range files { files[i].Version = files[i].Version.Update(42) files[i].Blocks = genBlocks(len(files[i].Blocks) + 1) } db.updateLocalFiles([]byte("folder"), files, meta) // Verify that we now have *six* different block lists n, err = numBlockLists(db) if err != nil { t.Fatal(err) } if n != 2*len(files) { t.Fatal("expected both old and new block lists to exist") } // Run a GC pass db.gcIndirect(context.Background()) // Verify that we now have just the three we need, again n, err = numBlockLists(db) if err != nil { t.Fatal(err) } if n != len(files) { t.Fatal("expected GC to collect all but the needed ones") } // Double check the correctness by loading the block lists and comparing with what we stored tr, err := db.newReadOnlyTransaction() if err != nil { t.Fatal() } defer tr.Release() for _, f := range files { fi, ok, err := tr.getFile([]byte("folder"), protocol.LocalDeviceID[:], []byte(f.Name)) if err != nil { t.Fatal(err) } if !ok { t.Fatal("mysteriously missing") } if len(fi.Blocks) != len(f.Blocks) { t.Fatal("block list mismatch") } for i := range fi.Blocks { if !bytes.Equal(fi.Blocks[i].Hash, f.Blocks[i].Hash) { t.Fatal("hash mismatch") } } } } func TestUpdateTo14(t *testing.T) { db := NewLowlevel(backend.OpenMemory()) defer db.Close() folderStr := "default" folder := []byte(folderStr) name := []byte("foo") file := protocol.FileInfo{Name: string(name), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(blocksIndirectionCutoff - 1)} file.BlocksHash = protocol.BlocksHash(file.Blocks) fileWOBlocks := file fileWOBlocks.Blocks = nil meta := db.loadMetadataTracker(folderStr) // Initally add the correct file the usual way, all good here. if err := db.updateLocalFiles(folder, []protocol.FileInfo{file}, meta); err != nil { t.Fatal(err) } // Simulate the previous bug, where .putFile could write a file info without // blocks, even though the file has them (and thus a non-nil BlocksHash). trans, err := db.newReadWriteTransaction() if err != nil { t.Fatal(err) } defer trans.close() key, err := db.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], name) if err != nil { t.Fatal(err) } fiBs := mustMarshal(&fileWOBlocks) if err := trans.Put(key, fiBs); err != nil { t.Fatal(err) } if err := trans.Commit(); err != nil { t.Fatal(err) } trans.close() // Run migration, pretending were still on schema 13. if err := (&schemaUpdater{db}).updateSchemaTo14(13); err != nil { t.Fatal(err) } // checks ro, err := db.newReadOnlyTransaction() if err != nil { t.Fatal(err) } defer ro.close() if f, ok, err := ro.getFileByKey(key); err != nil { t.Fatal(err) } else if !ok { t.Error("file missing") } else if !f.MustRescan() { t.Error("file not marked as MustRescan") } if vl, err := ro.getGlobalVersions(nil, folder, name); err != nil { t.Fatal(err) } else if fv, ok := vl.GetGlobal(); !ok { t.Error("missing global") } else if !fv.IsInvalid() { t.Error("global not marked as invalid") } } func TestFlushRecursion(t *testing.T) { // Verify that a commit hook can write to the transaction without // causing another flush and thus recursion. // Badger doesn't work like this. if os.Getenv("USE_BADGER") != "" { t.Skip("Not supported on Badger") } db := NewLowlevel(backend.OpenMemory()) defer db.Close() // A commit hook that writes a small piece of data to the transaction. hookFired := 0 hook := func(tx backend.WriteTransaction) error { err := tx.Put([]byte(fmt.Sprintf("hook-key-%d", hookFired)), []byte(fmt.Sprintf("hook-value-%d", hookFired))) if err != nil { t.Fatal(err) } hookFired++ return nil } // A transaction. tx, err := db.NewWriteTransaction(hook) if err != nil { t.Fatal(err) } defer tx.Release() // Write stuff until the transaction flushes, thus firing the hook. i := 0 for hookFired == 0 { err := tx.Put([]byte(fmt.Sprintf("key-%d", i)), []byte(fmt.Sprintf("value-%d", i))) if err != nil { t.Fatal(err) } i++ } // The hook should have fired precisely once. if hookFired != 1 { t.Error("expect one hook fire, not", hookFired) } } func TestCheckLocalNeed(t *testing.T) { db := NewLowlevel(backend.OpenMemory()) defer db.Close() folderStr := "test" fs := NewFileSet(folderStr, fs.NewFilesystem(fs.FilesystemTypeFake, ""), db) // Add files such that we are in sync for a and b, and need c and d. files := []protocol.FileInfo{ {Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}, {Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}, {Name: "c", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}, {Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}, } fs.Update(protocol.LocalDeviceID, files) files[2].Version = files[2].Version.Update(remoteDevice0.Short()) files[3].Version = files[2].Version.Update(remoteDevice0.Short()) fs.Update(remoteDevice0, files) checkNeed := func() { snap := fs.Snapshot() defer snap.Release() c := snap.NeedSize(protocol.LocalDeviceID) if c.Files != 2 { t.Errorf("Expected 2 needed files locally, got %v in meta", c.Files) } needed := make([]protocol.FileInfo, 0, 2) snap.WithNeed(protocol.LocalDeviceID, func(fi protocol.FileIntf) bool { needed = append(needed, fi.(protocol.FileInfo)) return true }) if l := len(needed); l != 2 { t.Errorf("Expected 2 needed files locally, got %v in db", l) } else if needed[0].Name != "c" || needed[1].Name != "d" { t.Errorf("Expected files c and d to be needed, got %v and %v", needed[0].Name, needed[1].Name) } } checkNeed() trans, err := db.newReadWriteTransaction() if err != nil { t.Fatal(err) } defer trans.close() // Add "b" to needed and remove "d" folder := []byte(folderStr) key, err := trans.keyer.GenerateNeedFileKey(nil, folder, []byte(files[1].Name)) if err != nil { t.Fatal(err) } if err = trans.Put(key, nil); err != nil { t.Fatal(err) } key, err = trans.keyer.GenerateNeedFileKey(nil, folder, []byte(files[3].Name)) if err != nil { t.Fatal(err) } if err = trans.Delete(key); err != nil { t.Fatal(err) } if err := trans.Commit(); err != nil { t.Fatal(err) } if repaired, err := db.checkLocalNeed(folder); err != nil { t.Fatal(err) } else if repaired != 2 { t.Error("Expected 2 repaired local need items, got", repaired) } checkNeed() } func TestDuplicateNeedCount(t *testing.T) { db := NewLowlevel(backend.OpenMemory()) defer db.Close() folder := "test" testFs := fs.NewFilesystem(fs.FilesystemTypeFake, "") fs := NewFileSet(folder, testFs, db) files := []protocol.FileInfo{{Name: "foo", Version: protocol.Vector{}.Update(myID), Sequence: 1}} fs.Update(protocol.LocalDeviceID, files) files[0].Version = files[0].Version.Update(remoteDevice0.Short()) fs.Update(remoteDevice0, files) db.checkRepair() fs = NewFileSet(folder, testFs, db) found := false for _, c := range fs.meta.counts.Counts { if bytes.Equal(protocol.LocalDeviceID[:], c.DeviceID) && c.LocalFlags == needFlag { if found { t.Fatal("second need count for local device encountered") } found = true } } if !found { t.Fatal("no need count for local device encountered") } } func numBlockLists(db *Lowlevel) (int, error) { it, err := db.Backend.NewPrefixIterator([]byte{KeyTypeBlockList}) if err != nil { return 0, err } defer it.Release() n := 0 for it.Next() { n++ } if err := it.Error(); err != nil { return 0, err } return n, nil }