package snapshot import ( "os" "path/filepath" "testing" "time" ) func TestFileCTime_RealFile(t *testing.T) { // Create a temporary file dir := t.TempDir() path := filepath.Join(dir, "testfile.txt") if err := os.WriteFile(path, []byte("hello"), 0644); err != nil { t.Fatal(err) } info, err := os.Stat(path) if err != nil { t.Fatal(err) } ctime := fileCTime(info) // ctime should be a valid time (not zero) if ctime.IsZero() { t.Fatal("fileCTime returned zero time") } // ctime should be close to now (within a few seconds) diff := time.Since(ctime) if diff < 0 || diff > 5*time.Second { t.Fatalf("fileCTime returned unexpected time: %v (diff from now: %v)", ctime, diff) } // ctime should not equal mtime exactly in all cases, but for a freshly // created file they should be very close mtime := info.ModTime() ctimeMtimeDiff := ctime.Sub(mtime) if ctimeMtimeDiff < 0 { ctimeMtimeDiff = -ctimeMtimeDiff } // For a freshly created file, ctime and mtime should be within 1 second if ctimeMtimeDiff > time.Second { t.Fatalf("ctime and mtime differ by too much for a new file: ctime=%v, mtime=%v, diff=%v", ctime, mtime, ctimeMtimeDiff) } } func TestFileCTime_AfterMtimeChange(t *testing.T) { // Create a temporary file dir := t.TempDir() path := filepath.Join(dir, "testfile.txt") if err := os.WriteFile(path, []byte("hello"), 0644); err != nil { t.Fatal(err) } // Get initial ctime info1, err := os.Stat(path) if err != nil { t.Fatal(err) } ctime1 := fileCTime(info1) // Change mtime to a time in the past pastTime := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) if err := os.Chtimes(path, pastTime, pastTime); err != nil { t.Fatal(err) } // Get new stats info2, err := os.Stat(path) if err != nil { t.Fatal(err) } ctime2 := fileCTime(info2) mtime2 := info2.ModTime() // mtime should now be in the past if mtime2.Year() != 2020 { t.Fatalf("mtime not set correctly: %v", mtime2) } // On macOS: ctime (birth time) should remain unchanged since birth time // doesn't change when mtime is updated. // On Linux: ctime (inode change time) will be updated to ~now because // changing mtime is a metadata change. // Either way, ctime should NOT equal the past mtime we just set. if ctime2.Equal(pastTime) { t.Fatal("ctime should not equal the artificially set past mtime") } // ctime should still be a recent time (the original creation time or // the metadata change time, depending on platform) _ = ctime1 // used for reference; both platforms will have a recent ctime2 if time.Since(ctime2) > 10*time.Second { t.Fatalf("ctime is unexpectedly old: %v", ctime2) } } // TestFileCTime_NonSyscallFileInfo verifies the fallback to mtime when // the FileInfo doesn't have a *syscall.Stat_t (e.g. afero.MemMapFs). type mockFileInfo struct { name string size int64 mode os.FileMode modTime time.Time isDir bool } func (m *mockFileInfo) Name() string { return m.name } func (m *mockFileInfo) Size() int64 { return m.size } func (m *mockFileInfo) Mode() os.FileMode { return m.mode } func (m *mockFileInfo) ModTime() time.Time { return m.modTime } func (m *mockFileInfo) IsDir() bool { return m.isDir } func (m *mockFileInfo) Sys() interface{} { return nil } // No syscall.Stat_t func TestFileCTime_FallbackToMtime(t *testing.T) { now := time.Now().UTC().Truncate(time.Second) info := &mockFileInfo{ name: "test.txt", size: 100, mode: 0644, modTime: now, } ctime := fileCTime(info) if !ctime.Equal(now) { t.Fatalf("expected fallback to mtime %v, got %v", now, ctime) } }