turbo-editors/turbo-corepublic Fork 0
v1.0.3
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

watch_test.go · 201 lines · 6.5 KBGo Blame HistoryRaw
📦 Turbo Core — files rewritten outside reload into their windows; the agent window wraps what you type and keeps a long paste aside as a token (#28); configrepo fetches a shared .turbo-<slug> from a forge URL (#25) fe23288 k33g 9h ago1package app
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/gdamore/tcell/v2"
11)
12
13// newWatchedFile returns an editor with a file open in the front window, and
14// that file's path. The contents written later are deliberately of a different
15// length: on a filesystem that stamps modification times by the second, two
16// writes in the same second are told apart by size or not at all.
17func newWatchedFile(t *testing.T) (*App, tcell.SimulationScreen, string) {
18 t.Helper()
19
20 a, screen := newTestApp(t)
21 path := filepath.Join(t.TempDir(), "main.go")
22 writeTestFile(t, path, "package main\n")
23 a.Open(path)
24 a.tick() // the loop has seen the file as it was opened
25 return a, screen, path
26}
27
28func TestAFileRewrittenByAnotherProgramIsReloaded(t *testing.T) {
29 // This is the whole feature: a formatter, a generator or an agent in
30 // another terminal rewrites the file, and the window shows it without
31 // closing and reopening.
32 a, _, path := newWatchedFile(t)
33
34 writeTestFile(t, path, "package main\n\n// rewritten outside the editor\n")
35 a.tick()
36
37 if got := activeEditorText(t, a, path); !strings.Contains(got, "rewritten outside") {
38 t.Errorf("the window still shows %q after the file changed on disk", got)
39 }
40 if got := a.status.Message(); got != "Reloaded main.go" {
41 t.Errorf("status message = %q, want %q", got, "Reloaded main.go")
42 }
43 if title := a.desktop.Active().Title(); title != "main.go" {
44 t.Errorf("window title = %q after a reload, want %q", title, "main.go")
45 }
46}
47
48func TestAFileWithUnsavedChangesIsKeptWhenTheDiskChanges(t *testing.T) {
49 // The user's work and the other program's genuinely conflict, and the
50 // editor is not the one to decide — so it keeps the work and says so.
51 a, _, path := newWatchedFile(t)
52 typeText(a, "// mine")
53 edited := activeEditorText(t, a, path)
54
55 writeTestFile(t, path, "package main\n\n// rewritten outside the editor\n")
56 a.tick()
57
58 if got := activeEditorText(t, a, path); got != edited {
59 t.Errorf("the modified buffer was reloaded: %q, want the unsaved edit %q", got, edited)
60 }
61 if got := a.status.Message(); !strings.Contains(got, "changed on disk") {
62 t.Errorf("status message = %q, want it to say the file changed on disk", got)
63 }
64}
65
66func TestAChangeOnDiskIsReportedOncePerChange(t *testing.T) {
67 // The message would otherwise be re-issued on every turn of the loop for as
68 // long as the window stays modified, hiding everything else the status bar
69 // has to say.
70 a, _, path := newWatchedFile(t)
71 typeText(a, "// mine")
72 writeTestFile(t, path, "package main\n\n// rewritten outside the editor\n")
73 a.tick()
74
75 a.Message("something else")
76 a.tick()
77
78 if got := a.status.Message(); got != "something else" {
79 t.Errorf("status message = %q; the change on disk was reported a second time", got)
80 }
81}
82
83func TestTheEditorsOwnSaveIsNotMistakenForAChangeOnDisk(t *testing.T) {
84 // A save rewrites the file — through a rename, so with a new inode and
85 // stamp — and must not come round as a reload of the text just written.
86 a, _, path := newWatchedFile(t)
87 typeText(a, "// mine")
88 a.SaveFile()
89 a.tick()
90
91 if got := a.status.Message(); got != "Saved main.go" {
92 t.Errorf("status message = %q after a save, want %q", got, "Saved main.go")
93 }
94 if got := activeEditorText(t, a, path); !strings.Contains(got, "// mine") {
95 t.Errorf("the saved text was lost: %q", got)
96 }
97}
98
99func TestAFileThatDisappearsIsLeftAsItWasUntilItComesBack(t *testing.T) {
100 // Some programs replace a file by deleting it and writing a new one; a
101 // stat between the two sees nothing. The window keeps what it has, and
102 // picks the file up when it is there again.
103 a, _, path := newWatchedFile(t)
104
105 if err := os.Remove(path); err != nil {
106 t.Fatal(err)
107 }
108 a.tick()
109 if got := activeEditorText(t, a, path); got != "package main\n" {
110 t.Errorf("the window lost its text when the file went: %q", got)
111 }
112
113 writeTestFile(t, path, "package main\n\n// back again, longer\n")
114 a.tick()
115 if got := activeEditorText(t, a, path); !strings.Contains(got, "back again") {
116 t.Errorf("the window did not pick the file up when it came back: %q", got)
117 }
118}
119
120func TestAReloadTellsTheLanguageServerWhatTheFileNowSays(t *testing.T) {
121 // A server answering completions from the text before the formatter ran
122 // is the same stale-copy problem in another place.
123 a, server, project := newCodeApp(t)
124 path := filepath.Join(project, "main.go")
125 waitForMethod(t, server, "textDocument/didOpen")
126 a.tick()
127 before := server.methodCount("textDocument/didChange")
128
129 writeTestFile(t, path, "package main\n\nfunc main() { println() }\n")
130 a.tick()
131
132 deadline := time.After(2 * time.Second)
133 for server.methodCount("textDocument/didChange") == before {
134 select {
135 case <-deadline:
136 t.Fatal("the server was never told the reloaded text")
137 case <-time.After(time.Millisecond):
138 }
139 }
140}
141
142func TestAClosedWindowsFileIsNoLongerWatched(t *testing.T) {
143 a, _, path := newWatchedFile(t)
144
145 a.CloseFile()
146 a.tick()
147 writeTestFile(t, path, "package main\n\n// nobody is looking\n")
148
149 if len(a.fileStamps) != 0 {
150 t.Errorf("%d file(s) still stamped after the only window closed", len(a.fileStamps))
151 }
152 if a.fileWatch.changed() {
153 t.Error("the watcher would wake the loop for a file no window shows")
154 }
155}
156
157func TestTheWatcherWakesTheLoopWhenAnOpenFileChanges(t *testing.T) {
158 // Run sits in PollEvent; without this wake the reload would wait for the
159 // next keystroke, which is the defect being fixed.
160 a, screen, path := newWatchedFile(t)
161 a.fileWatchInterval = 5 * time.Millisecond
162 stop := a.watchFiles()
163 defer stop()
164
165 writeTestFile(t, path, "package main\n\n// rewritten outside the editor\n")
166
167 if !wokeWithin(screen, time.Second) {
168 t.Fatal("the event loop was never woken for a file that changed on disk")
169 }
170}
171
172func TestTheWatcherLeavesAnIdleEditorAlone(t *testing.T) {
173 // Waking the loop every second regardless would make the editor redraw
174 // forever with nothing to show; the wake is for a change, not for time.
175 a, screen, _ := newWatchedFile(t)
176 a.fileWatchInterval = 5 * time.Millisecond
177 stop := a.watchFiles()
178 defer stop()
179
180 if wokeWithin(screen, 100*time.Millisecond) {
181 t.Fatal("the event loop was woken though no file changed")
182 }
183}
184
185// wokeWithin reports whether the watcher posted its wake-up to the screen
186// before the deadline.
187func wokeWithin(screen tcell.SimulationScreen, within time.Duration) bool {
188 deadline := time.After(within)
189 for {
190 for screen.HasPendingEvent() {
191 if _, ok := screen.PollEvent().(*tcell.EventInterrupt); ok {
192 return true
193 }
194 }
195 select {
196 case <-deadline:
197 return false
198 case <-time.After(2 * time.Millisecond):
199 }
200 }
201}