turbo-editors/turbo-moonbitpublic Fork 0
cc1f59535e31ad5d5a940f517c7375fbc8c663a0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-moonbit.git
git clone ssh://git@rickub.com/turbo-editors/turbo-moonbit.git

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

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