diff options
| -rw-r--r-- | integrationtests/cmd/ioworkload/main.go | 6 | ||||
| -rw-r--r-- | integrationtests/harness.go | 23 | ||||
| -rw-r--r-- | integrationtests/readwrite_test.go | 35 | ||||
| -rw-r--r-- | internal/flags/flags_test.go | 4 | ||||
| -rw-r--r-- | internal/flamegraph/liveserver.go | 18 |
5 files changed, 67 insertions, 19 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. diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go index 81d01ad..54c65b8 100644 --- a/internal/flags/flags_test.go +++ b/internal/flags/flags_test.go @@ -5,7 +5,6 @@ import ( "io" "os" "strings" - "sync" "testing" "time" ) @@ -16,7 +15,6 @@ func parseForTest(t *testing.T, args ...string) (Flags, error) { oldCommandLine := flag.CommandLine oldArgs := os.Args oldSingleton := singleton - oldOnce := once oldParseErr := parseErr oldPID := pidFilter.Load() oldTID := tidFilter.Load() @@ -28,7 +26,6 @@ func parseForTest(t *testing.T, args ...string) (Flags, error) { os.Args = append([]string{"ior"}, args...) singleton = Flags{TUIExportEnable: true} - once = sync.Once{} parseErr = nil pidFilter.Store(-1) tidFilter.Store(-1) @@ -41,7 +38,6 @@ func parseForTest(t *testing.T, args ...string) (Flags, error) { flag.CommandLine = oldCommandLine os.Args = oldArgs singleton = oldSingleton - once = oldOnce parseErr = oldParseErr pidFilter.Store(oldPID) tidFilter.Store(oldTID) diff --git a/internal/flamegraph/liveserver.go b/internal/flamegraph/liveserver.go index 4cc5629..8ae2b82 100644 --- a/internal/flamegraph/liveserver.go +++ b/internal/flamegraph/liveserver.go @@ -71,16 +71,32 @@ func openBrowserURL(url, openCommand string) error { waitCh := make(chan error, 1) go func() { waitCh <- cmd.Wait() }() + timer := time.NewTimer(750 * time.Millisecond) + defer stopAndDrainTimer(timer) + select { case waitErr := <-waitCh: if waitErr != nil { return fmt.Errorf("browser command exited early: %w", waitErr) } - case <-time.After(750 * time.Millisecond): + case <-timer.C: } return nil } +func stopAndDrainTimer(timer *time.Timer) { + if timer == nil { + return + } + if timer.Stop() { + return + } + select { + case <-timer.C: + default: + } +} + func notifyLiveWarning(warningCb func(string), message string) { if message == "" { return |
