summaryrefslogtreecommitdiff
path: root/integrationtests/harness.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-21 19:28:23 +0200
committerPaul Buetow <paul@buetow.org>2026-02-21 19:28:23 +0200
commita5b711c5f221704209706b79fbf310a18e079391 (patch)
tree84615902f79a901aa9d98e3423c4756477b7cf4b /integrationtests/harness.go
parent2c2cbe07f5e10fdb996e2a039cde84be44866f18 (diff)
more on integration tests
Diffstat (limited to 'integrationtests/harness.go')
-rw-r--r--integrationtests/harness.go179
1 files changed, 179 insertions, 0 deletions
diff --git a/integrationtests/harness.go b/integrationtests/harness.go
new file mode 100644
index 0000000..315fec4
--- /dev/null
+++ b/integrationtests/harness.go
@@ -0,0 +1,179 @@
+package integrationtests
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+)
+
+const (
+ workloadStartupTimeout = 5 * time.Second
+ iorShutdownGrace = 3 * time.Second
+)
+
+// TestHarness orchestrates integration tests by starting an ior trace
+// against a known ioworkload process and collecting the .ior.zst output.
+type TestHarness struct {
+ IorBinary string // path to built ior binary
+ WorkloadBinary string // path to built ioworkload binary
+ BpfObject string // path to ior.bpf.o
+ OutputDir string // temp dir for .ior.zst output
+}
+
+// Run executes a single integration test scenario. It starts the ioworkload
+// binary, reads its PID from stdout, launches ior with a PID filter, waits
+// for both to finish, and parses the resulting .ior.zst file.
+func (h *TestHarness) Run(scenario string, duration int) (TestResult, int, error) {
+ workloadCmd, workloadPID, err := h.startWorkload(scenario)
+ if err != nil {
+ return TestResult{}, 0, err
+ }
+
+ iorCmd, err := h.startIor(workloadPID, scenario, duration)
+ if err != nil {
+ workloadCmd.Process.Kill()
+ workloadCmd.Wait()
+ return TestResult{}, workloadPID, err
+ }
+
+ workloadErr, iorErr := waitBoth(workloadCmd, iorCmd, duration)
+
+ if iorErr != nil {
+ return TestResult{}, workloadPID, fmt.Errorf("ior: %w", iorErr)
+ }
+ if workloadErr != nil {
+ return TestResult{}, workloadPID, fmt.Errorf("workload: %w", workloadErr)
+ }
+
+ iorFile, err := findIorZstFile(h.OutputDir, scenario)
+ if err != nil {
+ return TestResult{}, workloadPID, fmt.Errorf("find .ior.zst: %w", err)
+ }
+
+ result, err := LoadTestResult(iorFile)
+ if err != nil {
+ return TestResult{}, workloadPID, fmt.Errorf("parse result: %w", err)
+ }
+
+ return result, workloadPID, nil
+}
+
+func (h *TestHarness) startWorkload(scenario string) (*exec.Cmd, int, error) {
+ cmd := exec.Command(h.WorkloadBinary, "--scenario="+scenario)
+ cmd.Stderr = os.Stderr
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, 0, fmt.Errorf("workload stdout pipe: %w", err)
+ }
+
+ if err := cmd.Start(); err != nil {
+ return nil, 0, fmt.Errorf("start workload: %w", err)
+ }
+
+ pidCh := make(chan int, 1)
+ errCh := make(chan error, 1)
+ go func() {
+ scanner := bufio.NewScanner(stdout)
+ if scanner.Scan() {
+ pid, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
+ if err != nil {
+ errCh <- fmt.Errorf("parse workload PID: %w", err)
+ return
+ }
+ pidCh <- pid
+ } else if err := scanner.Err(); err != nil {
+ errCh <- fmt.Errorf("reading workload stdout: %w", err)
+ } else {
+ errCh <- fmt.Errorf("workload produced no output")
+ }
+ }()
+
+ select {
+ case pid := <-pidCh:
+ return cmd, pid, nil
+ case err := <-errCh:
+ cmd.Process.Kill()
+ cmd.Wait()
+ return nil, 0, err
+ case <-time.After(workloadStartupTimeout):
+ cmd.Process.Kill()
+ cmd.Wait()
+ return nil, 0, fmt.Errorf("timeout waiting for workload PID")
+ }
+}
+
+func (h *TestHarness) startIor(pid int, scenario string, duration int) (*exec.Cmd, error) {
+ cmd := exec.Command(h.IorBinary,
+ "-pid", strconv.Itoa(pid),
+ "-flamegraph",
+ "-name", scenario,
+ "-duration", strconv.Itoa(duration),
+ )
+ cmd.Dir = h.OutputDir
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ if err := cmd.Start(); err != nil {
+ return nil, fmt.Errorf("start ior: %w", err)
+ }
+ return cmd, nil
+}
+
+// waitBoth waits for both the workload and ior commands concurrently.
+// If ior does not finish within duration + grace period, it is killed.
+func waitBoth(workloadCmd, iorCmd *exec.Cmd, duration int) (workloadErr, iorErr error) {
+ workloadDone := make(chan error, 1)
+ iorDone := make(chan error, 1)
+
+ go func() { workloadDone <- workloadCmd.Wait() }()
+ go func() { iorDone <- iorCmd.Wait() }()
+
+ timeout := time.After(time.Duration(duration)*time.Second + iorShutdownGrace)
+
+ for workloadDone != nil || iorDone != nil {
+ select {
+ case err := <-workloadDone:
+ workloadErr = err
+ workloadDone = nil
+ case err := <-iorDone:
+ iorErr = err
+ iorDone = nil
+ case <-timeout:
+ if iorDone != nil {
+ iorCmd.Process.Kill()
+ iorErr = fmt.Errorf("ior timed out")
+ iorDone = nil
+ }
+ if workloadDone != nil {
+ workloadCmd.Process.Kill()
+ workloadErr = fmt.Errorf("workload timed out")
+ workloadDone = nil
+ }
+ return
+ }
+ }
+ return
+}
+
+// findIorZstFile locates the .ior.zst file matching the scenario name in the output directory.
+func findIorZstFile(dir, scenario string) (string, error) {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return "", fmt.Errorf("read output dir: %w", err)
+ }
+
+ for _, e := range entries {
+ name := e.Name()
+ if strings.Contains(name, scenario) && strings.HasSuffix(name, ".ior.zst") {
+ return filepath.Join(dir, name), nil
+ }
+ }
+
+ return "", fmt.Errorf("no .ior.zst file found for scenario %q in %s", scenario, dir)
+}