blob: e28f9c5dfbb5dc1d6ad9512eb1ab1577ed8cebf4 [file] [log] [blame]
Jan Schäre4c48542025-03-20 08:39:10 +00001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package structfs_test
5
6import (
7 "errors"
8 "io"
9 "io/fs"
10 "slices"
11 "syscall"
12 "testing"
13 "time"
14
15 . "source.monogon.dev/osbase/structfs"
16)
17
18func TestOptions(t *testing.T) {
19 testTimestamp := time.Date(2022, 03, 04, 5, 6, 8, 0, time.UTC)
20 var tree Tree
21 tree.PlaceDir("dir", Tree{},
22 WithModTime(testTimestamp),
23 WithPerm(0o700|fs.ModeSetuid|fs.ModeDevice),
24 WithSys("fakesys"),
25 )
26 node := tree[0]
27 if node.ModTime != testTimestamp {
28 t.Errorf("Got ModTime %v, expected %v", node.ModTime, testTimestamp)
29 }
30 expectMode := 0o700 | fs.ModeSetuid | fs.ModeDir
31 if node.Mode != expectMode {
32 t.Errorf("Got Mode %s, expected %s", node.Mode, expectMode)
33 }
34 if node.Sys != "fakesys" {
35 t.Errorf("Got Sys %v, expected %v", node.Sys, "fakesys")
36 }
37}
38
39func treeToStrings(t *testing.T, tree Tree) []string {
40 var out []string
41 for path, node := range tree.Walk() {
42 s := path + " " + node.Mode.String()[:1]
43 if node.Mode.IsRegular() {
44 content, err := node.Content.Open()
45 if err != nil {
46 t.Errorf("Failed to open %q: %v", path, err)
47 continue
48 }
49 b, err := io.ReadAll(content)
50 if err != nil {
51 t.Errorf("Failed to read %q: %v", path, err)
52 continue
53 }
54 s += " " + string(b)
55 content.Close()
56 }
57 out = append(out, s)
58 }
59 return out
60}
61
62func TestWalk(t *testing.T) {
63 testCases := []struct {
64 desc string
65 tree Tree
66 expected []string
67 }{
68 {
69 desc: "example",
70 tree: Tree{
71 File("file1a", Bytes("content1a")),
72 Dir("dir1", Tree{
73 File("file2", Bytes("content2")),
74 Dir("dir2", nil),
75 }),
76 File("file1b", Bytes("content1b")),
77 },
78 expected: []string{
79 "file1a - content1a",
80 "dir1 d",
81 "dir1/file2 - content2",
82 "dir1/dir2 d",
83 "file1b - content1b",
84 },
85 },
86 {
87 desc: "empty",
88 tree: nil,
89 expected: nil,
90 },
91 {
92 desc: "ignore file children",
93 // Non-directories should not have children and Walk should ignore them.
94 tree: Tree{{
95 Name: "file1",
96 Content: Bytes("content1"),
97 Children: Tree{
98 File("file2", Bytes("content2")),
99 Dir("dir2", nil),
100 },
101 }},
102 expected: []string{
103 "file1 - content1",
104 },
105 },
106 {
107 desc: "skip invalid name",
108 tree: Tree{
109 File("", Bytes("invalid")),
110 File(".", Bytes("invalid")),
111 File("a/b", Bytes("invalid")),
112 File("file1a", Bytes("content1a")),
113 Dir("..", Tree{
114 File("file2", Bytes("content2")),
115 Dir("dir2", nil),
116 }),
117 File("file1b", Bytes("content1b")),
118 },
119 expected: []string{
120 "file1a - content1a",
121 "file1b - content1b",
122 },
123 },
124 }
125 for _, tC := range testCases {
126 t.Run(tC.desc, func(t *testing.T) {
127 actual := treeToStrings(t, tC.tree)
128 if !slices.Equal(actual, tC.expected) {
129 t.Errorf("Walk result %v differs from expected %v", actual, tC.expected)
130 }
131 })
132 }
133}
134
135func TestPlace(t *testing.T) {
136 testCases := []struct {
137 desc string
138 tree Tree
139 place func(*Tree) error
140 expected []string
141 err error
142 errPath string
143 }{
144 {
145 desc: "file",
146 tree: Tree{
147 File("file1a", Bytes("content1a")),
148 Dir("dir1", Tree{
149 File("file2", Bytes("content2")),
150 Dir("dir2", nil),
151 }),
152 File("file1b", Bytes("content1b")),
153 },
154 place: func(tree *Tree) error {
155 return tree.PlaceFile("dir1/dir3/file4", Bytes("content4"))
156 },
157 expected: []string{
158 "file1a - content1a",
159 "dir1 d",
160 "dir1/file2 - content2",
161 "dir1/dir2 d",
162 "dir1/dir3 d",
163 "dir1/dir3/file4 - content4",
164 "file1b - content1b",
165 },
166 },
167 {
168 desc: "dir",
169 tree: Tree{
170 File("file1a", Bytes("content1a")),
171 Dir("dir1", Tree{
172 File("file2", Bytes("content2")),
173 }),
174 },
175 place: func(tree *Tree) error {
176 return tree.PlaceDir("dir1/dir3", Tree{
177 File("file4", Bytes("content4")),
178 Dir("dir4", nil),
179 })
180 },
181 expected: []string{
182 "file1a - content1a",
183 "dir1 d",
184 "dir1/file2 - content2",
185 "dir1/dir3 d",
186 "dir1/dir3/file4 - content4",
187 "dir1/dir3/dir4 d",
188 },
189 },
190 {
191 desc: "empty",
192 tree: nil,
193 place: func(tree *Tree) error {
194 return tree.PlaceFile("dir1/dir2/file3", Bytes("content"))
195 },
196 expected: []string{
197 "dir1 d",
198 "dir1/dir2 d",
199 "dir1/dir2/file3 - content",
200 },
201 },
202 {
203 desc: "root",
204 tree: Tree{
205 File("file1", Bytes("content1")),
206 },
207 place: func(tree *Tree) error {
208 return tree.PlaceFile("file2", Bytes("content2"))
209 },
210 expected: []string{
211 "file1 - content1",
212 "file2 - content2",
213 },
214 },
215 {
216 desc: "invalid path",
217 place: func(tree *Tree) error {
218 return tree.PlaceFile(".", Bytes("content"))
219 },
220 err: fs.ErrInvalid,
221 errPath: ".",
222 },
223 {
224 desc: "not a directory",
225 tree: Tree{
226 Dir("dir1", Tree{
227 File("file2", Bytes("content")),
228 }),
229 },
230 place: func(tree *Tree) error {
231 return tree.PlaceFile("dir1/file2/dir3/file4", Bytes("content"))
232 },
233 err: syscall.ENOTDIR,
234 errPath: "dir1/file2",
235 },
236 {
237 desc: "already exists",
238 tree: Tree{
239 Dir("dir1", Tree{
240 Dir("dir2", nil),
241 }),
242 },
243 place: func(tree *Tree) error {
244 return tree.PlaceDir("dir1/dir2", nil)
245 },
246 err: fs.ErrExist,
247 errPath: "dir1/dir2",
248 },
249 }
250 for _, tC := range testCases {
251 t.Run(tC.desc, func(t *testing.T) {
252 err := tC.place(&tC.tree)
253 if err != nil {
254 if tC.err == nil {
255 t.Fatalf("Place failed unexpectedly: %v", err)
256 }
257 if !errors.Is(err, tC.err) {
258 t.Errorf("Place failed with error %v, expected %v", err, tC.err)
259 }
260 var pe *fs.PathError
261 if !errors.As(err, &pe) {
262 t.Fatalf("Place(): error is %T, want *fs.PathError", err)
263 }
264 if pe.Path != tC.errPath {
265 t.Errorf("Place(): err.Path = %q, want %q", pe.Path, tC.errPath)
266 }
267 } else if tC.err != nil {
268 t.Error("Expected place to fail but it did not")
269 } else {
270 actual := treeToStrings(t, tC.tree)
271 if !slices.Equal(actual, tC.expected) {
272 t.Errorf("Result %v differs from expected %v", actual, tC.expected)
273 }
274 }
275 })
276 }
277}
278
279func TestValidName(t *testing.T) {
280 isValidNameTests := []struct {
281 name string
282 ok bool
283 }{
284 {"x", true},
285 {"", false},
286 {"..", false},
287 {".", false},
288 {"x/y", false},
289 {"/", false},
290 {"x/", false},
291 {"/x", false},
292 {`x\y`, true},
293 }
294 for _, tt := range isValidNameTests {
295 ok := ValidName(tt.name)
296 if ok != tt.ok {
297 t.Errorf("ValidName(%q) = %v, want %v", tt.name, ok, tt.ok)
298 }
299 }
300}