package main import ( "context" "os" "path/filepath" "testing" "time" ignore "github.com/sabhiram/go-gitignore" ) func TestWatcher_Create(t *testing.T) { host := t.TempDir() data := t.TempDir() rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) writeFile(t, host, "new.txt", "hello") assertFileEventually(t, data, "new.txt", "hello", 2*time.Second) } func TestWatcher_CreateInSubdir(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "src/existing.go", "old") rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) writeFile(t, host, "src/new.go", "package main") assertFileEventually(t, data, "src/new.go", "package main", 2*time.Second) } func TestWatcher_Write(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "file.txt", "original") rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "file.txt", "original") ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) writeFile(t, host, "file.txt", "updated") assertFileEventually(t, data, "file.txt", "updated", 2*time.Second) } func TestWatcher_Remove(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "file.txt", "content") rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "file.txt", "content") ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) os.Remove(filepath.Join(host, "file.txt")) assertFileRemovedEventually(t, data, "file.txt", 2*time.Second) } func TestWatcher_Rename(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "Foo.txt", "content") rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "Foo.txt", "content") ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) // Rename: delete old, create new (Linux is case-sensitive) os.Rename(filepath.Join(host, "Foo.txt"), filepath.Join(host, "foo.txt")) writeFile(t, host, "foo.txt", "content") assertFileRemovedEventually(t, data, "Foo.txt", 2*time.Second) assertFileEventually(t, data, "foo.txt", "content", 2*time.Second) } func TestWatcher_IgnoredFileEvents(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, ".gitignore", "*.log\nbuild/\n") rules, err := loadGitignore(host) if err != nil { t.Fatal(err) } manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) // Create ignored files - should NOT sync writeFile(t, host, "debug.log", "log data") writeFile(t, host, "build/output.o", "binary") time.Sleep(500 * time.Millisecond) assertFileNotExists(t, data, "debug.log") assertFileNotExists(t, data, "build/output.o") // Create non-ignored file - should sync writeFile(t, host, "app.go", "package main") assertFileEventually(t, data, "app.go", "package main", 2*time.Second) } func TestWatcher_Debouncing(t *testing.T) { host := t.TempDir() data := t.TempDir() rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) // Rapid writes to same file writeFile(t, host, "file.txt", "v1") writeFile(t, host, "file.txt", "v2") writeFile(t, host, "file.txt", "v3") writeFile(t, host, "file.txt", "final") // Wait for debounce + sync assertFileEventually(t, data, "file.txt", "final", 2*time.Second) // Check that intermediate states don't matter - final content is correct content, err := os.ReadFile(filepath.Join(data, "file.txt")) if err != nil { t.Fatal(err) } if string(content) != "final" { t.Errorf("content = %q, want %q", string(content), "final") } } func TestWatcher_CreateDirectory(t *testing.T) { host := t.TempDir() data := t.TempDir() rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) // Create directory and file inside it if err := os.MkdirAll(filepath.Join(host, "newdir"), 0755); err != nil { t.Fatal(err) } time.Sleep(300 * time.Millisecond) // let watcher register new dir writeFile(t, host, "newdir/file.txt", "nested") assertDirExistsEventually(t, data, "newdir", 2*time.Second) assertFileEventually(t, data, "newdir/file.txt", "nested", 2*time.Second) } func TestWatcher_RemoveDirectory(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "dir/file.txt", "content") rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } assertFileExists(t, data, "dir/file.txt", "content") ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) os.RemoveAll(filepath.Join(host, "dir")) assertFileRemovedEventually(t, data, "dir/file.txt", 2*time.Second) } func TestWatcher_VerifyManifestUpdated(t *testing.T) { host := t.TempDir() data := t.TempDir() writeFile(t, host, "file.txt", "v1") rules := ignore.CompileIgnoreLines() manifest, err := initialSync(host, data, rules, false) if err != nil { t.Fatal(err) } if manifest.Files["file.txt"].Size != 2 { t.Errorf("initial manifest size = %d, want 2", manifest.Files["file.txt"].Size) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() go watch(ctx, host, data, manifest, rules, false, false) time.Sleep(100 * time.Millisecond) writeFile(t, host, "file.txt", "version two") // Wait for sync time.Sleep(500 * time.Millisecond) if manifest.Files["file.txt"].Size != 11 { t.Errorf("manifest size after write = %d, want 11", manifest.Files["file.txt"].Size) } } // Helpers func assertFileEventually(t *testing.T, dir, path, expectedContent string, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { content, err := os.ReadFile(filepath.Join(dir, path)) if err == nil && string(content) == expectedContent { return } time.Sleep(50 * time.Millisecond) } content, _ := os.ReadFile(filepath.Join(dir, path)) t.Errorf("file %s not as expected within %v: got %q, want %q", path, timeout, string(content), expectedContent) } func assertFileRemovedEventually(t *testing.T, dir, path string, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { _, err := os.Stat(filepath.Join(dir, path)) if os.IsNotExist(err) { return } time.Sleep(50 * time.Millisecond) } t.Errorf("file %s still exists after %v", path, timeout) } func assertDirExistsEventually(t *testing.T, dir, path string, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { info, err := os.Stat(filepath.Join(dir, path)) if err == nil && info.IsDir() { return } time.Sleep(50 * time.Millisecond) } t.Errorf("directory %s not found within %v", path, timeout) }