1// Copyright (C) 2016 The Syncthing Authors. 2// 3// This Source Code Form is subject to the terms of the Mozilla Public 4// License, v. 2.0. If a copy of the MPL was not distributed with this file, 5// You can obtain one at https://mozilla.org/MPL/2.0/. 6 7package model 8 9import ( 10 "context" 11 "io/ioutil" 12 "os" 13 "testing" 14 "time" 15 16 "github.com/syncthing/syncthing/lib/config" 17 "github.com/syncthing/syncthing/lib/db" 18 "github.com/syncthing/syncthing/lib/db/backend" 19 "github.com/syncthing/syncthing/lib/events" 20 "github.com/syncthing/syncthing/lib/fs" 21 "github.com/syncthing/syncthing/lib/ignore" 22 "github.com/syncthing/syncthing/lib/protocol" 23 "github.com/syncthing/syncthing/lib/rand" 24) 25 26var ( 27 myID, device1, device2 protocol.DeviceID 28 defaultCfgWrapper config.Wrapper 29 defaultCfgWrapperCancel context.CancelFunc 30 defaultFolderConfig config.FolderConfiguration 31 defaultFs fs.Filesystem 32 defaultCfg config.Configuration 33 defaultAutoAcceptCfg config.Configuration 34) 35 36func init() { 37 myID, _ = protocol.DeviceIDFromString("ZNWFSWE-RWRV2BD-45BLMCV-LTDE2UR-4LJDW6J-R5BPWEB-TXD27XJ-IZF5RA4") 38 device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR") 39 device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY") 40 41 defaultCfgWrapper, defaultCfgWrapperCancel = createTmpWrapper(config.New(myID)) 42 43 defaultFolderConfig = testFolderConfig("testdata") 44 defaultFs = defaultFolderConfig.Filesystem() 45 46 waiter, _ := defaultCfgWrapper.Modify(func(cfg *config.Configuration) { 47 cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device1, "device1")) 48 cfg.SetFolder(defaultFolderConfig) 49 cfg.Options.KeepTemporariesH = 1 50 }) 51 waiter.Wait() 52 53 defaultCfg = defaultCfgWrapper.RawCopy() 54 55 defaultAutoAcceptCfg = config.Configuration{ 56 Version: config.CurrentVersion, 57 Devices: []config.DeviceConfiguration{ 58 { 59 DeviceID: myID, // self 60 }, 61 { 62 DeviceID: device1, 63 AutoAcceptFolders: true, 64 }, 65 { 66 DeviceID: device2, 67 AutoAcceptFolders: true, 68 }, 69 }, 70 Defaults: config.Defaults{ 71 Folder: config.FolderConfiguration{ 72 Path: ".", 73 }, 74 }, 75 } 76} 77 78func createTmpWrapper(cfg config.Configuration) (config.Wrapper, context.CancelFunc) { 79 tmpFile, err := ioutil.TempFile("", "syncthing-testConfig-") 80 if err != nil { 81 panic(err) 82 } 83 wrapper := config.Wrap(tmpFile.Name(), cfg, myID, events.NoopLogger) 84 tmpFile.Close() 85 ctx, cancel := context.WithCancel(context.Background()) 86 go wrapper.Serve(ctx) 87 return wrapper, cancel 88} 89 90func tmpDefaultWrapper() (config.Wrapper, config.FolderConfiguration, context.CancelFunc) { 91 w, cancel := createTmpWrapper(defaultCfgWrapper.RawCopy()) 92 fcfg := testFolderConfigTmp() 93 _, _ = w.Modify(func(cfg *config.Configuration) { 94 cfg.SetFolder(fcfg) 95 }) 96 return w, fcfg, cancel 97} 98 99func testFolderConfigTmp() config.FolderConfiguration { 100 tmpDir := createTmpDir() 101 return testFolderConfig(tmpDir) 102} 103 104func testFolderConfig(path string) config.FolderConfiguration { 105 cfg := newFolderConfiguration(defaultCfgWrapper, "default", "default", fs.FilesystemTypeBasic, path) 106 cfg.FSWatcherEnabled = false 107 cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{DeviceID: device1}) 108 return cfg 109} 110 111func testFolderConfigFake() config.FolderConfiguration { 112 cfg := newFolderConfiguration(defaultCfgWrapper, "default", "default", fs.FilesystemTypeFake, rand.String(32)+"?content=true") 113 cfg.FSWatcherEnabled = false 114 cfg.Devices = append(cfg.Devices, config.FolderDeviceConfiguration{DeviceID: device1}) 115 return cfg 116} 117 118func setupModelWithConnection(t testing.TB) (*testModel, *fakeConnection, config.FolderConfiguration, context.CancelFunc) { 119 t.Helper() 120 w, fcfg, cancel := tmpDefaultWrapper() 121 m, fc := setupModelWithConnectionFromWrapper(t, w) 122 return m, fc, fcfg, cancel 123} 124 125func setupModelWithConnectionFromWrapper(t testing.TB, w config.Wrapper) (*testModel, *fakeConnection) { 126 t.Helper() 127 m := setupModel(t, w) 128 129 fc := addFakeConn(m, device1, "default") 130 fc.folder = "default" 131 132 _ = m.ScanFolder("default") 133 134 return m, fc 135} 136 137func setupModel(t testing.TB, w config.Wrapper) *testModel { 138 t.Helper() 139 m := newModel(t, w, myID, "syncthing", "dev", nil) 140 m.ServeBackground() 141 <-m.started 142 143 m.ScanFolders() 144 145 return m 146} 147 148type testModel struct { 149 *model 150 t testing.TB 151 cancel context.CancelFunc 152 evCancel context.CancelFunc 153 stopped chan struct{} 154} 155 156func newModel(t testing.TB, cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, protectedFiles []string) *testModel { 157 t.Helper() 158 evLogger := events.NewLogger() 159 ldb, err := db.NewLowlevel(backend.OpenMemory(), evLogger) 160 if err != nil { 161 t.Fatal(err) 162 } 163 m := NewModel(cfg, id, clientName, clientVersion, ldb, protectedFiles, evLogger).(*model) 164 ctx, cancel := context.WithCancel(context.Background()) 165 go evLogger.Serve(ctx) 166 return &testModel{ 167 model: m, 168 evCancel: cancel, 169 stopped: make(chan struct{}), 170 t: t, 171 } 172} 173 174func (m *testModel) ServeBackground() { 175 ctx, cancel := context.WithCancel(context.Background()) 176 m.cancel = cancel 177 go func() { 178 m.model.Serve(ctx) 179 close(m.stopped) 180 }() 181 <-m.started 182} 183 184func (m *testModel) testAvailability(folder string, file protocol.FileInfo, block protocol.BlockInfo) []Availability { 185 av, err := m.model.Availability(folder, file, block) 186 must(m.t, err) 187 return av 188} 189 190func (m *testModel) testCurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) { 191 f, ok, err := m.model.CurrentFolderFile(folder, file) 192 must(m.t, err) 193 return f, ok 194} 195 196func (m *testModel) testCompletion(device protocol.DeviceID, folder string) FolderCompletion { 197 comp, err := m.Completion(device, folder) 198 must(m.t, err) 199 return comp 200} 201 202func cleanupModel(m *testModel) { 203 if m.cancel != nil { 204 m.cancel() 205 <-m.stopped 206 } 207 m.evCancel() 208 m.db.Close() 209 os.Remove(m.cfg.ConfigPath()) 210} 211 212func cleanupModelAndRemoveDir(m *testModel, dir string) { 213 cleanupModel(m) 214 os.RemoveAll(dir) 215} 216 217func createTmpDir() string { 218 tmpDir, err := ioutil.TempDir("", "syncthing_testFolder-") 219 if err != nil { 220 panic("Failed to create temporary testing dir") 221 } 222 return tmpDir 223} 224 225type alwaysChangedKey struct { 226 fs fs.Filesystem 227 name string 228} 229 230// alwaysChanges is an ignore.ChangeDetector that always returns true on Changed() 231type alwaysChanged struct { 232 seen map[alwaysChangedKey]struct{} 233} 234 235func newAlwaysChanged() *alwaysChanged { 236 return &alwaysChanged{ 237 seen: make(map[alwaysChangedKey]struct{}), 238 } 239} 240 241func (c *alwaysChanged) Remember(fs fs.Filesystem, name string, _ time.Time) { 242 c.seen[alwaysChangedKey{fs, name}] = struct{}{} 243} 244 245func (c *alwaysChanged) Reset() { 246 c.seen = make(map[alwaysChangedKey]struct{}) 247} 248 249func (c *alwaysChanged) Seen(fs fs.Filesystem, name string) bool { 250 _, ok := c.seen[alwaysChangedKey{fs, name}] 251 return ok 252} 253 254func (c *alwaysChanged) Changed() bool { 255 return true 256} 257 258func localSize(t *testing.T, m Model, folder string) db.Counts { 259 t.Helper() 260 snap := dbSnapshot(t, m, folder) 261 defer snap.Release() 262 return snap.LocalSize() 263} 264 265func globalSize(t *testing.T, m Model, folder string) db.Counts { 266 t.Helper() 267 snap := dbSnapshot(t, m, folder) 268 defer snap.Release() 269 return snap.GlobalSize() 270} 271 272func receiveOnlyChangedSize(t *testing.T, m Model, folder string) db.Counts { 273 t.Helper() 274 snap := dbSnapshot(t, m, folder) 275 defer snap.Release() 276 return snap.ReceiveOnlyChangedSize() 277} 278 279func needSizeLocal(t *testing.T, m Model, folder string) db.Counts { 280 t.Helper() 281 snap := dbSnapshot(t, m, folder) 282 defer snap.Release() 283 return snap.NeedSize(protocol.LocalDeviceID) 284} 285 286func dbSnapshot(t *testing.T, m Model, folder string) *db.Snapshot { 287 t.Helper() 288 snap, err := m.DBSnapshot(folder) 289 if err != nil { 290 t.Fatal(err) 291 } 292 return snap 293} 294 295func fsetSnapshot(t *testing.T, fset *db.FileSet) *db.Snapshot { 296 t.Helper() 297 snap, err := fset.Snapshot() 298 if err != nil { 299 t.Fatal(err) 300 } 301 return snap 302} 303 304// Reach in and update the ignore matcher to one that always does 305// reloads when asked to, instead of checking file mtimes. This is 306// because we will be changing the files on disk often enough that the 307// mtimes will be unreliable to determine change status. 308func folderIgnoresAlwaysReload(t testing.TB, m *testModel, fcfg config.FolderConfiguration) { 309 t.Helper() 310 m.removeFolder(fcfg) 311 fset := newFileSet(t, fcfg.ID, fcfg.Filesystem(), m.db) 312 ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged())) 313 m.fmut.Lock() 314 m.addAndStartFolderLockedWithIgnores(fcfg, fset, ignores) 315 m.fmut.Unlock() 316} 317 318func basicClusterConfig(local, remote protocol.DeviceID, folders ...string) protocol.ClusterConfig { 319 var cc protocol.ClusterConfig 320 for _, folder := range folders { 321 cc.Folders = append(cc.Folders, protocol.Folder{ 322 ID: folder, 323 Devices: []protocol.Device{ 324 { 325 ID: local, 326 }, 327 { 328 ID: remote, 329 }, 330 }, 331 }) 332 } 333 return cc 334} 335 336func localIndexUpdate(m *testModel, folder string, fs []protocol.FileInfo) { 337 m.fmut.RLock() 338 fset := m.folderFiles[folder] 339 m.fmut.RUnlock() 340 341 fset.Update(protocol.LocalDeviceID, fs) 342 seq := fset.Sequence(protocol.LocalDeviceID) 343 filenames := make([]string, len(fs)) 344 for i, file := range fs { 345 filenames[i] = file.Name 346 } 347 m.evLogger.Log(events.LocalIndexUpdated, map[string]interface{}{ 348 "folder": folder, 349 "items": len(fs), 350 "filenames": filenames, 351 "sequence": seq, 352 "version": seq, // legacy for sequence 353 }) 354} 355 356func newDeviceConfiguration(defaultCfg config.DeviceConfiguration, id protocol.DeviceID, name string) config.DeviceConfiguration { 357 cfg := defaultCfg.Copy() 358 cfg.DeviceID = id 359 cfg.Name = name 360 return cfg 361} 362 363func newFileSet(t testing.TB, folder string, fs fs.Filesystem, ldb *db.Lowlevel) *db.FileSet { 364 t.Helper() 365 fset, err := db.NewFileSet(folder, fs, ldb) 366 if err != nil { 367 t.Fatal(err) 368 } 369 return fset 370} 371 372func replace(t testing.TB, w config.Wrapper, to config.Configuration) { 373 t.Helper() 374 waiter, err := w.Modify(func(cfg *config.Configuration) { 375 *cfg = to 376 }) 377 if err != nil { 378 t.Fatal(err) 379 } 380 waiter.Wait() 381} 382 383func pauseFolder(t testing.TB, w config.Wrapper, id string, paused bool) { 384 t.Helper() 385 waiter, err := w.Modify(func(cfg *config.Configuration) { 386 _, i, _ := cfg.Folder(id) 387 cfg.Folders[i].Paused = paused 388 }) 389 if err != nil { 390 t.Fatal(err) 391 } 392 waiter.Wait() 393} 394 395func setFolder(t testing.TB, w config.Wrapper, fcfg config.FolderConfiguration) { 396 t.Helper() 397 waiter, err := w.Modify(func(cfg *config.Configuration) { 398 cfg.SetFolder(fcfg) 399 }) 400 if err != nil { 401 t.Fatal(err) 402 } 403 waiter.Wait() 404} 405 406func pauseDevice(t testing.TB, w config.Wrapper, id protocol.DeviceID, paused bool) { 407 t.Helper() 408 waiter, err := w.Modify(func(cfg *config.Configuration) { 409 _, i, _ := cfg.Device(id) 410 cfg.Devices[i].Paused = paused 411 }) 412 if err != nil { 413 t.Fatal(err) 414 } 415 waiter.Wait() 416} 417 418func setDevice(t testing.TB, w config.Wrapper, device config.DeviceConfiguration) { 419 t.Helper() 420 waiter, err := w.Modify(func(cfg *config.Configuration) { 421 cfg.SetDevice(device) 422 }) 423 if err != nil { 424 t.Fatal(err) 425 } 426 waiter.Wait() 427} 428 429func addDevice2(t testing.TB, w config.Wrapper, fcfg config.FolderConfiguration) { 430 waiter, err := w.Modify(func(cfg *config.Configuration) { 431 cfg.SetDevice(newDeviceConfiguration(cfg.Defaults.Device, device2, "device2")) 432 fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{DeviceID: device2}) 433 cfg.SetFolder(fcfg) 434 }) 435 must(t, err) 436 waiter.Wait() 437} 438