1package restic
2
3import (
4 "errors"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "git.secluded.site/keld/internal/config"
11)
12
13// fixtureLsJSON is real output from:
14//
15// keld --preset music-final@b2 ls --json --recursive 8bd729b1 /home/amolith/Music/Final/3nd
16//
17// 24 lines: 1 snapshot header + 4 directories + 19 .flac files across 3 album
18// directories. The snapshot header line has message_type "snapshot" and must be
19// skipped; all remaining lines have message_type "node".
20const fixtureLsJSON = `{"time":"2026-03-24T00:20:53.867687549-06:00","parent":"2a1eb755327d446b938ef5a6e991f83f86abf12911f3bc87602fc696a7d1d4af","tree":"011fe38da2e96cb6ea9b2bb8bbc710d5b92ab9f4ea2d91be089bba532d329aae","paths":["/home/amolith/Music/Final"],"hostname":"angmar","username":"amolith","uid":1000,"gid":1000,"program_version":"restic 0.18.1","summary":{"backup_start":"2026-03-24T00:20:53.867687549-06:00","backup_end":"2026-03-24T00:20:58.420948969-06:00","files_new":0,"files_changed":4,"files_unmodified":6010,"dirs_new":0,"dirs_changed":4,"dirs_unmodified":862,"data_blobs":1,"tree_blobs":5,"data_added":89361,"data_added_packed":13072,"total_files_processed":6014,"total_bytes_processed":121289853644},"id":"8bd729b16043cef621423ca2f6100832e8add471374a3a54c555d624b011988c","short_id":"8bd729b1","message_type":"snapshot","struct_type":"snapshot"}
21{"name":"3nd","type":"dir","path":"/home/amolith/Music/Final/3nd","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2021-04-17T18:01:57.376612-06:00","atime":"2021-04-17T18:01:57.376612-06:00","ctime":"2024-09-26T19:41:45.573886986-06:00","inode":42207275,"message_type":"node","struct_type":"node"}
22{"name":"World Tour","type":"dir","path":"/home/amolith/Music/Final/3nd/World Tour","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.86776-06:00","atime":"2024-06-10T09:00:35.86776-06:00","ctime":"2024-09-26T19:41:45.563886805-06:00","inode":42207421,"message_type":"node","struct_type":"node"}
23{"name":"01 - monsoon.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac","uid":1000,"gid":1000,"size":30037820,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:20.840315-07:00","atime":"2021-11-25T17:31:20.840315-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213020,"message_type":"node","struct_type":"node"}
24{"name":"02 - china.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/02 - china.flac","uid":1000,"gid":1000,"size":17705986,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:22.00033-07:00","atime":"2021-11-25T17:31:22.00033-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213321,"message_type":"node","struct_type":"node"}
25{"name":"03 - SSK.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/03 - SSK.flac","uid":1000,"gid":1000,"size":32263802,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:22.753673-07:00","atime":"2021-11-25T17:31:22.753673-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213685,"message_type":"node","struct_type":"node"}
26{"name":"04 - midroll.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/04 - midroll.flac","uid":1000,"gid":1000,"size":37431281,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:23.643684-07:00","atime":"2021-11-25T17:31:23.643684-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213686,"message_type":"node","struct_type":"node"}
27{"name":"05 - LOTUS.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/05 - LOTUS.flac","uid":1000,"gid":1000,"size":33509064,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.157024-07:00","atime":"2021-11-25T17:31:24.157024-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213687,"message_type":"node","struct_type":"node"}
28{"name":"06 - 眠る.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/06 - 眠る.flac","uid":1000,"gid":1000,"size":21319060,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.383693-07:00","atime":"2021-11-25T17:31:24.383693-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213249,"message_type":"node","struct_type":"node"}
29{"name":"07 - filter in dust.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/07 - filter in dust.flac","uid":1000,"gid":1000,"size":21849635,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.613696-07:00","atime":"2021-11-25T17:31:24.613696-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42213250,"message_type":"node","struct_type":"node"}
30{"name":"08 - 夏終る.flac","type":"file","path":"/home/amolith/Music/Final/3nd/World Tour/08 - 夏終る.flac","uid":1000,"gid":1000,"size":28061640,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:24.817032-07:00","atime":"2021-11-25T17:31:24.817032-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212532,"message_type":"node","struct_type":"node"}
31{"name":"view from here","type":"dir","path":"/home/amolith/Music/Final/3nd/view from here","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.874427-06:00","atime":"2024-06-10T09:00:35.874427-06:00","ctime":"2024-09-26T19:41:45.570553593-06:00","inode":42207422,"message_type":"node","struct_type":"node"}
32{"name":"01 - clockworker.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/01 - clockworker.flac","uid":1000,"gid":1000,"size":38342556,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:12.526878-07:00","atime":"2021-11-25T17:31:12.526878-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42214320,"message_type":"node","struct_type":"node"}
33{"name":"02 - augustline.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/02 - augustline.flac","uid":1000,"gid":1000,"size":27357365,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:13.413555-07:00","atime":"2021-11-25T17:31:13.413555-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212371,"message_type":"node","struct_type":"node"}
34{"name":"03 - season.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/03 - season.flac","uid":1000,"gid":1000,"size":26918886,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:14.256899-07:00","atime":"2021-11-25T17:31:14.256899-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212245,"message_type":"node","struct_type":"node"}
35{"name":"04 - ハルオト.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/04 - ハルオト.flac","uid":1000,"gid":1000,"size":45722382,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:15.253578-07:00","atime":"2021-11-25T17:31:15.253578-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212533,"message_type":"node","struct_type":"node"}
36{"name":"05 - walk in the brown.flac","type":"file","path":"/home/amolith/Music/Final/3nd/view from here/05 - walk in the brown.flac","uid":1000,"gid":1000,"size":38267170,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:16.300258-07:00","atime":"2021-11-25T17:31:16.300258-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42214321,"message_type":"node","struct_type":"node"}
37{"name":"we dance × avec toi","type":"dir","path":"/home/amolith/Music/Final/3nd/we dance × avec toi","uid":1000,"gid":1000,"mode":2147484157,"permissions":"drwxrwxr-x","mtime":"2024-06-10T09:00:35.87776-06:00","atime":"2024-06-10T09:00:35.87776-06:00","ctime":"2024-09-26T19:41:45.573886986-06:00","inode":42207423,"message_type":"node","struct_type":"node"}
38{"name":"01 - Bender.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/01 - Bender.flac","uid":1000,"gid":1000,"size":24044464,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:17.563608-07:00","atime":"2021-11-25T17:31:17.563608-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211884,"message_type":"node","struct_type":"node"}
39{"name":"02 - テレプシコラー.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/02 - テレプシコラー.flac","uid":1000,"gid":1000,"size":29003735,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:18.106948-07:00","atime":"2021-11-25T17:31:18.106948-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211885,"message_type":"node","struct_type":"node"}
40{"name":"03 - コロコロ転がる.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/03 - コロコロ転がる.flac","uid":1000,"gid":1000,"size":27127530,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:18.643621-07:00","atime":"2021-11-25T17:31:18.643621-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212285,"message_type":"node","struct_type":"node"}
41{"name":"04 - algorhythm.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/04 - algorhythm.flac","uid":1000,"gid":1000,"size":36728278,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.070293-07:00","atime":"2021-11-25T17:31:19.070293-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212286,"message_type":"node","struct_type":"node"}
42{"name":"05 - Rain Song.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/05 - Rain Song.flac","uid":1000,"gid":1000,"size":27059422,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.540299-07:00","atime":"2021-11-25T17:31:19.540299-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42212287,"message_type":"node","struct_type":"node"}
43{"name":"06 - untroubled terror.flac","type":"file","path":"/home/amolith/Music/Final/3nd/we dance × avec toi/06 - untroubled terror.flac","uid":1000,"gid":1000,"size":6965876,"mode":509,"permissions":"-rwxrwxr-x","mtime":"2021-11-25T17:31:19.853636-07:00","atime":"2021-11-25T17:31:19.853636-07:00","ctime":"2024-09-26T14:45:44.103763451-06:00","inode":42211886,"message_type":"node","struct_type":"node"}
44`
45
46func TestParseLsNodes(t *testing.T) {
47 t.Parallel()
48
49 nodes, err := ParseLsNodes([]byte(fixtureLsJSON))
50 if err != nil {
51 t.Fatalf("ParseLsNodes(): unexpected error: %v", err)
52 }
53
54 // 23 node lines (4 dirs + 19 files), snapshot header skipped.
55 if got := len(nodes); got != 23 {
56 t.Fatalf("len(nodes): got %d, want 23", got)
57 }
58
59 // First node should be the root dir of the listing.
60 first := nodes[0]
61 if first.Name != "3nd" {
62 t.Errorf("first node name: got %q, want %q", first.Name, "3nd")
63 }
64 if first.Type != "dir" {
65 t.Errorf("first node type: got %q, want %q", first.Type, "dir")
66 }
67 if first.Path != "/home/amolith/Music/Final/3nd" {
68 t.Errorf("first node path: got %q, want %q", first.Path, "/home/amolith/Music/Final/3nd")
69 }
70 if first.Size != 0 {
71 t.Errorf("first node size: got %d, want 0 (dirs have no size)", first.Size)
72 }
73 if first.Mode != 2147484157 {
74 t.Errorf("first node mode: got %d, want %d", first.Mode, 2147484157)
75 }
76 if first.Permissions != "drwxrwxr-x" {
77 t.Errorf("first node permissions: got %q, want %q", first.Permissions, "drwxrwxr-x")
78 }
79 wantMtime, _ := time.Parse(time.RFC3339Nano, "2021-04-17T18:01:57.376612-06:00")
80 if !first.Mtime.Equal(wantMtime) {
81 t.Errorf("first node mtime: got %v, want %v", first.Mtime, wantMtime)
82 }
83
84 // Spot-check a file node: first .flac in "World Tour".
85 monsoon := nodes[2]
86 if monsoon.Name != "01 - monsoon.flac" {
87 t.Errorf("monsoon name: got %q, want %q", monsoon.Name, "01 - monsoon.flac")
88 }
89 if monsoon.Type != "file" {
90 t.Errorf("monsoon type: got %q, want %q", monsoon.Type, "file")
91 }
92 if monsoon.Path != "/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac" {
93 t.Errorf("monsoon path: got %q, want %q",
94 monsoon.Path, "/home/amolith/Music/Final/3nd/World Tour/01 - monsoon.flac")
95 }
96 if monsoon.Size != 30037820 {
97 t.Errorf("monsoon size: got %d, want %d", monsoon.Size, 30037820)
98 }
99 if monsoon.Mode != 509 {
100 t.Errorf("monsoon mode: got %d, want %d", monsoon.Mode, 509)
101 }
102 if monsoon.Permissions != "-rwxrwxr-x" {
103 t.Errorf("monsoon permissions: got %q, want %q", monsoon.Permissions, "-rwxrwxr-x")
104 }
105
106 // Spot-check a file with Unicode in the name.
107 nemuru := nodes[7]
108 if nemuru.Name != "06 - 眠る.flac" {
109 t.Errorf("nemuru name: got %q, want %q", nemuru.Name, "06 - 眠る.flac")
110 }
111 if nemuru.Size != 21319060 {
112 t.Errorf("nemuru size: got %d, want %d", nemuru.Size, 21319060)
113 }
114
115 // Count dirs vs files.
116 var dirs, files int
117 for _, n := range nodes {
118 switch n.Type {
119 case "dir":
120 dirs++
121 case "file":
122 files++
123 default:
124 t.Errorf("unexpected node type %q for %q", n.Type, n.Path)
125 }
126 }
127 if dirs != 4 {
128 t.Errorf("dir count: got %d, want 4", dirs)
129 }
130 if files != 19 {
131 t.Errorf("file count: got %d, want 19", files)
132 }
133}
134
135func TestParseLsNodesEmpty(t *testing.T) {
136 t.Parallel()
137
138 nodes, err := ParseLsNodes([]byte(""))
139 if err != nil {
140 t.Fatalf("ParseLsNodes(empty): unexpected error: %v", err)
141 }
142 if len(nodes) != 0 {
143 t.Errorf("expected 0 nodes from empty input, got %d", len(nodes))
144 }
145}
146
147func TestParseLsNodesSnapshotOnly(t *testing.T) {
148 t.Parallel()
149
150 // A snapshot header with no node lines — e.g. an empty snapshot or a
151 // path filter that matched nothing.
152 input := `{"time":"2026-03-24T00:20:53.867687549-06:00","paths":["/home/amolith/Music/Final"],"hostname":"angmar","id":"8bd729b1","short_id":"8bd729b1","message_type":"snapshot","struct_type":"snapshot"}
153`
154
155 nodes, err := ParseLsNodes([]byte(input))
156 if err != nil {
157 t.Fatalf("ParseLsNodes(snapshot-only): unexpected error: %v", err)
158 }
159 if len(nodes) != 0 {
160 t.Errorf("expected 0 nodes from snapshot-only input, got %d", len(nodes))
161 }
162}
163
164func TestBuildLsCmd(t *testing.T) {
165 t.Parallel()
166
167 tests := []struct {
168 name string
169 cfg *config.ResolvedConfig
170 snapshot string
171 wantArgv []string
172 wantErr bool
173 }{
174 {
175 name: "basic repo flag",
176 cfg: &config.ResolvedConfig{
177 Command: "restore",
178 Flags: []config.Flag{
179 {Name: "--repo", Value: "/srv/backup"},
180 {Name: "--target", Value: "/tmp/restore"},
181 },
182 },
183 snapshot: "8bd729b1",
184 wantArgv: []string{"restic", "ls", "--json", "--repo", "/srv/backup", "8bd729b1"},
185 },
186 {
187 name: "repo with password-file and non-global flags stripped",
188 cfg: &config.ResolvedConfig{
189 Command: "restore",
190 Flags: []config.Flag{
191 {Name: "--repo", Value: "rclone:remote:/backup"},
192 {Name: "--password-file", Value: "/etc/restic/pw"},
193 {Name: "--target", Value: "/tmp/restore"},
194 {Name: "--overwrite", Value: "if-changed"},
195 },
196 },
197 snapshot: "latest",
198 wantArgv: []string{
199 "restic", "ls", "--json",
200 "--repo", "rclone:remote:/backup",
201 "--password-file", "/etc/restic/pw",
202 "latest",
203 },
204 },
205 {
206 name: "cache-dir and no-lock forwarded",
207 cfg: &config.ResolvedConfig{
208 Command: "restore",
209 Flags: []config.Flag{
210 {Name: "--repo", Value: "/srv/backup"},
211 {Name: "--cache-dir", Value: "/tmp/cache"},
212 {Name: "--no-lock"},
213 {Name: "--target", Value: "/tmp/restore"},
214 },
215 },
216 snapshot: "abc123",
217 wantArgv: []string{
218 "restic", "ls", "--json",
219 "--repo", "/srv/backup",
220 "--cache-dir", "/tmp/cache",
221 "--no-lock",
222 "abc123",
223 },
224 },
225 {
226 name: "snapshot selectors forwarded for latest",
227 cfg: &config.ResolvedConfig{
228 Command: "restore",
229 Flags: []config.Flag{
230 {Name: "--repo", Value: "/srv/backup"},
231 {Name: "--host", Value: "angmar"},
232 {Name: "--path", Value: "/home/amolith/Music"},
233 {Name: "--tag", Value: "daily"},
234 {Name: "--target", Value: "/tmp/restore"},
235 },
236 },
237 snapshot: "latest",
238 wantArgv: []string{
239 "restic", "ls", "--json",
240 "--repo", "/srv/backup",
241 "--host", "angmar",
242 "--path", "/home/amolith/Music",
243 "--tag", "daily",
244 "latest",
245 },
246 },
247 {
248 name: "short host flag forwarded",
249 cfg: &config.ResolvedConfig{
250 Command: "restore",
251 Flags: []config.Flag{
252 {Name: "--repo", Value: "/srv/backup"},
253 {Name: "-H", Value: "angmar"},
254 },
255 },
256 snapshot: "latest",
257 wantArgv: []string{
258 "restic", "ls", "--json",
259 "--repo", "/srv/backup",
260 "-H", "angmar",
261 "latest",
262 },
263 },
264 {
265 name: "repo via environ only",
266 cfg: &config.ResolvedConfig{
267 Command: "restore",
268 Flags: []config.Flag{
269 {Name: "--target", Value: "/tmp/restore"},
270 },
271 Environ: map[string]string{
272 "RESTIC_REPOSITORY": "/srv/backup",
273 },
274 },
275 snapshot: "8bd729b1",
276 wantArgv: []string{"restic", "ls", "--json", "8bd729b1"},
277 },
278 }
279
280 for _, tt := range tests {
281 tt := tt
282 t.Run(tt.name, func(t *testing.T) {
283 t.Parallel()
284
285 argv, err := buildLsCmd(tt.cfg, tt.snapshot)
286 if (err != nil) != tt.wantErr {
287 t.Fatalf("buildLsCmd(): err=%v, wantErr=%v", err, tt.wantErr)
288 }
289 if err != nil {
290 return
291 }
292
293 if len(argv) != len(tt.wantArgv) {
294 t.Fatalf("argv length: got %d %v, want %d %v",
295 len(argv), argv, len(tt.wantArgv), tt.wantArgv)
296 }
297 for i := range argv {
298 if argv[i] != tt.wantArgv[i] {
299 t.Errorf("argv[%d]: got %q, want %q", i, argv[i], tt.wantArgv[i])
300 }
301 }
302 })
303 }
304}
305
306func TestBuildLsCmdNoRepo(t *testing.T) {
307 t.Setenv("RESTIC_REPOSITORY", "")
308 t.Setenv("RESTIC_REPOSITORY_FILE", "")
309
310 cfg := &config.ResolvedConfig{
311 Command: "restore",
312 Flags: []config.Flag{
313 {Name: "--target", Value: "/tmp/restore"},
314 },
315 }
316
317 _, err := buildLsCmd(cfg, "8bd729b1")
318 if !errors.Is(err, ErrNoRepo) {
319 t.Errorf("expected ErrNoRepo, got %v", err)
320 }
321}
322
323// testdataRepoPath returns the absolute path to the committed test
324// restic repository. Tests using this fixture require restic to be
325// installed.
326func testdataRepoPath(t *testing.T) string {
327 t.Helper()
328 // Discover the repo relative to this test file's package dir.
329 path, err := filepath.Abs("testdata/repo")
330 if err != nil {
331 t.Fatalf("resolving testdata/repo: %v", err)
332 }
333 if _, err := os.Stat(path); err != nil {
334 t.Fatalf("testdata/repo not found at %s: %v", path, err)
335 }
336 return path
337}
338
339func TestRunLs(t *testing.T) {
340 t.Parallel()
341
342 repoPath := testdataRepoPath(t)
343 cfg := &config.ResolvedConfig{
344 Command: "restore",
345 Flags: []config.Flag{
346 {Name: "--repo", Value: repoPath},
347 },
348 Environ: map[string]string{
349 "RESTIC_PASSWORD": "test",
350 },
351 }
352
353 nodes, err := RunLs(cfg, "03b061a6")
354 if err != nil {
355 t.Fatalf("RunLs(): unexpected error: %v", err)
356 }
357
358 // The test repo has 29 files + 26 dirs = 55 nodes.
359 if got := len(nodes); got != 55 {
360 t.Fatalf("len(nodes): got %d, want 55", got)
361 }
362
363 // Count types.
364 var dirs, files int
365 for _, n := range nodes {
366 switch n.Type {
367 case "dir":
368 dirs++
369 case "file":
370 files++
371 default:
372 t.Errorf("unexpected node type %q for %q", n.Type, n.Path)
373 }
374 }
375 if dirs != 26 {
376 t.Errorf("dir count: got %d, want 26", dirs)
377 }
378 if files != 29 {
379 t.Errorf("file count: got %d, want 29", files)
380 }
381
382 // Spot-check a Unicode path.
383 var found bool
384 for _, n := range nodes {
385 if n.Name == "01 - 夜明け.flac" {
386 found = true
387 if n.Type != "file" {
388 t.Errorf("夜明け type: got %q, want %q", n.Type, "file")
389 }
390 break
391 }
392 }
393 if !found {
394 t.Error("expected to find node '01 - 夜明け.flac'")
395 }
396
397 // Spot-check a space-in-path entry.
398 found = false
399 for _, n := range nodes {
400 if n.Name == "Jazz Café" {
401 found = true
402 if n.Type != "dir" {
403 t.Errorf("Jazz Café type: got %q, want %q", n.Type, "dir")
404 }
405 break
406 }
407 }
408 if !found {
409 t.Error("expected to find node 'Jazz Café'")
410 }
411}