diff options
Diffstat (limited to 'test/internal/testsuite/testsuite.go')
| -rw-r--r-- | test/internal/testsuite/testsuite.go | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/test/internal/testsuite/testsuite.go b/test/internal/testsuite/testsuite.go index 6b4cd717..00eb2f92 100644 --- a/test/internal/testsuite/testsuite.go +++ b/test/internal/testsuite/testsuite.go @@ -5,12 +5,19 @@ package testsuite import ( + "bufio" + "context" + "crypto/sha512" + "errors" "log" "os" "os/exec" "os/signal" "os/user" + "strconv" + "sync" "syscall" + "time" ) // ReceiveSignals blocks until a termination signal arrives, and terminates. @@ -39,7 +46,231 @@ func MustRun(command ...string) { } } +// ErrUnexpectedSuccess is returned for processes expected to exit with a +// non-zero code, but failed to do so. +var ErrUnexpectedSuccess = errors.New("process unexpectedly exited with code 0") + +// MustFail runs command and terminates the testsuite if the program fails to +// start or exits with code 0. +func MustFail(command ...string) { + cmd := exec.Command(command[0], command[1:]...) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + if err := cmd.Run(); err == nil { + log.Fatal(ErrUnexpectedSuccess) + } else if e, ok := errors.AsType[*exec.ExitError](err); !ok { + log.Fatal(err) + } else if !e.Exited() { + log.Fatal(e) + } +} + // MustRunAs wraps [MustRun] for sudo. func MustRunAs(username string, command ...string) { MustRun(append([]string{"sudo", "-u", username}, command...)...) } + +// MustFailAs wraps [MustFail] for sudo. +func MustFailAs(username string, command ...string) { + MustFail(append([]string{"sudo", "-u", username}, command...)...) +} + +// MustStart starts cmd and returns a channel delivering its wait error. +func MustStart(cmd *exec.Cmd) (done <-chan error) { + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + d := make(chan error) + go func() { d <- cmd.Wait() }() + return d +} + +// MustStartAs wraps [MustStart] for sudo. +func MustStartAs( + ctx context.Context, + username string, + files []*os.File, + command ...string, +) (proc *os.Process, done <-chan error) { + sudoArgs := []string{ + "-u", username, + } + if len(files) != 0 { + sudoArgs = append(sudoArgs, "-C", strconv.Itoa(len(files)+4)) + } + sudoArgs = append(sudoArgs, "--") + cmd := exec.CommandContext(ctx, "sudo", append(sudoArgs, command...)...) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + cmd.ExtraFiles = files + cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGTERM} + return cmd.Process, MustStart(cmd) +} + +// MustCheckFilter is like [CheckFilter], but terminates the test suite if a +// non-nil error is returned. Otherwise, the tracee is terminated after it +// resumes. +func MustCheckFilter(pid int, sum [sha512.Size]byte) { + // podman installs its own filter + if err := CheckFilter(pid, 1, sum); err != nil { + log.Fatal(err) + } else if err = syscall.Kill(pid, syscall.SIGTERM); err != nil { + log.Fatalf("cannot terminate tracee: %v", err) + } +} + +// FilterTerminated returns a non-nil error if err is not an [exec.ExitError] +// describing a process terminated by a syscall.SIGTERM signal. +func FilterTerminated(err error) error { + if err == nil { + return ErrUnexpectedSuccess + } + + e, ok := errors.AsType[*exec.ExitError](err) + if !ok { + return err + } + + if e.ExitCode() == 0x80+int(syscall.SIGTERM) { + return nil + } + return e +} + +// Poll repeatedly runs command until it succeeds. +func Poll(d time.Duration, command ...string) { + for range time.NewTicker(d).C { + cmd := exec.Command(command[0], command[1:]...) + if err := cmd.Run(); err != nil { + if e, ok := errors.AsType[*exec.ExitError](err); ok && e.Exited() { + continue + } + log.Fatal(err) + } + break + } +} + +const ( + // XDGRuntimeDir is the hardcoded XDG runtime directory for the user + // described by [GetUser]. + XDGRuntimeDir = "/var/run/user/1000" + + // XDGRuntimeEnv is the environment variable string for XDG_RUNTIME_DIR. + XDGRuntimeEnv = "XDG_RUNTIME_DIR=" + XDGRuntimeDir +) + +// MustStartSessionBus starts a session bus that is never explicitly terminated. +// The test suite is terminated if the session bus daemon terminates. +func MustStartSessionBus(username string) (dbusEnv string) { + r, w, err := os.Pipe() + if err != nil { + log.Fatal(err) + } + + // this is never explicitly terminated + _, done := MustStartAs( + context.Background(), username, []*os.File{w}, + "dbus-daemon", + "--print-address=3", + "--address=unix:path="+XDGRuntimeDir+"/dbus", + "--session", + "--nofork", + "--nopidfile", + ) + + go func() { + if _err := <-done; _err != nil { + log.Fatal(_err) + } + log.Fatal("session bus terminated unexpectedly") + }() + + dbusEnv, err = bufio.NewReader(r).ReadString('\n') + if err != nil { + log.Fatal(err) + } + dbusEnv = dbusEnv[:len(dbusEnv)-1] + log.Printf("dbus listening on %s", dbusEnv) + dbusEnv = "DBUS_SESSION_BUS_ADDRESS=" + dbusEnv + + if err = r.Close(); err != nil { + log.Fatal(err) + } + return +} + +const ( + // SwayEnv is the environment variable string for the sway IPC socket. + SwayEnv = "SWAYSOCK=" + XDGRuntimeDir + "/sway" + // WaylandEnv is the environment variable string for the wayland display. + WaylandEnv = "WAYLAND_DISPLAY=wayland-1" +) + +// MustStartSway starts the sway wayland display server which must be terminated +// by calling [TerminateSway]. +func MustStartSway( + wg *sync.WaitGroup, + username, dbusEnv string, +) { + wg.Go(func() { + // this is terminated via swaymsg + _, done := MustStartAs( + context.Background(), username, nil, "env", + "WLR_BACKENDS=headless", + XDGRuntimeEnv, + SwayEnv, + dbusEnv, + "sway", + ) + if err := <-done; err != nil { + log.Fatal(err) + } + }) + + Poll(50*time.Millisecond, "sudo", "-u", username, SwayEnv, "swaymsg") + log.Printf("sway available via %s", SwayEnv) +} + +// TerminateSway requests for the sway server to terminate via sway IPC. +func TerminateSway(username string) { + MustFailAs(username, SwayEnv, "swaymsg", "exit") +} + +// MustStartPipeWire starts a PipeWire server that is never explicitly +// terminated. The test suite is terminated if the PipeWire server terminates. +func MustStartPipeWire(username, dbusEnv string) { + // this is never explicitly terminated + _, done := MustStartAs( + context.Background(), username, nil, "env", + XDGRuntimeEnv, + dbusEnv, + "pipewire", + ) + + go func() { + if _err := <-done; _err != nil { + log.Fatal(_err) + } + log.Fatal("pipewire terminated unexpectedly") + }() + + Poll(50*time.Millisecond, "sudo", "-u", username, + XDGRuntimeEnv, + dbusEnv, + "wpctl", + "status", + ) + + _, _done := MustStartAs( + context.Background(), username, nil, "env", + XDGRuntimeEnv, + dbusEnv, + "wireplumber", + ) + + go func() { + if _err := <-_done; _err != nil { + log.Fatal(_err) + } + log.Fatal("wireplumber terminated unexpectedly") + }() +} |
