turbo-editors/turbo-golopublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-golo.git
git clone ssh://git@rickub.com/turbo-editors/turbo-golo.git

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

templates_test.go · 453 lines · 13.9 KBGo Blame HistoryRaw
📦 Turbo Golo d710c1b k33g 15h ago1package gololang
2
3import (
4 "fmt"
5 "os"
6 "regexp"
7 "strings"
8 "testing"
9
10 "rickub.com/turbo-editors/turbo-core/settings"
11 "rickub.com/turbo-editors/turbo-core/snippets"
12 "rickub.com/turbo-editors/turbo-core/syntax"
13 "rickub.com/turbo-editors/turbo-core/tools"
14)
15
16// The starter files Turbo Golo writes are the one part of a project's
17// .turbo-golo directory that is about Golo, so this is where what is *in*
18// them is checked. That the file written is the profile's template at all is
19// turbo-core's test.
20
21// noUserSnippets points the user's own snippets at an empty directory, so a
22// test never reads whoever is running it.
23func noUserSnippets(t *testing.T) {
24 t.Helper()
25 t.Setenv(Profile().SnippetDirEnvVar(), t.TempDir())
26}
27
28// createTools writes a project's tools file and returns the project directory.
29func createTools(t *testing.T) string {
30 t.Helper()
31
32 dir := t.TempDir()
33 if _, err := tools.Create(Profile(), dir); err != nil {
34 t.Fatalf("tools.Create() error = %v", err)
35 }
36 return dir
37}
38
39// createSnippets writes a project's snippets file and returns the directory.
40func createSnippets(t *testing.T) string {
41 t.Helper()
42 noUserSnippets(t)
43
44 dir := t.TempDir()
45 if _, err := snippets.Create(Profile(), dir); err != nil {
46 t.Fatalf("snippets.Create() error = %v", err)
47 }
48 return dir
49}
50
51// createSettings writes a project's settings file and returns the directory.
52func createSettings(t *testing.T) string {
53 t.Helper()
54
55 dir := t.TempDir()
56 if _, err := settings.Create(Profile(), dir, "turbo-classic"); err != nil {
57 t.Fatalf("settings.Create() error = %v", err)
58 }
59 return dir
60}
61
62// readFile returns a file's contents.
63func readFile(t *testing.T, path string) string {
64 t.Helper()
65
66 data, err := os.ReadFile(path)
67 if err != nil {
68 t.Fatalf("reading %s: %v", path, err)
69 }
70 return string(data)
71}
72
73// --- the formatting contract ------------------------------------------------
74
75// profile.Templates documents how many verbs each template takes, and nothing
76// enforces it. A template with the wrong number produces %!q(MISSING) or
77// %!(EXTRA …) in a file that is written into somebody's project, opened, and
78// wrong — Go writes the marker into the output rather than failing.
79
80func TestEachTemplateTakesTheVerbsItsContractSays(t *testing.T) {
81 cases := []struct {
82 name string
83 template string
84 verb string
85 want int
86 }{
87 {"Settings", settingsTemplate, "%q", 2},
88 {"Snippets", snippetsTemplate, "%s", 2},
89 {"Tools", toolsTemplate, "%", 0},
90 }
91
92 for _, c := range cases {
93 if got := strings.Count(c.template, c.verb); got != c.want {
94 t.Errorf("%s template has %d %q verbs, want %d", c.name, got, c.verb, c.want)
95 }
96 }
97}
98
99func TestFillingATemplateLeavesNoMissingMarker(t *testing.T) {
100 filled := map[string]string{
101 "settings": fmt.Sprintf(settingsTemplate, "turbo-classic", "500ms"),
102 "snippets": fmt.Sprintf(snippetsTemplate, "Snippets", "/home/someone/.config/turbo-golo/snippets.toml"),
103 "tools": toolsTemplate,
104 }
105
106 for name, text := range filled {
107 if at := strings.Index(text, "%!"); at >= 0 {
108 t.Errorf("the %s template filled in with %q — the wrong number of verbs", name, text[at:min(at+24, len(text))])
109 }
110 }
111}
112
113// --- what the files say -----------------------------------------------------
114
115func TestNoTemplateNamesTheEditorThisOneWasAdaptedFrom(t *testing.T) {
116 // A leftover turbo-moonbit in a file written into somebody's Golo
117 // project is invisible to every other test here.
118 //
119 // Whole words, because turbo-go is a prefix of turbo-golo: a plain
120 // substring check would fail on this editor's own name.
121 strangers := regexp.MustCompile(`(?i)\b(turbo-moonbit|turbo-python|turbo-rust|turbo-go|moonbitlang|pythonlang|rustlang|golang|moonbit|moon|mbt|pyproject|cargo|pytest|clippy|gopls|pylsp)\b`)
122
123 for name, template := range map[string]string{
124 "settings": settingsTemplate,
125 "snippets": snippetsTemplate,
126 "tools": toolsTemplate,
127 } {
128 if stranger := strangers.FindString(template); stranger != "" {
129 t.Errorf("the %s template still says %q", name, stranger)
130 }
131 }
132}
133
134func TestTheSettingsFileTurnsAutosaveOn(t *testing.T) {
135 // A project that has gone to the trouble of creating a settings file has
136 // said what it wants. settings.Default() — what applies with no file at
137 // all — stays off, and that is checked below.
138 dir := createSettings(t)
139
140 loaded, err := settings.Load(Profile(), dir)
141 if err != nil {
142 t.Fatalf("settings.Load() error = %v", err)
143 }
144 if !loaded.Autosave {
145 t.Error("the starter settings file leaves autosave off, want it on")
146 }
147 if settings.Default().Autosave {
148 t.Error("settings.Default() has autosave on; the two statements have drifted together")
149 }
150}
151
152func TestTheSettingsFileNamesTheThemeItWasCreatedWith(t *testing.T) {
153 dir := createSettings(t)
154
155 loaded, err := settings.Load(Profile(), dir)
156 if err != nil {
157 t.Fatalf("settings.Load() error = %v", err)
158 }
159 if loaded.Theme != "turbo-classic" {
160 t.Errorf("theme = %q, want %q", loaded.Theme, "turbo-classic")
161 }
162}
163
164func TestTheSnippetsCommentNamesEveryLanguageTheEditorKnows(t *testing.T) {
165 // The comment is where a user finds out what they may write in a
166 // `languages` key. It fell behind the registry once already in this family,
167 // when turbo-core learnt YAML, XML and Dockerfiles — so the list is read
168 // from the registry rather than written down here.
169 Register()
170
171 list := languageListOf(t, snippetsTemplate)
172 for _, language := range syntax.Registered() {
173 if !strings.Contains(list, language.String()) {
174 t.Errorf("the snippets template's languages comment does not name %q; it reads %q", language, list)
175 }
176 }
177}
178
179// languageListOf returns the one sentence of the snippets template that lists
180// the language names, with its comment marks stripped.
181//
182// Only that sentence will do. Every snippet body below it carries a languages
183// key naming Golo, and the file's own first line names turbo-golo — so a
184// check against the whole template, or even against all of its comments, would
185// pass with the list itself saying nothing at all.
186func languageListOf(t *testing.T, template string) string {
187 t.Helper()
188
189 const marker = "editor uses:"
190 at := strings.Index(template, marker)
191 if at < 0 {
192 t.Fatalf("the snippets template no longer introduces its language list with %q", marker)
193 }
194
195 rest := template[at+len(marker):]
196 end := strings.Index(rest, ".")
197 if end < 0 {
198 t.Fatal("the snippets template's language list does not end in a full stop")
199 }
200 return strings.ReplaceAll(rest[:end], "#", "")
201}
202
203func TestEverySnippetLoadsAndIsForGolo(t *testing.T) {
204 Register()
205 dir := createSnippets(t)
206
207 list, err := snippets.Load(Profile(), dir)
208 if err != nil {
209 t.Fatalf("snippets.Load() error = %v", err)
210 }
211 if list.Len() == 0 {
212 t.Fatal("the starter snippets file holds none")
213 }
214
215 groups := list.Groups(Language.String())
216 var found bool
217 for _, group := range groups {
218 if group.Name == "Golo" {
219 found = true
220 }
221 }
222 if !found {
223 t.Errorf("no Golo group among %v", groups)
224 }
225}
226
227func TestSnippetBodiesAreIndentedTheWayGoloExamplesAre(t *testing.T) {
228 // Every example in the GoloScript documentation and its own templates
229 // indents with two spaces. Golo has no formatter, so the convention is the
230 // only authority, and a snippet that disagrees with it stands out in every
231 // file it is inserted into.
232 Register()
233 dir := createSnippets(t)
234
235 list, err := snippets.Load(Profile(), dir)
236 if err != nil {
237 t.Fatalf("snippets.Load() error = %v", err)
238 }
239
240 for _, group := range list.Groups(Language.String()) {
241 for _, snippet := range group.Snippets {
242 for _, line := range strings.Split(snippet.Body, "\n") {
243 if strings.Contains(line, "\t") {
244 t.Errorf("snippet %q has a tab in %q", snippet.Name, line)
245 }
246 indent := len(line) - len(strings.TrimLeft(line, " "))
247 if indent%2 != 0 {
248 t.Errorf("snippet %q indents %q by %d spaces, want a multiple of two", snippet.Name, line, indent)
249 }
250 }
251 }
252 }
253}
254
255func TestTheSnippetsFileIsTOMLWithLiteralBodies(t *testing.T) {
256 // A Golo string carries \n and \" the way a Go string does, and TOML
257 // interprets those escapes in a basic string before the editor ever sees
258 // them — so a snippet with an escaped quote would be inserted with the
259 // escape already resolved and the Golo broken. That the file parses is what
260 // createSnippets proves; that it really does hold a backslash is what makes
261 // the proof mean something.
262 Register()
263 dir := createSnippets(t)
264
265 written := readFile(t, snippets.ProjectPath(Profile(), dir))
266 if !strings.Contains(written, `\"`) {
267 t.Fatal("no snippet in the starter file escapes a quote, so nothing here tests the literal-string decision")
268 }
269 for _, line := range strings.Split(written, "\n") {
270 if strings.HasPrefix(line, `body = """`) {
271 t.Errorf("a body is opened with a TOML basic multi-line string: %q", line)
272 }
273 }
274}
275
276func TestEveryToolLoadsAndRunsGoloScript(t *testing.T) {
277 dir := createTools(t)
278
279 list, err := tools.Load(Profile(), dir)
280 if err != nil {
281 t.Fatalf("tools.Load() error = %v", err)
282 }
283 if list.Len() == 0 {
284 t.Fatal("the starter tools file holds none")
285 }
286
287 for _, tool := range list.In("Golo") {
288 if !runsGoloScript(tool.Command) {
289 t.Errorf("tool %q in the Golo menu runs %q, which is none of golo, gogolo or wagolo", tool.Name, tool.Command)
290 }
291 }
292}
293
294// runsGoloScript reports whether a command starts one of GoloScript's three
295// binaries: the interpreter, or either compiler.
296func runsGoloScript(command string) bool {
297 for _, binary := range []string{"golo", "gogolo", "wagolo"} {
298 if command == binary || strings.HasPrefix(command, binary+" ") {
299 return true
300 }
301 }
302 return false
303}
304
305func TestTheToolsFileShowsBothInvisibleFeatures(t *testing.T) {
306 // A {{placeholder}} and the `menu` key are invisible unless the starter
307 // file demonstrates them, and the starter file is where anyone learns they
308 // exist at all.
309 dir := createTools(t)
310
311 list, err := tools.Load(Profile(), dir)
312 if err != nil {
313 t.Fatalf("tools.Load() error = %v", err)
314 }
315
316 var asks, elsewhere int
317 for _, tool := range list.Tools() {
318 if len(tool.Placeholders()) > 0 {
319 asks++
320 }
321 if tool.Menu != list.DefaultMenu() {
322 elsewhere++
323 }
324 }
325 if asks == 0 {
326 t.Error("no tool asks for a value, so nothing shows the {{placeholder}} form")
327 }
328 if elsewhere == 0 {
329 t.Error("no tool names a menu of its own, so nothing shows the menu key")
330 }
331}
332
333func TestTheDefaultMenuIsTheGoloOne(t *testing.T) {
334 dir := createTools(t)
335
336 list, err := tools.Load(Profile(), dir)
337 if err != nil {
338 t.Fatalf("tools.Load() error = %v", err)
339 }
340 if got := list.DefaultMenu(); got != "Golo" {
341 t.Errorf("DefaultMenu() = %q, want %q", got, "Golo")
342 }
343}
344
345func TestNoTwoToolsInOneMenuClaimTheSameHotKey(t *testing.T) {
346 dir := createTools(t)
347
348 list, err := tools.Load(Profile(), dir)
349 if err != nil {
350 t.Fatalf("tools.Load() error = %v", err)
351 }
352
353 for _, menu := range list.MenuNames() {
354 taken := map[rune]string{}
355 for _, tool := range list.In(menu) {
356 key, ok := hotKey(tool.Name)
357 if !ok {
358 continue
359 }
360 if other, clash := taken[key]; clash {
361 t.Errorf("in the %s menu, %q and %q both claim %q", menu, other, tool.Name, key)
362 }
363 taken[key] = tool.Name
364 }
365 }
366}
367
368// hotKey returns the upper-case letter a tool's name marks between tildes.
369func hotKey(name string) (rune, bool) {
370 open := strings.Index(name, "~")
371 if open < 0 || len(name) < open+3 || name[open+2] != '~' {
372 return 0, false
373 }
374 return []rune(strings.ToUpper(name[open+1 : open+2]))[0], true
375}
376
377func TestTheRunToolGetsATerminal(t *testing.T) {
378 // A program that reads the keyboard has to be answerable, and one that runs
379 // long has to be interruptible. A popup is neither.
380 dir := createTools(t)
381
382 list, err := tools.Load(Profile(), dir)
383 if err != nil {
384 t.Fatalf("tools.Load() error = %v", err)
385 }
386
387 for _, tool := range list.Tools() {
388 if strings.HasPrefix(tool.Command, "golo {{") && tool.Output != tools.OutputTerminal {
389 t.Errorf("the run tool %q sends its output to %q, want a terminal", tool.Name, tool.Output)
390 }
391 }
392}
393
394func TestEveryPlaceholderAsksForSomething(t *testing.T) {
395 // A half-typed {{ is refused when the file is read, which tools.Load
396 // already proves. This checks the other half: that each label says what it
397 // wants, because the label is the whole of what the box shows.
398 dir := createTools(t)
399
400 list, err := tools.Load(Profile(), dir)
401 if err != nil {
402 t.Fatalf("tools.Load() error = %v", err)
403 }
404
405 for _, tool := range list.Tools() {
406 for _, placeholder := range tool.Placeholders() {
407 if strings.TrimSpace(placeholder.Label) == "" {
408 t.Errorf("tool %q has a placeholder with no label", tool.Name)
409 }
410 }
411 }
412}
413
414// The tools reference prints the starter file's table. Turbo Python's shipped
415// five rows for a file that had six, and claimed `Alt-T` for a menu whose key
416// is `Alt-P` — both inherited from Turbo Rust by a mechanical substitution that
417// only looked at identifiers. Nothing in either repository could see it.
418//
419// So the table is read out of the page and held to the file the editor
420// actually writes, in both languages.
421func TestTheToolsReferenceMatchesTheStarterFile(t *testing.T) {
422 dir := createTools(t)
423
424 list, err := tools.Load(Profile(), dir)
425 if err != nil {
426 t.Fatalf("tools.Load() error = %v", err)
427 }
428
429 for _, page := range []string{"../../docs/en/reference/golo-tools.md", "../../docs/fr/reference/golo-tools.md"} {
430 raw, err := os.ReadFile(page)
431 if err != nil {
432 t.Fatalf("reading %s: %v", page, err)
433 }
434 text := string(raw)
435
436 for _, tool := range list.Tools() {
437 if !strings.Contains(text, "| `"+tool.Name+"` |") {
438 t.Errorf("%s has no row for the tool %q", page, tool.Name)
439 }
440 if !strings.Contains(text, "`"+tool.Command+"`") {
441 t.Errorf("%s does not print the command %q", page, tool.Command)
442 }
443 }
444 if !strings.Contains(text, "`Alt-G`") {
445 t.Errorf("%s never names Alt-G, the key the Golo menu really answers to", page)
446 }
447 for _, stale := range []string{"`Alt-M`", "`Alt-T`, then", "`Alt-T`, puis", "`Alt-P`"} {
448 if strings.Contains(text, stale) {
449 t.Errorf("%s still opens the toolchain menu with %s, which belongs to another editor", page, stale)
450 }
451 }
452 }
453}