hakurei/container/dispatcher_test.go
Ophestra 49600a6f46
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 1m53s
Test / Hakurei (push) Successful in 3m18s
Test / Sandbox (race detector) (push) Successful in 3m40s
Test / Hpkg (push) Successful in 3m35s
Test / Hakurei (race detector) (push) Successful in 5m19s
Test / Flake checks (push) Successful in 1m39s
container/stub: export stub helpers
These are very useful in many packages containing relatively large amount of code making calls to difficult or impossible to stub functions.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-08-31 23:11:25 +09:00

714 lines
18 KiB
Go

package container
import (
"bytes"
"io"
"io/fs"
"os"
"os/exec"
"reflect"
"runtime"
"slices"
"strings"
"syscall"
"testing"
"time"
"hakurei.app/container/seccomp"
"hakurei.app/container/stub"
)
type opValidTestCase struct {
name string
op Op
want bool
}
func checkOpsValid(t *testing.T, testCases []opValidTestCase) {
t.Helper()
t.Run("valid", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
if got := tc.op.Valid(); got != tc.want {
t.Errorf("Valid: %v, want %v", got, tc.want)
}
})
}
})
}
type opsBuilderTestCase struct {
name string
ops *Ops
want Ops
}
func checkOpsBuilder(t *testing.T, testCases []opsBuilderTestCase) {
t.Helper()
t.Run("build", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
if !slices.EqualFunc(*tc.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) {
t.Errorf("Ops: %#v, want %#v", tc.ops, tc.want)
}
})
}
})
}
type opIsTestCase struct {
name string
op, v Op
want bool
}
func checkOpIs(t *testing.T, testCases []opIsTestCase) {
t.Helper()
t.Run("is", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
if got := tc.op.Is(tc.v); got != tc.want {
t.Errorf("Is: %v, want %v", got, tc.want)
}
})
}
})
}
type opMetaTestCase struct {
name string
op Op
wantPrefix string
wantString string
}
func checkOpMeta(t *testing.T, testCases []opMetaTestCase) {
t.Helper()
t.Run("meta", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
t.Run("prefix", func(t *testing.T) {
t.Helper()
if got := tc.op.prefix(); got != tc.wantPrefix {
t.Errorf("prefix: %q, want %q", got, tc.wantPrefix)
}
})
t.Run("string", func(t *testing.T) {
t.Helper()
if got := tc.op.String(); got != tc.wantString {
t.Errorf("String: %s, want %s", got, tc.wantString)
}
})
})
}
})
}
type simpleTestCase struct {
name string
f func(k syscallDispatcher) error
want stub.Expect
wantErr error
}
func checkSimple(t *testing.T, fname string, testCases []simpleTestCase) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
defer stub.HandleExit()
k := &kstub{stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, tc.want)}
if err := tc.f(k); !reflect.DeepEqual(err, tc.wantErr) {
t.Errorf("%s: error = %v, want %v", fname, err, tc.wantErr)
}
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
t.Helper()
t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len())
})
})
}
}
type opBehaviourTestCase struct {
name string
params *Params
op Op
early []stub.Call
wantErrEarly error
apply []stub.Call
wantErrApply error
}
func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
t.Helper()
t.Run("behaviour", func(t *testing.T) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Helper()
defer stub.HandleExit()
state := &setupState{Params: tc.params}
k := &kstub{stub.New(t,
func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} },
stub.Expect{Calls: slices.Concat(tc.early, []stub.Call{{Name: stub.CallSeparator}}, tc.apply)},
)}
errEarly := tc.op.early(state, k)
k.Expects(stub.CallSeparator)
if !reflect.DeepEqual(errEarly, tc.wantErrEarly) {
t.Errorf("early: error = %v, want %v", errEarly, tc.wantErrEarly)
}
if errEarly != nil {
goto out
}
if err := tc.op.apply(state, k); !reflect.DeepEqual(err, tc.wantErrApply) {
t.Errorf("apply: error = %v, want %v", err, tc.wantErrApply)
}
out:
k.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) {
count := k.Pos() - 1 // separator
if count < len(tc.early) {
t.Errorf("early: %d calls, want %d", count, len(tc.early))
} else {
t.Errorf("apply: %d calls, want %d", count-len(tc.early), len(tc.apply))
}
})
})
}
})
}
func sliceAddr[S any](s []S) *[]S { return &s }
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
// check happens in Close, and cleanup is not guaranteed to run, so relying on it for sloppy implementations will cause sporadic test results
f.cleanup = runtime.AddCleanup(f, func(name string) { f.t.Fatalf("checkedOsFile %s became unreachable without a call to Close", name) }, f.name)
return f
}
type checkedOsFile struct {
t *testing.T
name string
want string
closeErr error
cleanup runtime.Cleanup
bytes.Buffer
}
func (f *checkedOsFile) Name() string { return f.name }
func (f *checkedOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (f *checkedOsFile) Close() error {
defer f.cleanup.Stop()
if f.String() != f.want {
f.t.Errorf("checkedOsFile:\n%s\nwant\n%s", f.String(), f.want)
return syscall.ENOTRECOVERABLE
}
return f.closeErr
}
func newConstFile(s string) osFile { return &readerOsFile{Reader: strings.NewReader(s)} }
type readerOsFile struct {
closed bool
io.Reader
}
func (*readerOsFile) Name() string { panic("unreachable") }
func (*readerOsFile) Write([]byte) (int, error) { panic("unreachable") }
func (*readerOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (r *readerOsFile) Close() error {
if r.closed {
return os.ErrClosed
}
r.closed = true
return nil
}
type writeErrOsFile struct{ err error }
func (writeErrOsFile) Name() string { panic("unreachable") }
func (f writeErrOsFile) Write([]byte) (int, error) { return 0, f.err }
func (writeErrOsFile) Stat() (fs.FileInfo, error) { panic("unreachable") }
func (writeErrOsFile) Read([]byte) (int, error) { panic("unreachable") }
func (writeErrOsFile) Close() error { panic("unreachable") }
type isDirFi bool
func (isDirFi) Name() string { panic("unreachable") }
func (isDirFi) Size() int64 { panic("unreachable") }
func (isDirFi) Mode() fs.FileMode { panic("unreachable") }
func (isDirFi) ModTime() time.Time { panic("unreachable") }
func (fi isDirFi) IsDir() bool { return bool(fi) }
func (isDirFi) Sys() any { panic("unreachable") }
func stubDir(names ...string) []os.DirEntry {
d := make([]os.DirEntry, len(names))
for i, name := range names {
d[i] = nameDentry(name)
}
return d
}
type nameDentry string
func (e nameDentry) Name() string { return string(e) }
func (nameDentry) IsDir() bool { panic("unreachable") }
func (nameDentry) Type() fs.FileMode { panic("unreachable") }
func (nameDentry) Info() (fs.FileInfo, error) { panic("unreachable") }
type kstub struct{ *stub.Stub[syscallDispatcher] }
func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) }
func (k *kstub) lockOSThread() { k.Helper(); k.Expects("lockOSThread") }
func (k *kstub) setPtracer(pid uintptr) error {
k.Helper()
return k.Expects("setPtracer").Error(
stub.CheckArg(k.Stub, "pid", pid, 0))
}
func (k *kstub) setDumpable(dumpable uintptr) error {
k.Helper()
return k.Expects("setDumpable").Error(
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
}
func (k *kstub) setNoNewPrivs() error { k.Helper(); return k.Expects("setNoNewPrivs").Err }
func (k *kstub) lastcap() uintptr { k.Helper(); return k.Expects("lastcap").Ret.(uintptr) }
func (k *kstub) capset(hdrp *capHeader, datap *[2]capData) error {
k.Helper()
return k.Expects("capset").Error(
stub.CheckArgReflect(k.Stub, "hdrp", hdrp, 0),
stub.CheckArgReflect(k.Stub, "datap", datap, 1))
}
func (k *kstub) capBoundingSetDrop(cap uintptr) error {
k.Helper()
return k.Expects("capBoundingSetDrop").Error(
stub.CheckArg(k.Stub, "cap", cap, 0))
}
func (k *kstub) capAmbientClearAll() error { k.Helper(); return k.Expects("capAmbientClearAll").Err }
func (k *kstub) capAmbientRaise(cap uintptr) error {
k.Helper()
return k.Expects("capAmbientRaise").Error(
stub.CheckArg(k.Stub, "cap", cap, 0))
}
func (k *kstub) isatty(fd int) bool {
k.Helper()
expect := k.Expects("isatty")
if !stub.CheckArg(k.Stub, "fd", fd, 0) {
k.FailNow()
}
return expect.Ret.(bool)
}
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
k.Helper()
expect := k.Expects("receive")
var closed bool
closeFunc = func() error {
if closed {
k.Error("closeFunc called more than once")
return os.ErrClosed
}
closed = true
if expect.Ret != nil {
// use return stored in kexpect for closeFunc instead
return expect.Ret.(error)
}
return nil
}
err = expect.Error(
stub.CheckArg(k.Stub, "key", key, 0),
stub.CheckArgReflect(k.Stub, "e", e, 1),
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
// 3 is unused so stores params
if expect.Args[3] != nil {
if v, ok := expect.Args[3].(*initParams); ok && v != nil {
if p, ok0 := e.(*initParams); ok0 && p != nil {
*p = *v
}
}
}
// 4 is unused so stores fd
if expect.Args[4] != nil {
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
if fdp != nil {
*fdp = v
}
}
}
return
}
func (k *kstub) bindMount(source, target string, flags uintptr, eq bool) error {
k.Helper()
return k.Expects("bindMount").Error(
stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "flags", flags, 2),
stub.CheckArg(k.Stub, "eq", eq, 3))
}
func (k *kstub) remount(target string, flags uintptr) error {
k.Helper()
return k.Expects("remount").Error(
stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1))
}
func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
k.Helper()
return k.Expects("mountTmpfs").Error(
stub.CheckArg(k.Stub, "fsname", fsname, 0),
stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "flags", flags, 2),
stub.CheckArg(k.Stub, "size", size, 3),
stub.CheckArg(k.Stub, "perm", perm, 4))
}
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
k.Helper()
return k.Expects("ensureFile").Error(
stub.CheckArg(k.Stub, "name", name, 0),
stub.CheckArg(k.Stub, "perm", perm, 1),
stub.CheckArg(k.Stub, "pperm", pperm, 2))
}
func (k *kstub) seccompLoad(rules []seccomp.NativeRule, flags seccomp.ExportFlag) error {
k.Helper()
return k.Expects("seccompLoad").Error(
stub.CheckArgReflect(k.Stub, "rules", rules, 0),
stub.CheckArg(k.Stub, "flags", flags, 1))
}
func (k *kstub) notify(c chan<- os.Signal, sig ...os.Signal) {
k.Helper()
expect := k.Expects("notify")
if c == nil || expect.Error(
stub.CheckArgReflect(k.Stub, "sig", sig, 1)) != nil {
k.FailNow()
}
// export channel for external instrumentation
if chanf, ok := expect.Args[0].(func(c chan<- os.Signal)); ok && chanf != nil {
chanf(c)
}
}
func (k *kstub) start(c *exec.Cmd) error {
k.Helper()
expect := k.Expects("start")
err := expect.Error(
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3))
if process, ok := expect.Ret.(*os.Process); ok && process != nil {
c.Process = process
}
return err
}
func (k *kstub) signal(c *exec.Cmd, sig os.Signal) error {
k.Helper()
return k.Expects("signal").Error(
stub.CheckArg(k.Stub, "c.Path", c.Path, 0),
stub.CheckArgReflect(k.Stub, "c.Args", c.Args, 1),
stub.CheckArgReflect(k.Stub, "c.Env", c.Env, 2),
stub.CheckArg(k.Stub, "c.Dir", c.Dir, 3),
stub.CheckArg(k.Stub, "sig", sig, 4))
}
func (k *kstub) evalSymlinks(path string) (string, error) {
k.Helper()
expect := k.Expects("evalSymlinks")
return expect.Ret.(string), expect.Error(
stub.CheckArg(k.Stub, "path", path, 0))
}
func (k *kstub) exit(code int) {
k.Helper()
k.Expects("exit")
if !stub.CheckArg(k.Stub, "code", code, 0) {
k.FailNow()
}
panic(stub.PanicExit)
}
func (k *kstub) getpid() int { k.Helper(); return k.Expects("getpid").Ret.(int) }
func (k *kstub) stat(name string) (os.FileInfo, error) {
k.Helper()
expect := k.Expects("stat")
return expect.Ret.(os.FileInfo), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) mkdir(name string, perm os.FileMode) error {
k.Helper()
return k.Expects("mkdir").Error(
stub.CheckArg(k.Stub, "name", name, 0),
stub.CheckArg(k.Stub, "perm", perm, 1))
}
func (k *kstub) mkdirTemp(dir, pattern string) (string, error) {
k.Helper()
expect := k.Expects("mkdirTemp")
return expect.Ret.(string), expect.Error(
stub.CheckArg(k.Stub, "dir", dir, 0),
stub.CheckArg(k.Stub, "pattern", pattern, 1))
}
func (k *kstub) mkdirAll(path string, perm os.FileMode) error {
k.Helper()
return k.Expects("mkdirAll").Error(
stub.CheckArg(k.Stub, "path", path, 0),
stub.CheckArg(k.Stub, "perm", perm, 1))
}
func (k *kstub) readdir(name string) ([]os.DirEntry, error) {
k.Helper()
expect := k.Expects("readdir")
return expect.Ret.([]os.DirEntry), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) openNew(name string) (osFile, error) {
k.Helper()
expect := k.Expects("openNew")
return expect.Ret.(osFile), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) writeFile(name string, data []byte, perm os.FileMode) error {
k.Helper()
return k.Expects("writeFile").Error(
stub.CheckArg(k.Stub, "name", name, 0),
stub.CheckArgReflect(k.Stub, "data", data, 1),
stub.CheckArg(k.Stub, "perm", perm, 2))
}
func (k *kstub) createTemp(dir, pattern string) (osFile, error) {
k.Helper()
expect := k.Expects("createTemp")
return expect.Ret.(osFile), expect.Error(
stub.CheckArg(k.Stub, "dir", dir, 0),
stub.CheckArg(k.Stub, "pattern", pattern, 1))
}
func (k *kstub) remove(name string) error {
k.Helper()
return k.Expects("remove").Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) newFile(fd uintptr, name string) *os.File {
k.Helper()
expect := k.Expects("newFile")
if expect.Error(
stub.CheckArg(k.Stub, "fd", fd, 0),
stub.CheckArg(k.Stub, "name", name, 1)) != nil {
k.FailNow()
}
return expect.Ret.(*os.File)
}
func (k *kstub) symlink(oldname, newname string) error {
k.Helper()
return k.Expects("symlink").Error(
stub.CheckArg(k.Stub, "oldname", oldname, 0),
stub.CheckArg(k.Stub, "newname", newname, 1))
}
func (k *kstub) readlink(name string) (string, error) {
k.Helper()
expect := k.Expects("readlink")
return expect.Ret.(string), expect.Error(
stub.CheckArg(k.Stub, "name", name, 0))
}
func (k *kstub) umask(mask int) (oldmask int) {
k.Helper()
expect := k.Expects("umask")
if !stub.CheckArg(k.Stub, "mask", mask, 0) {
k.FailNow()
}
return expect.Ret.(int)
}
func (k *kstub) sethostname(p []byte) (err error) {
k.Helper()
return k.Expects("sethostname").Error(
stub.CheckArgReflect(k.Stub, "p", p, 0))
}
func (k *kstub) chdir(path string) (err error) {
k.Helper()
return k.Expects("chdir").Error(
stub.CheckArg(k.Stub, "path", path, 0))
}
func (k *kstub) fchdir(fd int) (err error) {
k.Helper()
return k.Expects("fchdir").Error(
stub.CheckArg(k.Stub, "fd", fd, 0))
}
func (k *kstub) open(path string, mode int, perm uint32) (fd int, err error) {
k.Helper()
expect := k.Expects("open")
return expect.Ret.(int), expect.Error(
stub.CheckArg(k.Stub, "path", path, 0),
stub.CheckArg(k.Stub, "mode", mode, 1),
stub.CheckArg(k.Stub, "perm", perm, 2))
}
func (k *kstub) close(fd int) (err error) {
k.Helper()
return k.Expects("close").Error(
stub.CheckArg(k.Stub, "fd", fd, 0))
}
func (k *kstub) pivotRoot(newroot, putold string) (err error) {
k.Helper()
return k.Expects("pivotRoot").Error(
stub.CheckArg(k.Stub, "newroot", newroot, 0),
stub.CheckArg(k.Stub, "putold", putold, 1))
}
func (k *kstub) mount(source, target, fstype string, flags uintptr, data string) (err error) {
k.Helper()
return k.Expects("mount").Error(
stub.CheckArg(k.Stub, "source", source, 0),
stub.CheckArg(k.Stub, "target", target, 1),
stub.CheckArg(k.Stub, "fstype", fstype, 2),
stub.CheckArg(k.Stub, "flags", flags, 3),
stub.CheckArg(k.Stub, "data", data, 4))
}
func (k *kstub) unmount(target string, flags int) (err error) {
k.Helper()
return k.Expects("unmount").Error(
stub.CheckArg(k.Stub, "target", target, 0),
stub.CheckArg(k.Stub, "flags", flags, 1))
}
func (k *kstub) wait4(pid int, wstatus *syscall.WaitStatus, options int, rusage *syscall.Rusage) (wpid int, err error) {
k.Helper()
expect := k.Expects("wait4")
// special case to prevent leaking the wait4 goroutine when testing initEntrypoint
if v, ok := expect.Args[4].(int); ok && v == stub.PanicExit {
k.Log("terminating current goroutine as requested by kexpect")
panic(stub.PanicExit)
}
wpid = expect.Ret.(int)
err = expect.Error(
stub.CheckArg(k.Stub, "pid", pid, 0),
stub.CheckArg(k.Stub, "options", options, 2))
if wstatusV, ok := expect.Args[1].(syscall.WaitStatus); wstatus != nil && ok {
*wstatus = wstatusV
}
if rusageV, ok := expect.Args[3].(syscall.Rusage); rusage != nil && ok {
*rusage = rusageV
}
return
}
func (k *kstub) printf(format string, v ...any) {
k.Helper()
if k.Expects("printf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()
}
}
func (k *kstub) fatal(v ...any) {
k.Helper()
if k.Expects("fatal").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
k.FailNow()
}
panic(stub.PanicExit)
}
func (k *kstub) fatalf(format string, v ...any) {
k.Helper()
if k.Expects("fatalf").Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()
}
panic(stub.PanicExit)
}
func (k *kstub) verbose(v ...any) {
k.Helper()
if k.Expects("verbose").Error(
stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil {
k.FailNow()
}
}
func (k *kstub) verbosef(format string, v ...any) {
k.Helper()
if k.Expects("verbosef").Error(
stub.CheckArg(k.Stub, "format", format, 0),
stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil {
k.FailNow()
}
}
func (k *kstub) suspend() { k.Helper(); k.Expects("suspend") }
func (k *kstub) resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) }
func (k *kstub) beforeExit() { k.Helper(); k.Expects("beforeExit") }