1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
# Integration Tests Plan
> Individual implementation tasks are tracked in **Taskwarrior** (`task project:ior +integrationtests`).
## Overview
End-to-end integration tests that verify ior correctly captures real I/O syscalls from a
known workload process via BPF tracepoints. A standalone Go binary performs deterministic
I/O operations, ior traces it by PID, and the test harness asserts the captured `.ior.zst`
output matches expectations.
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Go Test Harness (*_test.go) │
│ │
│ 1. Start ioworkload --scenario=X │
│ 2. Read PID from workload's stdout (line 1) │
│ 3. Start ior -pid=PID -flamegraph -duration=N │
│ 4. Workload sleeps 2s, then performs I/O, exits │
│ 5. ior finishes, produces .ior.zst │
│ 6. Parse .ior.zst → assert expected events present │
└─────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ ioworkload │ │ ior │
│ (separate │ │ (BPF tracing │
│ binary) │ │ -pid=WORKLOAD) │
│ │ │ │
│ prints PID │ │ writes .ior.zst │
│ sleeps 2s │ └──────────────────┘
│ does I/O │
│ exits │
└──────────────┘
```
## Directory Layout
```
integrationtests/
├── cmd/
│ └── ioworkload/
│ └── main.go # Standalone I/O workload binary
├── harness.go # Test orchestration (start ior + workload, collect output)
├── parse.go # Parse .ior.zst into assertable TestResult
├── expectations.go # ExpectedEvent type & assertion helpers
├── open_test.go # open, openat, creat, open_by_handle_at
├── readwrite_test.go # read, write, pread64, pwrite64, readv, writev
├── close_test.go # close, close_range
├── dup_test.go # dup, dup2, dup3
├── fcntl_test.go # fcntl (F_DUPFD, F_SETFL, F_DUPFD_CLOEXEC)
├── rename_test.go # rename, renameat, renameat2
├── link_test.go # link, linkat, symlink, symlinkat, readlink
├── unlink_test.go # unlink, unlinkat, rmdir
├── dir_test.go # mkdir, mkdirat, chdir, getdents
├── stat_test.go # stat, fstat, lstat, statx, access, faccessat
├── sync_test.go # fsync, fdatasync, sync, sync_file_range
├── truncate_test.go # truncate, ftruncate
├── iouring_test.go # io_uring_setup, io_uring_enter, io_uring_register
└── README.md
```
## Components
### 1. I/O Workload Binary (`cmd/ioworkload/main.go`)
A standalone Go binary that:
- Accepts `--scenario=<name>` flag (e.g. `open-basic`, `dup-dup3-cloexec`)
- Prints its PID to stdout on line 1
- Sleeps 2 seconds (gives harness time to start ior with PID filter)
- Performs deterministic, known I/O operations in a temp directory
- Cleans up and exits with code 0
### 2. Test Harness (`harness.go`)
```go
type TestHarness struct {
IorBinary string // path to built ior binary
WorkloadBinary string // path to built ioworkload binary
BpfObject string // path to ior.bpf.o
OutputDir string // temp dir for .ior.zst output
}
func (h *TestHarness) Run(scenario string, duration int) (*TestResult, error)
```
`Run()` sequence:
1. Start `ioworkload --scenario=<name>`
2. Read PID from workload stdout (line 1)
3. Start `ior -pid=<PID> -flamegraph -name=<scenario> -duration=<N>`
4. Workload's 2s sleep expires, it performs I/O, then exits
5. ior finishes (duration expires or SIGTERM), writes `.ior.zst`
6. Parse `.ior.zst` into `TestResult`
Requires root/CAP_BPF. Tests skip with `t.Skip("requires root for BPF")` if not root.
### 3. Parser (`parse.go`)
Reuses existing `flamegraph.newIorDataFromFile()` and `iorData.iter()` to deserialize
`.ior.zst` into an assertable `TestResult` struct containing all captured events.
### 4. Assertions (`expectations.go`)
```go
type ExpectedEvent struct {
PathContains string // substring match on file path
Tracepoint string // e.g. "sys_enter_openat"
Comm string // e.g. "ioworkload"
MinCount uint64 // minimum occurrences
}
func AssertEventsPresent(t *testing.T, result *TestResult, expected []ExpectedEvent)
func AssertNoUnexpectedComm(t *testing.T, result *TestResult, expectedComm string)
```
### 5. Test Files (per syscall family)
Each `*_test.go` defines scenarios and expectations for its syscall family. Example:
```go
// open_test.go
func TestOpenBasic(t *testing.T) { runScenario(t, "open-basic", ...) }
func TestOpenCreat(t *testing.T) { runScenario(t, "open-creat", ...) }
func TestOpenByHandleAt(t *testing.T) { runScenario(t, "open-by-handle-at", ...) }
```
### 6. Mage Target (`Magefile.go`)
```go
func IntegrationTest() error {
mg.SerialDeps(All)
// build ioworkload
// sudo go test ./integrationtests/... -v -failfast -count=1
}
```
## Key Design Decisions
- **Separate binary**: syscalls come from a real process with a distinct PID, matching production
- **`.ior.zst` as verification format**: reuses existing serialization, avoids parsing noisy stdout
- **`-pid` filter**: isolates workload I/O from system noise
- **2s sleep**: simple timing-based coordination, no synchronization pipes needed
- **One `*_test.go` per syscall family**: scales to many scenarios without monolithic test files
- **Standard `go test`**: no custom runner; `t.Skip` for non-root environments
|