package gololang import ( "fmt" "os" "regexp" "strings" "testing" "rickub.com/turbo-editors/turbo-core/settings" "rickub.com/turbo-editors/turbo-core/snippets" "rickub.com/turbo-editors/turbo-core/syntax" "rickub.com/turbo-editors/turbo-core/tools" ) // The starter files Turbo Golo writes are the one part of a project's // .turbo-golo directory that is about Golo, so this is where what is *in* // them is checked. That the file written is the profile's template at all is // turbo-core's test. // noUserSnippets points the user's own snippets at an empty directory, so a // test never reads whoever is running it. func noUserSnippets(t *testing.T) { t.Helper() t.Setenv(Profile().SnippetDirEnvVar(), t.TempDir()) } // createTools writes a project's tools file and returns the project directory. func createTools(t *testing.T) string { t.Helper() dir := t.TempDir() if _, err := tools.Create(Profile(), dir); err != nil { t.Fatalf("tools.Create() error = %v", err) } return dir } // createSnippets writes a project's snippets file and returns the directory. func createSnippets(t *testing.T) string { t.Helper() noUserSnippets(t) dir := t.TempDir() if _, err := snippets.Create(Profile(), dir); err != nil { t.Fatalf("snippets.Create() error = %v", err) } return dir } // createSettings writes a project's settings file and returns the directory. func createSettings(t *testing.T) string { t.Helper() dir := t.TempDir() if _, err := settings.Create(Profile(), dir, "turbo-classic"); err != nil { t.Fatalf("settings.Create() error = %v", err) } return dir } // readFile returns a file's contents. func readFile(t *testing.T, path string) string { t.Helper() data, err := os.ReadFile(path) if err != nil { t.Fatalf("reading %s: %v", path, err) } return string(data) } // --- the formatting contract ------------------------------------------------ // profile.Templates documents how many verbs each template takes, and nothing // enforces it. A template with the wrong number produces %!q(MISSING) or // %!(EXTRA …) in a file that is written into somebody's project, opened, and // wrong — Go writes the marker into the output rather than failing. func TestEachTemplateTakesTheVerbsItsContractSays(t *testing.T) { cases := []struct { name string template string verb string want int }{ {"Settings", settingsTemplate, "%q", 2}, {"Snippets", snippetsTemplate, "%s", 2}, {"Tools", toolsTemplate, "%", 0}, } for _, c := range cases { if got := strings.Count(c.template, c.verb); got != c.want { t.Errorf("%s template has %d %q verbs, want %d", c.name, got, c.verb, c.want) } } } func TestFillingATemplateLeavesNoMissingMarker(t *testing.T) { filled := map[string]string{ "settings": fmt.Sprintf(settingsTemplate, "turbo-classic", "500ms"), "snippets": fmt.Sprintf(snippetsTemplate, "Snippets", "/home/someone/.config/turbo-golo/snippets.toml"), "tools": toolsTemplate, } for name, text := range filled { if at := strings.Index(text, "%!"); at >= 0 { t.Errorf("the %s template filled in with %q — the wrong number of verbs", name, text[at:min(at+24, len(text))]) } } } // --- what the files say ----------------------------------------------------- func TestNoTemplateNamesTheEditorThisOneWasAdaptedFrom(t *testing.T) { // A leftover turbo-moonbit in a file written into somebody's Golo // project is invisible to every other test here. // // Whole words, because turbo-go is a prefix of turbo-golo: a plain // substring check would fail on this editor's own name. 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`) for name, template := range map[string]string{ "settings": settingsTemplate, "snippets": snippetsTemplate, "tools": toolsTemplate, } { if stranger := strangers.FindString(template); stranger != "" { t.Errorf("the %s template still says %q", name, stranger) } } } func TestTheSettingsFileTurnsAutosaveOn(t *testing.T) { // A project that has gone to the trouble of creating a settings file has // said what it wants. settings.Default() — what applies with no file at // all — stays off, and that is checked below. dir := createSettings(t) loaded, err := settings.Load(Profile(), dir) if err != nil { t.Fatalf("settings.Load() error = %v", err) } if !loaded.Autosave { t.Error("the starter settings file leaves autosave off, want it on") } if settings.Default().Autosave { t.Error("settings.Default() has autosave on; the two statements have drifted together") } } func TestTheSettingsFileNamesTheThemeItWasCreatedWith(t *testing.T) { dir := createSettings(t) loaded, err := settings.Load(Profile(), dir) if err != nil { t.Fatalf("settings.Load() error = %v", err) } if loaded.Theme != "turbo-classic" { t.Errorf("theme = %q, want %q", loaded.Theme, "turbo-classic") } } func TestTheSnippetsCommentNamesEveryLanguageTheEditorKnows(t *testing.T) { // The comment is where a user finds out what they may write in a // `languages` key. It fell behind the registry once already in this family, // when turbo-core learnt YAML, XML and Dockerfiles — so the list is read // from the registry rather than written down here. Register() list := languageListOf(t, snippetsTemplate) for _, language := range syntax.Registered() { if !strings.Contains(list, language.String()) { t.Errorf("the snippets template's languages comment does not name %q; it reads %q", language, list) } } } // languageListOf returns the one sentence of the snippets template that lists // the language names, with its comment marks stripped. // // Only that sentence will do. Every snippet body below it carries a languages // key naming Golo, and the file's own first line names turbo-golo — so a // check against the whole template, or even against all of its comments, would // pass with the list itself saying nothing at all. func languageListOf(t *testing.T, template string) string { t.Helper() const marker = "editor uses:" at := strings.Index(template, marker) if at < 0 { t.Fatalf("the snippets template no longer introduces its language list with %q", marker) } rest := template[at+len(marker):] end := strings.Index(rest, ".") if end < 0 { t.Fatal("the snippets template's language list does not end in a full stop") } return strings.ReplaceAll(rest[:end], "#", "") } func TestEverySnippetLoadsAndIsForGolo(t *testing.T) { Register() dir := createSnippets(t) list, err := snippets.Load(Profile(), dir) if err != nil { t.Fatalf("snippets.Load() error = %v", err) } if list.Len() == 0 { t.Fatal("the starter snippets file holds none") } groups := list.Groups(Language.String()) var found bool for _, group := range groups { if group.Name == "Golo" { found = true } } if !found { t.Errorf("no Golo group among %v", groups) } } func TestSnippetBodiesAreIndentedTheWayGoloExamplesAre(t *testing.T) { // Every example in the GoloScript documentation and its own templates // indents with two spaces. Golo has no formatter, so the convention is the // only authority, and a snippet that disagrees with it stands out in every // file it is inserted into. Register() dir := createSnippets(t) list, err := snippets.Load(Profile(), dir) if err != nil { t.Fatalf("snippets.Load() error = %v", err) } for _, group := range list.Groups(Language.String()) { for _, snippet := range group.Snippets { for _, line := range strings.Split(snippet.Body, "\n") { if strings.Contains(line, "\t") { t.Errorf("snippet %q has a tab in %q", snippet.Name, line) } indent := len(line) - len(strings.TrimLeft(line, " ")) if indent%2 != 0 { t.Errorf("snippet %q indents %q by %d spaces, want a multiple of two", snippet.Name, line, indent) } } } } } func TestTheSnippetsFileIsTOMLWithLiteralBodies(t *testing.T) { // A Golo string carries \n and \" the way a Go string does, and TOML // interprets those escapes in a basic string before the editor ever sees // them — so a snippet with an escaped quote would be inserted with the // escape already resolved and the Golo broken. That the file parses is what // createSnippets proves; that it really does hold a backslash is what makes // the proof mean something. Register() dir := createSnippets(t) written := readFile(t, snippets.ProjectPath(Profile(), dir)) if !strings.Contains(written, `\"`) { t.Fatal("no snippet in the starter file escapes a quote, so nothing here tests the literal-string decision") } for _, line := range strings.Split(written, "\n") { if strings.HasPrefix(line, `body = """`) { t.Errorf("a body is opened with a TOML basic multi-line string: %q", line) } } } func TestEveryToolLoadsAndRunsGoloScript(t *testing.T) { dir := createTools(t) list, err := tools.Load(Profile(), dir) if err != nil { t.Fatalf("tools.Load() error = %v", err) } if list.Len() == 0 { t.Fatal("the starter tools file holds none") } for _, tool := range list.In("Golo") { if !runsGoloScript(tool.Command) { t.Errorf("tool %q in the Golo menu runs %q, which is none of golo, gogolo or wagolo", tool.Name, tool.Command) } } } // runsGoloScript reports whether a command starts one of GoloScript's three // binaries: the interpreter, or either compiler. func runsGoloScript(command string) bool { for _, binary := range []string{"golo", "gogolo", "wagolo"} { if command == binary || strings.HasPrefix(command, binary+" ") { return true } } return false } func TestTheToolsFileShowsBothInvisibleFeatures(t *testing.T) { // A {{placeholder}} and the `menu` key are invisible unless the starter // file demonstrates them, and the starter file is where anyone learns they // exist at all. dir := createTools(t) list, err := tools.Load(Profile(), dir) if err != nil { t.Fatalf("tools.Load() error = %v", err) } var asks, elsewhere int for _, tool := range list.Tools() { if len(tool.Placeholders()) > 0 { asks++ } if tool.Menu != list.DefaultMenu() { elsewhere++ } } if asks == 0 { t.Error("no tool asks for a value, so nothing shows the {{placeholder}} form") } if elsewhere == 0 { t.Error("no tool names a menu of its own, so nothing shows the menu key") } } func TestTheDefaultMenuIsTheGoloOne(t *testing.T) { dir := createTools(t) list, err := tools.Load(Profile(), dir) if err != nil { t.Fatalf("tools.Load() error = %v", err) } if got := list.DefaultMenu(); got != "Golo" { t.Errorf("DefaultMenu() = %q, want %q", got, "Golo") } } func TestNoTwoToolsInOneMenuClaimTheSameHotKey(t *testing.T) { dir := createTools(t) list, err := tools.Load(Profile(), dir) if err != nil { t.Fatalf("tools.Load() error = %v", err) } for _, menu := range list.MenuNames() { taken := map[rune]string{} for _, tool := range list.In(menu) { key, ok := hotKey(tool.Name) if !ok { continue } if other, clash := taken[key]; clash { t.Errorf("in the %s menu, %q and %q both claim %q", menu, other, tool.Name, key) } taken[key] = tool.Name } } } // hotKey returns the upper-case letter a tool's name marks between tildes. func hotKey(name string) (rune, bool) { open := strings.Index(name, "~") if open < 0 || len(name) < open+3 || name[open+2] != '~' { return 0, false } return []rune(strings.ToUpper(name[open+1 : open+2]))[0], true } func TestTheRunToolGetsATerminal(t *testing.T) { // A program that reads the keyboard has to be answerable, and one that runs // long has to be interruptible. A popup is neither. dir := createTools(t) list, err := tools.Load(Profile(), dir) if err != nil { t.Fatalf("tools.Load() error = %v", err) } for _, tool := range list.Tools() { if strings.HasPrefix(tool.Command, "golo {{") && tool.Output != tools.OutputTerminal { t.Errorf("the run tool %q sends its output to %q, want a terminal", tool.Name, tool.Output) } } } func TestEveryPlaceholderAsksForSomething(t *testing.T) { // A half-typed {{ is refused when the file is read, which tools.Load // already proves. This checks the other half: that each label says what it // wants, because the label is the whole of what the box shows. dir := createTools(t) list, err := tools.Load(Profile(), dir) if err != nil { t.Fatalf("tools.Load() error = %v", err) } for _, tool := range list.Tools() { for _, placeholder := range tool.Placeholders() { if strings.TrimSpace(placeholder.Label) == "" { t.Errorf("tool %q has a placeholder with no label", tool.Name) } } } } // The tools reference prints the starter file's table. Turbo Python's shipped // five rows for a file that had six, and claimed `Alt-T` for a menu whose key // is `Alt-P` — both inherited from Turbo Rust by a mechanical substitution that // only looked at identifiers. Nothing in either repository could see it. // // So the table is read out of the page and held to the file the editor // actually writes, in both languages. func TestTheToolsReferenceMatchesTheStarterFile(t *testing.T) { dir := createTools(t) list, err := tools.Load(Profile(), dir) if err != nil { t.Fatalf("tools.Load() error = %v", err) } for _, page := range []string{"../../docs/en/reference/golo-tools.md", "../../docs/fr/reference/golo-tools.md"} { raw, err := os.ReadFile(page) if err != nil { t.Fatalf("reading %s: %v", page, err) } text := string(raw) for _, tool := range list.Tools() { if !strings.Contains(text, "| `"+tool.Name+"` |") { t.Errorf("%s has no row for the tool %q", page, tool.Name) } if !strings.Contains(text, "`"+tool.Command+"`") { t.Errorf("%s does not print the command %q", page, tool.Command) } } if !strings.Contains(text, "`Alt-G`") { t.Errorf("%s never names Alt-G, the key the Golo menu really answers to", page) } for _, stale := range []string{"`Alt-M`", "`Alt-T`, then", "`Alt-T`, puis", "`Alt-P`"} { if strings.Contains(text, stale) { t.Errorf("%s still opens the toolchain menu with %s, which belongs to another editor", page, stale) } } } }