mirror of
https://github.com/syncthing/syncthing.git
synced 2024-11-16 18:41:59 -07:00
f822b10550
Adds a receive only folder type that does not send changes, and where the user can optionally revert local changes. Also changes some of the icons to make the three folder types distinguishable.
613 lines
14 KiB
Go
613 lines
14 KiB
Go
// Copyright (C) 2014 The Protocol Authors.
|
|
|
|
package protocol
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"testing/quick"
|
|
|
|
"github.com/syncthing/syncthing/lib/rand"
|
|
)
|
|
|
|
var (
|
|
c0ID = NewDeviceID([]byte{1})
|
|
c1ID = NewDeviceID([]byte{2})
|
|
quickCfg = &quick.Config{}
|
|
)
|
|
|
|
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, 0, 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 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 TestBlockSize(t *testing.T) {
|
|
cases := []struct {
|
|
fileSize int64
|
|
blockSize int
|
|
}{
|
|
{1 << KiB, 128 << KiB},
|
|
{1 << MiB, 128 << KiB},
|
|
{499 << MiB, 256 << KiB},
|
|
{500 << MiB, 512 << KiB},
|
|
{501 << MiB, 512 << KiB},
|
|
{1 << GiB, 1 << MiB},
|
|
{2 << GiB, 2 << MiB},
|
|
{3 << GiB, 2 << MiB},
|
|
{500 << GiB, 16 << MiB},
|
|
{50000 << GiB, 16 << MiB},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
size := BlockSize(tc.fileSize)
|
|
if size != tc.blockSize {
|
|
t.Errorf("BlockSize(%d), size=%d, expected %d", tc.fileSize, size, tc.blockSize)
|
|
}
|
|
}
|
|
}
|
|
|
|
var blockSize int
|
|
|
|
func BenchmarkBlockSize(b *testing.B) {
|
|
for i := 0; i < b.N; i++ {
|
|
blockSize = BlockSize(16 << 30)
|
|
}
|
|
}
|
|
|
|
func TestLocalFlagBits(t *testing.T) {
|
|
var f FileInfo
|
|
if f.IsIgnored() || f.MustRescan() || f.IsInvalid() {
|
|
t.Error("file should have no weird bits set by default")
|
|
}
|
|
|
|
f.SetIgnored(42)
|
|
if !f.IsIgnored() || f.MustRescan() || !f.IsInvalid() {
|
|
t.Error("file should be ignored and invalid")
|
|
}
|
|
|
|
f.SetMustRescan(42)
|
|
if f.IsIgnored() || !f.MustRescan() || !f.IsInvalid() {
|
|
t.Error("file should be must-rescan and invalid")
|
|
}
|
|
|
|
f.SetUnsupported(42)
|
|
if f.IsIgnored() || f.MustRescan() || !f.IsInvalid() {
|
|
t.Error("file should be invalid")
|
|
}
|
|
}
|
|
|
|
func TestIsEquivalent(t *testing.T) {
|
|
b := func(v bool) *bool {
|
|
return &v
|
|
}
|
|
|
|
type testCase struct {
|
|
a FileInfo
|
|
b FileInfo
|
|
ignPerms *bool // nil means should not matter, we'll test both variants
|
|
ignBlocks *bool
|
|
ignFlags uint32
|
|
eq bool
|
|
}
|
|
cases := []testCase{
|
|
// Empty FileInfos are equivalent
|
|
{eq: true},
|
|
|
|
// Various basic attributes, all of which cause ineqality when
|
|
// they differ
|
|
{
|
|
a: FileInfo{Name: "foo"},
|
|
b: FileInfo{Name: "bar"},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{Type: FileInfoTypeFile},
|
|
b: FileInfo{Type: FileInfoTypeDirectory},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{Size: 1234},
|
|
b: FileInfo{Size: 2345},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{Deleted: false},
|
|
b: FileInfo{Deleted: true},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{RawInvalid: false},
|
|
b: FileInfo{RawInvalid: true},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{ModifiedS: 1234},
|
|
b: FileInfo{ModifiedS: 2345},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{ModifiedNs: 1234},
|
|
b: FileInfo{ModifiedNs: 2345},
|
|
eq: false,
|
|
},
|
|
|
|
// Special handling of local flags and invalidity. "MustRescan"
|
|
// files are never equivalent to each other. Otherwise, equivalence
|
|
// is based just on whether the file becomes IsInvalid() or not, not
|
|
// the specific reason or flag bits.
|
|
{
|
|
a: FileInfo{LocalFlags: FlagLocalMustRescan},
|
|
b: FileInfo{LocalFlags: FlagLocalMustRescan},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{RawInvalid: true},
|
|
b: FileInfo{RawInvalid: true},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: FlagLocalUnsupported},
|
|
b: FileInfo{LocalFlags: FlagLocalUnsupported},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{RawInvalid: true},
|
|
b: FileInfo{LocalFlags: FlagLocalUnsupported},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: 0},
|
|
b: FileInfo{LocalFlags: FlagLocalReceiveOnly},
|
|
eq: false,
|
|
},
|
|
{
|
|
a: FileInfo{LocalFlags: 0},
|
|
b: FileInfo{LocalFlags: FlagLocalReceiveOnly},
|
|
ignFlags: FlagLocalReceiveOnly,
|
|
eq: true,
|
|
},
|
|
|
|
// Difference in blocks is not OK
|
|
{
|
|
a: FileInfo{Blocks: []BlockInfo{{Hash: []byte{1, 2, 3, 4}}}},
|
|
b: FileInfo{Blocks: []BlockInfo{{Hash: []byte{2, 3, 4, 5}}}},
|
|
ignBlocks: b(false),
|
|
eq: false,
|
|
},
|
|
|
|
// ... unless we say it is
|
|
{
|
|
a: FileInfo{Blocks: []BlockInfo{{Hash: []byte{1, 2, 3, 4}}}},
|
|
b: FileInfo{Blocks: []BlockInfo{{Hash: []byte{2, 3, 4, 5}}}},
|
|
ignBlocks: b(true),
|
|
eq: true,
|
|
},
|
|
|
|
// Difference in permissions is not OK.
|
|
{
|
|
a: FileInfo{Permissions: 0444},
|
|
b: FileInfo{Permissions: 0666},
|
|
ignPerms: b(false),
|
|
eq: false,
|
|
},
|
|
|
|
// ... unless we say it is
|
|
{
|
|
a: FileInfo{Permissions: 0666},
|
|
b: FileInfo{Permissions: 0444},
|
|
ignPerms: b(true),
|
|
eq: true,
|
|
},
|
|
|
|
// These attributes are not checked at all
|
|
{
|
|
a: FileInfo{NoPermissions: false},
|
|
b: FileInfo{NoPermissions: true},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{Version: Vector{Counters: []Counter{{ID: 1, Value: 42}}}},
|
|
b: FileInfo{Version: Vector{Counters: []Counter{{ID: 42, Value: 1}}}},
|
|
eq: true,
|
|
},
|
|
{
|
|
a: FileInfo{Sequence: 1},
|
|
b: FileInfo{Sequence: 2},
|
|
eq: true,
|
|
},
|
|
|
|
// The block size is not checked (but this would fail the blocks
|
|
// check in real world)
|
|
{
|
|
a: FileInfo{RawBlockSize: 1},
|
|
b: FileInfo{RawBlockSize: 2},
|
|
eq: true,
|
|
},
|
|
|
|
// The symlink target is checked for symlinks
|
|
{
|
|
a: FileInfo{Type: FileInfoTypeSymlink, SymlinkTarget: "a"},
|
|
b: FileInfo{Type: FileInfoTypeSymlink, SymlinkTarget: "b"},
|
|
eq: false,
|
|
},
|
|
|
|
// ... but not for non-symlinks
|
|
{
|
|
a: FileInfo{Type: FileInfoTypeFile, SymlinkTarget: "a"},
|
|
b: FileInfo{Type: FileInfoTypeFile, SymlinkTarget: "b"},
|
|
eq: true,
|
|
},
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
// On windows we only check the user writable bit of the permission
|
|
// set, so these are equivalent.
|
|
cases = append(cases, testCase{
|
|
a: FileInfo{Permissions: 0777},
|
|
b: FileInfo{Permissions: 0600},
|
|
ignPerms: b(false),
|
|
eq: true,
|
|
})
|
|
}
|
|
|
|
for i, tc := range cases {
|
|
// Check the standard attributes with all permutations of the
|
|
// special ignore flags, unless the value of those flags are given
|
|
// in the tests.
|
|
for _, ignPerms := range []bool{true, false} {
|
|
for _, ignBlocks := range []bool{true, false} {
|
|
if tc.ignPerms != nil && *tc.ignPerms != ignPerms {
|
|
continue
|
|
}
|
|
if tc.ignBlocks != nil && *tc.ignBlocks != ignBlocks {
|
|
continue
|
|
}
|
|
|
|
if res := tc.a.isEquivalent(tc.b, ignPerms, ignBlocks, tc.ignFlags); res != tc.eq {
|
|
t.Errorf("Case %d:\na: %v\nb: %v\na.IsEquivalent(b, %v, %v) => %v, expected %v", i, tc.a, tc.b, ignPerms, ignBlocks, res, tc.eq)
|
|
}
|
|
if res := tc.b.isEquivalent(tc.a, ignPerms, ignBlocks, tc.ignFlags); res != tc.eq {
|
|
t.Errorf("Case %d:\na: %v\nb: %v\nb.IsEquivalent(a, %v, %v) => %v, expected %v", i, tc.a, tc.b, ignPerms, ignBlocks, res, tc.eq)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|