1package ns23import (4 "testing"56 memfs "gno.land/p/moul/x/plan9/memfs/v0"7 ninep "gno.land/p/moul/x/plan9/ninep/v0"8)910// 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()), fs20}2122func names(ents []ninep.Stat) []string {23 out := []string{}24 for _, e := range ents {25 out = append(out, e.Name)26 }27 return out28}2930func eq(a, b []string) bool {31 if len(a) != len(b) {32 return false33 }34 for i := range a {35 if a[i] != b[i] {36 return false37 }38 }39 return true40}4142func 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}5960func 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}7374func 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}8586func 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}99100func 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}116117func 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}127128func TestUnionShadowingIsVisible(t *testing.T) {129 // Both directories hold a file called "ls". Plan 9 concatenates, so the130 // 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())135136 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}156157func 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}174175func 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())180181 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 the189 // 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}202203func 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}212213func 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())218219 // 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 }223224 // 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 }231232 // 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}248249func 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 }256257 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}268269func 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}283284func 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}306307func 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")312313 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}320321func TestFlagString(t *testing.T) {322 tests := []struct {323 flag Flag324 want string325 }{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}339340func 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 error348 for i := 0; i < MaxUnion+2; i++ {349 err = n.Bind("/d"+string(rune('a'+i)), "/bin", MAFTER)350 if err != nil {351 break352 }353 }354 if err != ErrWideUnion {355 t.Errorf("got %v, want ErrWideUnion", err)356 }357}358359func 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}365366func 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}376Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.