Disks and fault injection
Provium's disk support has two goals: give the test direct sector-level access to the backing image, and inject faults that exercise the guest's error-handling paths.
The exhaustive method reference is on Disk.
Attaching a disk #
local img = "/tmp/test.img"
-- Pre-create a backing file; Provium does not auto-create.
io.::
local vm = provium::
local disk = vm:
The two opts that matter for fault-injection work are id (names the disk so you can re-look it up via vm:disk(id)) and image (the backing file — required for read_sectors / write_sectors). The full opts table, types, and defaults are in the Disk reference.
Reading and writing sectors #
Sectors are 512 bytes throughout. Offsets and counts are in sectors, not bytes.
-- Read sector 0 (the first 512 bytes).
local sec0 = disk:
-- Read 4 sectors starting at sector 100 (bytes 51200..53247).
local block = disk:
assert
-- Write at sector 50.
disk:
Without a backing image, both ops error with a no backing image — disk:with_image required message.
Fault injection #
Three modes:
| Mode | Effect |
|---|---|
eio_read | Every read_sectors short-circuits to EIO. |
eio_write | Every write_sectors short-circuits to EIO. |
slow | Every read_sectors / write_sectors sleeps 50 ms before doing the I/O. |
Modes are activated with disk:fault_inject(mode) and cleared with disk:clear_faults(). Multiple modes can be active simultaneously — with slow + eio_read both set, the EIO check wins: the read errors immediately, without the 50 ms delay.
Inject EIO #
test
Inject slow I/O #
slow delays each sector op but doesn't change its outcome. Assert both halves: the op took the hit, and it still worked. The guest Clock gives you a sub-millisecond time source (os.time() only has 1-second resolution):
test
The 50 ms delay is fixed per call; it is not currently configurable from test code.
Combine modes #
test
Concurrent injection during I/O #
The harness re-checks the fault set after the slow-fault sleep AND after the actual I/O completes, so an EIO fault that lands mid-call still takes effect. There is currently no way to exercise this from a test, though: test code runs on a single thread, and workers run guest processes — they can't call disk:fault_inject. Inject faults up-front, act, then clear; the mid-call recheck is defensive insurance in the harness, not a pattern you can drive.
Inspecting state #
local active = disk: -- {"eio_read", "slow"}
disk: -- false
Clearing #
disk:
local r = disk: -- succeeds
Detaching a disk #
disk:
local ok = pcall
assert -- "disk is detached"
disk:detach() issues a best-effort QMP device_del against the parent VM and marks the local handle detached; after that, sector ops error. The exact behaviour for disks that were never QMP-added is in the Disk reference.
Common patterns #
"Does the guest retry after a transient EIO?" #
test
"Does the filesystem remount read-only after EIO?" #
test
"Does the guest panic on EIO at boot?" #
test
Multiple disks per VM #
local data = vm:
local logs = vm:
-- Inject EIO on data only; logs is unaffected.
data:
Use vm:disk(id) to look up an already-attached disk:
local data = vm:
data:
Caveats #
- Sector size is fixed at 512 bytes. Tests that need 4 KiB sectors should expect their guest to layer that on top.
read_sectorsandwrite_sectorsgo directly to the host file, not through QEMU's block backend. This means a test that exercises QEMU's block translation (sparse holes, compression, etc.) will not see those layers — the disk userdata is a direct view of the underlying image bytes.disk:size()reports the live image file size when an image is attached. Test code that resizes the underlying file (truncate,fallocate) sees the new size, not the modelledsizefromattach_disk.
See also #
- Disk reference — every method, every error message.
- VM reference —
vm:attach_disk,vm:disk.