Files
syncthing/lib/fs/mtimefs_test.go
Jakob Borg 95b39a791d Merge branch 'main' into v2
* main:
  fix(gui): fix previous commit
  fix(gui): mark unseen disconnected devices as inactive (#10048)
  fix(strings): differentiate setup(n) and set(v) up (#10024)
  chore(fs): changes to allow Filesystem to be implemented externally (#10040)
  chore(config): resolve primary STUN servers via SRV record (fixes #10029) (#10031)
  build: push artifacts to Azure (#10044)
  chore(gui, man, authors): update docs, translations, and contributors
2025-04-09 15:40:25 +02:00

268 lines
8.0 KiB
Go

// Copyright (C) 2016 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 fs
import (
"errors"
"os"
"path/filepath"
"testing"
"time"
"github.com/syncthing/syncthing/lib/build"
)
func TestMtimeFS(t *testing.T) {
td := t.TempDir()
os.Mkdir(filepath.Join(td, "testdata"), 0o755)
os.WriteFile(filepath.Join(td, "testdata", "exists0"), []byte("hello"), 0o644)
os.WriteFile(filepath.Join(td, "testdata", "exists1"), []byte("hello"), 0o644)
os.WriteFile(filepath.Join(td, "testdata", "exists2"), []byte("hello"), 0o644)
// a random time with nanosecond precision
testTime := time.Unix(1234567890, 123456789)
mtimefs := newMtimeFS(td, make(mapStore))
// Do one Chtimes call that will go through to the normal filesystem
mtimefs.chtimes = os.Chtimes
if err := mtimefs.Chtimes("testdata/exists0", testTime, testTime); err != nil {
t.Error("Should not have failed:", err)
}
// Do one call that gets an error back from the underlying Chtimes
mtimefs.chtimes = failChtimes
if err := mtimefs.Chtimes("testdata/exists1", testTime, testTime); err != nil {
t.Error("Should not have failed:", err)
}
// Do one call that gets struck by an exceptionally evil Chtimes
mtimefs.chtimes = evilChtimes
if err := mtimefs.Chtimes("testdata/exists2", testTime, testTime); err != nil {
t.Error("Should not have failed:", err)
}
// All of the calls were successful, so an Lstat on them should return
// the test timestamp.
for _, file := range []string{"testdata/exists0", "testdata/exists1", "testdata/exists2"} {
if info, err := mtimefs.Lstat(file); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if !info.ModTime().Equal(testTime) {
t.Errorf("Time mismatch; %v != expected %v", info.ModTime(), testTime)
}
}
// The two last files should certainly not have the correct timestamp
// when looking directly on disk though.
for _, file := range []string{"testdata/exists1", "testdata/exists2"} {
if info, err := os.Lstat(filepath.Join(td, file)); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if info.ModTime().Equal(testTime) {
t.Errorf("Unexpected time match; %v == %v", info.ModTime(), testTime)
}
}
// Changing the timestamp on disk should be reflected in a new Lstat
// call. Choose a time that is likely to be able to be on all reasonable
// filesystems.
testTime = time.Now().Add(5 * time.Hour).Truncate(time.Minute)
os.Chtimes(filepath.Join(td, "testdata/exists0"), testTime, testTime)
if info, err := mtimefs.Lstat("testdata/exists0"); err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if !info.ModTime().Equal(testTime) {
t.Errorf("Time mismatch; %v != expected %v", info.ModTime(), testTime)
}
}
func TestMtimeFSWalk(t *testing.T) {
dir := t.TempDir()
mtimefs, walkFs := newMtimeFSWithWalk(dir, make(mapStore))
underlying := mtimefs.Filesystem
mtimefs.chtimes = failChtimes
if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
oldStat, err := mtimefs.Lstat("file")
if err != nil {
t.Fatal(err)
}
newTime := time.Now().Add(-2 * time.Hour)
if err := mtimefs.Chtimes("file", newTime, newTime); err != nil {
t.Fatal(err)
}
if newStat, err := mtimefs.Lstat("file"); err != nil {
t.Fatal(err)
} else if !newStat.ModTime().Equal(newTime) {
t.Errorf("expected time %v, lstat time %v", newTime, newStat.ModTime())
}
if underlyingStat, err := underlying.Lstat("file"); err != nil {
t.Fatal(err)
} else if !underlyingStat.ModTime().Equal(oldStat.ModTime()) {
t.Errorf("expected time %v, lstat time %v", oldStat.ModTime(), underlyingStat.ModTime())
}
found := false
_ = walkFs.Walk("", func(path string, info FileInfo, err error) error {
if path == "file" {
found = true
if !info.ModTime().Equal(newTime) {
t.Errorf("expected time %v, lstat time %v", newTime, info.ModTime())
}
}
return nil
})
if !found {
t.Error("did not find")
}
}
func TestMtimeFSOpen(t *testing.T) {
dir := t.TempDir()
mtimefs := newMtimeFS(dir, make(mapStore))
underlying := mtimefs.Filesystem
mtimefs.chtimes = failChtimes
if err := os.WriteFile(filepath.Join(dir, "file"), []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
oldStat, err := mtimefs.Lstat("file")
if err != nil {
t.Fatal(err)
}
newTime := time.Now().Add(-2 * time.Hour)
if err := mtimefs.Chtimes("file", newTime, newTime); err != nil {
t.Fatal(err)
}
if newStat, err := mtimefs.Lstat("file"); err != nil {
t.Fatal(err)
} else if !newStat.ModTime().Equal(newTime) {
t.Errorf("expected time %v, lstat time %v", newTime, newStat.ModTime())
}
if underlyingStat, err := underlying.Lstat("file"); err != nil {
t.Fatal(err)
} else if !underlyingStat.ModTime().Equal(oldStat.ModTime()) {
t.Errorf("expected time %v, lstat time %v", oldStat.ModTime(), underlyingStat.ModTime())
}
fd, err := mtimefs.Open("file")
if err != nil {
t.Fatal(err)
}
defer fd.Close()
info, err := fd.Stat()
if err != nil {
t.Fatal(err)
}
if !info.ModTime().Equal(newTime) {
t.Errorf("expected time %v, lstat time %v", newTime, info.ModTime())
}
}
func TestMtimeFSInsensitive(t *testing.T) {
if build.IsDarwin || build.IsWindows {
// blatantly assume file systems here are case insensitive. Might be
// a spurious failure on oddly configured systems.
} else {
t.Skip("need case insensitive FS")
}
theTest := func(t *testing.T, fs *mtimeFS, shouldSucceed bool) {
fs.RemoveAll("testdata")
defer fs.RemoveAll("testdata")
fs.Mkdir("testdata", 0o755)
WriteFile(fs, "testdata/FiLe", []byte("hello"), 0o644)
// a random time with nanosecond precision
testTime := time.Unix(1234567890, 123456789)
// Do one call that gets struck by an exceptionally evil Chtimes, with a
// different case from what is on disk.
fs.chtimes = evilChtimes
if err := fs.Chtimes("testdata/fIlE", testTime, testTime); err != nil {
t.Error("Should not have failed:", err)
}
// Check that we get back the mtime we set, if we were supposed to succeed.
info, err := fs.Lstat("testdata/FILE")
if err != nil {
t.Error("Lstat shouldn't fail:", err)
} else if info.ModTime().Equal(testTime) != shouldSucceed {
t.Errorf("Time mismatch; got %v, comparison %v, expected equal=%v", info.ModTime(), testTime, shouldSucceed)
}
}
// The test should fail with a case sensitive mtimefs
t.Run("with case sensitive mtimefs", func(t *testing.T) {
theTest(t, newMtimeFS(t.TempDir(), make(mapStore)), false)
})
// And succeed with a case insensitive one.
t.Run("with case insensitive mtimefs", func(t *testing.T) {
theTest(t, newMtimeFS(t.TempDir(), make(mapStore), WithCaseInsensitivity(true)), true)
})
}
// The mapStore is a simple database
type mapStore map[string][2]time.Time
func (s mapStore) PutMtime(_, name string, real, virtual time.Time) error {
s[name] = [2]time.Time{real, virtual}
return nil
}
func (s mapStore) GetMtime(_, name string) (real, virtual time.Time) {
v := s[name]
return v[0], v[1]
}
func (s mapStore) DeleteMtime(_, name string) error {
delete(s, name)
return nil
}
// failChtimes does nothing, and fails
func failChtimes(_ string, _, _ time.Time) error {
return errors.New("no")
}
// evilChtimes will set an mtime that's 300 days in the future of what was
// asked for, and truncate the time to the closest hour.
func evilChtimes(name string, mtime, atime time.Time) error {
return os.Chtimes(name, mtime.Add(300*time.Hour).Truncate(time.Hour), atime.Add(300*time.Hour).Truncate(time.Hour))
}
func newMtimeFS(path string, db database, options ...MtimeFSOption) *mtimeFS {
mtimefs, _ := newMtimeFSWithWalk(path, db, options...)
return mtimefs
}
func newMtimeFSWithWalk(path string, db database, options ...MtimeFSOption) (*mtimeFS, *walkFilesystem) {
fs := NewFilesystem(FilesystemTypeBasic, path, NewMtimeOption(db, "", options...))
wfs, _ := unwrapFilesystem[*walkFilesystem](fs)
mfs, _ := unwrapFilesystem[*mtimeFS](fs)
return mfs, wfs
}