mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 01:05:48 +02:00
consolidate watch tests and add include-in-included-file test
- Merge TestWatchForConfigChangesIncludeAdded and TestWatchForConfigChangesIncludeRemoved into the main TestWatchForConfigChanges function, which now starts the watcher once and shares it across all integration subtests. - Add prime_watcher helper that retries writes until an action fires, replacing the blind time.Sleep(200ms) watcher-startup wait. - Add new subtest "include added to already-included file adds its parent dir": writes an include directive into sub/included.conf (itself included from kitty.conf), then verifies that changes to the newly referenced file trigger the action, confirming its parent directory was added to the watch set. - Fix TestWatchForConfigChangesDebounce to use prime_watcher instead of time.Sleep for startup; capture before_burst baseline before the burst loop so the burst-action count is computed correctly.
This commit is contained in:
committed by
GitHub
parent
7e96373515
commit
d80fd1c23d
@@ -15,6 +15,30 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// prime_watcher repeatedly writes to path until the watcher delivers an event and
|
||||
// the action fires, confirming the watcher goroutine is ready. A single write may
|
||||
// race the watcher's initialisation so retrying ensures at least one write lands
|
||||
// while the watcher is active. After success it waits one full debounce period
|
||||
// so the cooldown window is clear before returning.
|
||||
// Returns the current action count after settling.
|
||||
func prime_watcher(t *testing.T, path string, counter *atomic.Int32, debounce time.Duration) int32 {
|
||||
t.Helper()
|
||||
before := counter.Load()
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
n := 0
|
||||
for time.Now().Before(deadline) {
|
||||
write_file(t, path, fmt.Sprintf("# prime %d\n", n))
|
||||
n++
|
||||
if wait_for_count(counter, before+1, debounce+100*time.Millisecond) > before {
|
||||
// Action fired — clear the debounce window before returning.
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
return counter.Load()
|
||||
}
|
||||
}
|
||||
t.Fatal("watcher failed to become ready: no action fired within 5 seconds of repeated prime writes")
|
||||
return 0
|
||||
}
|
||||
|
||||
func write_file(t *testing.T, path string, data string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(data), 0o600); err != nil {
|
||||
@@ -197,22 +221,34 @@ func wait_for_count(counter *atomic.Int32, target int32, timeout time.Duration)
|
||||
return counter.Load()
|
||||
}
|
||||
|
||||
// TestWatchForConfigChanges consolidates all watcher integration tests into a single
|
||||
// function that starts the watcher once and confirms readiness via prime_watcher
|
||||
// instead of a blind time.Sleep. All include-watching scenarios (basic, added,
|
||||
// removed, and added via an already-included file) run as sequential subtests.
|
||||
func TestWatchForConfigChanges(t *testing.T) {
|
||||
tdir := t.TempDir()
|
||||
tdir := resolve_path(t.TempDir())
|
||||
subdir := filepath.Join(tdir, "sub")
|
||||
if err := os.Mkdir(subdir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
extradir := filepath.Join(tdir, "extra")
|
||||
extra2dir := filepath.Join(tdir, "extra2")
|
||||
for _, d := range []string{subdir, extradir, extra2dir} {
|
||||
if err := os.Mkdir(d, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
main_conf := filepath.Join(tdir, "kitty.conf")
|
||||
included_conf := filepath.Join(subdir, "included.conf")
|
||||
dark_theme := filepath.Join(tdir, "dark-theme.auto.conf")
|
||||
unrelated := filepath.Join(tdir, "unrelated.txt")
|
||||
extra_conf := filepath.Join(extradir, "custom.conf")
|
||||
extra2_conf := filepath.Join(extra2dir, "another.conf")
|
||||
|
||||
write_file(t, main_conf, "include sub/included.conf\n")
|
||||
write_file(t, included_conf, "background black\n")
|
||||
write_file(t, dark_theme, "background #000000\n")
|
||||
write_file(t, unrelated, "this file should not trigger action\n")
|
||||
write_file(t, extra_conf, "background black\n")
|
||||
write_file(t, extra2_conf, "background black\n")
|
||||
|
||||
var action_count atomic.Int32
|
||||
action := func() error {
|
||||
@@ -229,47 +265,102 @@ func TestWatchForConfigChanges(t *testing.T) {
|
||||
done <- watch_for_config_changes(ctx, action, debounce, []string{main_conf})
|
||||
}()
|
||||
|
||||
// Give the watcher time to start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
// Confirm the watcher is ready by writing to the dark-theme auto conf (an
|
||||
// always-watched file that does not affect the include graph) and waiting for
|
||||
// an action to fire. This replaces the blind time.Sleep used previously.
|
||||
prime_watcher(t, dark_theme, &action_count, debounce)
|
||||
|
||||
t.Run("main config change triggers action", func(t *testing.T) {
|
||||
before := action_count.Load()
|
||||
write_file(t, main_conf, "include sub/included.conf\nfont_size 13\n")
|
||||
count := wait_for_count(&action_count, before+1, 2*time.Second)
|
||||
if count <= before {
|
||||
t.Fatalf("Expected action to be called after main config change, count=%d", count)
|
||||
if wait_for_count(&action_count, before+1, 2*time.Second) <= before {
|
||||
t.Fatalf("Expected action to be called after main config change, count=%d", action_count.Load())
|
||||
}
|
||||
})
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
|
||||
t.Run("included file change triggers action", func(t *testing.T) {
|
||||
before := action_count.Load()
|
||||
write_file(t, included_conf, "background white\n")
|
||||
count := wait_for_count(&action_count, before+1, 2*time.Second)
|
||||
if count <= before {
|
||||
t.Fatalf("Expected action to be called after included file change, count=%d", count)
|
||||
if wait_for_count(&action_count, before+1, 2*time.Second) <= before {
|
||||
t.Fatalf("Expected action to be called after included file change, count=%d", action_count.Load())
|
||||
}
|
||||
})
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
|
||||
t.Run("auto color scheme file change triggers action", func(t *testing.T) {
|
||||
before := action_count.Load()
|
||||
write_file(t, dark_theme, "background #111111\n")
|
||||
count := wait_for_count(&action_count, before+1, 2*time.Second)
|
||||
if count <= before {
|
||||
t.Fatalf("Expected action to be called after dark-theme.auto.conf change, count=%d", count)
|
||||
if wait_for_count(&action_count, before+1, 2*time.Second) <= before {
|
||||
t.Fatalf("Expected action to be called after dark-theme.auto.conf change, count=%d", action_count.Load())
|
||||
}
|
||||
})
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
|
||||
t.Run("unrelated file change does not trigger action", func(t *testing.T) {
|
||||
before := action_count.Load()
|
||||
write_file(t, unrelated, "still unrelated\n")
|
||||
// Wait debounce + a bit more to ensure no spurious call
|
||||
time.Sleep(debounce + 200*time.Millisecond)
|
||||
after := action_count.Load()
|
||||
if after != before {
|
||||
if after := action_count.Load(); after != before {
|
||||
t.Fatalf("Expected action NOT to be called for unrelated file, count went from %d to %d", before, after)
|
||||
}
|
||||
})
|
||||
|
||||
// include added to main config: extradir must become watched.
|
||||
// sync_watched_dirs() runs before action() in the event loop, so by the time
|
||||
// wait_for_count returns the new directory is already registered.
|
||||
t.Run("include added to main config is watched", func(t *testing.T) {
|
||||
before := action_count.Load()
|
||||
write_file(t, main_conf, "include sub/included.conf\ninclude extra/custom.conf\n")
|
||||
if wait_for_count(&action_count, before+1, 2*time.Second) <= before {
|
||||
t.Fatalf("Expected action after kitty.conf gained include directive")
|
||||
}
|
||||
// Let the debounce window clear before writing to the newly watched file.
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
before = action_count.Load()
|
||||
write_file(t, extra_conf, "background white\n")
|
||||
if wait_for_count(&action_count, before+1, 2*time.Second) <= before {
|
||||
t.Fatalf("Expected action after modifying newly included file")
|
||||
}
|
||||
})
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
|
||||
// include added to an already-included file: extra2dir must become watched.
|
||||
t.Run("include added to already-included file adds its parent dir", func(t *testing.T) {
|
||||
// Add an include to sub/included.conf that points into extra2dir, which is
|
||||
// not currently watched. The watcher re-scans the full include graph on
|
||||
// every conf-file change, so extra2dir must be added to the watch set.
|
||||
before := action_count.Load()
|
||||
write_file(t, included_conf, "include ../extra2/another.conf\n")
|
||||
if wait_for_count(&action_count, before+1, 2*time.Second) <= before {
|
||||
t.Fatalf("Expected action after sub/included.conf gained include directive")
|
||||
}
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
// extra2dir is now watched; a change to extra2/another.conf must fire.
|
||||
before = action_count.Load()
|
||||
write_file(t, extra2_conf, "background blue\n")
|
||||
if wait_for_count(&action_count, before+1, 2*time.Second) <= before {
|
||||
t.Fatalf("Expected action after modifying file included from an already-included conf file")
|
||||
}
|
||||
})
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
|
||||
// include removed from main config: extradir must be dropped from the watch set.
|
||||
t.Run("include removed from main config is no longer watched", func(t *testing.T) {
|
||||
before := action_count.Load()
|
||||
write_file(t, main_conf, "include sub/included.conf\n")
|
||||
if wait_for_count(&action_count, before+1, 2*time.Second) <= before {
|
||||
t.Fatalf("Expected action after kitty.conf lost include directive")
|
||||
}
|
||||
time.Sleep(debounce + 20*time.Millisecond)
|
||||
before = action_count.Load()
|
||||
write_file(t, extra_conf, "background green\n")
|
||||
time.Sleep(debounce + 200*time.Millisecond)
|
||||
if after := action_count.Load(); after != before {
|
||||
t.Fatalf("Expected NO action after modifying removed-include file, count went from %d to %d", before, after)
|
||||
}
|
||||
})
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
@@ -301,29 +392,31 @@ func TestWatchForConfigChangesDebounce(t *testing.T) {
|
||||
done <- watch_for_config_changes(ctx, action, debounce, []string{main_conf})
|
||||
}()
|
||||
|
||||
// Give the watcher time to start
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
// Confirm the watcher is ready before sending the burst.
|
||||
prime_watcher(t, main_conf, &action_count, debounce)
|
||||
|
||||
// Write to the file several times rapidly within the debounce window.
|
||||
// The fswatcher debouncer drops events that occur within the cooldown period
|
||||
// after the first event, so only the first write should produce an action call.
|
||||
before_burst := action_count.Load()
|
||||
for i := range 5 {
|
||||
write_file(t, main_conf, fmt.Sprintf("font_size %d\n", 12+i))
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Wait for up to one full debounce period for the first action to fire
|
||||
count_after_burst := wait_for_count(&action_count, 1, debounce+500*time.Millisecond)
|
||||
// Wait for up to one full debounce period for the first action to fire.
|
||||
count_after_burst := wait_for_count(&action_count, before_burst+1, debounce+500*time.Millisecond)
|
||||
burst_actions := count_after_burst - before_burst
|
||||
|
||||
// Debouncing should have collapsed the burst into at most 2 calls.
|
||||
// The fswatcher debouncer uses leading-edge logic: the first event fires
|
||||
// immediately and subsequent events within the cooldown window are dropped.
|
||||
// A trailing event may fire at the end of the cooldown window, giving at most 2.
|
||||
if count_after_burst == 0 {
|
||||
if burst_actions == 0 {
|
||||
t.Fatalf("Expected at least one action call after burst of writes, got 0")
|
||||
}
|
||||
if count_after_burst > 2 {
|
||||
t.Fatalf("Expected debouncing to collapse burst: want ≤2 calls, got %d", count_after_burst)
|
||||
if burst_actions > 2 {
|
||||
t.Fatalf("Expected debouncing to collapse burst: want ≤2 calls, got %d", burst_actions)
|
||||
}
|
||||
|
||||
// After waiting well past the debounce window, a new write should trigger exactly one more call.
|
||||
@@ -345,141 +438,3 @@ func TestWatchForConfigChangesDebounce(t *testing.T) {
|
||||
t.Fatal("watch_for_config_changes did not exit after context cancel")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchForConfigChangesIncludeAdded verifies that when the main config gains a new
|
||||
// include directive the parent directory of the new include file is added to the watcher,
|
||||
// and subsequent changes to that include file trigger the action.
|
||||
func TestWatchForConfigChangesIncludeAdded(t *testing.T) {
|
||||
tdir := resolve_path(t.TempDir())
|
||||
extradir := filepath.Join(tdir, "extra")
|
||||
if err := os.Mkdir(extradir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
main_conf := filepath.Join(tdir, "kitty.conf")
|
||||
extra_conf := filepath.Join(extradir, "custom.conf")
|
||||
|
||||
// Start with no includes so extradir is NOT initially watched.
|
||||
write_file(t, main_conf, "font_size 12\n")
|
||||
// Create the include file before modifying kitty.conf so the re-scan can find it.
|
||||
write_file(t, extra_conf, "background black\n")
|
||||
|
||||
var action_count atomic.Int32
|
||||
action := func() error {
|
||||
action_count.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
const debounce = 50 * time.Millisecond
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- watch_for_config_changes(ctx, action, debounce, []string{main_conf})
|
||||
}()
|
||||
|
||||
// Give the watcher time to start.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Step 1: add the include directive to kitty.conf.
|
||||
before_add := action_count.Load()
|
||||
write_file(t, main_conf, "font_size 12\ninclude extra/custom.conf\n")
|
||||
count := wait_for_count(&action_count, before_add+1, 2*time.Second)
|
||||
if count <= before_add {
|
||||
t.Fatalf("Expected action after kitty.conf gained include directive, count=%d", count)
|
||||
}
|
||||
|
||||
// Give the watcher a moment to register the new directory.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Step 2: modify the newly included file; it should now be watched.
|
||||
before_extra := action_count.Load()
|
||||
write_file(t, extra_conf, "background white\n")
|
||||
count = wait_for_count(&action_count, before_extra+1, 2*time.Second)
|
||||
if count <= before_extra {
|
||||
t.Fatalf("Expected action after modifying newly included file, count=%d", count)
|
||||
}
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("watch_for_config_changes returned error: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("watch_for_config_changes did not exit after context cancel")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatchForConfigChangesIncludeRemoved verifies that when the main config loses an
|
||||
// include directive the parent directory of the removed include file is dropped from the
|
||||
// watcher, and subsequent changes to that file no longer trigger the action.
|
||||
func TestWatchForConfigChangesIncludeRemoved(t *testing.T) {
|
||||
tdir := resolve_path(t.TempDir())
|
||||
subdir := filepath.Join(tdir, "sub")
|
||||
if err := os.Mkdir(subdir, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
main_conf := filepath.Join(tdir, "kitty.conf")
|
||||
sub_conf := filepath.Join(subdir, "extra.conf")
|
||||
|
||||
// Start with an include so subdir IS initially watched.
|
||||
write_file(t, main_conf, "font_size 12\ninclude sub/extra.conf\n")
|
||||
write_file(t, sub_conf, "background black\n")
|
||||
|
||||
var action_count atomic.Int32
|
||||
action := func() error {
|
||||
action_count.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
const debounce = 50 * time.Millisecond
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- watch_for_config_changes(ctx, action, debounce, []string{main_conf})
|
||||
}()
|
||||
|
||||
// Give the watcher time to start.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Confirm sub_conf is currently watched by verifying a change triggers action.
|
||||
before_verify := action_count.Load()
|
||||
write_file(t, sub_conf, "background blue\n")
|
||||
if wait_for_count(&action_count, before_verify+1, 2*time.Second) <= before_verify {
|
||||
t.Fatal("sub_conf changes should trigger action before include is removed")
|
||||
}
|
||||
|
||||
// Step 1: remove the include directive from kitty.conf.
|
||||
before_remove := action_count.Load()
|
||||
write_file(t, main_conf, "font_size 12\n")
|
||||
if wait_for_count(&action_count, before_remove+1, 2*time.Second) <= before_remove {
|
||||
t.Fatal("Expected action after kitty.conf lost include directive")
|
||||
}
|
||||
|
||||
// Give the watcher a moment to drop the old directory.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Step 2: modify the no-longer-included file; the watch should have been removed.
|
||||
before_after := action_count.Load()
|
||||
write_file(t, sub_conf, "background green\n")
|
||||
time.Sleep(debounce + 200*time.Millisecond)
|
||||
after := action_count.Load()
|
||||
if after != before_after {
|
||||
t.Fatalf("Expected NO action after modifying removed-include file, count went from %d to %d", before_after, after)
|
||||
}
|
||||
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("watch_for_config_changes returned error: %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("watch_for_config_changes did not exit after context cancel")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user