package snippets import ( "errors" "os" "path/filepath" "strings" "testing" "rickub.com/turbo-editors/turbo-core/profile" ) // project builds a project directory holding a snippets file. func project(t *testing.T, contents string) string { t.Helper() dir := t.TempDir() if contents == "" { return dir } path := ProjectPath(testProfile(), dir) if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("creating the snippets directory: %v", err) } if err := os.WriteFile(path, []byte(contents), 0o644); err != nil { t.Fatalf("writing the snippets file: %v", err) } return dir } // testProfile is a fictional editor. Its template is a real snippets file, but // a small one: what is in Turbo Go's template is Turbo Go's own test. func testProfile() profile.Profile { return profile.Profile{ Name: "Turbo Test", Slug: "turbo-test", Templates: profile.Templates{Snippets: testSnippetsTemplate}, } } // testSnippetsTemplate takes the name of the ungrouped group and the user's own // snippets path, in that order, as profile.Templates says it must. const testSnippetsTemplate = `# turbo-test snippets. # # A snippet with no group goes into %s. # Your own snippets live in: # %s [[snippet]] name = "guard" group = "Test" languages = ["test"] body = """ if broken { return }""" [[snippet]] name = "TODO" body = "TODO: " ` // noUserSnippets points the user's snippets at an empty directory, so a test // never reads whoever is running it. func noUserSnippets(t *testing.T) { t.Helper() t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir()) } // userSnippets writes a user-level snippets file and points the package at it. func userSnippets(t *testing.T, contents string) { t.Helper() dir := t.TempDir() t.Setenv(testProfile().SnippetDirEnvVar(), dir) if err := os.WriteFile(filepath.Join(dir, FileName), []byte(contents), 0o644); err != nil { t.Fatalf("writing the user snippets file: %v", err) } } // load reads a project's snippets, failing the test if it cannot. func load(t *testing.T, dir string) List { t.Helper() list, err := Load(testProfile(), dir) if err != nil { t.Fatalf("Load(testProfile(), %q) error = %v", dir, err) } return list } // summary renders the groups as "Group/name" strings, in order. func summary(groups []Group) []string { var out []string for _, group := range groups { for _, snippet := range group.Snippets { out = append(out, group.Name+"/"+snippet.Name) } } return out } // wantGroups compares the grouped snippets against what they should be. func wantGroups(t *testing.T, groups []Group, want ...string) { t.Helper() got := summary(groups) if len(got) != len(want) { t.Fatalf("got\n %v\nwant\n %v", got, want) } for i := range want { if got[i] != want[i] { t.Errorf("entry %d = %q, want %q (all: %v)", i, got[i], want[i], got) } } } func TestLoadReadsAProjectsSnippets(t *testing.T) { noUserSnippets(t) dir := project(t, ` [[snippet]] name = "if err" group = "Go" body = "if err != nil {}" [[snippet]] name = "TODO" body = "TODO: " `) list := load(t, dir) if list.Len() != 2 { t.Fatalf("Len() = %d, want 2", list.Len()) } wantGroups(t, list.Groups("go"), "Go/if err", "General/TODO") } func TestASnippetWithNoGroupGoesIntoTheGeneralOne(t *testing.T) { noUserSnippets(t) dir := project(t, "[[snippet]]\nname = \"x\"\nbody = \"y\"\n") wantGroups(t, load(t, dir).Groups(""), ungroupedName+"/x") } func TestGroupsKeepTheOrderOfTheFile(t *testing.T) { // The menu should match the file, so someone reordering the file sees the // menu reorder. noUserSnippets(t) dir := project(t, ` [[snippet]] name = "b" group = "Second" body = "x" [[snippet]] name = "a" group = "First" body = "x" [[snippet]] name = "c" group = "Second" body = "x" `) wantGroups(t, load(t, dir).Groups(""), "Second/b", "Second/c", "First/a") } func TestALanguageFiltersTheSnippetsOffered(t *testing.T) { noUserSnippets(t) dir := project(t, ` [[snippet]] name = "go thing" group = "Go" languages = ["go"] body = "x" [[snippet]] name = "shell thing" group = "Shell" languages = ["bash"] body = "x" [[snippet]] name = "anywhere" body = "x" `) list := load(t, dir) wantGroups(t, list.Groups("go"), "Go/go thing", "General/anywhere") wantGroups(t, list.Groups("bash"), "Shell/shell thing", "General/anywhere") wantGroups(t, list.Groups("markdown"), "General/anywhere") } func TestAGroupLeftEmptyByFilteringDoesNotAppear(t *testing.T) { noUserSnippets(t) dir := project(t, "[[snippet]]\nname = \"x\"\ngroup = \"Go\"\nlanguages = [\"go\"]\nbody = \"y\"\n") if groups := load(t, dir).Groups("markdown"); len(groups) != 0 { t.Errorf("Groups() = %v, want no group at all", summary(groups)) } } func TestASnippetForSeveralLanguagesAppliesToEach(t *testing.T) { noUserSnippets(t) dir := project(t, "[[snippet]]\nname = \"x\"\nlanguages = [\"go\", \"bash\"]\nbody = \"y\"\n") list := load(t, dir) for _, language := range []string{"go", "bash"} { if got := len(list.Groups(language)); got != 1 { t.Errorf("Groups(%q) gave %d groups, want 1", language, got) } } if got := len(list.Groups("html")); got != 0 { t.Errorf("Groups(\"html\") gave %d groups, want none", got) } } func TestTheUsersSnippetsAndTheProjectsAreBothOffered(t *testing.T) { userSnippets(t, "[[snippet]]\nname = \"mine\"\ngroup = \"Mine\"\nbody = \"x\"\n") dir := project(t, "[[snippet]]\nname = \"theirs\"\ngroup = \"Theirs\"\nbody = \"x\"\n") // The user's come first, so a project adds to what you already have. wantGroups(t, load(t, dir).Groups(""), "Mine/mine", "Theirs/theirs") } func TestTheProjectWinsWhenANameClashes(t *testing.T) { // The project's file is the more specific statement of the two. userSnippets(t, "[[snippet]]\nname = \"header\"\ngroup = \"Go\"\nbody = \"mine\"\n") dir := project(t, "[[snippet]]\nname = \"header\"\ngroup = \"Go\"\nbody = \"theirs\"\n") groups := load(t, dir).Groups("") if len(groups) != 1 || len(groups[0].Snippets) != 1 { t.Fatalf("got %v, want one snippet", summary(groups)) } if got := groups[0].Snippets[0].Body; got != "theirs" { t.Errorf("body = %q, want the project's", got) } } func TestTheSameNameInADifferentGroupIsADifferentSnippet(t *testing.T) { userSnippets(t, "[[snippet]]\nname = \"header\"\ngroup = \"Go\"\nbody = \"a\"\n") dir := project(t, "[[snippet]]\nname = \"header\"\ngroup = \"Shell\"\nbody = \"b\"\n") wantGroups(t, load(t, dir).Groups(""), "Go/header", "Shell/header") } func TestAProjectWithNoSnippetsFileIsNotAnError(t *testing.T) { noUserSnippets(t) list := load(t, t.TempDir()) if list.Len() != 0 { t.Errorf("Len() = %d, want 0", list.Len()) } if got := len(list.Groups("go")); got != 0 { t.Errorf("Groups() gave %d groups", got) } } func TestAFileThatIsNotTOMLIsReported(t *testing.T) { // A typo must be reported rather than silently dropping every snippet. noUserSnippets(t) dir := project(t, "[[snippet]\nname = ") _, err := Load(testProfile(), dir) if err == nil { t.Fatal("Load(testProfile()) accepted a file that is not TOML") } if !strings.Contains(err.Error(), ProjectPath(testProfile(), dir)) { t.Errorf("the error does not name the file: %v", err) } } func TestASnippetWithNoNameOrNoBodyIsRefused(t *testing.T) { noUserSnippets(t) for name, contents := range map[string]string{ "no name": "[[snippet]]\nbody = \"x\"\n", "no body": "[[snippet]]\nname = \"x\"\n", } { t.Run(name, func(t *testing.T) { if _, err := Load(testProfile(), project(t, contents)); err == nil { t.Errorf("Load(testProfile()) accepted a snippet with %s", name) } }) } } func TestADirectoryWhereTheFileGoesCountsAsAbsent(t *testing.T) { noUserSnippets(t) dir := t.TempDir() if err := os.MkdirAll(ProjectPath(testProfile(), dir), 0o755); err != nil { t.Fatalf("creating a directory where the file goes: %v", err) } if Exists(testProfile(), dir) { t.Error("Exists(testProfile()) called a directory a snippets file") } } func TestCreateWritesAFileThatLoadsBack(t *testing.T) { noUserSnippets(t) dir := t.TempDir() path, err := Create(testProfile(), dir) if err != nil { t.Fatalf("Create(testProfile()) error = %v", err) } if path != ProjectPath(testProfile(), dir) { t.Errorf("Create(testProfile()) returned %q, want %q", path, ProjectPath(testProfile(), dir)) } if !Exists(testProfile(), dir) { t.Fatal("Create(testProfile()) reported success but wrote no file") } list := load(t, dir) if list.Len() == 0 { t.Fatal("the created file holds no snippets") } // The examples in it must be usable, not just parseable. for _, group := range list.Groups("go") { for _, snippet := range group.Snippets { if snippet.Name == "" || snippet.Body == "" { t.Errorf("the created file holds an unusable snippet %+v", snippet) } } } } func TestCreateRefusesToOverwriteAnExistingFile(t *testing.T) { noUserSnippets(t) original := "[[snippet]]\nname = \"mine\"\nbody = \"hand written\"\n" dir := project(t, original) _, err := Create(testProfile(), dir) if !errors.Is(err, ErrExists) { t.Fatalf("Create(testProfile()) error = %v, want ErrExists", err) } if got := readFile(t, ProjectPath(testProfile(), dir)); got != original { t.Errorf("the existing file was changed:\n%s", got) } } func TestUserDirFollowsTheEnvironmentVariable(t *testing.T) { t.Setenv(testProfile().SnippetDirEnvVar(), "/somewhere") if got := UserDir(testProfile()); got != "/somewhere" { t.Errorf("UserDir(testProfile()) = %q", got) } if got, want := UserPath(testProfile()), filepath.Join("/somewhere", FileName); got != want { t.Errorf("UserPath(testProfile()) = %q, want %q", got, want) } } // 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) } func TestCreateWritesTheProfilesTemplateWithItsBlanksFilledIn(t *testing.T) { // Two things the template asks for and cannot know itself: the name of the // group an ungrouped snippet falls into, and where the user's own file is. // What is *in* Turbo Go's template is Turbo Go's own test. userDir := t.TempDir() t.Setenv(testProfile().SnippetDirEnvVar(), userDir) dir := t.TempDir() if _, err := Create(testProfile(), dir); err != nil { t.Fatalf("Create() error = %v", err) } contents := readFile(t, ProjectPath(testProfile(), dir)) for _, want := range []string{ungroupedName, UserPath(testProfile())} { if !strings.Contains(contents, want) { t.Errorf("the created file never mentions %q:\n%s", want, contents) } } } func TestCreateSaysSoWhenThereIsNowhereForTheUsersOwnFile(t *testing.T) { // A system with no configuration directory leaves UserPath empty, and a // comment reading "Your own snippets go in:" followed by nothing is worse // than one that says there is nowhere. t.Setenv(testProfile().SnippetDirEnvVar(), "") t.Setenv("XDG_CONFIG_HOME", "") t.Setenv("HOME", "") if UserPath(testProfile()) != "" { t.Skip("this system still reports a configuration directory") } dir := t.TempDir() if _, err := Create(testProfile(), dir); err != nil { t.Fatalf("Create() error = %v", err) } contents := readFile(t, ProjectPath(testProfile(), dir)) if !strings.Contains(contents, "no configuration directory") { t.Errorf("the created file does not say there is nowhere to put user snippets:\n%s", contents) } }