Files
syncthing/internal/db/sqlite/db_global_test.go
Simon Frei e54f51c9c5 chore(db): cleanup DB in tests and remove OpenTemp (#10282)
Filled up my tmpfs with test DBs when running benchmarks :)
2025-08-24 09:58:56 +00:00

627 lines
15 KiB
Go

// Copyright (C) 2025 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 sqlite
import (
"slices"
"testing"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestNeed(t *testing.T) {
t.Helper()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Some local files
var v protocol.Vector
baseV := v.Update(1)
newerV := baseV.Update(42)
files := []protocol.FileInfo{
genFile("test1", 1, 0), // remote need
genFile("test2", 2, 0), // local need
genFile("test3", 3, 0), // global
}
files[0].Version = baseV
files[1].Version = baseV
files[2].Version = newerV
err = db.Update(folderID, protocol.LocalDeviceID, files)
if err != nil {
t.Fatal(err)
}
// Some remote files
remote := []protocol.FileInfo{
genFile("test2", 2, 100), // global
genFile("test3", 3, 101), // remote need
genFile("test4", 4, 102), // local need
}
remote[0].Version = newerV
remote[1].Version = baseV
remote[2].Version = newerV
err = db.Update(folderID, protocol.DeviceID{42}, remote)
if err != nil {
t.Fatal(err)
}
// A couple are needed locally
localNeed := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0)))
if !slices.Equal(localNeed, []string{"test2", "test4"}) {
t.Log(localNeed)
t.Fatal("bad local need")
}
// Another couple are needed remotely
remoteNeed := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0)))
if !slices.Equal(remoteNeed, []string{"test1", "test3"}) {
t.Log(remoteNeed)
t.Fatal("bad remote need")
}
}
func TestDropRecalcsGlobal(t *testing.T) {
// When we drop a device we may get a new global
t.Parallel()
t.Run("DropAllFiles", func(t *testing.T) {
t.Parallel()
testDropWithDropper(t, func(t *testing.T, db *DB) {
t.Helper()
if err := db.DropAllFiles(folderID, protocol.DeviceID{42}); err != nil {
t.Fatal(err)
}
})
})
t.Run("DropDevice", func(t *testing.T) {
t.Parallel()
testDropWithDropper(t, func(t *testing.T, db *DB) {
t.Helper()
if err := db.DropDevice(protocol.DeviceID{42}); err != nil {
t.Fatal(err)
}
})
})
t.Run("DropFilesNamed", func(t *testing.T) {
t.Parallel()
testDropWithDropper(t, func(t *testing.T, db *DB) {
t.Helper()
if err := db.DropFilesNamed(folderID, protocol.DeviceID{42}, []string{"test1", "test42"}); err != nil {
t.Fatal(err)
}
})
})
}
func testDropWithDropper(t *testing.T, dropper func(t *testing.T, db *DB)) {
t.Helper()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Some local files
err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
genFile("test1", 1, 0),
genFile("test2", 2, 0),
})
if err != nil {
t.Fatal(err)
}
// Some remote files
remote := []protocol.FileInfo{
genFile("test1", 3, 0),
}
remote[0].Version = remote[0].Version.Update(42)
err = db.Update(folderID, protocol.DeviceID{42}, remote)
if err != nil {
t.Fatal(err)
}
// Remote test1 wins as the global, verify.
count, err := db.CountGlobal(folderID)
if err != nil {
t.Fatal(err)
}
if count.Bytes != (2+3)*128<<10 {
t.Log(count)
t.Fatal("bad global size to begin with")
}
if g, ok, err := db.GetGlobalFile(folderID, "test1"); err != nil || !ok {
t.Fatal("missing global to begin with")
} else if g.Size != 3*128<<10 {
t.Fatal("remote test1 should be the global")
}
// Now remove that remote device
dropper(t, db)
// Our test1 should now be the global
count, err = db.CountGlobal(folderID)
if err != nil {
t.Fatal(err)
}
if count.Bytes != (1+2)*128<<10 {
t.Log(count)
t.Fatal("bad global size after drop")
}
if g, ok, err := db.GetGlobalFile(folderID, "test1"); err != nil || !ok {
t.Fatal("missing global after drop")
} else if g.Size != 1*128<<10 {
t.Fatal("local test1 should be the global")
}
}
func TestNeedDeleted(t *testing.T) {
t.Parallel()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Some local files
err = db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{
genFile("test1", 1, 0),
genFile("test2", 2, 0),
})
if err != nil {
t.Fatal(err)
}
// A remote deleted file
remote := []protocol.FileInfo{
genFile("test1", 1, 101),
}
remote[0].SetDeleted(42)
err = db.Update(folderID, protocol.DeviceID{42}, remote)
if err != nil {
t.Fatal(err)
}
// We need the one deleted file
s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Deleted != 1 {
t.Log(s)
t.Error("bad need")
}
}
func TestDontNeedIgnored(t *testing.T) {
t.Parallel()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A remote file
files := []protocol.FileInfo{
genFile("test1", 1, 103),
}
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// Which we've ignored locally
files[0].SetIgnored()
err = db.Update(folderID, protocol.LocalDeviceID, files)
if err != nil {
t.Fatal(err)
}
// We don't need it
s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in the need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestDontNeedRemoteInvalid(t *testing.T) {
t.Parallel()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A remote file with the invalid bit set
files := []protocol.FileInfo{
genFile("test1", 1, 103),
}
files[0].LocalFlags = protocol.FlagLocalRemoteInvalid
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// It's not part of the global size
s, err := db.CountGlobal(folderID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 {
t.Log(s)
t.Error("bad global")
}
// We don't need it
s, err = db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in the need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestRemoteDontNeedLocalIgnored(t *testing.T) {
t.Parallel()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A local ignored file
file := genFile("test1", 1, 103)
file.SetIgnored()
files := []protocol.FileInfo{file}
err = db.Update(folderID, protocol.LocalDeviceID, files)
if err != nil {
t.Fatal(err)
}
// Which the remote doesn't have (no update)
// They don't need it
s, err := db.CountNeed(folderID, protocol.DeviceID{42})
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in their need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestLocalDontNeedDeletedMissing(t *testing.T) {
t.Parallel()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A remote deleted file
file := genFile("test1", 1, 103)
file.SetDeleted(42)
files := []protocol.FileInfo{file}
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// Which we don't have (no local update)
// We don't need it
s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in the need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestRemoteDontNeedDeletedMissing(t *testing.T) {
t.Parallel()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A local deleted file
file := genFile("test1", 1, 103)
file.SetDeleted(42)
files := []protocol.FileInfo{file}
err = db.Update(folderID, protocol.LocalDeviceID, files)
if err != nil {
t.Fatal(err)
}
// Which the remote doesn't have (no local update)
// They don't need it
s, err := db.CountNeed(folderID, protocol.DeviceID{42})
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in their need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
// Another remote has announced it, but has set the invalid bit,
// presumably it's being ignored.
file = genFile("test1", 1, 103)
file.SetIgnored()
err = db.Update(folderID, protocol.DeviceID{43}, []protocol.FileInfo{file})
if err != nil {
t.Fatal(err)
}
// They don't need it, either
s, err = db.CountNeed(folderID, protocol.DeviceID{43})
if err != nil {
t.Fatal(err)
}
if s.Bytes != 0 || s.Files != 0 || s.Deleted != 0 {
t.Log(s)
t.Error("bad need")
}
// It shouldn't show up in their need list
names = mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.DeviceID{42}, config.PullOrderAlphabetic, 0, 0))
if len(names) != 0 {
t.Log(names)
t.Error("need no files")
}
}
func TestNeedRemoteSymlinkAndDir(t *testing.T) {
t.Parallel()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Two remote "specials", a symlink and a directory
var v protocol.Vector
v.Update(1)
files := []protocol.FileInfo{
{Name: "sym", Type: protocol.FileInfoTypeSymlink, Sequence: 100, Version: v, Blocks: genBlocks("symlink", 0, 1)},
{Name: "dir", Type: protocol.FileInfoTypeDirectory, Sequence: 101, Version: v},
}
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// We need them
s, err := db.CountNeed(folderID, protocol.LocalDeviceID)
if err != nil {
t.Fatal(err)
}
if s.Directories != 1 || s.Symlinks != 1 {
t.Log(s)
t.Error("bad need")
}
// They should be in the need list
names := mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 0, 0))
if len(names) != 2 {
t.Log(names)
t.Error("bad need")
}
}
func TestNeedPagination(t *testing.T) {
t.Parallel()
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// Several remote files
var v protocol.Vector
v.Update(1)
files := []protocol.FileInfo{
genFile("test0", 1, 100),
genFile("test1", 1, 101),
genFile("test2", 1, 102),
genFile("test3", 1, 103),
genFile("test4", 1, 104),
genFile("test5", 1, 105),
genFile("test6", 1, 106),
genFile("test7", 1, 107),
genFile("test8", 1, 108),
genFile("test9", 1, 109),
}
err = db.Update(folderID, protocol.DeviceID{42}, files)
if err != nil {
t.Fatal(err)
}
// We should get the first two
names := fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 2, 0)))
if !slices.Equal(names, []string{"test0", "test1"}) {
t.Log(names)
t.Error("bad need")
}
// We should get the next three
names = fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 3, 2)))
if !slices.Equal(names, []string{"test2", "test3", "test4"}) {
t.Log(names)
t.Error("bad need")
}
// We should get the last five
names = fiNames(mustCollect[protocol.FileInfo](t)(db.AllNeededGlobalFiles(folderID, protocol.LocalDeviceID, config.PullOrderAlphabetic, 5, 5)))
if !slices.Equal(names, []string{"test5", "test6", "test7", "test8", "test9"}) {
t.Log(names)
t.Error("bad need")
}
}
func TestDeletedAfterConflict(t *testing.T) {
t.Parallel()
// A delete that comes after a conflict should be applied, not lose the
// conflict and suddenly cause an old conflict version to become
// promoted.
// D:\syncthing-windows-amd64-v2.0.0-rc.22.dev.11.gff88430e>syncthing --home=c:\PortableApp\SyncTrayzorPortable-x64\data\syncthing debug database-file tnhbr-gxtuf TreeSizeFreeSetup.exe
// DEVICE TYPE NAME SEQUENCE DELETED MODIFIED SIZE FLAGS VERSION BLOCKLIST
// -local- FILE TreeSizeFreeSetup.exe 499 del 2025-07-04T11:52:36.2804841Z 0 ------- HZJYWFM:1751507473,OMKHRPB:1751629956 -nil-
// J5WNYJ6 FILE TreeSizeFreeSetup.exe 500 del 2025-07-04T11:52:36.2804841Z 0 ------- HZJYWFM:1751507473,OMKHRPB:1751629956 -nil-
// 23NHXGS FILE TreeSizeFreeSetup.exe 445 --- 2025-06-23T03:16:10.2804841Z 13832808 -nG---- HZJYWFM:1751507473 7B4kLitF
// JKX6ZDN FILE TreeSizeFreeSetup.exe 320 --- 2025-06-23T03:16:10.2804841Z 13832808 ------- JKX6ZDN:1750992570 7B4kLitF
db, err := Open(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Fatal(err)
}
})
// A file, updated by some remote device. This file is an old, conflicted copy.
file := genFile("test1", 1, 101)
file.ModifiedS = 1750992570
file.Version = protocol.Vector{Counters: []protocol.Counter{{ID: 5 << 60, Value: 1750992570}}}
if err := db.Update(folderID, protocol.DeviceID{5}, []protocol.FileInfo{file}); err != nil {
t.Fatal(err)
}
// The file, updated by a newer remote device. This file is the newer, conflict-winning copy.
file.ModifiedS = 1751507473
file.Version = protocol.Vector{Counters: []protocol.Counter{{ID: 2 << 60, Value: 1751507473}}}
if err := db.Update(folderID, protocol.DeviceID{2}, []protocol.FileInfo{file}); err != nil {
t.Fatal(err)
}
// The file, deleted locally after syncing the file from the remote above..
file.SetDeleted(4)
if err := db.Update(folderID, protocol.LocalDeviceID, []protocol.FileInfo{file}); err != nil {
t.Fatal(err)
}
// The delete should be the global version
f, _, err := db.GetGlobalFile(folderID, "test1")
if err != nil {
t.Fatal(err)
}
if !f.IsDeleted() {
t.Log(f)
t.Error("should be deleted")
}
}