pkg/watcher/watcher_test.go
Rama main · réplica de lectura del repositorio original.
package watcher
import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
// TestCloseWakesWaitForEvents verifies that Close releases a caller parked
// in WaitForEvents, rather than leaving it asleep on the condition forever.
func TestCloseWakesWaitForEvents(t *testing.T) {
w, err := New("test-app")
if err != nil {
t.Fatal(err)
}
if err := w.RecursivelyWatch(t.TempDir()); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = w.Close() })
done := make(chan bool, 1)
go func() {
_, ok := w.WaitForEvents()
done <- ok
}()
// Wait until the caller is genuinely parked on the condition, so the
// test can't pass merely by observing an already-closed watcher. There
// is no hook between WaitForEvents' stop-check and its Wait, so stack
// inspection is the only way to detect this.
waitUntilParked(t)
_ = w.Close()
select {
case ok := <-done:
if ok {
t.Fatal("WaitForEvents reported events after Close, want ok=false")
}
case <-time.After(5 * time.Second):
t.Fatal("WaitForEvents did not return after Close; its caller is leaked")
}
}
// waitUntilParked blocks until a goroutine is parked on the condition from
// inside WaitForEvents. It matches both frames in the same goroutine's
// stack, so an unrelated goroutine waiting on some other condition doesn't
// count.
func waitUntilParked(t *testing.T) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
buf := make([]byte, 1<<20)
for time.Now().Before(deadline) {
n := runtime.Stack(buf, true)
for _, g := range strings.Split(string(buf[:n]), "\n\n") {
if strings.Contains(g, "sync.(*Cond).Wait") &&
strings.Contains(g, "(*Watcher).WaitForEvents") {
return
}
}
time.Sleep(5 * time.Millisecond)
}
t.Fatal("timed out waiting for a caller to park in WaitForEvents")
}
// TestFilesInNewDirectory verifies that files created together with a new
// directory are reported, even though they exist before the directory is watched.
// The directory is populated elsewhere and moved in, so the file never gets
// an event of its own.
func TestFilesInNewDirectory(t *testing.T) {
w, err := New("test-app")
if err != nil {
t.Fatal(err)
}
root := t.TempDir()
if err := w.RecursivelyWatch(root); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = w.Close() })
staging := t.TempDir()
staged := filepath.Join(staging, "a", "migrations", "1_init.up.sql")
if err := os.MkdirAll(filepath.Dir(staged), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(staged, []byte("CREATE TABLE t (id INT);"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.Rename(filepath.Join(staging, "a"), filepath.Join(root, "a")); err != nil {
t.Fatal(err)
}
file := filepath.Join(root, "a", "migrations", "1_init.up.sql")
found := make(chan struct{})
go func() {
for {
events, ok := w.WaitForEvents()
if !ok {
return
}
for _, ev := range events {
if ev.Path == file && ev.EventType == CREATED {
close(found)
return
}
}
}
}()
select {
case <-found:
case <-time.After(5 * time.Second):
t.Fatal("no event for a file created together with its directory")
}
}