aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-10-03 21:04:24 +0900
committerOphestra <cat@gensokyo.uk>2026-10-03 21:20:25 +0900
commitb452e1047ccd3e1826da16416430e6638270155b (patch)
tree7a2338fb7db0b625b38dab1911edcde65271b679
parentaa35067f0caeb709edd60dc76fc68ac33c3e1a25 (diff)
test/internal/testsuite: move fs checker
This also cleans up its API and improves error messages. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--test/internal/sandbox/assert.go4
-rw-r--r--test/internal/sandbox/fs.go102
-rw-r--r--test/internal/sandbox/fs_test.go80
-rw-r--r--test/internal/testsuite/fs.go129
-rw-r--r--test/internal/testsuite/fs_test.go85
-rw-r--r--test/internal/testsuite/testsuite.go10
-rw-r--r--test/internal/testsuite/testsuite_root.go3
7 files changed, 219 insertions, 194 deletions
diff --git a/test/internal/sandbox/assert.go b/test/internal/sandbox/assert.go
index e2bcc8ae..1194befb 100644
--- a/test/internal/sandbox/assert.go
+++ b/test/internal/sandbox/assert.go
@@ -33,7 +33,7 @@ func fatalf(format string, v ...any) { fatalfFunc(format, v...) }
type TestCase struct {
Env []string `json:"env"`
- FS *FS `json:"fs"`
+ FS *testsuite.FS `json:"fs"`
Mount []*mountinfo.Entry `json:"mount"`
Seccomp bool `json:"seccomp"`
@@ -110,7 +110,7 @@ func (t *T) MustCheck(want *TestCase) {
}
if want.FS != nil && t.FS != nil {
- if err := want.FS.Compare(".", t.FS); err != nil {
+ if err := want.FS.Compare(printfFunc, ".", t.FS); err != nil {
fatalf("%v", err)
}
} else {
diff --git a/test/internal/sandbox/fs.go b/test/internal/sandbox/fs.go
deleted file mode 100644
index b37d30b9..00000000
--- a/test/internal/sandbox/fs.go
+++ /dev/null
@@ -1,102 +0,0 @@
-//go:build testtool
-
-package sandbox
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "path/filepath"
- "strings"
-)
-
-var (
- ErrFSBadLength = errors.New("bad dir length")
- ErrFSBadData = errors.New("data differs")
- ErrFSBadMode = errors.New("mode differs")
- ErrFSInvalidEnt = errors.New("invalid entry condition")
-)
-
-type FS struct {
- Mode fs.FileMode `json:"mode"`
- Dir map[string]*FS `json:"dir"`
- Data *string `json:"data"`
-}
-
-func printDir(prefix string, dir []fs.DirEntry) {
- names := make([]string, len(dir))
- for i, ent := range dir {
- name := ent.Name()
- if ent.IsDir() {
- name += "/"
- }
- names[i] = fmt.Sprintf("%q", name)
- }
- printf("[FAIL] d %s: %s", prefix, strings.Join(names, " "))
-}
-
-func (s *FS) Compare(prefix string, e fs.FS) error {
- if s.Data != nil {
- if s.Dir != nil {
- panic("invalid state")
- }
- panic("invalid compare call")
- }
-
- if s.Dir == nil {
- printf("[ OK ] s %s", prefix)
- return nil
- }
-
- var dir []fs.DirEntry
- if d, err := fs.ReadDir(e, prefix); err != nil {
- return err
- } else if len(d) != len(s.Dir) {
- printDir(prefix, d)
- return ErrFSBadLength
- } else {
- dir = d
- }
-
- for _, got := range dir {
- name := got.Name()
-
- if want, ok := s.Dir[name]; !ok {
- printDir(prefix, dir)
- return fs.ErrNotExist
- } else if want.Dir != nil && !got.IsDir() {
- printDir(prefix, dir)
- return ErrFSInvalidEnt
- } else {
- name = filepath.Join(prefix, name)
-
- if fi, err := got.Info(); err != nil {
- return err
- } else if fi.Mode() != want.Mode {
- printf("[FAIL] m %s: %x, want %x",
- name, uint32(fi.Mode()), uint32(want.Mode))
- return ErrFSBadMode
- }
-
- if want.Data != nil {
- if want.Dir != nil {
- panic("invalid state")
- }
- if v, err := fs.ReadFile(e, name); err != nil {
- return err
- } else if string(v) != *want.Data {
- printf("[FAIL] f %s", name)
- printf("got: %s", v)
- printf("want: %s", *want.Data)
- return ErrFSBadData
- }
- printf("[ OK ] f %s", name)
- } else if err := want.Compare(name, e); err != nil {
- return err
- }
- }
- }
- printf("[ OK ] d %s", prefix)
-
- return nil
-}
diff --git a/test/internal/sandbox/fs_test.go b/test/internal/sandbox/fs_test.go
deleted file mode 100644
index a7813ee5..00000000
--- a/test/internal/sandbox/fs_test.go
+++ /dev/null
@@ -1,80 +0,0 @@
-//go:build testtool
-
-package sandbox_test
-
-import (
- "errors"
- "fmt"
- "io/fs"
- "strings"
- "testing"
- "testing/fstest"
-
- "hakurei.app/test/internal/sandbox"
-)
-
-var (
- fsPasswdSample = "u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh"
- fsGroupSample = "hakurei:x:65534:"
-)
-
-func TestCompare(t *testing.T) {
- testCases := []struct {
- name string
-
- sample fstest.MapFS
- want *sandbox.FS
- wantOut string
- wantErr error
- }{
- {"skip", fstest.MapFS{}, &sandbox.FS{}, "[ OK ] s .\x00", nil},
- {"simple pass", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
- &sandbox.FS{Dir: map[string]*sandbox.FS{".hakurei": {Mode: 0x800001ed}}},
- "[ OK ] s .hakurei\x00[ OK ] d .\x00", nil},
- {"bad length", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
- &sandbox.FS{Dir: make(map[string]*sandbox.FS)},
- "[FAIL] d .: \".hakurei/\"\x00", sandbox.ErrFSBadLength},
- {"top level bad mode", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
- &sandbox.FS{Dir: map[string]*sandbox.FS{".hakurei": {Mode: 0xdeadbeef}}},
- "[FAIL] m .hakurei: 800001ed, want deadbeef\x00", sandbox.ErrFSBadMode},
- {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
- &sandbox.FS{Dir: map[string]*sandbox.FS{"test": {Dir: make(map[string]*sandbox.FS)}}},
- "[FAIL] d .: \"test\"\x00", sandbox.ErrFSInvalidEnt},
- {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
- &sandbox.FS{Dir: map[string]*sandbox.FS{".test": {}}},
- "[FAIL] d .: \"test\"\x00", fs.ErrNotExist},
- {"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
- "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
- "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
- }, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
- "passwd": {Mode: 0x1a4, Data: &fsPasswdSample},
- "group": {Mode: 0x1a4, Data: &fsGroupSample},
- }}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil},
- {"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0},
- "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
- "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
- }, &sandbox.FS{Dir: map[string]*sandbox.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*sandbox.FS{
- "passwd": {Mode: 0x1a4, Data: &fsGroupSample},
- "group": {Mode: 0x1a4, Data: &fsGroupSample},
- }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\x00got: u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh\x00want: hakurei:x:65534:\x00", sandbox.ErrFSBadData},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- gotOut := new(strings.Builder)
- oldPrint := sandbox.SwapPrint(func(format string, v ...any) { _, _ = fmt.Fprintf(gotOut, format+"\x00", v...) })
- t.Cleanup(func() { sandbox.SwapPrint(oldPrint) })
-
- err := tc.want.Compare(".", tc.sample)
- if !errors.Is(err, tc.wantErr) {
- t.Errorf("Compare: error = %v; wantErr %v",
- err, tc.wantErr)
- }
-
- if gotOut.String() != tc.wantOut {
- t.Errorf("Compare: output %q; want %q",
- gotOut, tc.wantOut)
- }
- })
- }
-}
diff --git a/test/internal/testsuite/fs.go b/test/internal/testsuite/fs.go
new file mode 100644
index 00000000..9acd24b6
--- /dev/null
+++ b/test/internal/testsuite/fs.go
@@ -0,0 +1,129 @@
+package testsuite
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "path/filepath"
+ "strings"
+)
+
+var (
+ // ErrFSBadLength is returned by [FS.Compare] for a directory with an
+ // unexpected amount of dents.
+ ErrFSBadLength = errors.New("bad dir length")
+ // ErrFSBadData is returned by [FS.Compare] for a file with unexpected
+ // contents.
+ ErrFSBadData = errors.New("data differs")
+ // ErrFSBadMode is returned by [FS.Compare] for an entry with unexpected
+ // mode.
+ ErrFSBadMode = errors.New("mode differs")
+ // ErrFSInvalidEnt is returned by [FS.Compare] if an invalid [FS] is visited.
+ ErrFSInvalidEnt = errors.New("invalid entry condition")
+)
+
+// FS represents part of a filesystem hierarchy.
+type FS struct {
+ // Expected mode of corresponding entry.
+ Mode fs.FileMode `json:"mode"`
+ // Expected directory contents. The directory is not descended if Dir is nil.
+ Dir map[string]*FS `json:"dir"`
+ // Expected file contents. The file is not read if Data is nil.
+ Data *string `json:"data"`
+}
+
+// dprintf calls printf if it is non-nil.
+func dprintf(printf func(format string, a ...any), format string, a ...any) {
+ if printf == nil {
+ return
+ }
+ printf(format, a...)
+}
+
+// printDir prints a failed [FS.Compare] directory.
+func printDir(
+ printf func(format string, a ...any),
+ prefix string,
+ dir []fs.DirEntry,
+) {
+ names := make([]string, len(dir))
+ for i, ent := range dir {
+ name := ent.Name()
+ if ent.IsDir() {
+ name += "/"
+ }
+ names[i] = fmt.Sprintf("%q", name)
+ }
+ dprintf(printf, "[FAIL] d %s: %s", prefix, strings.Join(names, " "))
+}
+
+// Compare compares the contents of prefix against the hierarchy described by s.
+func (s *FS) Compare(
+ printf func(format string, a ...any),
+ prefix string,
+ e fs.FS,
+) error {
+ if s.Data != nil {
+ if s.Dir != nil {
+ panic("invalid state")
+ }
+ panic("invalid compare call")
+ }
+
+ if s.Dir == nil {
+ dprintf(printf, "[ OK ] s %s", prefix)
+ return nil
+ }
+
+ var dir []fs.DirEntry
+ if d, err := fs.ReadDir(e, prefix); err != nil {
+ return err
+ } else if len(d) != len(s.Dir) {
+ printDir(printf, prefix, d)
+ return ErrFSBadLength
+ } else {
+ dir = d
+ }
+
+ for _, got := range dir {
+ name := got.Name()
+
+ if want, ok := s.Dir[name]; !ok {
+ printDir(printf, prefix, dir)
+ return fs.ErrNotExist
+ } else if want.Dir != nil && !got.IsDir() {
+ printDir(printf, prefix, dir)
+ return ErrFSInvalidEnt
+ } else {
+ name = filepath.Join(prefix, name)
+
+ if fi, err := got.Info(); err != nil {
+ return err
+ } else if fi.Mode() != want.Mode {
+ dprintf(printf, "[FAIL] m %s: %#o, want %#o",
+ name, uint32(fi.Mode()), uint32(want.Mode))
+ return ErrFSBadMode
+ }
+
+ if want.Data != nil {
+ if want.Dir != nil {
+ panic("invalid state")
+ }
+ if v, err := fs.ReadFile(e, name); err != nil {
+ return err
+ } else if string(v) != *want.Data {
+ dprintf(printf,
+ "[FAIL] f %s\n\t got: %s\n\twant: %s",
+ name, v, *want.Data,
+ )
+ return ErrFSBadData
+ }
+ dprintf(printf, "[ OK ] f %s", name)
+ } else if err := want.Compare(printf, name, e); err != nil {
+ return err
+ }
+ }
+ }
+ dprintf(printf, "[ OK ] d %s", prefix)
+ return nil
+}
diff --git a/test/internal/testsuite/fs_test.go b/test/internal/testsuite/fs_test.go
new file mode 100644
index 00000000..5c93bb46
--- /dev/null
+++ b/test/internal/testsuite/fs_test.go
@@ -0,0 +1,85 @@
+package testsuite_test
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io/fs"
+ "testing"
+ "testing/fstest"
+
+ "hakurei.app/test/internal/testsuite"
+)
+
+func TestCompare(t *testing.T) {
+ var (
+ fsPasswdSample = "u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh"
+ fsGroupSample = "hakurei:x:65534:"
+ )
+
+ testCases := []struct {
+ name string
+
+ sample fstest.MapFS
+ want *testsuite.FS
+ wantOut string
+ wantErr error
+ }{
+ {"skip", fstest.MapFS{}, &testsuite.FS{}, "[ OK ] s .\x00", nil},
+ {"simple pass", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
+ &testsuite.FS{Dir: map[string]*testsuite.FS{".hakurei": {Mode: 0x800001ed}}},
+ "[ OK ] s .hakurei\x00[ OK ] d .\x00", nil},
+ {"bad length", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
+ &testsuite.FS{Dir: make(map[string]*testsuite.FS)},
+ "[FAIL] d .: \".hakurei/\"\x00", testsuite.ErrFSBadLength},
+ {"top level bad mode", fstest.MapFS{".hakurei": {Mode: 0x800001ed}},
+ &testsuite.FS{Dir: map[string]*testsuite.FS{".hakurei": {Mode: 0xdeadbeef}}},
+ "[FAIL] m .hakurei: 020000000755, want 033653337357\x00", testsuite.ErrFSBadMode},
+ {"invalid entry condition", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
+ &testsuite.FS{Dir: map[string]*testsuite.FS{"test": {Dir: make(map[string]*testsuite.FS)}}},
+ "[FAIL] d .: \"test\"\x00", testsuite.ErrFSInvalidEnt},
+ {"nonexistent", fstest.MapFS{"test": {Data: []byte{'0'}, Mode: 0644}},
+ &testsuite.FS{Dir: map[string]*testsuite.FS{".test": {}}},
+ "[FAIL] d .: \"test\"\x00", fs.ErrNotExist},
+ {"file", fstest.MapFS{"etc": {Mode: 0x800001c0},
+ "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
+ "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
+ }, &testsuite.FS{Dir: map[string]*testsuite.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*testsuite.FS{
+ "passwd": {Mode: 0x1a4, Data: &fsPasswdSample},
+ "group": {Mode: 0x1a4, Data: &fsGroupSample},
+ }}}}, "[ OK ] f etc/group\x00[ OK ] f etc/passwd\x00[ OK ] d etc\x00[ OK ] d .\x00", nil},
+ {"file differ", fstest.MapFS{"etc": {Mode: 0x800001c0},
+ "etc/passwd": {Data: []byte(fsPasswdSample), Mode: 0644},
+ "etc/group": {Data: []byte(fsGroupSample), Mode: 0644},
+ }, &testsuite.FS{Dir: map[string]*testsuite.FS{"etc": {Mode: 0x800001c0, Dir: map[string]*testsuite.FS{
+ "passwd": {Mode: 0x1a4, Data: &fsGroupSample},
+ "group": {Mode: 0x1a4, Data: &fsGroupSample},
+ }}}}, "[ OK ] f etc/group\x00[FAIL] f etc/passwd\n\t got: u0_a20:x:65534:65534:Hakurei:/var/lib/persist/module/hakurei/u0/a20:/run/current-system/sw/bin/zsh\n\twant: hakurei:x:65534:\x00", testsuite.ErrFSBadData},
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ var buf bytes.Buffer
+
+ err := tc.want.Compare(
+ func(format string, a ...any) {
+ _, _ = fmt.Fprintf(&buf, format+"\x00", a...)
+ },
+ ".", tc.sample,
+ )
+ if !errors.Is(err, tc.wantErr) {
+ t.Errorf(
+ "Compare: error = %v; wantErr %v",
+ err, tc.wantErr,
+ )
+ }
+
+ if buf.String() != tc.wantOut {
+ t.Errorf(
+ "Compare: output %q; want %q",
+ &buf, tc.wantOut,
+ )
+ }
+ })
+ }
+}
diff --git a/test/internal/testsuite/testsuite.go b/test/internal/testsuite/testsuite.go
index ef250ec8..6b4cd717 100644
--- a/test/internal/testsuite/testsuite.go
+++ b/test/internal/testsuite/testsuite.go
@@ -11,18 +11,8 @@ import (
"os/signal"
"os/user"
"syscall"
- "testing"
)
-func init() {
- if testing.Testing() {
- return
- }
-
- log.SetFlags(0)
- log.SetPrefix("testsuite: ")
-}
-
// ReceiveSignals blocks until a termination signal arrives, and terminates.
func ReceiveSignals() {
s := make(chan os.Signal, 3)
diff --git a/test/internal/testsuite/testsuite_root.go b/test/internal/testsuite/testsuite_root.go
index 830e272c..11f503ef 100644
--- a/test/internal/testsuite/testsuite_root.go
+++ b/test/internal/testsuite/testsuite_root.go
@@ -11,4 +11,7 @@ func init() {
if os.Geteuid() != 0 {
log.Fatal("this program must run as root")
}
+
+ log.SetFlags(0)
+ log.SetPrefix("testsuite: ")
}