turbo-editors/turbo-corepublic Fork 0
v1.0.1
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.

tree_test.go · 324 lines · 8.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1package filetree
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10// makeTree builds a directory from a list of paths; one ending in a slash
11// becomes a directory, and intermediate directories are made as needed.
12func makeTree(t *testing.T, paths ...string) string {
13 t.Helper()
14
15 root := t.TempDir()
16 for _, path := range paths {
17 full := filepath.Join(root, strings.TrimSuffix(path, "/"))
18 if strings.HasSuffix(path, "/") {
19 if err := os.MkdirAll(full, 0o755); err != nil {
20 t.Fatalf("creating %s: %v", path, err)
21 }
22 continue
23 }
24 if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
25 t.Fatalf("creating the parent of %s: %v", path, err)
26 }
27 if err := os.WriteFile(full, nil, 0o644); err != nil {
28 t.Fatalf("creating %s: %v", path, err)
29 }
30 }
31 return root
32}
33
34// names renders the visible rows as " ".Depth + name, which reads like the
35// tree does on screen and makes a failure legible.
36func names(t *Tree) []string {
37 rows := t.Rows()
38 out := make([]string, len(rows))
39 for i, row := range rows {
40 name := row.Node.Name()
41 if row.Node.IsDir() {
42 name += "/"
43 }
44 out[i] = strings.Repeat(" ", row.Depth) + name
45 }
46 return out
47}
48
49// wantRows compares the visible rows against what they should be.
50func wantRows(t *testing.T, tree *Tree, want ...string) {
51 t.Helper()
52
53 got := names(tree)
54 if len(got) != len(want) {
55 t.Fatalf("the tree shows\n%s\nwant\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
56 }
57 for i := range want {
58 if got[i] != want[i] {
59 t.Errorf("row %d is %q, want %q (whole tree:\n%s)", i, got[i], want[i], strings.Join(got, "\n"))
60 }
61 }
62}
63
64// find returns the node with a name, failing the test when there is none.
65func find(t *testing.T, tree *Tree, name string) *Node {
66 t.Helper()
67
68 for _, row := range tree.Rows() {
69 if row.Node.Name() == name {
70 return row.Node
71 }
72 }
73 t.Fatalf("no visible row is called %q; the tree shows:\n%s", name, strings.Join(names(tree), "\n"))
74 return nil
75}
76
77// open returns a tree on a directory, failing the test if it cannot be read.
78func open(t *testing.T, root string) *Tree {
79 t.Helper()
80
81 tree, err := New(root)
82 if err != nil {
83 t.Fatalf("New(%q) error = %v", root, err)
84 }
85 return tree
86}
87
88func TestANewTreeShowsTheRootsContents(t *testing.T) {
89 tree := open(t, makeTree(t, "main.go", "go.mod"))
90
91 wantRows(t, tree, "go.mod", "main.go")
92}
93
94func TestDirectoriesComeBeforeFilesEachSorted(t *testing.T) {
95 tree := open(t, makeTree(t, "zebra.go", "alpha.go", "ui/", "app/"))
96
97 wantRows(t, tree, "app/", "ui/", "alpha.go", "zebra.go")
98}
99
100func TestTheGitDirectoryIsHidden(t *testing.T) {
101 tree := open(t, makeTree(t, ".git/config", "main.go"))
102
103 wantRows(t, tree, "main.go")
104}
105
106func TestEveryOtherDotEntryIsShown(t *testing.T) {
107 // .turbo-go holds the project's own settings, which the editor encourages
108 // people to edit; a tree that hid it would make them unreachable from it.
109 tree := open(t, makeTree(t, ".turbo-go/settings.toml", ".gitignore", "main.go"))
110
111 wantRows(t, tree, ".turbo-go/", ".gitignore", "main.go")
112}
113
114func TestADirectoryIsNotReadUntilItIsExpanded(t *testing.T) {
115 tree := open(t, makeTree(t, "internal/app/app.go"))
116
117 if got := find(t, tree, "internal").Children(); len(got) != 0 {
118 t.Errorf("a closed directory already holds %d children", len(got))
119 }
120 wantRows(t, tree, "internal/")
121}
122
123func TestExpandingADirectoryShowsItsContents(t *testing.T) {
124 tree := open(t, makeTree(t, "internal/app/app.go", "main.go"))
125
126 find(t, tree, "internal").Expand()
127
128 wantRows(t, tree, "internal/", " app/", "main.go")
129}
130
131func TestExpandingNestsAsDeepAsItIsOpened(t *testing.T) {
132 tree := open(t, makeTree(t, "internal/app/app.go", "main.go"))
133
134 find(t, tree, "internal").Expand()
135 find(t, tree, "app").Expand()
136
137 wantRows(t, tree, "internal/", " app/", " app.go", "main.go")
138}
139
140func TestCollapsingHidesTheContentsWithoutForgettingThem(t *testing.T) {
141 tree := open(t, makeTree(t, "internal/app.go"))
142 node := find(t, tree, "internal")
143 node.Expand()
144
145 node.Collapse()
146 wantRows(t, tree, "internal/")
147
148 if len(node.Children()) == 0 {
149 t.Error("collapsing threw away what had been read")
150 }
151 node.Expand()
152 wantRows(t, tree, "internal/", " app.go")
153}
154
155func TestToggleOpensThenCloses(t *testing.T) {
156 tree := open(t, makeTree(t, "internal/app.go"))
157 node := find(t, tree, "internal")
158
159 node.Toggle()
160 if !node.Expanded() {
161 t.Error("Toggle() did not open a closed directory")
162 }
163 node.Toggle()
164 if node.Expanded() {
165 t.Error("Toggle() did not close an open directory")
166 }
167}
168
169func TestExpandingAFileDoesNothing(t *testing.T) {
170 tree := open(t, makeTree(t, "main.go"))
171 node := find(t, tree, "main.go")
172
173 node.Expand()
174
175 if node.Expanded() {
176 t.Error("a file reports itself as expanded")
177 }
178 wantRows(t, tree, "main.go")
179}
180
181func TestANodeCarriesItsAbsolutePath(t *testing.T) {
182 root := makeTree(t, "internal/app.go")
183 tree := open(t, root)
184 find(t, tree, "internal").Expand()
185
186 if got, want := find(t, tree, "app.go").Path(), filepath.Join(root, "internal", "app.go"); got != want {
187 t.Errorf("Path() = %q, want %q", got, want)
188 }
189}
190
191func TestTheRootIsNamedAfterItsDirectory(t *testing.T) {
192 root := makeTree(t, "main.go")
193
194 tree := open(t, root)
195
196 if got := tree.Root().Name(); got != filepath.Base(root) {
197 t.Errorf("Root().Name() = %q, want %q", got, filepath.Base(root))
198 }
199 if !tree.Root().IsDir() {
200 t.Error("the root does not call itself a directory")
201 }
202}
203
204func TestNewRefusesAFileAndAMissingPath(t *testing.T) {
205 root := makeTree(t, "main.go")
206
207 if _, err := New(filepath.Join(root, "main.go")); err == nil {
208 t.Error("New() accepted a file as the root of a tree")
209 }
210 if _, err := New(filepath.Join(root, "nowhere")); err == nil {
211 t.Error("New() accepted a path that does not exist")
212 }
213}
214
215func TestAnUnreadableDirectoryShowsAsEmptyRatherThanFailing(t *testing.T) {
216 if os.Geteuid() == 0 {
217 t.Skip("running as root, which can read a directory with no permissions")
218 }
219 root := makeTree(t, "locked/secret.go", "main.go")
220 locked := filepath.Join(root, "locked")
221 if err := os.Chmod(locked, 0o000); err != nil {
222 t.Fatalf("removing the permissions on %s: %v", locked, err)
223 }
224 t.Cleanup(func() { os.Chmod(locked, 0o755) })
225
226 tree := open(t, root)
227 find(t, tree, "locked").Expand()
228
229 // One unreadable directory is not a reason to refuse the rest of the tree.
230 wantRows(t, tree, "locked/", "main.go")
231}
232
233func TestRefreshShowsAFileMadeAfterTheTreeWasRead(t *testing.T) {
234 root := makeTree(t, "main.go")
235 tree := open(t, root)
236
237 if err := os.WriteFile(filepath.Join(root, "other.go"), nil, 0o644); err != nil {
238 t.Fatalf("creating the new file: %v", err)
239 }
240 wantRows(t, tree, "main.go") // not yet
241
242 tree.Refresh()
243
244 wantRows(t, tree, "main.go", "other.go")
245}
246
247func TestRefreshDropsAFileThatHasGone(t *testing.T) {
248 root := makeTree(t, "main.go", "other.go")
249 tree := open(t, root)
250
251 if err := os.Remove(filepath.Join(root, "other.go")); err != nil {
252 t.Fatalf("removing the file: %v", err)
253 }
254 tree.Refresh()
255
256 wantRows(t, tree, "main.go")
257}
258
259func TestRefreshKeepsWhatWasOpenOpen(t *testing.T) {
260 root := makeTree(t, "internal/app/app.go", "main.go")
261 tree := open(t, root)
262 find(t, tree, "internal").Expand()
263 find(t, tree, "app").Expand()
264
265 if err := os.WriteFile(filepath.Join(root, "internal", "app", "new.go"), nil, 0o644); err != nil {
266 t.Fatalf("creating the new file: %v", err)
267 }
268 tree.Refresh()
269
270 // The whole shape survives, and the new file appears in the right branch.
271 wantRows(t, tree, "internal/", " app/", " app.go", " new.go", "main.go")
272}
273
274func TestRefreshLeavesUnopenedDirectoriesUnread(t *testing.T) {
275 // Refreshing a large project must cost what is on screen, not a walk of
276 // everything under it.
277 root := makeTree(t, "internal/app/app.go")
278 tree := open(t, root)
279
280 tree.Refresh()
281
282 if got := find(t, tree, "internal").Children(); len(got) != 0 {
283 t.Errorf("refreshing read a directory nobody opened: %d children", len(got))
284 }
285}
286
287func TestRefreshTakesAwayTheBranchOfADeletedDirectory(t *testing.T) {
288 root := makeTree(t, "internal/app/app.go", "main.go")
289 tree := open(t, root)
290 find(t, tree, "internal").Expand()
291
292 if err := os.RemoveAll(filepath.Join(root, "internal")); err != nil {
293 t.Fatalf("removing the directory: %v", err)
294 }
295 tree.Refresh()
296
297 wantRows(t, tree, "main.go")
298}
299
300func TestAnEmptyProjectHasNoRows(t *testing.T) {
301 tree := open(t, t.TempDir())
302
303 if got := len(tree.Rows()); got != 0 {
304 t.Errorf("an empty project shows %d rows", got)
305 }
306}
307
308// writeFile creates an empty file, failing the test if it cannot.
309func writeFile(t *testing.T, path string) {
310 t.Helper()
311
312 if err := os.WriteFile(path, nil, 0o644); err != nil {
313 t.Fatalf("creating %s: %v", path, err)
314 }
315}
316
317// removeFile deletes a file, failing the test if it cannot.
318func removeFile(t *testing.T, path string) {
319 t.Helper()
320
321 if err := os.Remove(path); err != nil {
322 t.Fatalf("removing %s: %v", path, err)
323 }
324}