On this page
- Common methods
- :next(timeout?)
- :read_until(pattern, timeout?)
- :expect(pattern, timeout?)
- :drain(timeout?)
- :close()
- :eof()
- :creation_site()
- Tail-only behaviour
- Capture-only notes
- ConsoleStream-only notes
- Example patterns
- Wait for a log line
- Race a process against a timeout
- Drain a capture and pass through pcap-parser
- See also
Streams
Provium has three concrete stream types. They expose a shared surface, so test code can :next / :read_until / :expect / :drain / :close / :eof / :creation_site against any of them without caring which kind it is.
| Type | Returned by | Backing |
|---|---|---|
| Tail | vm:tail_file, vm:fd_stream, file:tail_stream, proc:stdout_stream, proc:stderr_stream |
Frame-based agent stream over vsock. |
| Capture | bridge:capture, nic:capture |
tcpdump child stdout — pcap bytes, not framed. |
| ConsoleStream | console:read (returned by vm:console():read()) |
Unix socket connected to QEMU's console chardev. |
All three buffer "leftover bytes" past the last expect/read_until match. A subsequent :next returns those leftover bytes as the next frame, so test code never silently loses data.
Common methods
:next(timeout?)
Returns the next chunk of bytes as a Lua string, or nil at EOF / timeout. The exact frame shape:
- Tail: one frame per agent-side write. Each frame is whatever bytes the guest had emitted since the previous frame.
- Capture: chunks of pcap bytes from tcpdump's pipe (variable size, up to 64 KiB).
- ConsoleStream: chunks of console bytes (variable size, up to 64 KiB).
The timeout argument accepts seconds (number) or a string with ms/s/m/h suffix. Default: 10 seconds.
:read_until(pattern, timeout?)
Pull frames until pattern (a Lua string) is found in the accumulated buffer. Returns the prefix up to and including the matched bytes. Errors with a pattern-naming message on timeout (read_until timed out waiting for \X`) or stream EOF (stream EOF`).
Bytes past the matched suffix are kept as pending and replayed on the next call.
Default timeout: 10 seconds.
:expect(pattern, timeout?)
Like :read_until but discards the matched prefix. Returns nothing. Useful as an assertion: "the stream produced this; I don't care about the bytes". Same error shape as read_until.
Default timeout: 10 seconds.
:drain(timeout?)
Collect every frame currently available and return them as a Lua array. Stops when the next read would block past the timeout, when EOF is reached, or when no more data is available.
Frame-per-element semantics — each iteration of the inner read produces one Lua-string entry. Pending bytes from a prior expect/read_until come out as the first entry.
Default timeout: 0.5 seconds (much shorter than next/read_until/expect because drain's intent is "what's available right now, then return").
:close()
Close the underlying transport. Subsequent :next returns nil; subsequent :read_until/:expect error with stream closed / console closed / capture closed. Idempotent.
Pending bytes are dropped on close so a :next after close can't drain stale data.
:eof()
Returns true once the stream's transport has reported EOF AND no pending bytes remain. Pending bytes mean there's still data to deliver before EOF is honest, so eof() returns false until they've been consumed.
:creation_site()
Returns {file=string, line=int} for the test-author frame that opened the stream, or nil if no *.test.lua/*.fixture.lua frame is on the stack at creation. Useful for snapshot diagnostics — when vm:snapshot() refuses because of a live stream, the error names the offending stream's creation site so you can fix it.
Tail-only behaviour
vm:tail_file(path, opts?) accepts a start opt:
| Value | Effect |
|---|---|
"end" (default) |
Stream only bytes appended after the call. |
"beginning" or "start" |
Replay the whole file from byte 0, then continue tailing. |
| Integer | Start streaming from that exact byte offset. |
A Tail closed after EOF returns nil from :next — the closed stream is semantically EOF. Earlier versions raised tail closed; the nil-return behaviour matches Capture and ConsoleStream so while s:next() do … end loops terminate cleanly.
Tails carry per-frame metadata visible to scope-end diagnostics: kind (e.g. tail_file, fd_stream, file_tail_stream, proc_stdout_stream), detail (the path or fd), creation_site, and test_name.
Capture-only notes
Capture spawns tcpdump -i <iface> -U -w - -s 65535. The output is pcap-formatted bytes — the standard pcap file format with global header followed by per-packet records. Feed it through pcap-parser, tshark -r -, or pyshark to interpret.
The Capture holds a guard against the bridge's active_captures counter so a vm:snapshot() while capture is live errors instead of silently producing a half-captured pcap.
Drop on close prevents :next from returning stale tcpdump bytes after the child has been killed.
ConsoleStream-only notes
ConsoleStream connects via UnixStream to the VMM's console chardev path (vm:console_socket_path()). Reads come back as raw bytes from the chardev — typically the boot console + login prompt + anything the guest has written to /dev/hvc0 since the last read.
The connect-time read timeout is 50 ms. The :next(timeout) arg overrides it for the duration of the call and the previous timeout is restored afterwards, so a next("5s") followed by a bare next() doesn't inherit the 5s deadline forever.
Mid-stream ConnectionReset and BrokenPipe errors are mapped to "console EOF" — they happen when the QEMU chardev closes mid-read on a VM reset or shutdown. Semantically EOF for a console stream, not a real I/O error.
Example patterns
Wait for a log line
local f = vm:open_file("/var/log/messages", {read=true})
local stream = f:tail_stream()
stream:expect("ready", "30s")
Race a process against a timeout
local proc = vm:run_async("slow-server")
local out = proc:stdout_stream()
out:expect("listening", "10s")
-- now hit it
Drain a capture and pass through pcap-parser
local cap = bridge:capture()
vm:run("ping -c 5 peer.lan")
local frames = cap:drain("2s")
local pcap = table.concat(frames)
-- write pcap to a file or feed it to a parser