summaryrefslogtreecommitdiff
path: root/integrationtests
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-04 20:19:37 +0200
committerPaul Buetow <paul@buetow.org>2026-03-04 20:20:25 +0200
commit96225fb6159212a8851043a08d781aba721b4e78 (patch)
treeabebcaa74594886c3f130a6c03b7aa2691849cf5 /integrationtests
parent1f8b6804f69632914ad0ab64892021315e99f421 (diff)
Fix Go mistake findings and stabilize integration timing
Diffstat (limited to 'integrationtests')
-rw-r--r--integrationtests/cmd/ioworkload/main.go6
-rw-r--r--integrationtests/harness.go23
-rw-r--r--integrationtests/readwrite_test.go35
3 files changed, 50 insertions, 14 deletions
diff --git a/integrationtests/cmd/ioworkload/main.go b/integrationtests/cmd/ioworkload/main.go
index 1261c9f..0276a9c 100644
--- a/integrationtests/cmd/ioworkload/main.go
+++ b/integrationtests/cmd/ioworkload/main.go
@@ -12,9 +12,9 @@ import (
)
// Give ior enough time to attach tracepoints before scenarios emit syscalls.
-// Under parallel integration load, 2s can be too short and cause missed
-// first-call events for single-shot scenarios.
-const startupDelay = 5 * time.Second
+// Under slower CI or locally saturated systems, 5s can still miss first-call
+// events for single-shot scenarios. Use a slightly larger delay for stability.
+const startupDelay = 8 * time.Second
func main() {
scenario := flag.String("scenario", "", "I/O scenario to execute")
diff --git a/integrationtests/harness.go b/integrationtests/harness.go
index a8a73d0..17ae994 100644
--- a/integrationtests/harness.go
+++ b/integrationtests/harness.go
@@ -102,6 +102,9 @@ func (h *TestHarness) startWorkload(scenario string) (*exec.Cmd, int, error) {
io.Copy(io.Discard, stdout) //nolint:errcheck
}()
+ startupTimer := time.NewTimer(workloadStartupTimeout)
+ defer stopAndDrainTimer(startupTimer)
+
select {
case pid := <-pidCh:
return cmd, pid, nil
@@ -109,7 +112,7 @@ func (h *TestHarness) startWorkload(scenario string) (*exec.Cmd, int, error) {
cmd.Process.Kill()
cmd.Wait()
return nil, 0, err
- case <-time.After(workloadStartupTimeout):
+ case <-startupTimer.C:
cmd.Process.Kill()
cmd.Wait()
return nil, 0, fmt.Errorf("timeout waiting for workload PID")
@@ -151,7 +154,8 @@ func waitBoth(workloadCmd, iorCmd *exec.Cmd, duration int, grace time.Duration)
go func(ch chan error) { ch <- workloadCmd.Wait() }(workloadDone)
go func(ch chan error) { ch <- iorCmd.Wait() }(iorDone)
- timeout := time.After(time.Duration(duration)*time.Second + grace)
+ timeout := time.NewTimer(time.Duration(duration)*time.Second + grace)
+ defer stopAndDrainTimer(timeout)
for workloadDone != nil || iorDone != nil {
select {
@@ -161,7 +165,7 @@ func waitBoth(workloadCmd, iorCmd *exec.Cmd, duration int, grace time.Duration)
case err := <-iorDone:
iorErr = err
iorDone = nil
- case <-timeout:
+ case <-timeout.C:
if iorDone != nil {
iorCmd.Process.Kill()
iorErr = fmt.Errorf("ior timed out")
@@ -178,6 +182,19 @@ func waitBoth(workloadCmd, iorCmd *exec.Cmd, duration int, grace time.Duration)
return
}
+func stopAndDrainTimer(timer *time.Timer) {
+ if timer == nil {
+ return
+ }
+ if timer.Stop() {
+ return
+ }
+ select {
+ case <-timer.C:
+ default:
+ }
+}
+
// 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)
diff --git a/integrationtests/readwrite_test.go b/integrationtests/readwrite_test.go
index a55bf78..4e8cbef 100644
--- a/integrationtests/readwrite_test.go
+++ b/integrationtests/readwrite_test.go
@@ -98,11 +98,11 @@ func TestReadwriteWronlyRead(t *testing.T) {
MinCount: 1,
},
})
- assertEventBytesAtLeast(t, result, ExpectedEvent{
+ assertEventBytesEqual(t, result, ExpectedEvent{
PathContains: "wronlyfile.txt",
Tracepoint: "enter_read",
Comm: "ioworkload",
- }, 1)
+ }, 0)
assertEventBytesReasonable(t, result, ExpectedEvent{
PathContains: "wronlyfile.txt",
Tracepoint: "enter_read",
@@ -119,11 +119,11 @@ func TestReadwriteRdonlyWrite(t *testing.T) {
MinCount: 1,
},
})
- assertEventBytesAtLeast(t, result, ExpectedEvent{
+ assertEventBytesEqual(t, result, ExpectedEvent{
PathContains: "rdonlywritefile.txt",
Tracepoint: "enter_write",
Comm: "ioworkload",
- }, 1)
+ }, 0)
assertEventBytesReasonable(t, result, ExpectedEvent{
PathContains: "rdonlywritefile.txt",
Tracepoint: "enter_write",
@@ -140,11 +140,11 @@ func TestReadwritePreadInvalid(t *testing.T) {
MinCount: 1,
},
})
- assertEventBytesAtLeast(t, result, ExpectedEvent{
+ assertEventBytesEqual(t, result, ExpectedEvent{
PathContains: "preadinvalid.txt",
Tracepoint: "enter_pread64",
Comm: "ioworkload",
- }, 1)
+ }, 0)
assertEventBytesReasonable(t, result, ExpectedEvent{
PathContains: "preadinvalid.txt",
Tracepoint: "enter_pread64",
@@ -161,11 +161,11 @@ func TestReadwritePwriteInvalid(t *testing.T) {
MinCount: 1,
},
})
- assertEventBytesAtLeast(t, result, ExpectedEvent{
+ assertEventBytesEqual(t, result, ExpectedEvent{
PathContains: "pwriteinvalid.txt",
Tracepoint: "enter_pwrite64",
Comm: "ioworkload",
- }, 1)
+ }, 0)
assertEventBytesReasonable(t, result, ExpectedEvent{
PathContains: "pwriteinvalid.txt",
Tracepoint: "enter_pwrite64",
@@ -192,6 +192,25 @@ func assertEventBytesAtLeast(t *testing.T, result TestResult, exp ExpectedEvent,
}
}
+func assertEventBytesEqual(t *testing.T, result TestResult, exp ExpectedEvent, wantBytes uint64) {
+ t.Helper()
+ var matched bool
+ var totalBytes uint64
+ for _, rec := range result.Records {
+ if !matchesExpectation(rec, exp) {
+ continue
+ }
+ matched = true
+ totalBytes += rec.Cnt.Bytes
+ }
+ if !matched {
+ t.Fatalf("expected event not found while asserting bytes: %+v", exp)
+ }
+ if totalBytes != wantBytes {
+ t.Fatalf("bytes for %+v mismatch: got=%d want=%d", exp, totalBytes, wantBytes)
+ }
+}
+
func assertEventBytesReasonable(t *testing.T, result TestResult, exp ExpectedEvent) {
t.Helper()
const maxReasonableBytes = 1 << 20 // 1MiB is far above any bytes used in these scenarios.