Lab

A Lab is the unit of resource ownership in Provium. The provium global is the root Lab; you can carve out sub-labs with provium:lab("name"). Both shapes expose the same surface, so this page is the canonical reference.

Membership #

lab:vm(name, profile?, opts?) #

Three call shapes:

  • lab:vm("name") — lookup. Errors if no VM with that name has been declared.
  • lab:vm("name", "profile") — create.
  • lab:vm("name", "profile", opts) — create with sizing opts (memory, cpus). See VM boot opts for the split between creation-time and boot-time keys.

Returns a VM userdata.

lab:bridge(name, opts?) #

Two call shapes:

  • lab:bridge("name") — lookup or create. If the bridge already exists in the lab, returns it; otherwise creates it.
  • lab:bridge("name", opts) — same. Opts are reserved.

Returns a Bridge userdata.

lab:lab(name?) #

Create a sub-lab. Without a name, an anonymous name is generated (__lab_0, __lab_1, …). Returns a Lab userdata. Sub-labs share the parent's config, VMM, event sink, and pool, but have their own membership graph.

local dc1 = provium:lab("dc1")
dc1:vm("a", "peios")
dc1:vm("b", "peios")
dc1:bridge("lan"):attach({dc1.a, dc1.b})

lab:depends_on_file(host_path) #

Declare an external host-file as a fixture-cache dependency. The call itself is a no-op at runtime — its purpose is to be picked up by the cache-key scanner so that editing host_path invalidates the fixture next run.

provium:depends_on_file("../templates/sshd_config.in")

Relative paths are resolved against the directory of the file containing the call. The path must be a string literal at the call site; non-literal paths (variables, concatenation) are not detected. Folds path + mtime + size into the cache key. See Fixtures and dependencies — External host-file deps.

Files pushed into the guest via vm:push_file already track themselves — depends_on_file is for files the fixture reads on the host side.

lab:include(resource_or_list) #

Add an existing resource (VM, Bridge, or sub-Lab) to this lab. Accepts:

  • A single userdata — lab:include(other_lab.shared_vm).
  • An array — lab:include({a, b, c}).

Names must not already be taken in lab (errors with DuplicateVmName / DuplicateBridgeName). Reserved names (pack, unpack, vm_fixture, lab_fixture) are also rejected. When called from a test scope, names that already exist in a parent scope (the file root) also error to avoid silent shadowing — see labs and scope.

lab:remove(name_or_userdata) #

Remove a resource. Accepts a bare name (tries vm → bridge → sub-lab in order) or a userdata. Removal is graph-state only — the underlying VM, bridge, or sub-lab is NOT shut down or unrealised.

lab:members() #

Returns an array of {kind=string, name=string} entries listing every direct child of the lab. kind is "vm", "bridge", or "sub_lab".

lab:vm_names() / lab:bridge_names() / lab:sub_lab_names() #

Arrays of names of each kind directly in this lab (does not recurse into sub-labs).

lab.<name> (dot sugar) #

Looks up a named resource. Lookup order:

  1. Reserved keys: pack, unpack, vm_fixture, lab_fixture.
  2. VM with that name (walks parent scope chain on miss).
  3. Bridge with that name (walks parent scope chain on miss).
  4. Sub-lab with that name (walks parent scope chain on miss).
  5. Returns nil.

The dot form does not create. Use lab:vm("name", "profile") for that. The parent-scope walk applies when this lab is a per-test scope (the runner sets provium to a test-scope LabUd whose parent chain includes the file root). Federation sub-labs created via lab:lab(name) have an empty parent chain and don't fall through to siblings.

Batch lifecycle #

Each method here applies to every direct VM child of the lab. They do not recurse into sub-labs.

lab:boot() #

Boot every VM in the lab. Returns self so you can chain.

lab:shutdown() #

Shutdown every VM.

lab:pause() / lab:resume() #

Pause / resume every VM. Errors if any VM is in the wrong state for the transition.

Resource claims #

Each test file may make at most one claim against the dispatcher's resource pool. The claim sits across the entire file's lifetime; it is released at file end (or scope-walker end).

lab:claim({memory=…, cpus=…}) #

Reserve resources from the pool. Either field may be omitted (treated as zero). Both are accepted as integers; memory may also be a string with K/M/G suffix.

FieldTypeEffect
memoryint (bytes) or string "512M" / "2G"Memory budget.
cpusintvCPU budget.
provium:claim({memory = "4G", cpus = 4})

test("", function(t) end)
test("", function(t) end)

A second :claim call on the same file errors — lab claim already held; one-shot per lab. When no pool is wired (REPL / single-file ad-hoc runs) the call still records the one-shot constraint but is otherwise a no-op.

The claim emits a claim_acquired event when it goes through; a matching claim_released fires at file end.

Barriers #

lab:barrier(name, count, timeout?) #

Block until count callers have hit the same name-keyed barrier. Returns true when the barrier releases and false on timeout — it does not raise. Default timeout is 60 seconds. The timeout argument accepts a number (seconds) or nil. The count is locked in by the first arrival; a later call with a mismatching count for the same name returns false with a host-side warning. Barriers are reusable across rounds under the same name.

-- count = 1: satisfied immediately.
assert(provium:barrier("checkpoint", 1))

-- Unmet count: times out and returns false.
assert(not provium:barrier("pair", 2, 0.1))

Meeting a count > 1 requires a second concurrent host-side caller. Test files run single-threaded, and workers run guest processes (which can't call barrier), so a count > 1 barrier currently times out in an ordinary test file; the multi-caller form is aimed at thread-mode workers, which aren't supported yet. See Labs and scope — Barriers.

Snapshot and restore #

lab:snapshot(dir?) #

Take a whole-lab snapshot. With an explicit dir arg, writes there and returns the dir path as a string. Without args, writes to a fresh tempdir and returns a LabSnapshot userdata.

lab:restore(dir_or_snapshot) #

Restore from a directory path or a LabSnapshot userdata. Reads lab.json for the per-VM index plus each VM's snapshot file under the same dir.

Fixtures #

lab:vm_fixture(path) #

Build (or restore from cache) a single-VM fixture. path is the test-root-relative path of a *.fixture.lua file with the .fixture.lua suffix omitted.

local vm = provium:vm_fixture("base")
local vm = provium:vm_fixture("setups/networked")

The cached snapshot is hashed by source bytes + every transitive vm_fixture/lab_fixture reference + every require()d helper + every profile's kernel and initrd identifier. Editing any of those invalidates the cache for that fixture (and every fixture that transitively depends on it).

Returns a VM userdata in the Booted state.

lab:lab_fixture(path) #

Build (or restore from cache) a multi-VM lab fixture. The fixture's chunk must end with return provium:snapshot(). Cached as a directory <key>.lab/ containing the per-VM snapshots and lab.json.

Returns a Lab userdata wrapping a fresh sub-lab into which the fixture has been restored.

Binary helpers #

lab:pack(fmt, ...) / lab:unpack(fmt, s) #

Pass-throughs to Lua 5.4's string.pack / string.unpack. Available so test code that processes binary protocols doesn't need to reach into string.*. Same call shape and semantics as the standard library.

Accessors #

  • lab:name() — Returns the lab's name. The root lab is named "provium".

See also #

Edit this page