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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

rc_test.gno

10.21 Kb · 358 lines
  1package rc
  2
  3import (
  4	"strings"
  5	"testing"
  6
  7	memfs "gno.land/p/nym-moultest123/x/plan9/memfs/v0"
  8	ninep "gno.land/p/nym-moultest123/x/plan9/ninep/v0"
  9	ns "gno.land/p/nym-moultest123/x/plan9/ns/v0"
 10	synfs "gno.land/p/nym-moultest123/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}