mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-09 23:00:58 +00:00
6d3f9d5154
When scanner.Walk detects a change, it now returns the new file info as well as the old file info. It also finds deleted and ignored files while scanning. Also directory deletions are now always committed to db after their children to prevent temporary failure on remote due to non-empty directory.
429 lines
10 KiB
Go
429 lines
10 KiB
Go
// Copyright (C) 2014 The Protocol Authors.
|
|
|
|
package protocol
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"strings"
|
|
"testing"
|
|
"testing/quick"
|
|
"time"
|
|
|
|
"encoding/hex"
|
|
|
|
"github.com/syncthing/syncthing/lib/rand"
|
|
)
|
|
|
|
var (
|
|
c0ID = NewDeviceID([]byte{1})
|
|
c1ID = NewDeviceID([]byte{2})
|
|
quickCfg = &quick.Config{}
|
|
)
|
|
|
|
const (
|
|
fileSize = 1 << 40
|
|
)
|
|
|
|
func TestPing(t *testing.T) {
|
|
ar, aw := io.Pipe()
|
|
br, bw := io.Pipe()
|
|
|
|
c0 := NewConnection(c0ID, ar, bw, newTestModel(), "name", CompressAlways).(wireFormatConnection).Connection.(*rawConnection)
|
|
c0.Start()
|
|
c1 := NewConnection(c1ID, br, aw, newTestModel(), "name", CompressAlways).(wireFormatConnection).Connection.(*rawConnection)
|
|
c1.Start()
|
|
c0.ClusterConfig(ClusterConfig{})
|
|
c1.ClusterConfig(ClusterConfig{})
|
|
|
|
if ok := c0.ping(); !ok {
|
|
t.Error("c0 ping failed")
|
|
}
|
|
if ok := c1.ping(); !ok {
|
|
t.Error("c1 ping failed")
|
|
}
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
m0 := newTestModel()
|
|
m1 := newTestModel()
|
|
|
|
ar, aw := io.Pipe()
|
|
br, bw := io.Pipe()
|
|
|
|
c0 := NewConnection(c0ID, ar, bw, m0, "name", CompressAlways).(wireFormatConnection).Connection.(*rawConnection)
|
|
c0.Start()
|
|
c1 := NewConnection(c1ID, br, aw, m1, "name", CompressAlways)
|
|
c1.Start()
|
|
c0.ClusterConfig(ClusterConfig{})
|
|
c1.ClusterConfig(ClusterConfig{})
|
|
|
|
c0.close(errors.New("manual close"))
|
|
|
|
<-c0.closed
|
|
if err := m0.closedError(); err == nil || !strings.Contains(err.Error(), "manual close") {
|
|
t.Fatal("Connection should be closed")
|
|
}
|
|
|
|
// None of these should panic, some should return an error
|
|
|
|
if c0.ping() {
|
|
t.Error("Ping should not return true")
|
|
}
|
|
|
|
c0.Index("default", nil)
|
|
c0.Index("default", nil)
|
|
|
|
if _, err := c0.Request("default", "foo", 0, 0, nil, false); err == nil {
|
|
t.Error("Request should return an error")
|
|
}
|
|
}
|
|
|
|
func TestMarshalIndexMessage(t *testing.T) {
|
|
if testing.Short() {
|
|
quickCfg.MaxCount = 10
|
|
}
|
|
|
|
f := func(m1 Index) bool {
|
|
if len(m1.Files) == 0 {
|
|
m1.Files = nil
|
|
}
|
|
for i, f := range m1.Files {
|
|
if len(f.Blocks) == 0 {
|
|
m1.Files[i].Blocks = nil
|
|
} else {
|
|
for j := range f.Blocks {
|
|
f.Blocks[j].Offset = 0
|
|
if len(f.Blocks[j].Hash) == 0 {
|
|
f.Blocks[j].Hash = nil
|
|
}
|
|
}
|
|
}
|
|
if len(f.Version.Counters) == 0 {
|
|
m1.Files[i].Version.Counters = nil
|
|
}
|
|
}
|
|
|
|
return testMarshal(t, "index", &m1, &Index{})
|
|
}
|
|
|
|
if err := quick.Check(f, quickCfg); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestMarshalRequestMessage(t *testing.T) {
|
|
if testing.Short() {
|
|
quickCfg.MaxCount = 10
|
|
}
|
|
|
|
f := func(m1 Request) bool {
|
|
if len(m1.Hash) == 0 {
|
|
m1.Hash = nil
|
|
}
|
|
return testMarshal(t, "request", &m1, &Request{})
|
|
}
|
|
|
|
if err := quick.Check(f, quickCfg); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestMarshalResponseMessage(t *testing.T) {
|
|
if testing.Short() {
|
|
quickCfg.MaxCount = 10
|
|
}
|
|
|
|
f := func(m1 Response) bool {
|
|
if len(m1.Data) == 0 {
|
|
m1.Data = nil
|
|
}
|
|
return testMarshal(t, "response", &m1, &Response{})
|
|
}
|
|
|
|
if err := quick.Check(f, quickCfg); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestMarshalClusterConfigMessage(t *testing.T) {
|
|
if testing.Short() {
|
|
quickCfg.MaxCount = 10
|
|
}
|
|
|
|
f := func(m1 ClusterConfig) bool {
|
|
if len(m1.Folders) == 0 {
|
|
m1.Folders = nil
|
|
}
|
|
for i := range m1.Folders {
|
|
if len(m1.Folders[i].Devices) == 0 {
|
|
m1.Folders[i].Devices = nil
|
|
}
|
|
}
|
|
return testMarshal(t, "clusterconfig", &m1, &ClusterConfig{})
|
|
}
|
|
|
|
if err := quick.Check(f, quickCfg); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestMarshalCloseMessage(t *testing.T) {
|
|
if testing.Short() {
|
|
quickCfg.MaxCount = 10
|
|
}
|
|
|
|
f := func(m1 Close) bool {
|
|
return testMarshal(t, "close", &m1, &Close{})
|
|
}
|
|
|
|
if err := quick.Check(f, quickCfg); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestMarshalFDPU(t *testing.T) {
|
|
if testing.Short() {
|
|
quickCfg.MaxCount = 10
|
|
}
|
|
|
|
f := func(m1 FileDownloadProgressUpdate) bool {
|
|
if len(m1.Version.Counters) == 0 {
|
|
m1.Version.Counters = nil
|
|
}
|
|
return testMarshal(t, "close", &m1, &FileDownloadProgressUpdate{})
|
|
}
|
|
|
|
if err := quick.Check(f, quickCfg); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalFDPUv16v17(t *testing.T) {
|
|
var fdpu FileDownloadProgressUpdate
|
|
|
|
m0, _ := hex.DecodeString("08cda1e2e3011278f3918787f3b89b8af2958887f0aa9389f3a08588f3aa8f96f39aa8a5f48b9188f19286a0f3848da4f3aba799f3beb489f0a285b9f487b684f2a3bda2f48598b4f2938a89f2a28badf187a0a2f2aebdbdf4849494f4808fbbf2b3a2adf2bb95bff0a6ada4f198ab9af29a9c8bf1abb793f3baabb2f188a6ba1a0020bb9390f60220f6d9e42220b0c7e2b2fdffffffff0120fdb2dfcdfbffffffff0120cedab1d50120bd8784c0feffffffff0120ace99591fdffffffff0120eed7d09af9ffffffff01")
|
|
if err := fdpu.Unmarshal(m0); err != nil {
|
|
t.Fatal("Unmarshalling message from v0.14.16:", err)
|
|
}
|
|
|
|
m1, _ := hex.DecodeString("0880f1969905128401f099b192f0abb1b9f3b280aff19e9aa2f3b89e84f484b39df1a7a6b0f1aea4b1f0adac94f3b39caaf1939281f1928a8af0abb1b0f0a8b3b3f3a88e94f2bd85acf29c97a9f2969da6f0b7a188f1908ea2f09a9c9bf19d86a6f29aada8f389bb95f0bf9d88f1a09d89f1b1a4b5f29b9eabf298a59df1b2a589f2979ebdf0b69880f18986b21a440a1508c7d8fb8897ca93d90910e8c4d8e8f2f8f0ccee010a1508afa8ffd8c085b393c50110e5bdedc3bddefe9b0b0a1408a1bedddba4cac5da3c10b8e5d9958ca7e3ec19225ae2f88cb2f8ffffffff018ceda99cfbffffffff01b9c298a407e295e8e9fcffffffff01f3b9ade5fcffffffff01c08bfea9fdffffffff01a2c2e5e1ffffffffff0186dcc5dafdffffffff01e9ffc7e507c9d89db8fdffffffff01")
|
|
if err := fdpu.Unmarshal(m1); err != nil {
|
|
t.Fatal("Unmarshalling message from v0.14.16:", err)
|
|
}
|
|
}
|
|
|
|
func testMarshal(t *testing.T, prefix string, m1, m2 message) bool {
|
|
buf, err := m1.Marshal()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err = m2.Unmarshal(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
bs1, _ := json.MarshalIndent(m1, "", " ")
|
|
bs2, _ := json.MarshalIndent(m2, "", " ")
|
|
if !bytes.Equal(bs1, bs2) {
|
|
ioutil.WriteFile(prefix+"-1.txt", bs1, 0644)
|
|
ioutil.WriteFile(prefix+"-2.txt", bs2, 0644)
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func TestMarshalledIndexMessageSize(t *testing.T) {
|
|
// We should be able to handle a 1 TiB file without
|
|
// blowing the default max message size.
|
|
|
|
if testing.Short() {
|
|
t.Skip("this test requires a lot of memory")
|
|
return
|
|
}
|
|
|
|
f := FileInfo{
|
|
Name: "a normal length file name withoout any weird stuff.txt",
|
|
Type: FileInfoTypeFile,
|
|
Size: fileSize,
|
|
Permissions: 0666,
|
|
ModifiedS: time.Now().Unix(),
|
|
Version: Vector{Counters: []Counter{{ID: 1 << 60, Value: 1}, {ID: 2 << 60, Value: 1}}},
|
|
Blocks: make([]BlockInfo, fileSize/BlockSize),
|
|
}
|
|
|
|
for i := 0; i < fileSize/BlockSize; i++ {
|
|
f.Blocks[i].Offset = int64(i) * BlockSize
|
|
f.Blocks[i].Size = BlockSize
|
|
f.Blocks[i].Hash = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 20, 1, 2, 3, 4, 5, 6, 7, 8, 9, 30, 1, 2}
|
|
}
|
|
|
|
idx := Index{
|
|
Folder: "some folder ID",
|
|
Files: []FileInfo{f},
|
|
}
|
|
|
|
msgSize := idx.ProtoSize()
|
|
if msgSize > MaxMessageLen {
|
|
t.Errorf("Message size %d bytes is larger than max %d", msgSize, MaxMessageLen)
|
|
}
|
|
}
|
|
|
|
func TestLZ4Compression(t *testing.T) {
|
|
c := new(rawConnection)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
dataLen := 150 + rand.Intn(150)
|
|
data := make([]byte, dataLen)
|
|
_, err := io.ReadFull(rand.Reader, data[100:])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
comp, err := c.lz4Compress(data)
|
|
if err != nil {
|
|
t.Errorf("compressing %d bytes: %v", dataLen, err)
|
|
continue
|
|
}
|
|
|
|
res, err := c.lz4Decompress(comp)
|
|
if err != nil {
|
|
t.Errorf("decompressing %d bytes to %d: %v", len(comp), dataLen, err)
|
|
continue
|
|
}
|
|
if len(res) != len(data) {
|
|
t.Errorf("Incorrect len %d != expected %d", len(res), len(data))
|
|
}
|
|
if !bytes.Equal(data, res) {
|
|
t.Error("Incorrect decompressed data")
|
|
}
|
|
t.Logf("OK #%d, %d -> %d -> %d", i, dataLen, len(comp), dataLen)
|
|
}
|
|
}
|
|
|
|
func TestCheckFilename(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
ok bool
|
|
}{
|
|
// Valid filenames
|
|
{"foo", true},
|
|
{"foo/bar/baz", true},
|
|
{"foo/bar:baz", true}, // colon is ok in general, will be filtered on windows
|
|
{`\`, true}, // path separator on the wire is forward slash, so as above
|
|
{`\.`, true},
|
|
{`\..`, true},
|
|
{".foo", true},
|
|
{"foo..", true},
|
|
|
|
// Invalid filenames
|
|
{"foo/..", false},
|
|
{"foo/../bar", false},
|
|
{"../foo/../bar", false},
|
|
{"", false},
|
|
{".", false},
|
|
{"..", false},
|
|
{"/", false},
|
|
{"/.", false},
|
|
{"/..", false},
|
|
{"/foo", false},
|
|
{"./foo", false},
|
|
{"foo./", false},
|
|
{"foo/.", false},
|
|
{"foo/", false},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
err := checkFilename(tc.name)
|
|
if (err == nil) != tc.ok {
|
|
t.Errorf("Unexpected result for checkFilename(%q): %v", tc.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCheckConsistency(t *testing.T) {
|
|
cases := []struct {
|
|
fi FileInfo
|
|
ok bool
|
|
}{
|
|
{
|
|
// valid
|
|
fi: FileInfo{
|
|
Name: "foo",
|
|
Type: FileInfoTypeFile,
|
|
Blocks: []BlockInfo{{Size: 1234, Offset: 0, Hash: []byte{1, 2, 3, 4}}},
|
|
},
|
|
ok: true,
|
|
},
|
|
{
|
|
// deleted with blocks
|
|
fi: FileInfo{
|
|
Name: "foo",
|
|
Deleted: true,
|
|
Type: FileInfoTypeFile,
|
|
Blocks: []BlockInfo{{Size: 1234, Offset: 0, Hash: []byte{1, 2, 3, 4}}},
|
|
},
|
|
ok: false,
|
|
},
|
|
{
|
|
// no blocks
|
|
fi: FileInfo{
|
|
Name: "foo",
|
|
Type: FileInfoTypeFile,
|
|
},
|
|
ok: false,
|
|
},
|
|
{
|
|
// directory with blocks
|
|
fi: FileInfo{
|
|
Name: "foo",
|
|
Type: FileInfoTypeDirectory,
|
|
Blocks: []BlockInfo{{Size: 1234, Offset: 0, Hash: []byte{1, 2, 3, 4}}},
|
|
},
|
|
ok: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
err := checkFileInfoConsistency(tc.fi)
|
|
if tc.ok && err != nil {
|
|
t.Errorf("Unexpected error %v (want nil) for %v", err, tc.fi)
|
|
}
|
|
if !tc.ok && err == nil {
|
|
t.Errorf("Unexpected nil error for %v", tc.fi)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCopyFileInfo(t *testing.T) {
|
|
f := FileInfo{
|
|
Name: "a normal length file name withoout any weird stuff.txt",
|
|
Type: FileInfoTypeFile,
|
|
Size: fileSize,
|
|
Permissions: 0666,
|
|
ModifiedS: time.Now().Unix(),
|
|
Version: Vector{Counters: []Counter{{ID: 1 << 60, Value: 1}, {ID: 2 << 60, Value: 1}}},
|
|
Blocks: make([]BlockInfo, fileSize/BlockSize),
|
|
}
|
|
|
|
del := f.DeletedCopy(LocalDeviceID.Short())
|
|
if f.Deleted {
|
|
t.Errorf("Source file info was deleted on copy")
|
|
}
|
|
if !del.Deleted {
|
|
t.Errorf("Returned file info was not deleted on copy")
|
|
}
|
|
|
|
inv := f.InvalidatedCopy(LocalDeviceID.Short())
|
|
if f.Invalid {
|
|
t.Errorf("Source file info was invalid on copy")
|
|
}
|
|
if !inv.Invalid {
|
|
t.Errorf("Returned file info was not invalid on copy")
|
|
}
|
|
}
|