package main import ( "flag" "os" "path/filepath" "slices" "testing" "rickub.com/turbo-editors/turbo-core/app" "rickub.com/turbo-editors/turbo-core/settings" "rickub.com/turbo-editors/turbo-moonbit/internal/moonbitlang" ) // withArgs runs the command line parser against a fixed argument list, and // restores the real one afterwards so tests do not affect each other. func withArgs(t *testing.T, args ...string) options { t.Helper() realArgs, realFlags := os.Args, flag.CommandLine t.Cleanup(func() { os.Args, flag.CommandLine = realArgs, realFlags }) os.Args = append([]string{"turbo-moonbit"}, args...) flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) return parseFlags() } func TestFilesAreWhatIsLeftAfterTheFlags(t *testing.T) { opts := withArgs(t, "-no-lsp", "main.mbt", "lib/x.mbt") if !opts.noLSP { t.Error("-no-lsp was not read") } if want := []string{"main.mbt", "lib/x.mbt"}; !slices.Equal(opts.files, want) { t.Errorf("files = %v, want %v", opts.files, want) } } func TestTheThemeFlagDefaultsToEmptyRatherThanToAName(t *testing.T) { // Empty is what lets "was -theme given?" still be answered afterwards, and // that is what lets the project's settings fill it in without overriding an // explicit choice. if opts := withArgs(t); opts.theme != "" { t.Errorf("theme = %q with no flag, want the empty string", opts.theme) } } func TestTheThemeFlagBeatsTheProjectSettings(t *testing.T) { project := settings.Default() project.Theme = "cobalt" if got := themeName(options{theme: "monochrome"}, project); got != "monochrome" { t.Errorf("themeName() = %q, want the flag's %q", got, "monochrome") } } func TestTheProjectSettingsBeatTheBuiltInDefault(t *testing.T) { project := settings.Default() project.Theme = "cobalt" if got := themeName(options{}, project); got != "cobalt" { t.Errorf("themeName() = %q, want the project's %q", got, "cobalt") } } func TestTheBuiltInDefaultIsUsedWhenNobodySaysOtherwise(t *testing.T) { if got := themeName(options{}, settings.Default()); got == "" { t.Error("themeName() = \"\" with nothing set, want the library's default") } } func TestTheProjectRootIsTheNearestMoonModule(t *testing.T) { // app.ProjectRoot walks up looking for the profile's RootMarkers. This is // what decides the directory moon-lsp is started in, and starting it // anywhere else is how a server answers nothing for a whole session. root := t.TempDir() nested := filepath.Join(root, "cmd", "main") if err := os.MkdirAll(nested, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "moon.mod"), []byte(`name = "u/m"`), 0o644); err != nil { t.Fatal(err) } file := filepath.Join(nested, "main.mbt") if got := app.ProjectRoot(moonbitlang.Profile(), []string{file}); got != root { t.Errorf("ProjectRoot(%q) = %q, want %q", file, got, root) } } func TestTheLegacyJSONModuleFileIsAlsoARoot(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "moon.mod.json"), []byte(`{"name":"u/m"}`), 0o644); err != nil { t.Fatal(err) } file := filepath.Join(root, "main.mbt") if got := app.ProjectRoot(moonbitlang.Profile(), []string{file}); got != root { t.Errorf("ProjectRoot(%q) = %q, want %q", file, got, root) } } func TestTheNearestModuleWinsOverTheOneAboveIt(t *testing.T) { // A workspace holds several modules. The server belongs to the one the // file is in, not to the outermost directory that happens to have a // manifest. outer := t.TempDir() inner := filepath.Join(outer, "member") if err := os.MkdirAll(inner, 0o755); err != nil { t.Fatal(err) } for _, dir := range []string{outer, inner} { if err := os.WriteFile(filepath.Join(dir, "moon.mod"), []byte(`name = "u/m"`), 0o644); err != nil { t.Fatal(err) } } file := filepath.Join(inner, "lib.mbt") if got := app.ProjectRoot(moonbitlang.Profile(), []string{file}); got != inner { t.Errorf("ProjectRoot(%q) = %q, want the nearer %q", file, got, inner) } } func TestLoadingSettingsFromADirectoryWithNoneGivesTheDefaults(t *testing.T) { // A directory somebody merely started the editor in has said nothing, and // the editor must not write to it. The starter file turns autosave on; the // default leaves it off. dir := t.TempDir() t.Chdir(dir) project, loaded := loadProjectSettings(moonbitlang.Profile()) if project == "" { t.Error("loadProjectSettings returned no project directory") } if loaded.Autosave { t.Error("autosave is on with no settings file, want it off") } } func TestABrokenSettingsFileDoesNotStopTheEditorOpening(t *testing.T) { // A broken settings file must not stop the editor opening, because the // editor is how you would fix it. dir := t.TempDir() p := moonbitlang.Profile() if err := os.MkdirAll(filepath.Join(dir, p.ProjectDir()), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(settings.Path(p, dir), []byte("this is not ["), 0o644); err != nil { t.Fatal(err) } t.Chdir(dir) if _, loaded := loadProjectSettings(p); loaded != settings.Default() { t.Errorf("loadProjectSettings() = %+v with a broken file, want the defaults", loaded) } }