diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-22 22:31:35 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-22 22:31:35 +0200 |
| commit | 4cd2c4e818a1438bf63d1ca05a6cf134f39bc06b (patch) | |
| tree | 40b4ad1ab60a6f50973a66c4e273c91e0d8d265b /integrationtests | |
| parent | 8e52ba5a8661c717f45e00608ad64f0adc6de3e1 (diff) | |
Add copy_file_range support and tracepoint attach tests
Diffstat (limited to 'integrationtests')
| -rw-r--r-- | integrationtests/attach_tracepoints_test.go | 59 | ||||
| -rw-r--r-- | integrationtests/cmd/ioworkload/scenario_copy_file_range.go | 78 | ||||
| -rw-r--r-- | integrationtests/cmd/ioworkload/scenarios.go | 2 | ||||
| -rw-r--r-- | integrationtests/copy_file_range_test.go | 25 | ||||
| -rw-r--r-- | integrationtests/harness.go | 15 |
5 files changed, 175 insertions, 4 deletions
diff --git a/integrationtests/attach_tracepoints_test.go b/integrationtests/attach_tracepoints_test.go new file mode 100644 index 0000000..89db494 --- /dev/null +++ b/integrationtests/attach_tracepoints_test.go @@ -0,0 +1,59 @@ +package integrationtests + +import "testing" + +func TestAttachTracepointsIncludeFilter(t *testing.T) { + h := newTestHarness(t) + + // Only load openat tracepoints so write events from the workload are not captured. + result, pid, err := h.RunWithIorArgs("open-rdonly-write", defaultDuration, []string{ + "-tps", "^sys_enter_openat$,^sys_exit_openat$", + }) + if err != nil { + t.Fatalf("run scenario open-rdonly-write with include filter: %v", err) + } + + AssertNoUnexpectedPID(t, result, pid) + AssertNoUnexpectedComm(t, result, "ioworkload") + AssertEventsPresent(t, result, []ExpectedEvent{ + { + PathContains: "rdonlyfile.txt", + Tracepoint: "enter_openat", + Comm: "ioworkload", + MinCount: 1, + }, + }) + AssertEventsAbsent(t, result, []ExpectedEvent{ + { + PathContains: "rdonlyfile.txt", + Tracepoint: "enter_write", + Comm: "ioworkload", + }, + }) +} + +func TestAttachTracepointsExcludeByInclusion(t *testing.T) { + h := newTestHarness(t) + + // Negative case: include only write tracepoints; openat must not be captured. + result, pid, err := h.RunWithIorArgs("open-rdonly-write", defaultDuration, []string{ + "-tps", "^sys_enter_write$,^sys_exit_write$", + }) + if err != nil { + t.Fatalf("run scenario open-rdonly-write with write-only include filter: %v", err) + } + + AssertNoUnexpectedPID(t, result, pid) + AssertNoUnexpectedComm(t, result, "ioworkload") + AssertEventsPresent(t, result, []ExpectedEvent{ + { + Tracepoint: "enter_write", + MinCount: 1, + }, + }) + AssertEventsAbsent(t, result, []ExpectedEvent{ + { + Tracepoint: "enter_openat", + }, + }) +} diff --git a/integrationtests/cmd/ioworkload/scenario_copy_file_range.go b/integrationtests/cmd/ioworkload/scenario_copy_file_range.go new file mode 100644 index 0000000..87531b0 --- /dev/null +++ b/integrationtests/cmd/ioworkload/scenario_copy_file_range.go @@ -0,0 +1,78 @@ +package main + +import ( + "fmt" + "path/filepath" + "syscall" +) + +// SYS_COPY_FILE_RANGE on x86_64 Linux. +const sysCopyFileRange = 326 + +// copyFileRangeBasic copies bytes from a source file to a destination file +// using copy_file_range(2) with flags=0 as required by the manpage. +func copyFileRangeBasic() error { + dir, cleanup, err := makeTempDir("copy-file-range-basic") + if err != nil { + return err + } + defer cleanup() + + srcPath := filepath.Join(dir, "copyrangesrc.txt") + dstPath := filepath.Join(dir, "copyrangedst.txt") + + srcFd, err := syscall.Open(srcPath, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open source: %w", err) + } + defer syscall.Close(srcFd) + + dstFd, err := syscall.Open(dstPath, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open destination: %w", err) + } + defer syscall.Close(dstFd) + + data := []byte("copy_file_range integration data") + if _, err := syscall.Write(srcFd, data); err != nil { + return fmt.Errorf("write source: %w", err) + } + + n, _, errno := syscall.Syscall6(uintptr(sysCopyFileRange), uintptr(srcFd), 0, uintptr(dstFd), 0, uintptr(len(data)), 0) + if errno != 0 { + return fmt.Errorf("copy_file_range: %w", errno) + } + if n == 0 { + return fmt.Errorf("copy_file_range copied 0 bytes") + } + + return nil +} + +// copyFileRangeBadDstFd calls copy_file_range(2) with an invalid destination fd. +// The syscall should fail with EBADF, while still emitting the enter tracepoint. +func copyFileRangeBadDstFd() error { + dir, cleanup, err := makeTempDir("copy-file-range-bad-dst") + if err != nil { + return err + } + defer cleanup() + + srcPath := filepath.Join(dir, "copyrangeebadfsrc.txt") + srcFd, err := syscall.Open(srcPath, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open source: %w", err) + } + defer syscall.Close(srcFd) + + if _, err := syscall.Write(srcFd, []byte("copy_file_range ebadf data")); err != nil { + return fmt.Errorf("write source: %w", err) + } + + _, _, errno := syscall.Syscall6(uintptr(sysCopyFileRange), uintptr(srcFd), 0, uintptr(99999), 0, uintptr(16), 0) + if errno != syscall.EBADF { + return fmt.Errorf("expected EBADF from copy_file_range with invalid dst fd, got %v", errno) + } + + return nil +} diff --git a/integrationtests/cmd/ioworkload/scenarios.go b/integrationtests/cmd/ioworkload/scenarios.go index d99b584..f9a8e47 100644 --- a/integrationtests/cmd/ioworkload/scenarios.go +++ b/integrationtests/cmd/ioworkload/scenarios.go @@ -85,6 +85,8 @@ var scenarios = map[string]func() error{ "mmap-basic": mmapBasic, "mmap-msync-sync": mmapMsyncSync, "mmap-msync-invalid-flags": mmapMsyncInvalidFlags, + "copy-file-range-basic": copyFileRangeBasic, + "copy-file-range-bad-dst-fd": copyFileRangeBadDstFd, "truncate-basic": truncateBasic, "truncate-ftruncate": truncateFtruncate, "truncate-enoent": truncateEnoent, diff --git a/integrationtests/copy_file_range_test.go b/integrationtests/copy_file_range_test.go new file mode 100644 index 0000000..d87c5af --- /dev/null +++ b/integrationtests/copy_file_range_test.go @@ -0,0 +1,25 @@ +package integrationtests + +import "testing" + +func TestCopyFileRangeBasic(t *testing.T) { + runScenario(t, "copy-file-range-basic", []ExpectedEvent{ + { + PathContains: "copyrangesrc.txt", + Tracepoint: "enter_copy_file_range", + Comm: "ioworkload", + MinCount: 1, + }, + }) +} + +func TestCopyFileRangeBadDstFd(t *testing.T) { + runScenario(t, "copy-file-range-bad-dst-fd", []ExpectedEvent{ + { + PathContains: "copyrangeebadfsrc.txt", + Tracepoint: "enter_copy_file_range", + Comm: "ioworkload", + MinCount: 1, + }, + }) +} diff --git a/integrationtests/harness.go b/integrationtests/harness.go index fde52e6..a130c85 100644 --- a/integrationtests/harness.go +++ b/integrationtests/harness.go @@ -30,12 +30,17 @@ type TestHarness struct { // 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) { + return h.RunWithIorArgs(scenario, duration, nil) +} + +// RunWithIorArgs behaves like Run but forwards additional args to ior. +func (h *TestHarness) RunWithIorArgs(scenario string, duration int, extraIorArgs []string) (TestResult, int, error) { workloadCmd, workloadPID, err := h.startWorkload(scenario) if err != nil { return TestResult{}, 0, err } - iorCmd, err := h.startIor(workloadPID, scenario, duration) + iorCmd, err := h.startIor(workloadPID, scenario, duration, extraIorArgs) if err != nil { workloadCmd.Process.Kill() workloadCmd.Wait() @@ -111,18 +116,20 @@ func (h *TestHarness) startWorkload(scenario string) (*exec.Cmd, int, error) { } } -func (h *TestHarness) startIor(pid int, scenario string, duration int) (*exec.Cmd, error) { +func (h *TestHarness) startIor(pid int, scenario string, duration int, extraArgs []string) (*exec.Cmd, error) { bpfLink := filepath.Join(h.OutputDir, "ior.bpf.o") if err := os.Symlink(h.BpfObject, bpfLink); err != nil { return nil, fmt.Errorf("symlink bpf object: %w", err) } - cmd := exec.Command(h.IorBinary, + args := []string{ "-pid", strconv.Itoa(pid), "-flamegraph", "-name", scenario, "-duration", strconv.Itoa(duration), - ) + } + args = append(args, extraArgs...) + cmd := exec.Command(h.IorBinary, args...) cmd.Dir = h.OutputDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr |
