diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-21 19:28:23 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-21 19:28:23 +0200 |
| commit | a5b711c5f221704209706b79fbf310a18e079391 (patch) | |
| tree | 84615902f79a901aa9d98e3423c4756477b7cf4b /integrationtests/harness.go | |
| parent | 2c2cbe07f5e10fdb996e2a039cde84be44866f18 (diff) | |
more on integration tests
Diffstat (limited to 'integrationtests/harness.go')
| -rw-r--r-- | integrationtests/harness.go | 179 |
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) +} |
