turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
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.

tools_test.go · 387 lines · 10.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 13h ago1package tools
2
3import (
4 "errors"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "codeberg.org/turbo-editors/turbo-core/profile"
11)
12
13// testProfile is a fictional editor. The template it carries is a small but
14// real tools file, because Create's job is to write the profile's template and
15// a test asserting on what is in it would only be reading its own fixture back.
16func testProfile() profile.Profile {
17 return profile.Profile{
18 Name: "Turbo Test",
19 Slug: "turbo-test",
20 Language: "Test",
21 ToolsMenu: "~T~est",
22 Templates: profile.Templates{Tools: testToolsTemplate},
23 }
24}
25
26const testToolsTemplate = `# turbo-test tools.
27
28[[tool]]
29name = "~B~uild"
30command = "make build"
31output = "popup"
32
33[[tool]]
34name = "~R~un"
35command = "make run"
36output = "terminal"
37`
38
39// menu is the plain name of the toolchain menu the test profile uses.
40var menu = DefaultMenuName(testProfile())
41
42// project builds a project directory holding a tools file.
43func project(t *testing.T, contents string) string {
44 t.Helper()
45
46 dir := t.TempDir()
47 if contents == "" {
48 return dir
49 }
50 if err := os.MkdirAll(filepath.Dir(Path(testProfile(), dir)), 0o755); err != nil {
51 t.Fatalf("creating the tools directory: %v", err)
52 }
53 if err := os.WriteFile(Path(testProfile(), dir), []byte(contents), 0o644); err != nil {
54 t.Fatalf("writing the tools file: %v", err)
55 }
56 return dir
57}
58
59// load reads a project's tools, failing the test if it cannot.
60func load(t *testing.T, dir string) List {
61 t.Helper()
62
63 list, err := Load(testProfile(), dir)
64 if err != nil {
65 t.Fatalf("Load(testProfile(), %q) error = %v", dir, err)
66 }
67 return list
68}
69
70// summary renders the tools as "name=command" strings, in order.
71func summary(list List) []string {
72 out := make([]string, 0, list.Len())
73 for _, tool := range list.Tools() {
74 out = append(out, tool.Name+"="+tool.Command)
75 }
76 return out
77}
78
79func TestLoadReadsTheToolsInFileOrder(t *testing.T) {
80 // The menu should match the file, so someone reordering the file sees the
81 // menu reorder.
82 dir := project(t, `
83[[tool]]
84name = "Test"
85command = "go test ./..."
86
87[[tool]]
88name = "Build"
89command = "go build ./..."
90`)
91
92 got := summary(load(t, dir))
93 want := []string{"Test=go test ./...", "Build=go build ./..."}
94 if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
95 t.Errorf("got %v, want %v", got, want)
96 }
97}
98
99func TestAProjectWithNoToolsFileIsNotAnError(t *testing.T) {
100 list := load(t, t.TempDir())
101
102 if list.Len() != 0 {
103 t.Errorf("Len() = %d, want 0", list.Len())
104 }
105 if got := list.Tools(); got != nil {
106 t.Errorf("Tools() = %v, want nothing", got)
107 }
108}
109
110func TestAFileThatIsNotTOMLIsReported(t *testing.T) {
111 // A typo must be reported rather than silently leaving the menu empty.
112 dir := project(t, "[[tool]\nname = ")
113
114 _, err := Load(testProfile(), dir)
115 if err == nil {
116 t.Fatal("Load() accepted a file that is not TOML")
117 }
118 if !strings.Contains(err.Error(), Path(testProfile(), dir)) {
119 t.Errorf("the error does not name the file: %v", err)
120 }
121}
122
123func TestAToolWithNoNameOrNoCommandIsRefused(t *testing.T) {
124 for name, contents := range map[string]string{
125 "no name": "[[tool]]\ncommand = \"go build\"\n",
126 "no command": "[[tool]]\nname = \"Build\"\n",
127 } {
128 t.Run(name, func(t *testing.T) {
129 if _, err := Load(testProfile(), project(t, contents)); err == nil {
130 t.Errorf("Load() accepted a tool with %s", name)
131 }
132 })
133 }
134}
135
136func TestACommandMayBeAWholeSequence(t *testing.T) {
137 // It goes to sh -c, so one entry can be several commands.
138 dir := project(t, "[[tool]]\nname = \"Check\"\ncommand = \"go vet ./... && go test ./...\"\n")
139
140 list := load(t, dir)
141 if list.Len() != 1 {
142 t.Fatalf("Len() = %d", list.Len())
143 }
144 if got := list.Tools()[0].Command; got != "go vet ./... && go test ./..." {
145 t.Errorf("Command = %q, want the sequence unchanged", got)
146 }
147}
148
149func TestADirectoryWhereTheFileGoesCountsAsAbsent(t *testing.T) {
150 dir := t.TempDir()
151 if err := os.MkdirAll(Path(testProfile(), dir), 0o755); err != nil {
152 t.Fatalf("creating a directory where the file goes: %v", err)
153 }
154
155 if Exists(testProfile(), dir) {
156 t.Error("Exists() called a directory a tools file")
157 }
158}
159
160func TestPathIsUnderTheEditorsOwnDirectory(t *testing.T) {
161 // The directory is the editor's, so two editors in one repository keep
162 // their tools apart rather than fighting over one file.
163 want := filepath.Join("/src/p", ".turbo-test", "tools.toml")
164 if got := Path(testProfile(), "/src/p"); got != want {
165 t.Errorf("Path() = %q, want %q", got, want)
166 }
167}
168
169func TestCreateWritesAFileThatLoadsBack(t *testing.T) {
170 dir := t.TempDir()
171
172 path, err := Create(testProfile(), dir)
173 if err != nil {
174 t.Fatalf("Create() error = %v", err)
175 }
176 if path != Path(testProfile(), dir) {
177 t.Errorf("Create() returned %q, want %q", path, Path(testProfile(), dir))
178 }
179 if !Exists(testProfile(), dir) {
180 t.Fatal("Create() reported success but wrote no file")
181 }
182 if load(t, dir).Len() == 0 {
183 t.Error("the created file holds no tools")
184 }
185}
186
187func TestCreateRefusesToOverwriteAnExistingFile(t *testing.T) {
188 original := "[[tool]]\nname = \"Mine\"\ncommand = \"make\"\n"
189 dir := project(t, original)
190
191 _, err := Create(testProfile(), dir)
192 if !errors.Is(err, ErrExists) {
193 t.Fatalf("Create() error = %v, want ErrExists", err)
194 }
195 if got := readFile(t, Path(testProfile(), dir)); got != original {
196 t.Errorf("the existing file was changed:\n%s", got)
197 }
198}
199
200// plain strips the tilde hot-key markers from a label.
201func plain(label string) string { return strings.ReplaceAll(label, "~", "") }
202
203// hotKey returns the character between the tildes, or 0 when there is none.
204func hotKey(label string) rune {
205 first := strings.IndexByte(label, '~')
206 if first < 0 || first+1 >= len(label) {
207 return 0
208 }
209 return rune(label[first+1])
210}
211
212// readFile returns a file's contents.
213func readFile(t *testing.T, path string) string {
214 t.Helper()
215
216 data, err := os.ReadFile(path)
217 if err != nil {
218 t.Fatalf("reading %s: %v", path, err)
219 }
220 return string(data)
221}
222
223func TestAToolWithNoOutputDefaultsToAPopup(t *testing.T) {
224 dir := project(t, "[[tool]]\nname = \"Test\"\ncommand = \"go test ./...\"\n")
225
226 tool := load(t, dir).Tools()[0]
227 if tool.Output != "" {
228 t.Errorf("Output = %q, want it left empty in the file", tool.Output)
229 }
230 if got := tool.Where(); got != OutputPopup {
231 t.Errorf("Where() = %q, want %q", got, OutputPopup)
232 }
233}
234
235func TestEveryOutputTheFileMayNameIsAccepted(t *testing.T) {
236 for _, output := range outputs {
237 t.Run(string(output), func(t *testing.T) {
238 dir := project(t, "[[tool]]\nname = \"X\"\ncommand = \"true\"\noutput = \""+string(output)+"\"\n")
239
240 if got := load(t, dir).Tools()[0].Where(); got != output {
241 t.Errorf("Where() = %q, want %q", got, output)
242 }
243 })
244 }
245}
246
247func TestAnUnknownOutputIsRefusedRatherThanCorrected(t *testing.T) {
248 // Silently falling back would send the output somewhere the file did not
249 // ask for, and "termnial" would look like it worked.
250 dir := project(t, "[[tool]]\nname = \"Test\"\ncommand = \"true\"\noutput = \"termnial\"\n")
251
252 _, err := Load(testProfile(), dir)
253 if err == nil {
254 t.Fatal("Load() accepted an output nobody defined")
255 }
256 for _, want := range []string{"Test", "termnial", "popup", "terminal", "editor"} {
257 if !strings.Contains(err.Error(), want) {
258 t.Errorf("the error never mentions %q: %v", want, err)
259 }
260 }
261}
262
263func TestAToolWithNoMenuGoesIntoTheEditorsToolchainMenu(t *testing.T) {
264 dir := project(t, "[[tool]]\nname = \"Test\"\ncommand = \"go test ./...\"\n")
265
266 if got := load(t, dir).Tools()[0].Menu; got != menu {
267 t.Errorf("MenuName() = %q, want %q", got, menu)
268 }
269}
270
271func TestAToolCanNameItsOwnMenu(t *testing.T) {
272 dir := project(t, "[[tool]]\nname = \"Echo\"\ncommand = \"echo x\"\nmenu = \"Tools\"\n")
273
274 if got := load(t, dir).Tools()[0].Menu; got != "Tools" {
275 t.Errorf("MenuName() = %q, want Tools", got)
276 }
277}
278
279func TestMenuNamesStartWithTheToolchainMenuAndFollowTheFile(t *testing.T) {
280 // Go is always first, whether or not a tool named it: the item that creates
281 // the tools file has to live somewhere even when there is no file.
282 dir := project(t, `
283[[tool]]
284name = "Up"
285command = "docker compose up"
286menu = "Docker"
287
288[[tool]]
289name = "Echo"
290command = "echo x"
291menu = "Tools"
292
293[[tool]]
294name = "Down"
295command = "docker compose down"
296menu = "Docker"
297`)
298
299 got := load(t, dir).MenuNames()
300 want := []string{menu, "Docker", "Tools"}
301 if len(got) != len(want) {
302 t.Fatalf("MenuNames() = %v, want %v", got, want)
303 }
304 for i := range want {
305 if got[i] != want[i] {
306 t.Errorf("menu %d = %q, want %q", i, got[i], want[i])
307 }
308 }
309}
310
311func TestTheToolchainMenuIsTheOnlyOneWhenNothingNamesAnother(t *testing.T) {
312 dir := project(t, "[[tool]]\nname = \"Test\"\ncommand = \"go test\"\n")
313
314 if got := load(t, dir).MenuNames(); len(got) != 1 || got[0] != menu {
315 t.Errorf("MenuNames() = %v, want just %q", got, menu)
316 }
317}
318
319func TestTheToolchainMenuExistsEvenWithNoToolsAtAll(t *testing.T) {
320 // A project with no tools file still gets the menu, because that is where
321 // the item creating one lives.
322 list, err := Load(testProfile(), t.TempDir())
323 if err != nil {
324 t.Fatalf("Load() error = %v", err)
325 }
326
327 if got := list.MenuNames(); len(got) != 1 || got[0] != menu {
328 t.Errorf("MenuNames() = %v, want just %q", got, menu)
329 }
330}
331
332func TestInReturnsOneMenusToolsInFileOrder(t *testing.T) {
333 dir := project(t, `
334[[tool]]
335name = "a"
336command = "true"
337menu = "Tools"
338
339[[tool]]
340name = "b"
341command = "true"
342
343[[tool]]
344name = "c"
345command = "true"
346menu = "Tools"
347`)
348 list := load(t, dir)
349
350 mine := list.In("Tools")
351 if len(mine) != 2 || mine[0].Name != "a" || mine[1].Name != "c" {
352 t.Errorf("In(\"Tools\") = %v", mine)
353 }
354 if goTools := list.In(menu); len(goTools) != 1 || goTools[0].Name != "b" {
355 t.Errorf("In(%q) = %v", menu, goTools)
356 }
357 if none := list.In("Nowhere"); none != nil {
358 t.Errorf("In(\"Nowhere\") = %v, want nothing", none)
359 }
360}
361
362func TestCreateWritesTheProfilesTemplateVerbatim(t *testing.T) {
363 // The commands themselves belong to the editor, not to this package: what
364 // is checked here is that the file written is the one the profile carries,
365 // byte for byte. What is *in* Turbo Go's template is Turbo Go's own test.
366 dir := t.TempDir()
367
368 if _, err := Create(testProfile(), dir); err != nil {
369 t.Fatalf("Create() error = %v", err)
370 }
371
372 if got := readFile(t, Path(testProfile(), dir)); got != testToolsTemplate {
373 t.Errorf("Create() wrote:\n%s\nwant the profile's template:\n%s", got, testToolsTemplate)
374 }
375}
376
377func TestDefaultMenuNameStripsTheHotKeyMarkers(t *testing.T) {
378 // A tools file writes the plain name; the hot key is the editor's to
379 // assign, because only it knows which letters the other menus have taken.
380 tests := map[string]string{"~G~o": "Go", "Rus~t~": "Rust", "Zig": "Zig"}
381
382 for label, want := range tests {
383 if got := DefaultMenuName(profile.Profile{ToolsMenu: label}); got != want {
384 t.Errorf("DefaultMenuName(%q) = %q, want %q", label, got, want)
385 }
386 }
387}