rc_test.gno
10.17 Kb · 358 lines
1package rc
2
3import (
4 "strings"
5 "testing"
6
7 memfs "gno.land/p/moul/x/plan9/memfs/v0"
8 ninep "gno.land/p/moul/x/plan9/ninep/v0"
9 ns "gno.land/p/moul/x/plan9/ns/v0"
10 synfs "gno.land/p/moul/x/plan9/synfs/v0"
11)
12
13func clock(h int64) func() int64 { return func() int64 { return h } }
14
15func shell(t *testing.T, files ...string) (*Shell, *memfs.FS) {
16 t.Helper()
17 sh, fs := NewMemShell("glenda", ReadWrite, clock(100))
18 for _, f := range files {
19 if err := fs.WriteFile(f, "contents of "+f+"\n", 100); err != nil {
20 t.Fatalf("seed %s: %v", f, err)
21 }
22 }
23 return sh, fs
24}
25
26func run(t *testing.T, sh *Shell, line string) string {
27 t.Helper()
28 out, err := sh.Run(line)
29 if err != nil {
30 t.Fatalf("%s: %v", line, err)
31 }
32 return out
33}
34
35func TestTokenize(t *testing.T) {
36 tests := []struct {
37 name string
38 in string
39 want []string
40 }{
41 {"plain", "ls -l /dev", []string{"ls", "-l", "/dev"}},
42 {"extra spaces", " ls /dev ", []string{"ls", "/dev"}},
43 {"quoted", "echo 'hello world'", []string{"echo", "hello world"}},
44 {"quote inside quote", "echo 'it''s'", []string{"echo", "it's"}},
45 {"empty quoted word", "echo ''", []string{"echo", ""}},
46 {"adjacent", "echo a'b c'd", []string{"echo", "ab cd"}},
47 {"empty line", "", []string{}},
48 }
49 for _, tt := range tests {
50 got, err := tokenize(tt.in)
51 if err != nil {
52 t.Errorf("%s: %v", tt.name, err)
53 continue
54 }
55 if len(got) != len(tt.want) {
56 t.Errorf("%s: got %v, want %v", tt.name, got, tt.want)
57 continue
58 }
59 for i := range got {
60 if got[i] != tt.want[i] {
61 t.Errorf("%s: token %d got %q, want %q", tt.name, i, got[i], tt.want[i])
62 }
63 }
64 }
65 if _, err := tokenize("echo 'unterminated"); err != ErrQuote {
66 t.Errorf("unterminated quote: got %v", err)
67 }
68}
69
70func TestSplitCommands(t *testing.T) {
71 got, err := splitCommands("pwd; ls /\necho 'a;b'")
72 if err != nil {
73 t.Fatalf("%v", err)
74 }
75 want := []string{"pwd", " ls /", "echo 'a;b'"}
76 if len(got) != len(want) {
77 t.Fatalf("got %v", got)
78 }
79 for i := range want {
80 if got[i] != want[i] {
81 t.Errorf("%d: got %q, want %q", i, got[i], want[i])
82 }
83 }
84}
85
86func TestPwdAndCd(t *testing.T) {
87 sh, _ := shell(t, "/usr/glenda/lib/profile")
88 if got := run(t, sh, "pwd"); got != "/\n" {
89 t.Errorf("pwd: %q", got)
90 }
91 run(t, sh, "cd /usr/glenda")
92 if got := run(t, sh, "pwd"); got != "/usr/glenda\n" {
93 t.Errorf("pwd after cd: %q", got)
94 }
95 if got := run(t, sh, "cat lib/profile"); got != "contents of /usr/glenda/lib/profile\n" {
96 t.Errorf("relative cat: %q", got)
97 }
98 run(t, sh, "cd")
99 if got := run(t, sh, "pwd"); got != "/\n" {
100 t.Errorf("bare cd should go to the root: %q", got)
101 }
102}
103
104func TestLs(t *testing.T) {
105 sh, _ := shell(t, "/bin/ls", "/bin/rc", "/tmp/x")
106 if got := run(t, sh, "ls /"); got != "bin/\ntmp/\n" {
107 t.Errorf("ls /: %q", got)
108 }
109 if got := run(t, sh, "ls /bin"); got != "ls\nrc\n" {
110 t.Errorf("ls /bin: %q", got)
111 }
112 long := run(t, sh, "ls -l /bin")
113 if !strings.Contains(long, "-rw-r--r-- glenda") {
114 t.Errorf("ls -l: %q", long)
115 }
116 if got := run(t, sh, "ls /bin/ls"); got != "ls\n" {
117 t.Errorf("ls of a file: %q", got)
118 }
119 if _, err := sh.Run("ls -Z"); err == nil {
120 t.Error("an unknown flag should fail")
121 }
122}
123
124func TestLsUnionAndUnique(t *testing.T) {
125 sh, fs := shell(t)
126 fs.WriteFile("/bin/ls", "system\n", 100)
127 fs.WriteFile("/usr/glenda/bin/ls", "mine\n", 100)
128 run(t, sh, "bind -b /usr/glenda/bin /bin")
129
130 if got := run(t, sh, "ls /bin"); got != "ls\nls\n" {
131 t.Errorf("a union lists both, as Plan 9 does: %q", got)
132 }
133 if got := run(t, sh, "ls -u /bin"); got != "ls\n" {
134 t.Errorf("ls -u: %q", got)
135 }
136 if got := run(t, sh, "cat /bin/ls"); got != "mine\n" {
137 t.Errorf("the member bound before wins the walk: %q", got)
138 }
139}
140
141func TestCatErrors(t *testing.T) {
142 sh, _ := shell(t, "/bin/ls")
143 if _, err := sh.Run("cat /bin"); err == nil {
144 t.Error("cat of a directory should fail")
145 }
146 if _, err := sh.Run("cat /absent"); err == nil {
147 t.Error("cat of a missing file should fail")
148 }
149 if _, err := sh.Run("cat"); err == nil {
150 t.Error("cat with no argument should fail")
151 }
152}
153
154func TestEchoAndRedirect(t *testing.T) {
155 sh, _ := shell(t)
156 if got := run(t, sh, "echo hello world"); got != "hello world\n" {
157 t.Errorf("echo: %q", got)
158 }
159 if got := run(t, sh, "echo -n hello"); got != "hello" {
160 t.Errorf("echo -n: %q", got)
161 }
162 run(t, sh, "echo hello > /greeting")
163 if got := run(t, sh, "cat /greeting"); got != "hello\n" {
164 t.Errorf("after redirect: %q", got)
165 }
166 run(t, sh, "echo again >> /greeting")
167 if got := run(t, sh, "cat /greeting"); got != "hello\nagain\n" {
168 t.Errorf("after append: %q", got)
169 }
170 run(t, sh, "echo replaced > /greeting")
171 if got := run(t, sh, "cat /greeting"); got != "replaced\n" {
172 t.Errorf("a plain redirect truncates: %q", got)
173 }
174 if _, err := sh.Run("echo hi >"); err == nil {
175 t.Error("a redirect with no target should fail")
176 }
177}
178
179func TestMkdirAndRm(t *testing.T) {
180 sh, _ := shell(t)
181 run(t, sh, "mkdir /tmp")
182 run(t, sh, "echo x > /tmp/f")
183 if got := run(t, sh, "ls /tmp"); got != "f\n" {
184 t.Errorf("ls /tmp: %q", got)
185 }
186 if _, err := sh.Run("rm /tmp"); err == nil || err.Error() != "rm: "+ninep.ErrNotEmpty.Error() {
187 t.Errorf("rm of a non-empty directory: got %v", err)
188 }
189 run(t, sh, "rm /tmp/f")
190 run(t, sh, "rm /tmp")
191 if got := run(t, sh, "ls /"); got != "" {
192 t.Errorf("root should be empty: %q", got)
193 }
194 if _, err := sh.Run("rm /absent"); err == nil {
195 t.Error("rm of a missing file should fail")
196 }
197}
198
199func TestBindAndNs(t *testing.T) {
200 sh, _ := shell(t, "/bin/ls", "/usr/glenda/bin/rc")
201 run(t, sh, "bind -a /usr/glenda/bin /bin")
202 if got := run(t, sh, "ls /bin"); got != "ls\nrc\n" {
203 t.Errorf("ls after bind: %q", got)
204 }
205 want := "bind -a /usr/glenda/bin /bin\ncd /\n"
206 if got := run(t, sh, "ns"); got != want {
207 t.Errorf("ns:\ngot %q\nwant %q", got, want)
208 }
209 run(t, sh, "unmount /usr/glenda/bin /bin")
210 if got := run(t, sh, "ls /bin"); got != "ls\n" {
211 t.Errorf("after unmount: %q", got)
212 }
213 if got := run(t, sh, "ns"); got != "cd /\n" {
214 t.Errorf("ns after unmount: %q", got)
215 }
216}
217
218func TestMountIsASynonym(t *testing.T) {
219 sh, _ := shell(t, "/bin/ls", "/srv/dev/height")
220 run(t, sh, "mkdir /dev")
221 run(t, sh, "mount /srv/dev /dev")
222 if got := run(t, sh, "ls /dev"); got != "height\n" {
223 t.Errorf("ls /dev: %q", got)
224 }
225 if !strings.HasPrefix(run(t, sh, "ns"), "mount /srv/dev /dev\n") {
226 t.Errorf("ns should record the verb used: %q", run(t, sh, "ns"))
227 }
228}
229
230func TestCreateInAUnionNeedsTheFlag(t *testing.T) {
231 sh, _ := shell(t, "/bin/ls", "/usr/glenda/bin/rc")
232 run(t, sh, "bind -a /usr/glenda/bin /bin")
233 if _, err := sh.Run("echo x > /bin/new"); err == nil {
234 t.Error("creating in a union with no -c should be refused")
235 }
236 run(t, sh, "bind -ac /usr/glenda/bin /bin")
237 run(t, sh, "echo x > /bin/new")
238 if got := run(t, sh, "cat /usr/glenda/bin/new"); got != "x\n" {
239 t.Errorf("the create should land in the -c member: %q", got)
240 }
241}
242
243func TestReadOnlyModeRefusesWrites(t *testing.T) {
244 sh, fs := NewMemShell("glenda", ReadOnly, clock(1))
245 fs.WriteFile("/bin/ls", "x\n", 1)
246
247 // Reads still work.
248 if got := run(t, sh, "ls /bin"); got != "ls\n" {
249 t.Errorf("ls: %q", got)
250 }
251 if got := run(t, sh, "cat /bin/ls"); got != "x\n" {
252 t.Errorf("cat: %q", got)
253 }
254 if got := run(t, sh, "echo hi"); got != "hi\n" {
255 t.Errorf("echo without a redirect is not a write: %q", got)
256 }
257 for _, line := range []string{
258 "mkdir /tmp",
259 "rm /bin/ls",
260 "bind /bin /bin",
261 "unmount /bin",
262 "echo hi > /f",
263 } {
264 _, err := sh.Run(line)
265 if err == nil {
266 t.Errorf("%q should be refused in ReadOnly mode", line)
267 continue
268 }
269 if !strings.Contains(err.Error(), ErrReadOnly.Error()) {
270 t.Errorf("%q: got %v, want a read-only error", line, err)
271 }
272 }
273}
274
275func TestStopsAtTheFirstError(t *testing.T) {
276 sh, _ := shell(t)
277 out, err := sh.Run("echo one; cat /absent; echo three")
278 if err == nil {
279 t.Fatal("expected an error")
280 }
281 if out != "one\n" {
282 t.Errorf("output should stop at the failure: %q", out)
283 }
284 if !strings.HasPrefix(err.Error(), "cat: ") {
285 t.Errorf("the error should name the command: %v", err)
286 }
287}
288
289func TestUnknownCommand(t *testing.T) {
290 sh, _ := shell(t)
291 if _, err := sh.Run("nosuchthing"); err == nil {
292 t.Error("expected an error")
293 }
294}
295
296func TestStatAndWalkShowTheUnion(t *testing.T) {
297 sh, fs := shell(t)
298 fs.WriteFile("/bin/ls", "a\n", 100)
299 fs.WriteFile("/usr/glenda/bin/rc", "b\n", 100)
300 run(t, sh, "bind -a /usr/glenda/bin /bin")
301
302 st := run(t, sh, "stat /bin")
303 if !strings.Contains(st, "union=2") {
304 t.Errorf("stat should report the union width: %q", st)
305 }
306 w := run(t, sh, "walk /bin/rc")
307 lines := strings.Split(strings.TrimSpace(w), "\n")
308 if len(lines) != 3 {
309 t.Fatalf("walk should report /, /bin and /bin/rc: %q", w)
310 }
311 if !strings.Contains(lines[1], "union=2") {
312 t.Errorf("the union should appear at /bin: %q", lines[1])
313 }
314 if !strings.Contains(lines[2], "union=1") {
315 t.Errorf("a union is top level only, /bin/rc is not one: %q", lines[2])
316 }
317}
318
319func TestReadOnlyServerMountedInAWritableNamespace(t *testing.T) {
320 // This is the cross-realm shape: a synthetic, read-only tree grafted into
321 // a namespace whose root is writable. Reads work; writes are refused
322 // where the server is read-only and still allowed elsewhere.
323 sh, fs := shell(t)
324 fs.MkdirAll("/dev", 100)
325 tr := synfs.New("dev", "sys", clock(100))
326 tr.Root().Add("height", func() string { return "100" })
327 if err := sh.Ns().Mount(tr.Root(), "#dev", "/dev", ns.MREPL); err != nil {
328 t.Fatalf("mount: %v", err)
329 }
330
331 if got := run(t, sh, "cat /dev/height"); got != "100" {
332 t.Errorf("read through the mount: %q", got)
333 }
334 if got := run(t, sh, "ls /dev"); got != "height\n" {
335 t.Errorf("ls /dev: %q", got)
336 }
337 if _, err := sh.Run("echo x > /dev/height"); err == nil {
338 t.Error("writing to a read-only server should be refused")
339 }
340 if _, err := sh.Run("rm /dev/height"); err == nil || err.Error() != "rm: "+ninep.ErrReadOnly.Error() {
341 t.Errorf("rm on a read-only server: got %v", err)
342 }
343 // The rest of the namespace is unaffected.
344 run(t, sh, "echo fine > /ok")
345 if got := run(t, sh, "cat /ok"); got != "fine\n" {
346 t.Errorf("got %q", got)
347 }
348}
349
350func TestHelpListsEveryCommand(t *testing.T) {
351 sh, _ := shell(t)
352 out := run(t, sh, "help")
353 for _, cmd := range []string{"bind", "cat", "cd", "echo", "ls", "mkdir", "mount", "ns", "pwd", "rm", "stat", "unmount", "walk"} {
354 if !strings.Contains(out, cmd) {
355 t.Errorf("help does not mention %q", cmd)
356 }
357 }
358}