summaryrefslogtreecommitdiff
path: root/integrationtests
diff options
context:
space:
mode:
Diffstat (limited to 'integrationtests')
-rw-r--r--integrationtests/cmd/ioworkload/scenarios.go80
-rw-r--r--integrationtests/expectations.go20
-rw-r--r--integrationtests/expectations_test.go111
-rw-r--r--integrationtests/open_test.go56
4 files changed, 267 insertions, 0 deletions
diff --git a/integrationtests/cmd/ioworkload/scenarios.go b/integrationtests/cmd/ioworkload/scenarios.go
index cb9f455..4ced6d5 100644
--- a/integrationtests/cmd/ioworkload/scenarios.go
+++ b/integrationtests/cmd/ioworkload/scenarios.go
@@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
+ "os/exec"
"path/filepath"
"runtime"
"syscall"
@@ -15,6 +16,9 @@ var scenarios = map[string]func() error{
"open-basic": openBasic,
"open-creat": openCreat,
"open-by-handle-at": openByHandleAt,
+ "open-enoent": openEnoent,
+ "open-rdonly-write": openRdonlyWrite,
+ "open-pid-filter": openPidFilter,
"readwrite-basic": readwriteBasic,
"readwrite-pread": readwritePread,
"readwrite-pwrite": readwritePwrite,
@@ -108,6 +112,82 @@ func openCreat() error {
return syscall.Close(int(fd))
}
+// openEnoent attempts to open a nonexistent file path. The openat syscall
+// returns ENOENT, but ior should still capture the enter_openat tracepoint
+// because the filename is read on entry before the syscall executes.
+func openEnoent() error {
+ dir, cleanup, err := makeTempDir("open-enoent")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "nonexistent", "enoentfile.txt")
+ _, err = syscall.Open(path, syscall.O_RDONLY, 0)
+ if err == nil {
+ return fmt.Errorf("expected ENOENT, but open succeeded")
+ }
+ return nil
+}
+
+// openRdonlyWrite opens a file O_RDONLY, then attempts to write to it.
+// The write fails with EBADF, but ior should capture both the openat
+// tracepoint and the write tracepoint.
+func openRdonlyWrite() error {
+ dir, cleanup, err := makeTempDir("open-rdonly-write")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "rdonlyfile.txt")
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("create file: %w", err)
+ }
+ syscall.Close(fd)
+
+ fd, err = syscall.Open(path, syscall.O_RDONLY, 0)
+ if err != nil {
+ return fmt.Errorf("open rdonly: %w", err)
+ }
+ defer syscall.Close(fd)
+
+ _, err = syscall.Write(fd, []byte("should fail"))
+ if err == nil {
+ return fmt.Errorf("expected write to rdonly fd to fail")
+ }
+ return nil
+}
+
+// openPidFilter spawns a child process that performs file I/O. Since ior
+// filters by the workload PID, the child's I/O should NOT appear in results.
+// The parent also performs its own open so the test can verify positive and
+// negative expectations simultaneously.
+func openPidFilter() error {
+ dir, cleanup, err := makeTempDir("open-pid-filter")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ // Parent opens a file (should be captured by ior).
+ parentPath := filepath.Join(dir, "parentfile.txt")
+ fd, err := syscall.Open(parentPath, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("parent open: %w", err)
+ }
+ syscall.Close(fd)
+
+ // Spawn a child process that creates a file with a distinctive name.
+ childPath := filepath.Join(dir, "childfile.txt")
+ cmd := exec.Command("touch", childPath)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("child touch: %w", err)
+ }
+ return nil
+}
+
// readwriteBasic opens a file, writes data, seeks to start, reads it back.
func readwriteBasic() error {
dir, cleanup, err := makeTempDir("readwrite-basic")
diff --git a/integrationtests/expectations.go b/integrationtests/expectations.go
index 6a816e6..21a5bda 100644
--- a/integrationtests/expectations.go
+++ b/integrationtests/expectations.go
@@ -81,6 +81,26 @@ func AssertNoUnexpectedPID(t *testing.T, result TestResult, expectedPID int) {
}
}
+// AssertEventsAbsent verifies that none of the specified events appear in the test result.
+// Each ExpectedEvent must have at least one filter field set to avoid accidentally
+// matching all records.
+func AssertEventsAbsent(t *testing.T, result TestResult, absent []ExpectedEvent) {
+ t.Helper()
+ for _, exp := range absent {
+ if exp.PathContains == "" && exp.Tracepoint == "" && exp.Comm == "" {
+ t.Errorf("AssertEventsAbsent: ExpectedEvent must have at least one filter field set: %+v", exp)
+ continue
+ }
+ for _, rec := range result.Records {
+ if matchesExpectation(rec, exp) {
+ t.Errorf("event should be absent but was found: %+v (path=%q tracepoint=%s comm=%q)",
+ exp, rec.Path, rec.TraceID.String(), rec.Comm)
+ break
+ }
+ }
+ }
+}
+
func matchesExpectation(rec flamegraph.IterRecord, exp ExpectedEvent) bool {
if exp.PathContains != "" && !strings.Contains(rec.Path, exp.PathContains) {
return false
diff --git a/integrationtests/expectations_test.go b/integrationtests/expectations_test.go
new file mode 100644
index 0000000..fd06e31
--- /dev/null
+++ b/integrationtests/expectations_test.go
@@ -0,0 +1,111 @@
+package integrationtests
+
+import (
+ "ior/internal/flamegraph"
+ "ior/internal/types"
+ "testing"
+)
+
+func TestAssertEventsAbsentNoMatch(t *testing.T) {
+ result := TestResult{
+ Records: []flamegraph.IterRecord{
+ {Path: "/tmp/testfile.txt", TraceID: types.SYS_ENTER_OPENAT, Comm: "ioworkload"},
+ },
+ }
+
+ mt := &testing.T{}
+ AssertEventsAbsent(mt, result, []ExpectedEvent{
+ {PathContains: "missing.txt"},
+ })
+ if mt.Failed() {
+ t.Error("AssertEventsAbsent should not fail when event is absent")
+ }
+}
+
+func TestAssertEventsAbsentWithMatch(t *testing.T) {
+ result := TestResult{
+ Records: []flamegraph.IterRecord{
+ {Path: "/tmp/testfile.txt", TraceID: types.SYS_ENTER_OPENAT, Comm: "ioworkload"},
+ },
+ }
+
+ mt := &testing.T{}
+ AssertEventsAbsent(mt, result, []ExpectedEvent{
+ {PathContains: "testfile.txt"},
+ })
+ if !mt.Failed() {
+ t.Error("AssertEventsAbsent should fail when event is present")
+ }
+}
+
+func TestAssertEventsAbsentEmptyResult(t *testing.T) {
+ result := TestResult{}
+
+ mt := &testing.T{}
+ AssertEventsAbsent(mt, result, []ExpectedEvent{
+ {PathContains: "anything.txt"},
+ })
+ if mt.Failed() {
+ t.Error("AssertEventsAbsent should not fail on empty result")
+ }
+}
+
+func TestAssertEventsAbsentMultiField(t *testing.T) {
+ result := TestResult{
+ Records: []flamegraph.IterRecord{
+ {Path: "/tmp/testfile.txt", TraceID: types.SYS_ENTER_OPENAT, Comm: "ioworkload"},
+ {Path: "/tmp/testfile.txt", TraceID: types.SYS_ENTER_WRITE, Comm: "ioworkload"},
+ },
+ }
+
+ // Multi-field match: path + tracepoint + comm — all match first record.
+ mt := &testing.T{}
+ AssertEventsAbsent(mt, result, []ExpectedEvent{
+ {PathContains: "testfile.txt", Tracepoint: "enter_openat", Comm: "ioworkload"},
+ })
+ if !mt.Failed() {
+ t.Error("AssertEventsAbsent should fail when multi-field event matches")
+ }
+
+ // Multi-field partial mismatch: path matches but tracepoint doesn't.
+ mt2 := &testing.T{}
+ AssertEventsAbsent(mt2, result, []ExpectedEvent{
+ {PathContains: "testfile.txt", Tracepoint: "enter_read"},
+ })
+ if mt2.Failed() {
+ t.Error("AssertEventsAbsent should pass when multi-field expectation partially mismatches")
+ }
+}
+
+func TestAssertEventsAbsentMultipleExpectations(t *testing.T) {
+ result := TestResult{
+ Records: []flamegraph.IterRecord{
+ {Path: "/tmp/found.txt", TraceID: types.SYS_ENTER_OPENAT, Comm: "ioworkload"},
+ },
+ }
+
+ // First expectation absent, second present — should fail.
+ mt := &testing.T{}
+ AssertEventsAbsent(mt, result, []ExpectedEvent{
+ {PathContains: "missing.txt"},
+ {PathContains: "found.txt"},
+ })
+ if !mt.Failed() {
+ t.Error("AssertEventsAbsent should fail when any expectation matches")
+ }
+}
+
+func TestAssertEventsAbsentRejectsZeroValue(t *testing.T) {
+ result := TestResult{
+ Records: []flamegraph.IterRecord{
+ {Path: "/tmp/testfile.txt", TraceID: types.SYS_ENTER_OPENAT, Comm: "ioworkload"},
+ },
+ }
+
+ // Zero-value ExpectedEvent should be rejected with an error.
+ mt := &testing.T{}
+ AssertEventsAbsent(mt, result, []ExpectedEvent{{}})
+ if !mt.Failed() {
+ t.Error("AssertEventsAbsent should reject zero-value ExpectedEvent")
+ }
+}
diff --git a/integrationtests/open_test.go b/integrationtests/open_test.go
index 917ce79..8dfbba6 100644
--- a/integrationtests/open_test.go
+++ b/integrationtests/open_test.go
@@ -34,3 +34,59 @@ func TestOpenByHandleAt(t *testing.T) {
},
})
}
+
+func TestOpenEnoent(t *testing.T) {
+ runScenario(t, "open-enoent", []ExpectedEvent{
+ {
+ PathContains: "enoentfile.txt",
+ Tracepoint: "enter_openat",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+}
+
+func TestOpenRdonlyWrite(t *testing.T) {
+ runScenario(t, "open-rdonly-write", []ExpectedEvent{
+ {
+ PathContains: "rdonlyfile.txt",
+ Tracepoint: "enter_openat",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ {
+ PathContains: "rdonlyfile.txt",
+ Tracepoint: "enter_write",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+}
+
+func TestOpenPidFilter(t *testing.T) {
+ h := newTestHarness(t)
+ result, pid, err := h.Run("open-pid-filter", defaultDuration)
+ if err != nil {
+ t.Fatalf("run scenario open-pid-filter: %v", err)
+ }
+
+ AssertNoUnexpectedPID(t, result, pid)
+ AssertNoUnexpectedComm(t, result, "ioworkload")
+
+ // Parent's file should be captured.
+ AssertEventsPresent(t, result, []ExpectedEvent{
+ {
+ PathContains: "parentfile.txt",
+ Tracepoint: "enter_openat",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+
+ // Child's file should NOT be captured (different PID).
+ AssertEventsAbsent(t, result, []ExpectedEvent{
+ {
+ PathContains: "childfile.txt",
+ },
+ })
+}