diff options
Diffstat (limited to 'integrationtests')
| -rw-r--r-- | integrationtests/cmd/ioworkload/scenarios.go | 80 | ||||
| -rw-r--r-- | integrationtests/expectations.go | 20 | ||||
| -rw-r--r-- | integrationtests/expectations_test.go | 111 | ||||
| -rw-r--r-- | integrationtests/open_test.go | 56 |
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", + }, + }) +} |
