1package modes
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "github.com/bnema/zut/packages/tui"
11)
12
13func writeWatchedTheme(t *testing.T, home, body string) string {
14 t.Helper()
15 path := filepath.Join(home, "themes", "live.json")
16 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
17 t.Fatal(err)
18 }
19 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
20 t.Fatal(err)
21 }
22 return path
23}
24
25func advanceThemeWatch(t *testing.T, ticks chan<- time.Time, processed <-chan struct{}, now time.Time) {
26 t.Helper()
27 deadline := time.NewTimer(time.Second)
28 defer deadline.Stop()
29 select {
30 case ticks <- now:
31 case <-deadline.C:
32 t.Fatal("watcher did not accept tick")
33 }
34 select {
35 case <-processed:
36 case <-deadline.C:
37 t.Fatal("watcher did not process tick")
38 }
39}
40
41func TestWatchThemeSourceTicksPublishesOnlyValidRevisions(t *testing.T) {
42 home := t.TempDir()
43 path := writeWatchedTheme(t, home, `{"colors":{"accent":20}}`)
44 accepted, err := tui.LoadThemeSource(home, "live")
45 if err != nil {
46 t.Fatal(err)
47 }
48 ctx, cancel := context.WithCancel(context.Background())
49 defer cancel()
50 ticks := make(chan time.Time)
51 processed := make(chan struct{})
52 events := make(chan themeWatchEvent, 3)
53 go watchThemeSourceTicks(ctx, home, "live", accepted, ticks, events, processed)
54
55 if err := os.WriteFile(path, []byte(`{"colors":{"accent":21}}`), 0o644); err != nil {
56 t.Fatal(err)
57 }
58 advanceThemeWatch(t, ticks, processed, time.Unix(1, 0))
59 select {
60 case event := <-events:
61 if event.source == nil || event.source.File.Colors.Base.Accent == nil || event.source.File.Colors.Base.Accent.Index != 21 {
62 t.Fatalf("valid revision event = %#v", event)
63 }
64 case <-time.After(time.Second):
65 t.Fatal("watcher did not publish valid revision")
66 }
67
68 if err := os.WriteFile(path, []byte(`{"colors":{"accent":999}}`), 0o644); err != nil {
69 t.Fatal(err)
70 }
71 advanceThemeWatch(t, ticks, processed, time.Unix(2, 0))
72 select {
73 case event := <-events:
74 if event.err == nil || event.source != nil {
75 t.Fatalf("invalid revision event = %#v", event)
76 }
77 case <-time.After(time.Second):
78 t.Fatal("watcher did not report invalid revision")
79 }
80}
81
82func TestWatchThemeSourceTicksRestartsDeletionGraceAfterInvalidReappearance(t *testing.T) {
83 home := t.TempDir()
84 path := writeWatchedTheme(t, home, `{}`)
85 accepted, err := tui.LoadThemeSource(home, "live")
86 if err != nil {
87 t.Fatal(err)
88 }
89 ctx, cancel := context.WithCancel(context.Background())
90 defer cancel()
91 ticks := make(chan time.Time)
92 processed := make(chan struct{})
93 events := make(chan themeWatchEvent, 1)
94 go watchThemeSourceTicks(ctx, home, "live", accepted, ticks, events, processed)
95
96 if err := os.Remove(path); err != nil {
97 t.Fatal(err)
98 }
99 advanceThemeWatch(t, ticks, processed, time.Unix(1, 0))
100 if err := os.WriteFile(path, []byte(`{"colors":{"accent":999}}`), 0o644); err != nil {
101 t.Fatal(err)
102 }
103 advanceThemeWatch(t, ticks, processed, time.Unix(2, 0))
104 select {
105 case event := <-events:
106 if event.err == nil {
107 t.Fatalf("invalid reappearance event = %#v", event)
108 }
109 case <-time.After(time.Second):
110 t.Fatal("watcher did not report invalid reappearance")
111 }
112
113 if err := os.Remove(path); err != nil {
114 t.Fatal(err)
115 }
116 advanceThemeWatch(t, ticks, processed, time.Unix(3, 0))
117 advanceThemeWatch(t, ticks, processed, time.Unix(4, 0))
118 select {
119 case event := <-events:
120 t.Fatalf("deletion happened before restarted grace period: %#v", event)
121 default:
122 }
123 advanceThemeWatch(t, ticks, processed, time.Unix(6, 0))
124 select {
125 case event := <-events:
126 if !event.deleted {
127 t.Fatalf("persistent deletion event = %#v", event)
128 }
129 case <-time.After(time.Second):
130 t.Fatal("watcher did not publish persistent deletion")
131 }
132}