# Lab

_Provium / Reference_

> The Lab userdata — VMs, bridges, and sub-labs, plus claims, barriers, whole-lab snapshots, and fixtures. The provium global is the root 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](/provium/reference/vm.md#boot-opts) for the split between creation-time and boot-time keys.

Returns a [VM](/provium/reference/vm.md) 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](/provium/reference/bridge.md) 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.

```lua
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.

```lua
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](/provium/running-tests/fixtures-and-dependencies.md#external-host-file-deps).

Files pushed into the guest via [`vm:push_file`](/provium/reference/vm.md#vmpush-filehost-path-guest-path-opts) 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](/provium/writing-tests/labs-and-scope.md#per-test-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](/provium/writing-tests/bridges-and-impairments.md#declared-vs-realised) — 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.

| Field | Type | Effect |
|---|---|---|
| `memory` | int (bytes) or string `"512M"` / `"2G"` | Memory budget. |
| `cpus` | int | vCPU budget. |

```lua
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.

```lua
-- 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](/provium/writing-tests/labs-and-scope.md#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](/provium/reference/snapshot.md) 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.

```lua
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](/provium/reference/vm.md) 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

- [provium global](/provium/reference/provium-module.md) — file-scope configuration globals on top of the root Lab.
- [VM](/provium/reference/vm.md), [Bridge](/provium/reference/bridge.md) — what lives in a lab.
- [Snapshot](/provium/reference/snapshot.md) — what `lab:snapshot()` returns.
- [Fixtures and dependencies](/provium/running-tests/fixtures-and-dependencies.md) — for the cache-key model and rebuild triggers.

Related content:

- [Labs and scope](/provium/writing-tests/labs-and-scope.md)
- [provium global](/provium/reference/provium-module.md)
- [VM](/provium/reference/vm.md)
- [Bridge](/provium/reference/bridge.md)
