mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-23 03:18:59 +00:00
523 lines
14 KiB
Go
523 lines
14 KiB
Go
package model
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/calmh/syncthing/protocol"
|
|
)
|
|
|
|
func TestNewModel(t *testing.T) {
|
|
m := NewModel("foo")
|
|
|
|
if m == nil {
|
|
t.Fatalf("NewModel returned nil")
|
|
}
|
|
|
|
if fs, _ := m.NeedFiles(); len(fs) > 0 {
|
|
t.Errorf("New model should have no Need")
|
|
}
|
|
|
|
if len(m.local) > 0 {
|
|
t.Errorf("New model should have no Have")
|
|
}
|
|
}
|
|
|
|
var testDataExpected = map[string]File{
|
|
"foo": File{
|
|
Name: "foo",
|
|
Flags: 0,
|
|
Modified: 0,
|
|
Blocks: []Block{{Offset: 0x0, Size: 0x7, Hash: []uint8{0xae, 0xc0, 0x70, 0x64, 0x5f, 0xe5, 0x3e, 0xe3, 0xb3, 0x76, 0x30, 0x59, 0x37, 0x61, 0x34, 0xf0, 0x58, 0xcc, 0x33, 0x72, 0x47, 0xc9, 0x78, 0xad, 0xd1, 0x78, 0xb6, 0xcc, 0xdf, 0xb0, 0x1, 0x9f}}},
|
|
},
|
|
"bar": File{
|
|
Name: "bar",
|
|
Flags: 0,
|
|
Modified: 0,
|
|
Blocks: []Block{{Offset: 0x0, Size: 0xa, Hash: []uint8{0x2f, 0x72, 0xcc, 0x11, 0xa6, 0xfc, 0xd0, 0x27, 0x1e, 0xce, 0xf8, 0xc6, 0x10, 0x56, 0xee, 0x1e, 0xb1, 0x24, 0x3b, 0xe3, 0x80, 0x5b, 0xf9, 0xa9, 0xdf, 0x98, 0xf9, 0x2f, 0x76, 0x36, 0xb0, 0x5c}}},
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
// Fix expected test data to match reality
|
|
for n, f := range testDataExpected {
|
|
fi, _ := os.Stat("testdata/" + n)
|
|
f.Flags = uint32(fi.Mode())
|
|
f.Modified = fi.ModTime().Unix()
|
|
testDataExpected[n] = f
|
|
}
|
|
}
|
|
|
|
func TestUpdateLocal(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
if fs, _ := m.NeedFiles(); len(fs) > 0 {
|
|
t.Fatalf("Model with only local data should have no need")
|
|
}
|
|
|
|
if l1, l2 := len(m.local), len(testDataExpected); l1 != l2 {
|
|
t.Fatalf("Model len(local) incorrect, %d != %d", l1, l2)
|
|
}
|
|
if l1, l2 := len(m.global), len(testDataExpected); l1 != l2 {
|
|
t.Fatalf("Model len(global) incorrect, %d != %d", l1, l2)
|
|
}
|
|
for name, file := range testDataExpected {
|
|
if f, ok := m.local[name]; ok {
|
|
if !reflect.DeepEqual(f, file) {
|
|
t.Errorf("Incorrect local\n%v !=\n%v\nfor file %q", f, file, name)
|
|
}
|
|
} else {
|
|
t.Errorf("Missing file %q in local table", name)
|
|
}
|
|
if f, ok := m.global[name]; ok {
|
|
if !reflect.DeepEqual(f, file) {
|
|
t.Errorf("Incorrect global\n%v !=\n%v\nfor file %q", f, file, name)
|
|
}
|
|
} else {
|
|
t.Errorf("Missing file %q in global table", name)
|
|
}
|
|
}
|
|
|
|
for _, f := range fs {
|
|
if hf, ok := m.local[f.Name]; !ok || hf.Modified != f.Modified {
|
|
t.Fatalf("Incorrect local for %q", f.Name)
|
|
}
|
|
if cf, ok := m.global[f.Name]; !ok || cf.Modified != f.Modified {
|
|
t.Fatalf("Incorrect global for %q", f.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRemoteUpdateExisting(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
newFile := protocol.FileInfo{
|
|
Name: "foo",
|
|
Modified: time.Now().Unix(),
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
}
|
|
m.Index("42", []protocol.FileInfo{newFile})
|
|
|
|
if fs, _ := m.NeedFiles(); len(fs) != 1 {
|
|
t.Errorf("Model missing Need for one file (%d != 1)", len(fs))
|
|
}
|
|
}
|
|
|
|
func TestRemoteAddNew(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
newFile := protocol.FileInfo{
|
|
Name: "a new file",
|
|
Modified: time.Now().Unix(),
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
}
|
|
m.Index("42", []protocol.FileInfo{newFile})
|
|
|
|
if fs, _ := m.NeedFiles(); len(fs) != 1 {
|
|
t.Errorf("Model len(m.need) incorrect (%d != 1)", len(fs))
|
|
}
|
|
}
|
|
|
|
func TestRemoteUpdateOld(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
oldTimeStamp := int64(1234)
|
|
newFile := protocol.FileInfo{
|
|
Name: "foo",
|
|
Modified: oldTimeStamp,
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
}
|
|
m.Index("42", []protocol.FileInfo{newFile})
|
|
|
|
if fs, _ := m.NeedFiles(); len(fs) != 0 {
|
|
t.Errorf("Model len(need) incorrect (%d != 0)", len(fs))
|
|
}
|
|
}
|
|
|
|
func TestRemoteIndexUpdate(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
foo := protocol.FileInfo{
|
|
Name: "foo",
|
|
Modified: time.Now().Unix(),
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
}
|
|
|
|
bar := protocol.FileInfo{
|
|
Name: "bar",
|
|
Modified: time.Now().Unix(),
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
}
|
|
|
|
m.Index("42", []protocol.FileInfo{foo})
|
|
|
|
if fs, _ := m.NeedFiles(); fs[0].Name != "foo" {
|
|
t.Error("Model doesn't need 'foo'")
|
|
}
|
|
|
|
m.IndexUpdate("42", []protocol.FileInfo{bar})
|
|
|
|
if fs, _ := m.NeedFiles(); fs[0].Name != "foo" {
|
|
t.Error("Model doesn't need 'foo'")
|
|
}
|
|
if fs, _ := m.NeedFiles(); fs[1].Name != "bar" {
|
|
t.Error("Model doesn't need 'bar'")
|
|
}
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
if l1, l2 := len(m.local), len(fs); l1 != l2 {
|
|
t.Errorf("Model len(local) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if l1, l2 := len(m.global), len(fs); l1 != l2 {
|
|
t.Errorf("Model len(global) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
|
|
ot := time.Now().Unix()
|
|
newFile := File{
|
|
Name: "a new file",
|
|
Modified: ot,
|
|
Blocks: []Block{{0, 100, []byte("some hash bytes")}},
|
|
}
|
|
m.updateLocal(newFile)
|
|
|
|
if l1, l2 := len(m.local), len(fs)+1; l1 != l2 {
|
|
t.Errorf("Model len(local) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if l1, l2 := len(m.global), len(fs)+1; l1 != l2 {
|
|
t.Errorf("Model len(global) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
|
|
// The deleted file is kept in the local and global tables and marked as deleted.
|
|
|
|
m.ReplaceLocal(fs)
|
|
|
|
if l1, l2 := len(m.local), len(fs)+1; l1 != l2 {
|
|
t.Errorf("Model len(local) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if l1, l2 := len(m.global), len(fs)+1; l1 != l2 {
|
|
t.Errorf("Model len(global) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
|
|
if m.local["a new file"].Flags&(1<<12) == 0 {
|
|
t.Error("Unexpected deleted flag = 0 in local table")
|
|
}
|
|
if len(m.local["a new file"].Blocks) != 0 {
|
|
t.Error("Unexpected non-zero blocks for deleted file in local")
|
|
}
|
|
if ft := m.local["a new file"].Modified; ft != ot {
|
|
t.Errorf("Unexpected time %d != %d for deleted file in local", ft, ot+1)
|
|
}
|
|
if fv := m.local["a new file"].Version; fv != 1 {
|
|
t.Errorf("Unexpected version %d != 1 for deleted file in local", fv)
|
|
}
|
|
|
|
if m.global["a new file"].Flags&(1<<12) == 0 {
|
|
t.Error("Unexpected deleted flag = 0 in global table")
|
|
}
|
|
if len(m.global["a new file"].Blocks) != 0 {
|
|
t.Error("Unexpected non-zero blocks for deleted file in global")
|
|
}
|
|
if ft := m.global["a new file"].Modified; ft != ot {
|
|
t.Errorf("Unexpected time %d != %d for deleted file in global", ft, ot+1)
|
|
}
|
|
if fv := m.local["a new file"].Version; fv != 1 {
|
|
t.Errorf("Unexpected version %d != 1 for deleted file in global", fv)
|
|
}
|
|
|
|
// Another update should change nothing
|
|
|
|
m.ReplaceLocal(fs)
|
|
|
|
if l1, l2 := len(m.local), len(fs)+1; l1 != l2 {
|
|
t.Errorf("Model len(local) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if l1, l2 := len(m.global), len(fs)+1; l1 != l2 {
|
|
t.Errorf("Model len(global) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
|
|
if m.local["a new file"].Flags&(1<<12) == 0 {
|
|
t.Error("Unexpected deleted flag = 0 in local table")
|
|
}
|
|
if len(m.local["a new file"].Blocks) != 0 {
|
|
t.Error("Unexpected non-zero blocks for deleted file in local")
|
|
}
|
|
if ft := m.local["a new file"].Modified; ft != ot {
|
|
t.Errorf("Unexpected time %d != %d for deleted file in local", ft, ot)
|
|
}
|
|
if fv := m.local["a new file"].Version; fv != 1 {
|
|
t.Errorf("Unexpected version %d != 1 for deleted file in local", fv)
|
|
}
|
|
|
|
if m.global["a new file"].Flags&(1<<12) == 0 {
|
|
t.Error("Unexpected deleted flag = 0 in global table")
|
|
}
|
|
if len(m.global["a new file"].Blocks) != 0 {
|
|
t.Error("Unexpected non-zero blocks for deleted file in global")
|
|
}
|
|
if ft := m.global["a new file"].Modified; ft != ot {
|
|
t.Errorf("Unexpected time %d != %d for deleted file in global", ft, ot)
|
|
}
|
|
if fv := m.local["a new file"].Version; fv != 1 {
|
|
t.Errorf("Unexpected version %d != 1 for deleted file in global", fv)
|
|
}
|
|
}
|
|
|
|
func TestForgetNode(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
if l1, l2 := len(m.local), len(fs); l1 != l2 {
|
|
t.Errorf("Model len(local) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if l1, l2 := len(m.global), len(fs); l1 != l2 {
|
|
t.Errorf("Model len(global) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if fs, _ := m.NeedFiles(); len(fs) != 0 {
|
|
t.Errorf("Model len(need) incorrect (%d != 0)", len(fs))
|
|
}
|
|
|
|
newFile := protocol.FileInfo{
|
|
Name: "new file",
|
|
Modified: time.Now().Unix(),
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
}
|
|
m.Index("42", []protocol.FileInfo{newFile})
|
|
|
|
if l1, l2 := len(m.local), len(fs); l1 != l2 {
|
|
t.Errorf("Model len(local) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if l1, l2 := len(m.global), len(fs)+1; l1 != l2 {
|
|
t.Errorf("Model len(global) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if fs, _ := m.NeedFiles(); len(fs) != 1 {
|
|
t.Errorf("Model len(need) incorrect (%d != 1)", len(fs))
|
|
}
|
|
|
|
m.Close("42", nil)
|
|
|
|
if l1, l2 := len(m.local), len(fs); l1 != l2 {
|
|
t.Errorf("Model len(local) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
if l1, l2 := len(m.global), len(fs); l1 != l2 {
|
|
t.Errorf("Model len(global) incorrect (%d != %d)", l1, l2)
|
|
}
|
|
|
|
if fs, _ := m.NeedFiles(); len(fs) != 1 {
|
|
t.Errorf("Model len(need) incorrect (%d != 1)", len(fs))
|
|
}
|
|
// The file will be removed from the need list when we notice there are no nodes that can provide it
|
|
_, ok := m.fq.Get("42")
|
|
if ok {
|
|
t.Errorf("Unexpected successfull Get()")
|
|
}
|
|
if fs, _ := m.NeedFiles(); len(fs) != 0 {
|
|
t.Errorf("Model len(need) incorrect (%d != 0)", len(fs))
|
|
}
|
|
}
|
|
|
|
func TestRequest(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
bs, err := m.Request("some node", "foo", 0, 6, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if bytes.Compare(bs, []byte("foobar")) != 0 {
|
|
t.Errorf("Incorrect data from request: %q", string(bs))
|
|
}
|
|
|
|
bs, err = m.Request("some node", "../walk.go", 0, 6, nil)
|
|
if err == nil {
|
|
t.Error("Unexpected nil error on insecure file read")
|
|
}
|
|
if bs != nil {
|
|
t.Errorf("Unexpected non nil data on insecure file read: %q", string(bs))
|
|
}
|
|
}
|
|
|
|
func TestSuppression(t *testing.T) {
|
|
var testdata = []struct {
|
|
lastChange time.Time
|
|
hold int
|
|
result bool
|
|
}{
|
|
{time.Unix(0, 0), 0, false}, // First change
|
|
{time.Now().Add(-1 * time.Second), 0, true}, // Changed once one second ago, suppress
|
|
{time.Now().Add(-119 * time.Second), 0, true}, // Changed once 119 seconds ago, suppress
|
|
{time.Now().Add(-121 * time.Second), 0, false}, // Changed once 121 seconds ago, permit
|
|
|
|
{time.Now().Add(-179 * time.Second), 1, true}, // Suppressed once 179 seconds ago, suppress again
|
|
{time.Now().Add(-181 * time.Second), 1, false}, // Suppressed once 181 seconds ago, permit
|
|
|
|
{time.Now().Add(-599 * time.Second), 99, true}, // Suppressed lots of times, last allowed 599 seconds ago, suppress again
|
|
{time.Now().Add(-601 * time.Second), 99, false}, // Suppressed lots of times, last allowed 601 seconds ago, permit
|
|
}
|
|
|
|
for i, tc := range testdata {
|
|
if shouldSuppressChange(tc.lastChange, tc.hold) != tc.result {
|
|
t.Errorf("Incorrect result for test #%d: %v", i, tc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIgnoreWithUnknownFlags(t *testing.T) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
valid := protocol.FileInfo{
|
|
Name: "valid",
|
|
Modified: time.Now().Unix(),
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
Flags: protocol.FlagDeleted | 0755,
|
|
}
|
|
|
|
invalid := protocol.FileInfo{
|
|
Name: "invalid",
|
|
Modified: time.Now().Unix(),
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
Flags: 1<<27 | protocol.FlagDeleted | 0755,
|
|
}
|
|
|
|
m.Index("42", []protocol.FileInfo{valid, invalid})
|
|
|
|
if _, ok := m.global[valid.Name]; !ok {
|
|
t.Error("Model should include", valid)
|
|
}
|
|
if _, ok := m.global[invalid.Name]; ok {
|
|
t.Error("Model not should include", invalid)
|
|
}
|
|
}
|
|
|
|
func prepareModel(n int, m *Model) []protocol.FileInfo {
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
files := make([]protocol.FileInfo, n)
|
|
t := time.Now().Unix()
|
|
for i := 0; i < n; i++ {
|
|
files[i] = protocol.FileInfo{
|
|
Name: fmt.Sprintf("file%d", i),
|
|
Modified: t,
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
}
|
|
}
|
|
|
|
m.Index("42", files)
|
|
return files
|
|
}
|
|
|
|
func BenchmarkRecomputeGlobal10k(b *testing.B) {
|
|
m := NewModel("testdata")
|
|
prepareModel(10000, m)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
m.recomputeGlobal()
|
|
}
|
|
}
|
|
|
|
func BenchmarkRecomputeNeed10K(b *testing.B) {
|
|
m := NewModel("testdata")
|
|
prepareModel(10000, m)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
m.recomputeNeed()
|
|
}
|
|
}
|
|
|
|
func BenchmarkIndexUpdate10000(b *testing.B) {
|
|
m := NewModel("testdata")
|
|
files := prepareModel(10000, m)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
m.IndexUpdate("42", files)
|
|
}
|
|
}
|
|
|
|
type FakeConnection struct {
|
|
id string
|
|
requestData []byte
|
|
}
|
|
|
|
func (FakeConnection) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (f FakeConnection) ID() string {
|
|
return string(f.id)
|
|
}
|
|
|
|
func (FakeConnection) Index([]protocol.FileInfo) {}
|
|
|
|
func (f FakeConnection) Request(name string, offset int64, size uint32, hash []byte) ([]byte, error) {
|
|
return f.requestData, nil
|
|
}
|
|
|
|
func (FakeConnection) Ping() bool {
|
|
return true
|
|
}
|
|
|
|
func (FakeConnection) Statistics() protocol.Statistics {
|
|
return protocol.Statistics{}
|
|
}
|
|
|
|
func BenchmarkRequest(b *testing.B) {
|
|
m := NewModel("testdata")
|
|
fs, _ := m.Walk(false)
|
|
m.ReplaceLocal(fs)
|
|
|
|
const n = 1000
|
|
files := make([]protocol.FileInfo, n)
|
|
t := time.Now().Unix()
|
|
for i := 0; i < n; i++ {
|
|
files[i] = protocol.FileInfo{
|
|
Name: fmt.Sprintf("file%d", i),
|
|
Modified: t,
|
|
Blocks: []protocol.BlockInfo{{100, []byte("some hash bytes")}},
|
|
}
|
|
}
|
|
|
|
fc := FakeConnection{
|
|
id: "42",
|
|
requestData: []byte("some data to return"),
|
|
}
|
|
m.AddConnection(fc, fc)
|
|
m.Index("42", files)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
data, err := m.requestGlobal("42", files[i%n].Name, 0, 32, nil)
|
|
if err != nil {
|
|
b.Error(err)
|
|
}
|
|
if data == nil {
|
|
b.Error("nil data")
|
|
}
|
|
}
|
|
}
|