1package picker
2
3import (
4 "testing"
5
6 "git.secluded.site/keld/internal/restic"
7)
8
9// testSelection builds a Selection from a small set of LsNodes.
10// Tree structure:
11//
12// music/
13// album-01/
14// 01-track.flac
15// 02-track.flac
16// album-02/
17// 01-song.flac
18// docs/
19// notes.txt
20// empty/
21// readme.txt
22func testSelection(t *testing.T) *Selection {
23 t.Helper()
24 nodes := []restic.LsNode{
25 {Name: "music", Type: "dir", Path: "/music"},
26 {Name: "album-01", Type: "dir", Path: "/music/album-01"},
27 {Name: "01-track.flac", Type: "file", Path: "/music/album-01/01-track.flac", Size: 1000},
28 {Name: "02-track.flac", Type: "file", Path: "/music/album-01/02-track.flac", Size: 2000},
29 {Name: "album-02", Type: "dir", Path: "/music/album-02"},
30 {Name: "01-song.flac", Type: "file", Path: "/music/album-02/01-song.flac", Size: 3000},
31 {Name: "docs", Type: "dir", Path: "/docs"},
32 {Name: "notes.txt", Type: "file", Path: "/docs/notes.txt", Size: 500},
33 {Name: "empty", Type: "dir", Path: "/empty"},
34 {Name: "readme.txt", Type: "file", Path: "/readme.txt", Size: 100},
35 }
36 sfs := restic.NewSnapshotFS(nodes)
37 return NewSelection(sfs)
38}
39
40func TestSelectionInitialState(t *testing.T) {
41 t.Parallel()
42 sel := testSelection(t)
43
44 // Everything starts unselected.
45 if sel.State(".") != CheckNone {
46 t.Errorf("root: got %v, want CheckNone", sel.State("."))
47 }
48 if sel.State("music") != CheckNone {
49 t.Errorf("music: got %v, want CheckNone", sel.State("music"))
50 }
51 if sel.State("readme.txt") != CheckNone {
52 t.Errorf("readme.txt: got %v, want CheckNone", sel.State("readme.txt"))
53 }
54 if sel.AllSelected() {
55 t.Error("AllSelected should be false initially")
56 }
57 if paths := sel.SelectedPaths(); len(paths) != 0 {
58 t.Errorf("SelectedPaths: got %v, want empty", paths)
59 }
60}
61
62func TestSelectionToggleFile(t *testing.T) {
63 t.Parallel()
64 sel := testSelection(t)
65
66 sel.Toggle("readme.txt")
67 if sel.State("readme.txt") != CheckAll {
68 t.Errorf("readme.txt after toggle: got %v, want CheckAll", sel.State("readme.txt"))
69 }
70 // Root should be partial now.
71 if sel.State(".") != CheckPartial {
72 t.Errorf("root after file toggle: got %v, want CheckPartial", sel.State("."))
73 }
74
75 // Toggle again to deselect.
76 sel.Toggle("readme.txt")
77 if sel.State("readme.txt") != CheckNone {
78 t.Errorf("readme.txt after second toggle: got %v, want CheckNone", sel.State("readme.txt"))
79 }
80 if sel.State(".") != CheckNone {
81 t.Errorf("root after deselect: got %v, want CheckNone", sel.State("."))
82 }
83}
84
85func TestSelectionToggleDirectory(t *testing.T) {
86 t.Parallel()
87 sel := testSelection(t)
88
89 // Selecting album-01 should select both tracks.
90 sel.Toggle("music/album-01")
91 if sel.State("music/album-01") != CheckAll {
92 t.Errorf("album-01: got %v, want CheckAll", sel.State("music/album-01"))
93 }
94 if sel.State("music/album-01/01-track.flac") != CheckAll {
95 t.Error("01-track.flac should be selected")
96 }
97 if sel.State("music/album-01/02-track.flac") != CheckAll {
98 t.Error("02-track.flac should be selected")
99 }
100 // music should be partial (album-02 is not selected).
101 if sel.State("music") != CheckPartial {
102 t.Errorf("music: got %v, want CheckPartial", sel.State("music"))
103 }
104
105 // Toggle album-01 again to deselect all.
106 sel.Toggle("music/album-01")
107 if sel.State("music/album-01") != CheckNone {
108 t.Errorf("album-01 after deselect: got %v, want CheckNone", sel.State("music/album-01"))
109 }
110 if sel.State("music") != CheckNone {
111 t.Errorf("music after deselect: got %v, want CheckNone", sel.State("music"))
112 }
113}
114
115func TestSelectionPartialThenToggle(t *testing.T) {
116 t.Parallel()
117 sel := testSelection(t)
118
119 // Select one track in album-01.
120 sel.Toggle("music/album-01/01-track.flac")
121 if sel.State("music/album-01") != CheckPartial {
122 t.Errorf("album-01: got %v, want CheckPartial", sel.State("music/album-01"))
123 }
124
125 // Toggling the partial directory should select ALL descendants.
126 sel.Toggle("music/album-01")
127 if sel.State("music/album-01") != CheckAll {
128 t.Errorf("album-01 after toggle from partial: got %v, want CheckAll", sel.State("music/album-01"))
129 }
130 if sel.State("music/album-01/02-track.flac") != CheckAll {
131 t.Error("02-track.flac should now be selected")
132 }
133}
134
135func TestSelectionEmptyDir(t *testing.T) {
136 t.Parallel()
137 sel := testSelection(t)
138
139 sel.Toggle("empty")
140 if sel.State("empty") != CheckAll {
141 t.Errorf("empty dir: got %v, want CheckAll", sel.State("empty"))
142 }
143
144 sel.Toggle("empty")
145 if sel.State("empty") != CheckNone {
146 t.Errorf("empty dir after deselect: got %v, want CheckNone", sel.State("empty"))
147 }
148}
149
150func TestSelectionAllSelected(t *testing.T) {
151 t.Parallel()
152 sel := testSelection(t)
153
154 // Select everything via root.
155 sel.Toggle(".")
156 if !sel.AllSelected() {
157 t.Error("AllSelected should be true after selecting root")
158 }
159 if sel.State(".") != CheckAll {
160 t.Errorf("root: got %v, want CheckAll", sel.State("."))
161 }
162
163 // SelectedPaths should return empty (full restore = no --include).
164 if paths := sel.SelectedPaths(); len(paths) != 0 {
165 t.Errorf("SelectedPaths when all selected: got %v, want empty", paths)
166 }
167
168 // Deselect root.
169 sel.Toggle(".")
170 if sel.AllSelected() {
171 t.Error("AllSelected should be false after deselecting root")
172 }
173}
174
175func TestSelectionSelectedPaths(t *testing.T) {
176 t.Parallel()
177 sel := testSelection(t)
178
179 // Select all of album-01 and the single file in docs.
180 sel.Toggle("music/album-01")
181 sel.Toggle("docs/notes.txt")
182
183 paths := sel.SelectedPaths()
184 // album-01 is fully selected → compressed to directory.
185 // docs has only one child (notes.txt), so selecting it makes docs
186 // fully selected → compressed to directory too.
187 want := map[string]bool{
188 "music/album-01": true,
189 "docs": true,
190 }
191 if len(paths) != len(want) {
192 t.Fatalf("SelectedPaths: got %v, want keys of %v", paths, want)
193 }
194 for _, p := range paths {
195 if !want[p] {
196 t.Errorf("unexpected path %q in SelectedPaths", p)
197 }
198 }
199}
200
201func TestSelectionSelectedPathsCompression(t *testing.T) {
202 t.Parallel()
203 sel := testSelection(t)
204
205 // Select all of music (both albums).
206 sel.Toggle("music")
207
208 paths := sel.SelectedPaths()
209 // Should compress to just "music".
210 if len(paths) != 1 || paths[0] != "music" {
211 t.Errorf("SelectedPaths: got %v, want [music]", paths)
212 }
213}
214
215func TestSelectionDeselectChildMakesParentPartial(t *testing.T) {
216 t.Parallel()
217 sel := testSelection(t)
218
219 // Select entire music tree, then deselect one track.
220 sel.Toggle("music")
221 sel.Toggle("music/album-01/01-track.flac")
222
223 if sel.State("music/album-01") != CheckPartial {
224 t.Errorf("album-01: got %v, want CheckPartial", sel.State("music/album-01"))
225 }
226 if sel.State("music") != CheckPartial {
227 t.Errorf("music: got %v, want CheckPartial", sel.State("music"))
228 }
229
230 // SelectedPaths should list individual files, not the dir.
231 paths := sel.SelectedPaths()
232 pathSet := make(map[string]bool, len(paths))
233 for _, p := range paths {
234 pathSet[p] = true
235 }
236 // album-01 has only 02-track selected; album-02 is fully selected.
237 if !pathSet["music/album-01/02-track.flac"] {
238 t.Error("expected music/album-01/02-track.flac in paths")
239 }
240 if !pathSet["music/album-02"] {
241 t.Error("expected music/album-02 (fully selected) in paths")
242 }
243 if pathSet["music/album-01"] {
244 t.Error("music/album-01 should NOT be in paths (only partially selected)")
245 }
246 if pathSet["music"] {
247 t.Error("music should NOT be in paths (only partially selected)")
248 }
249}