diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/testsuite/testsuite.go | 263 |
1 files changed, 261 insertions, 2 deletions
diff --git a/internal/testsuite/testsuite.go b/internal/testsuite/testsuite.go index 2c82e1e9..aa03327f 100644 --- a/internal/testsuite/testsuite.go +++ b/internal/testsuite/testsuite.go @@ -8,18 +8,27 @@ import ( "bufio" "context" "crypto/sha512" + "encoding/json" "errors" + "io" "log" "os" "os/exec" "os/signal" "os/user" + "path/filepath" + "slices" "strconv" "strings" "sync" "sync/atomic" "syscall" "time" + + "hakurei.app/check" + "hakurei.app/fhs" + "hakurei.app/hst" + "hakurei.app/internal/store" ) // serial is the next value returned by [NewSerial]. @@ -147,6 +156,71 @@ func MustStartWith( return cmd.Process, MustStart(cmd) } +// MustOutput is like [MustRun] and [MustFail], but collects the entire output +// in memory and returns them as strings. +func MustOutput( + ctx context.Context, + cred *syscall.Credential, + extraEnv []string, + exitCode int, + name string, + command ...string, +) (string, string) { + var stdout, stderr strings.Builder + cmd := exec.CommandContext(ctx, command[0], command[1:]...) + cmd.Stdout, cmd.Stderr = &stdout, &stderr + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + Credential: cred, + } + MustAppendEnv(cmd, extraEnv...) + + err := cmd.Run() + if err == nil { + if exitCode == 0 { + return stdout.String(), stderr.String() + } + log.Fatalf( + "%s succeeded unexpectedly"+ + "\nstdout = %q\nstderr = %q", + name, + stdout.String(), + stderr.String(), + ) + } + + if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatal(err) + } else if e.ExitCode() != exitCode { + log.Fatalf( + "%s: %v"+ + "\nstdout = %q\nstderr = %q", + name, e, + stdout.String(), + stderr.String(), + ) + } + return stdout.String(), stderr.String() +} + +// AssertOutput returns a function that terminates the program if called with +// arguments differing from the ones it is initialised with. +func AssertOutput(action, stdout, stderr string) ( + assert func(stdout, stderr string), +) { + return func(gotStdout, gotStderr string) { + if stdout != gotStdout || stderr != gotStderr { + log.Fatalf( + "unexpected output %s"+ + "\nstdout = %q\nstderr = %q", + action, + gotStdout, + gotStderr, + ) + } + } +} + // MustCheckFilter is like [CheckFilter], but terminates the test suite if a // non-nil error is returned. Otherwise, the tracee is terminated after it // resumes. @@ -202,14 +276,43 @@ func Poll( } const ( - // XDGRuntimeDir is the hardcoded XDG runtime directory for the user - // described by [GetUser]. + // XDGRuntimeDir is the hardcoded XDG runtime directory of the only user + // mapped in hsurc. XDGRuntimeDir = "/var/run/user/1000" // XDGRuntimeEnv is the environment variable string for XDG_RUNTIME_DIR. XDGRuntimeEnv = "XDG_RUNTIME_DIR=" + XDGRuntimeDir + + // HakureiPrefix is the hakurei work directory of the only user mapped in + // hsurc. + HakureiPrefix = "/tmp/hakurei.0/" ) +// MustPrepareUserDirs creates user-specific directories. +func MustPrepareUserDirs(cred *syscall.Credential) { + if err := os.MkdirAll(XDGRuntimeDir, 0700); err != nil { + log.Fatal(err) + } else if err = os.Chown( + XDGRuntimeDir, + int(cred.Uid), + int(cred.Gid), + ); err != nil { + log.Fatal(err) + } +} + +// SessionEnv returns environment variables referring to the hosted session. +func SessionEnv(dbusEnv string) []string { + return []string{ + "PATH=" + os.Getenv("PATH"), + "TERM=xterm", + XDGRuntimeEnv, + WaylandEnv, + "DISPLAY=:0", + dbusEnv, + } +} + // MustStartSessionBus starts a session bus that is never explicitly terminated. // The test suite is terminated if the session bus daemon terminates. func MustStartSessionBus(cred *syscall.Credential) (dbusEnv string) { @@ -336,3 +439,159 @@ func MustStartPipeWire(cred *syscall.Credential, dbusEnv string) { log.Fatal("wireplumber terminated unexpectedly") }() } + +// MustScanFor continuously scans the proc filesystem and calls f for each entry +// visited. +func MustScanFor(f func(ps *StatScanner) bool) int { + var ps StatScanner + + for ps.Scan() { + if f(&ps) { + break + } + } + if err := ps.Err(); err != nil { + log.Fatal(err) + } + return ps.Stat().PID +} + +// MustStartHakurei starts a hakurei container configured by c. +func MustStartHakurei( + wg *sync.WaitGroup, + cred *syscall.Credential, + dbusEnv string, + onDone func(err error, output string), + c *hst.Config, +) (p hst.State, pid int) { + s := store.New(check.MustAbs("/tmp/hakurei.0/state")) + + cmd := exec.Command( + "script", "/dev/null", + "-E", "always", + "-qec", + "hakurei run "+ + "--identifier-fd=5"+ + " 4 1>&3", + ) + cmd.SysProcAttr = &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + Credential: cred, + } + var output strings.Builder + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, &output, &output + cmd.Env = SessionEnv(dbusEnv) + + var err error + var notify, _notify, _conf, conf, ident, _ident *os.File + if notify, _notify, err = os.Pipe(); err != nil { + log.Fatal(err) + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _notify) + if _conf, conf, err = os.Pipe(); err != nil { + log.Fatal(err) + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _conf) + if ident, _ident, err = os.Pipe(); err != nil { + log.Fatal(err) + } + cmd.ExtraFiles = append(cmd.ExtraFiles, _ident) + + done := MustStart(cmd) + wg.Go(func() { _err := <-done; onDone(_err, output.String()) }) + + if err = json.NewEncoder(conf).Encode(c); err != nil { + log.Fatal(err) + } else if err = conf.Close(); err != nil { + log.Fatal(err) + } + + var id hst.ID + if _, err = io.ReadFull(ident, id[:]); err != nil { + log.Fatal(err) + } else if err = ident.Close(); err != nil { + log.Fatal(err) + } + + if _, err = io.ReadFull(notify, make([]byte, 8)); err != nil { + log.Fatal(err) + } else if err = notify.Close(); err != nil { + log.Fatal(err) + } + + var ok bool + entries, copyError := s.All() + for entry := range entries { + if entry.ID == id { + ok = true + if _, err = entry.Load(&p, nil); err != nil { + log.Fatal(err) + } + break + } + } + if err = copyError(); err != nil { + log.Fatal(err) + } + if !ok { + log.Fatalf("instance %s is not present in store", id) + } + + var stat syscall.Stat_t + pid = MustScanFor(func(ps *StatScanner) bool { + select { + case err = <-done: + if err == nil { + log.Fatal("test process terminated unexpectedly") + } + log.Fatal(err) + default: + break + } + + var args []string + if args, err = ps.Stat().Args(); err != nil { + if IsNotExist(err) { + return false + } + log.Fatal(err) + } else if !slices.Equal(args, c.Container.Args) { + return false + } + + if err = ps.Stat().Stat(&stat); err != nil { + if IsNotExist(err) { + return false + } + log.Fatal(err) + } + + uid := hst.ToUser[uint32](0, uint32(c.Identity)) + if stat.Uid != uid || stat.Gid != uid { + return false + } + + var t []byte + if t, err = os.ReadFile(filepath.Join( + fhs.Proc, + strconv.Itoa(ps.Stat().PPID), + "stat", + )); err != nil { + if IsNotExist(err) { + return false + } + log.Fatal(err) + } + + var _stat Stat + if err = _stat.UnmarshalText(t); err != nil { + log.Fatal(err) + } + if _stat.PPID != p.ShimPID { + return false + } + + return true + }) + return +} |
