Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}