turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

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

snippets_test.go · 408 lines · 11.2 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 15h ago1package snippets
2
3import (
4 "errors"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "codeberg.org/turbo-editors/turbo-core/profile"
11)
12
13// project builds a project directory holding a snippets file.
14func project(t *testing.T, contents string) string {
15 t.Helper()
16
17 dir := t.TempDir()
18 if contents == "" {
19 return dir
20 }
21 path := ProjectPath(testProfile(), dir)
22 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
23 t.Fatalf("creating the snippets directory: %v", err)
24 }
25 if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
26 t.Fatalf("writing the snippets file: %v", err)
27 }
28 return dir
29}
30
31// testProfile is a fictional editor. Its template is a real snippets file, but
32// a small one: what is in Turbo Go's template is Turbo Go's own test.
33func testProfile() profile.Profile {
34 return profile.Profile{
35 Name: "Turbo Test",
36 Slug: "turbo-test",
37 Templates: profile.Templates{Snippets: testSnippetsTemplate},
38 }
39}
40
41// testSnippetsTemplate takes the name of the ungrouped group and the user's own
42// snippets path, in that order, as profile.Templates says it must.
43const testSnippetsTemplate = `# turbo-test snippets.
44#
45# A snippet with no group goes into %s.
46# Your own snippets live in:
47# %s
48
49[[snippet]]
50name = "guard"
51group = "Test"
52languages = ["test"]
53body = """
54if broken {
55 return
56}"""
57
58[[snippet]]
59name = "TODO"
60body = "TODO: "
61`
62
63// noUserSnippets points the user's snippets at an empty directory, so a test
64// never reads whoever is running it.
65func noUserSnippets(t *testing.T) {
66 t.Helper()
67 t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir())
68}
69
70// userSnippets writes a user-level snippets file and points the package at it.
71func userSnippets(t *testing.T, contents string) {
72 t.Helper()
73
74 dir := t.TempDir()
75 t.Setenv(testProfile().SnippetDirEnvVar(), dir)
76 if err := os.WriteFile(filepath.Join(dir, FileName), []byte(contents), 0o644); err != nil {
77 t.Fatalf("writing the user snippets file: %v", err)
78 }
79}
80
81// load reads a project's snippets, failing the test if it cannot.
82func load(t *testing.T, dir string) List {
83 t.Helper()
84
85 list, err := Load(testProfile(), dir)
86 if err != nil {
87 t.Fatalf("Load(testProfile(), %q) error = %v", dir, err)
88 }
89 return list
90}
91
92// summary renders the groups as "Group/name" strings, in order.
93func summary(groups []Group) []string {
94 var out []string
95 for _, group := range groups {
96 for _, snippet := range group.Snippets {
97 out = append(out, group.Name+"/"+snippet.Name)
98 }
99 }
100 return out
101}
102
103// wantGroups compares the grouped snippets against what they should be.
104func wantGroups(t *testing.T, groups []Group, want ...string) {
105 t.Helper()
106
107 got := summary(groups)
108 if len(got) != len(want) {
109 t.Fatalf("got\n %v\nwant\n %v", got, want)
110 }
111 for i := range want {
112 if got[i] != want[i] {
113 t.Errorf("entry %d = %q, want %q (all: %v)", i, got[i], want[i], got)
114 }
115 }
116}
117
118func TestLoadReadsAProjectsSnippets(t *testing.T) {
119 noUserSnippets(t)
120 dir := project(t, `
121[[snippet]]
122name = "if err"
123group = "Go"
124body = "if err != nil {}"
125
126[[snippet]]
127name = "TODO"
128body = "TODO: "
129`)
130
131 list := load(t, dir)
132
133 if list.Len() != 2 {
134 t.Fatalf("Len() = %d, want 2", list.Len())
135 }
136 wantGroups(t, list.Groups("go"), "Go/if err", "General/TODO")
137}
138
139func TestASnippetWithNoGroupGoesIntoTheGeneralOne(t *testing.T) {
140 noUserSnippets(t)
141 dir := project(t, "[[snippet]]\nname = \"x\"\nbody = \"y\"\n")
142
143 wantGroups(t, load(t, dir).Groups(""), ungroupedName+"/x")
144}
145
146func TestGroupsKeepTheOrderOfTheFile(t *testing.T) {
147 // The menu should match the file, so someone reordering the file sees the
148 // menu reorder.
149 noUserSnippets(t)
150 dir := project(t, `
151[[snippet]]
152name = "b"
153group = "Second"
154body = "x"
155
156[[snippet]]
157name = "a"
158group = "First"
159body = "x"
160
161[[snippet]]
162name = "c"
163group = "Second"
164body = "x"
165`)
166
167 wantGroups(t, load(t, dir).Groups(""), "Second/b", "Second/c", "First/a")
168}
169
170func TestALanguageFiltersTheSnippetsOffered(t *testing.T) {
171 noUserSnippets(t)
172 dir := project(t, `
173[[snippet]]
174name = "go thing"
175group = "Go"
176languages = ["go"]
177body = "x"
178
179[[snippet]]
180name = "shell thing"
181group = "Shell"
182languages = ["bash"]
183body = "x"
184
185[[snippet]]
186name = "anywhere"
187body = "x"
188`)
189 list := load(t, dir)
190
191 wantGroups(t, list.Groups("go"), "Go/go thing", "General/anywhere")
192 wantGroups(t, list.Groups("bash"), "Shell/shell thing", "General/anywhere")
193 wantGroups(t, list.Groups("markdown"), "General/anywhere")
194}
195
196func TestAGroupLeftEmptyByFilteringDoesNotAppear(t *testing.T) {
197 noUserSnippets(t)
198 dir := project(t, "[[snippet]]\nname = \"x\"\ngroup = \"Go\"\nlanguages = [\"go\"]\nbody = \"y\"\n")
199
200 if groups := load(t, dir).Groups("markdown"); len(groups) != 0 {
201 t.Errorf("Groups() = %v, want no group at all", summary(groups))
202 }
203}
204
205func TestASnippetForSeveralLanguagesAppliesToEach(t *testing.T) {
206 noUserSnippets(t)
207 dir := project(t, "[[snippet]]\nname = \"x\"\nlanguages = [\"go\", \"bash\"]\nbody = \"y\"\n")
208 list := load(t, dir)
209
210 for _, language := range []string{"go", "bash"} {
211 if got := len(list.Groups(language)); got != 1 {
212 t.Errorf("Groups(%q) gave %d groups, want 1", language, got)
213 }
214 }
215 if got := len(list.Groups("html")); got != 0 {
216 t.Errorf("Groups(\"html\") gave %d groups, want none", got)
217 }
218}
219
220func TestTheUsersSnippetsAndTheProjectsAreBothOffered(t *testing.T) {
221 userSnippets(t, "[[snippet]]\nname = \"mine\"\ngroup = \"Mine\"\nbody = \"x\"\n")
222 dir := project(t, "[[snippet]]\nname = \"theirs\"\ngroup = \"Theirs\"\nbody = \"x\"\n")
223
224 // The user's come first, so a project adds to what you already have.
225 wantGroups(t, load(t, dir).Groups(""), "Mine/mine", "Theirs/theirs")
226}
227
228func TestTheProjectWinsWhenANameClashes(t *testing.T) {
229 // The project's file is the more specific statement of the two.
230 userSnippets(t, "[[snippet]]\nname = \"header\"\ngroup = \"Go\"\nbody = \"mine\"\n")
231 dir := project(t, "[[snippet]]\nname = \"header\"\ngroup = \"Go\"\nbody = \"theirs\"\n")
232
233 groups := load(t, dir).Groups("")
234 if len(groups) != 1 || len(groups[0].Snippets) != 1 {
235 t.Fatalf("got %v, want one snippet", summary(groups))
236 }
237 if got := groups[0].Snippets[0].Body; got != "theirs" {
238 t.Errorf("body = %q, want the project's", got)
239 }
240}
241
242func TestTheSameNameInADifferentGroupIsADifferentSnippet(t *testing.T) {
243 userSnippets(t, "[[snippet]]\nname = \"header\"\ngroup = \"Go\"\nbody = \"a\"\n")
244 dir := project(t, "[[snippet]]\nname = \"header\"\ngroup = \"Shell\"\nbody = \"b\"\n")
245
246 wantGroups(t, load(t, dir).Groups(""), "Go/header", "Shell/header")
247}
248
249func TestAProjectWithNoSnippetsFileIsNotAnError(t *testing.T) {
250 noUserSnippets(t)
251
252 list := load(t, t.TempDir())
253
254 if list.Len() != 0 {
255 t.Errorf("Len() = %d, want 0", list.Len())
256 }
257 if got := len(list.Groups("go")); got != 0 {
258 t.Errorf("Groups() gave %d groups", got)
259 }
260}
261
262func TestAFileThatIsNotTOMLIsReported(t *testing.T) {
263 // A typo must be reported rather than silently dropping every snippet.
264 noUserSnippets(t)
265 dir := project(t, "[[snippet]\nname = ")
266
267 _, err := Load(testProfile(), dir)
268 if err == nil {
269 t.Fatal("Load(testProfile()) accepted a file that is not TOML")
270 }
271 if !strings.Contains(err.Error(), ProjectPath(testProfile(), dir)) {
272 t.Errorf("the error does not name the file: %v", err)
273 }
274}
275
276func TestASnippetWithNoNameOrNoBodyIsRefused(t *testing.T) {
277 noUserSnippets(t)
278
279 for name, contents := range map[string]string{
280 "no name": "[[snippet]]\nbody = \"x\"\n",
281 "no body": "[[snippet]]\nname = \"x\"\n",
282 } {
283 t.Run(name, func(t *testing.T) {
284 if _, err := Load(testProfile(), project(t, contents)); err == nil {
285 t.Errorf("Load(testProfile()) accepted a snippet with %s", name)
286 }
287 })
288 }
289}
290
291func TestADirectoryWhereTheFileGoesCountsAsAbsent(t *testing.T) {
292 noUserSnippets(t)
293 dir := t.TempDir()
294 if err := os.MkdirAll(ProjectPath(testProfile(), dir), 0o755); err != nil {
295 t.Fatalf("creating a directory where the file goes: %v", err)
296 }
297
298 if Exists(testProfile(), dir) {
299 t.Error("Exists(testProfile()) called a directory a snippets file")
300 }
301}
302
303func TestCreateWritesAFileThatLoadsBack(t *testing.T) {
304 noUserSnippets(t)
305 dir := t.TempDir()
306
307 path, err := Create(testProfile(), dir)
308 if err != nil {
309 t.Fatalf("Create(testProfile()) error = %v", err)
310 }
311 if path != ProjectPath(testProfile(), dir) {
312 t.Errorf("Create(testProfile()) returned %q, want %q", path, ProjectPath(testProfile(), dir))
313 }
314 if !Exists(testProfile(), dir) {
315 t.Fatal("Create(testProfile()) reported success but wrote no file")
316 }
317
318 list := load(t, dir)
319 if list.Len() == 0 {
320 t.Fatal("the created file holds no snippets")
321 }
322 // The examples in it must be usable, not just parseable.
323 for _, group := range list.Groups("go") {
324 for _, snippet := range group.Snippets {
325 if snippet.Name == "" || snippet.Body == "" {
326 t.Errorf("the created file holds an unusable snippet %+v", snippet)
327 }
328 }
329 }
330}
331
332func TestCreateRefusesToOverwriteAnExistingFile(t *testing.T) {
333 noUserSnippets(t)
334 original := "[[snippet]]\nname = \"mine\"\nbody = \"hand written\"\n"
335 dir := project(t, original)
336
337 _, err := Create(testProfile(), dir)
338 if !errors.Is(err, ErrExists) {
339 t.Fatalf("Create(testProfile()) error = %v, want ErrExists", err)
340 }
341 if got := readFile(t, ProjectPath(testProfile(), dir)); got != original {
342 t.Errorf("the existing file was changed:\n%s", got)
343 }
344}
345
346func TestUserDirFollowsTheEnvironmentVariable(t *testing.T) {
347 t.Setenv(testProfile().SnippetDirEnvVar(), "/somewhere")
348
349 if got := UserDir(testProfile()); got != "/somewhere" {
350 t.Errorf("UserDir(testProfile()) = %q", got)
351 }
352 if got, want := UserPath(testProfile()), filepath.Join("/somewhere", FileName); got != want {
353 t.Errorf("UserPath(testProfile()) = %q, want %q", got, want)
354 }
355}
356
357// readFile returns a file's contents.
358func readFile(t *testing.T, path string) string {
359 t.Helper()
360
361 data, err := os.ReadFile(path)
362 if err != nil {
363 t.Fatalf("reading %s: %v", path, err)
364 }
365 return string(data)
366}
367
368func TestCreateWritesTheProfilesTemplateWithItsBlanksFilledIn(t *testing.T) {
369 // Two things the template asks for and cannot know itself: the name of the
370 // group an ungrouped snippet falls into, and where the user's own file is.
371 // What is *in* Turbo Go's template is Turbo Go's own test.
372 userDir := t.TempDir()
373 t.Setenv(testProfile().SnippetDirEnvVar(), userDir)
374 dir := t.TempDir()
375
376 if _, err := Create(testProfile(), dir); err != nil {
377 t.Fatalf("Create() error = %v", err)
378 }
379
380 contents := readFile(t, ProjectPath(testProfile(), dir))
381 for _, want := range []string{ungroupedName, UserPath(testProfile())} {
382 if !strings.Contains(contents, want) {
383 t.Errorf("the created file never mentions %q:\n%s", want, contents)
384 }
385 }
386}
387
388func TestCreateSaysSoWhenThereIsNowhereForTheUsersOwnFile(t *testing.T) {
389 // A system with no configuration directory leaves UserPath empty, and a
390 // comment reading "Your own snippets go in:" followed by nothing is worse
391 // than one that says there is nowhere.
392 t.Setenv(testProfile().SnippetDirEnvVar(), "")
393 t.Setenv("XDG_CONFIG_HOME", "")
394 t.Setenv("HOME", "")
395 if UserPath(testProfile()) != "" {
396 t.Skip("this system still reports a configuration directory")
397 }
398 dir := t.TempDir()
399
400 if _, err := Create(testProfile(), dir); err != nil {
401 t.Fatalf("Create() error = %v", err)
402 }
403
404 contents := readFile(t, ProjectPath(testProfile(), dir))
405 if !strings.Contains(contents, "no configuration directory") {
406 t.Errorf("the created file does not say there is nowhere to put user snippets:\n%s", contents)
407 }
408}