ns_test.gno
10.15 Kb · 375 lines
1package ns
2
3import (
4 "testing"
5
6 memfs "gno.land/p/moul/x/plan9/memfs/v0"
7 ninep "gno.land/p/moul/x/plan9/ninep/v0"
8)
9
10// newNs builds a namespace over a fresh ram tree seeded with files.
11func newNs(t *testing.T, files ...string) (*Ns, *memfs.FS) {
12 t.Helper()
13 fs := memfs.New("glenda", 1)
14 for _, f := range files {
15 if err := fs.WriteFile(f, "contents of "+f+"\n", 1); err != nil {
16 t.Fatalf("seed %s: %v", f, err)
17 }
18 }
19 return New(fs.Root()), fs
20}
21
22func names(ents []ninep.Stat) []string {
23 out := []string{}
24 for _, e := range ents {
25 out = append(out, e.Name)
26 }
27 return out
28}
29
30func eq(a, b []string) bool {
31 if len(a) != len(b) {
32 return false
33 }
34 for i := range a {
35 if a[i] != b[i] {
36 return false
37 }
38 }
39 return true
40}
41
42func TestResolveRoot(t *testing.T) {
43 n, _ := newNs(t, "/bin/ls", "/bin/cat")
44 st, err := n.Stat("/")
45 if err != nil {
46 t.Fatalf("stat root: %v", err)
47 }
48 if !st.IsDir() {
49 t.Error("root is not a directory")
50 }
51 ents, err := n.ReadDir("/", false)
52 if err != nil {
53 t.Fatalf("readdir: %v", err)
54 }
55 if !eq(names(ents), []string{"bin"}) {
56 t.Errorf("root listing: %v", names(ents))
57 }
58}
59
60func TestReadFileAndMissing(t *testing.T) {
61 n, _ := newNs(t, "/bin/ls")
62 got, err := n.ReadFile("/bin/ls")
63 if err != nil {
64 t.Fatalf("readfile: %v", err)
65 }
66 if got != "contents of /bin/ls\n" {
67 t.Errorf("got %q", got)
68 }
69 if _, err := n.ReadFile("/bin/absent"); err != ninep.ErrNotExist {
70 t.Errorf("missing: got %v, want ErrNotExist", err)
71 }
72}
73
74func TestDotDotIsLexical(t *testing.T) {
75 n, _ := newNs(t, "/a/b/c")
76 // /a/b/../b/c never asks a server about "..".
77 if _, err := n.Stat("/a/b/../b/c"); err != nil {
78 t.Fatalf("lexical dotdot: %v", err)
79 }
80 // Above the root stays at the root.
81 if _, err := n.Stat("/../../a"); err != nil {
82 t.Fatalf("dotdot above root: %v", err)
83 }
84}
85
86func TestBindReplace(t *testing.T) {
87 n, _ := newNs(t, "/bin/ls", "/usr/glenda/bin/rc")
88 if err := n.Bind("/usr/glenda/bin", "/bin", MREPL); err != nil {
89 t.Fatalf("bind: %v", err)
90 }
91 ents, _ := n.ReadDir("/bin", false)
92 if !eq(names(ents), []string{"rc"}) {
93 t.Errorf("after MREPL /bin should hold only rc, got %v", names(ents))
94 }
95 if _, err := n.Stat("/bin/ls"); err != ninep.ErrNotExist {
96 t.Errorf("replaced file still reachable: %v", err)
97 }
98}
99
100func TestBindAfterUnion(t *testing.T) {
101 n, _ := newNs(t, "/bin/ls", "/usr/glenda/bin/rc")
102 if err := n.Bind("/usr/glenda/bin", "/bin", MAFTER); err != nil {
103 t.Fatalf("bind -a: %v", err)
104 }
105 ents, _ := n.ReadDir("/bin", false)
106 if !eq(names(ents), []string{"ls", "rc"}) {
107 t.Errorf("union order: got %v, want [ls rc]", names(ents))
108 }
109 if _, err := n.Stat("/bin/rc"); err != nil {
110 t.Errorf("unioned file not reachable: %v", err)
111 }
112 if _, err := n.Stat("/bin/ls"); err != nil {
113 t.Errorf("original file lost: %v", err)
114 }
115}
116
117func TestBindBeforeUnion(t *testing.T) {
118 n, _ := newNs(t, "/bin/ls", "/usr/glenda/bin/rc")
119 if err := n.Bind("/usr/glenda/bin", "/bin", MBEFORE); err != nil {
120 t.Fatalf("bind -b: %v", err)
121 }
122 ents, _ := n.ReadDir("/bin", false)
123 if !eq(names(ents), []string{"rc", "ls"}) {
124 t.Errorf("union order: got %v, want [rc ls]", names(ents))
125 }
126}
127
128func TestUnionShadowingIsVisible(t *testing.T) {
129 // Both directories hold a file called "ls". Plan 9 concatenates, so the
130 // listing shows the duplicate and makes the shadowing visible.
131 fs := memfs.New("glenda", 1)
132 fs.WriteFile("/bin/ls", "system ls\n", 1)
133 fs.WriteFile("/usr/glenda/bin/ls", "my ls\n", 1)
134 n := New(fs.Root())
135
136 if err := n.Bind("/usr/glenda/bin", "/bin", MBEFORE); err != nil {
137 t.Fatalf("bind: %v", err)
138 }
139 ents, _ := n.ReadDir("/bin", false)
140 if !eq(names(ents), []string{"ls", "ls"}) {
141 t.Errorf("concatenation should show both: got %v", names(ents))
142 }
143 uniq, _ := n.ReadDir("/bin", true)
144 if !eq(names(uniq), []string{"ls"}) {
145 t.Errorf("unique listing: got %v", names(uniq))
146 }
147 // A walk reaches the first member, which is the one bound before.
148 got, err := n.ReadFile("/bin/ls")
149 if err != nil {
150 t.Fatalf("read: %v", err)
151 }
152 if got != "my ls\n" {
153 t.Errorf("shadowing: got %q, want my ls", got)
154 }
155}
156
157func TestUnionIsTopLevelOnly(t *testing.T) {
158 // /a and /b each hold a directory "sub", but only /a/sub/x exists.
159 // Unioning /b onto /a does NOT union /a/sub with /b/sub.
160 fs := memfs.New("glenda", 1)
161 fs.WriteFile("/a/sub/x", "x\n", 1)
162 fs.WriteFile("/b/sub/y", "y\n", 1)
163 n := New(fs.Root())
164 if err := n.Bind("/b", "/a", MAFTER); err != nil {
165 t.Fatalf("bind: %v", err)
166 }
167 if _, err := n.Stat("/a/sub/x"); err != nil {
168 t.Errorf("/a/sub/x should still resolve: %v", err)
169 }
170 if _, err := n.Stat("/a/sub/y"); err != ninep.ErrNotExist {
171 t.Errorf("unions are top level only, /a/sub/y must not resolve: %v", err)
172 }
173}
174
175func TestBindCapturesAChannelNotAName(t *testing.T) {
176 fs := memfs.New("glenda", 1)
177 fs.WriteFile("/one/f", "one\n", 1)
178 fs.WriteFile("/two/f", "two\n", 1)
179 n := New(fs.Root())
180
181 if err := n.Bind("/one", "/mnt", MREPL); err != nil {
182 // /mnt does not exist yet, so this must fail: see the next test.
183 fs.MkdirAll("/mnt", 1)
184 if err := n.Bind("/one", "/mnt", MREPL); err != nil {
185 t.Fatalf("bind: %v", err)
186 }
187 }
188 // Now rebind the SOURCE name elsewhere. The earlier bind captured the
189 // directory, so /mnt must not follow.
190 if err := n.Bind("/two", "/one", MREPL); err != nil {
191 t.Fatalf("rebind source: %v", err)
192 }
193 got, _ := n.ReadFile("/mnt/f")
194 if got != "one\n" {
195 t.Errorf("bind should have captured the channel: /mnt/f is %q", got)
196 }
197 got, _ = n.ReadFile("/one/f")
198 if got != "two\n" {
199 t.Errorf("/one/f is %q, want two", got)
200 }
201}
202
203func TestBindTargetMustExist(t *testing.T) {
204 n, _ := newNs(t, "/bin/ls")
205 if err := n.Bind("/bin", "/nowhere", MREPL); err != ninep.ErrNotExist {
206 t.Errorf("bind onto a missing name: got %v, want ErrNotExist", err)
207 }
208 if err := n.Bind("/nowhere", "/bin", MREPL); err != ninep.ErrNotExist {
209 t.Errorf("bind from a missing name: got %v, want ErrNotExist", err)
210 }
211}
212
213func TestCreateTargetNeedsMCREATE(t *testing.T) {
214 fs := memfs.New("glenda", 1)
215 fs.MkdirAll("/bin", 1)
216 fs.MkdirAll("/usr/glenda/bin", 1)
217 n := New(fs.Root())
218
219 // A plain directory takes creates with no flag at all.
220 if _, err := n.CreateTarget("/bin"); err != nil {
221 t.Fatalf("plain directory should accept a create: %v", err)
222 }
223
224 // Made into a union with no MCREATE anywhere, creation is refused.
225 if err := n.Bind("/usr/glenda/bin", "/bin", MAFTER); err != nil {
226 t.Fatalf("bind: %v", err)
227 }
228 if _, err := n.CreateTarget("/bin"); err != ninep.ErrNoCreate {
229 t.Errorf("union without MCREATE: got %v, want ErrNoCreate", err)
230 }
231
232 // Rebinding with MCREATE picks that member.
233 if err := n.Bind("/usr/glenda/bin", "/bin", MAFTER|MCREATE); err != nil {
234 t.Fatalf("bind -ac: %v", err)
235 }
236 target, err := n.CreateTarget("/bin")
237 if err != nil {
238 t.Fatalf("union with MCREATE: %v", err)
239 }
240 if _, err := target.Create("new", ninep.FilePerm, 2); err != nil {
241 t.Fatalf("create: %v", err)
242 }
243 // It landed in the MCREATE member, not in /bin's own directory.
244 if _, err := ninep.Walk(fs.Root(), ninep.Elems("/usr/glenda/bin/new")); err != nil {
245 t.Errorf("create went to the wrong member: %v", err)
246 }
247}
248
249func TestUnmount(t *testing.T) {
250 n, _ := newNs(t, "/bin/ls", "/usr/glenda/bin/rc")
251 n.Bind("/usr/glenda/bin", "/bin", MAFTER)
252 ents, _ := n.ReadDir("/bin", false)
253 if len(ents) != 2 {
254 t.Fatalf("setup: %v", names(ents))
255 }
256
257 if err := n.Unmount("/usr/glenda/bin", "/bin"); err != nil {
258 t.Fatalf("unmount: %v", err)
259 }
260 ents, _ = n.ReadDir("/bin", false)
261 if !eq(names(ents), []string{"ls"}) {
262 t.Errorf("after unmount: %v", names(ents))
263 }
264 if err := n.Unmount("", "/bin"); err != ErrNotBound {
265 t.Errorf("unmount of an unbound name: got %v, want ErrNotBound", err)
266 }
267}
268
269func TestUnmountAll(t *testing.T) {
270 n, _ := newNs(t, "/bin/ls", "/usr/glenda/bin/rc")
271 n.Bind("/usr/glenda/bin", "/bin", MAFTER)
272 if err := n.Unmount("", "/bin"); err != nil {
273 t.Fatalf("unmount all: %v", err)
274 }
275 ents, _ := n.ReadDir("/bin", false)
276 if !eq(names(ents), []string{"ls"}) {
277 t.Errorf("name should be restored to its original contents: %v", names(ents))
278 }
279 if len(n.Ops()) != 0 {
280 t.Errorf("ops should be gone: %v", n.Ops())
281 }
282}
283
284func TestCd(t *testing.T) {
285 n, _ := newNs(t, "/usr/glenda/lib/profile")
286 if err := n.Cd("/usr/glenda"); err != nil {
287 t.Fatalf("cd: %v", err)
288 }
289 if n.Cwd() != "/usr/glenda" {
290 t.Errorf("cwd: %q", n.Cwd())
291 }
292 got, err := n.ReadFile("lib/profile")
293 if err != nil {
294 t.Fatalf("relative read: %v", err)
295 }
296 if got == "" {
297 t.Error("relative read came back empty")
298 }
299 if err := n.Cd("lib/profile"); err != ninep.ErrNotDir {
300 t.Errorf("cd into a file: got %v, want ErrNotDir", err)
301 }
302 if err := n.Cd("/nowhere"); err != ninep.ErrNotExist {
303 t.Errorf("cd nowhere: got %v", err)
304 }
305}
306
307func TestStringMatchesNsFormat(t *testing.T) {
308 n, _ := newNs(t, "/bin/ls", "/usr/glenda/bin/rc", "/tmp/x")
309 n.Bind("/usr/glenda/bin", "/bin", MAFTER|MCREATE)
310 n.Bind("/tmp", "/usr/glenda", MBEFORE)
311 n.Cd("/usr/glenda")
312
313 want := "bind -ac /usr/glenda/bin /bin\n" +
314 "bind -b /tmp /usr/glenda\n" +
315 "cd /usr/glenda\n"
316 if got := n.String(); got != want {
317 t.Errorf("ns output:\ngot:\n%s\nwant:\n%s", got, want)
318 }
319}
320
321func TestFlagString(t *testing.T) {
322 tests := []struct {
323 flag Flag
324 want string
325 }{
326 {MREPL, ""},
327 {MBEFORE, "-b"},
328 {MAFTER, "-a"},
329 {MCREATE, "-c"},
330 {MAFTER | MCREATE, "-ac"},
331 {MBEFORE | MCREATE, "-bc"},
332 }
333 for _, tt := range tests {
334 if got := tt.flag.String(); got != tt.want {
335 t.Errorf("Flag(%d): got %q, want %q", uint32(tt.flag), got, tt.want)
336 }
337 }
338}
339
340func TestUnionWidthIsBounded(t *testing.T) {
341 fs := memfs.New("glenda", 1)
342 fs.MkdirAll("/bin", 1)
343 for i := 0; i < MaxUnion+2; i++ {
344 fs.MkdirAll("/d"+string(rune('a'+i)), 1)
345 }
346 n := New(fs.Root())
347 var err error
348 for i := 0; i < MaxUnion+2; i++ {
349 err = n.Bind("/d"+string(rune('a'+i)), "/bin", MAFTER)
350 if err != nil {
351 break
352 }
353 }
354 if err != ErrWideUnion {
355 t.Errorf("got %v, want ErrWideUnion", err)
356 }
357}
358
359func TestReadDirOnAFile(t *testing.T) {
360 n, _ := newNs(t, "/bin/ls")
361 if _, err := n.ReadDir("/bin/ls", false); err != ninep.ErrNotDir {
362 t.Errorf("got %v, want ErrNotDir", err)
363 }
364}
365
366func TestDepthIsBounded(t *testing.T) {
367 n, _ := newNs(t, "/a")
368 p := ""
369 for i := 0; i <= ninep.MaxDepth; i++ {
370 p += "/x"
371 }
372 if _, err := n.Stat(p); err != ninep.ErrTooDeep {
373 t.Errorf("got %v, want ErrTooDeep", err)
374 }
375}