Suite manifest reference
A suite manifest is a TOML file named suite.toml, snag.toml, or
*.snag.toml (any name works when passed explicitly on the command line).
title = "Checkout API"
timeout = "10s"
[variables]
base_url = "https://api.example.com"
[[test]]
id = "cart-create"
name = "POST /carts creates a cart"
tags = ["smoke", "checkout"]
parallel_safe = true
timeout = "20s"
file = "./checkout/cart_create.snag"
[test.variables]
currency = "USD"
Top level
| Field | Type | Default | Description |
|---|---|---|---|
title | string | "Untitled suite" | Suite label. Appears in reports — JUnit classname, TeamCity suite name, the JSON suite field |
timeout | duration string | none | Default per-test timeout for this file |
variables | table of string → string | {} | Variables shared by every test in the file |
setup | hook | none | Script(s) run before every test in the file |
teardown | hook | none | Script(s) run after every test in the file |
test | array of tables | [] | The tests, written as repeated [[test]] blocks |
An empty manifest is valid and contributes zero tests.
[[test]]
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id | string | yes | — | Identifier, unique within the file |
file | path | yes | — | Script path, relative to this manifest |
name | string | no | the id | Human-readable label used by reporters |
tags | array of strings | no | [] | Selection tags, matched exactly by -t |
timeout | duration string | no | the suite timeout | Per-test timeout |
parallel_safe | bool | no | true | false pins the test to the serial phase |
variables | table of string → string | no | {} | Overrides of suite variables, for this test only |
setup | hook | no | none | Script(s) run before this test, after the suite's setup |
teardown | hook | no | none | Script(s) run after this test, before the suite's teardown |
[test.variables] attaches to the [[test]] block directly above it — that is
ordinary TOML table scoping, and the most common place to get a manifest subtly
wrong. snag list --long shows what was actually parsed.
Types
Duration strings
Parsed with humantime. A unit is mandatory.
| Valid | Meaning |
|---|---|
"500ms" | 500 milliseconds |
"30s" | 30 seconds |
"2m" | 2 minutes |
"1m 30s" | 90 seconds |
"10" is not a duration and fails the parse.
Variables
Both [variables] and [test.variables] are flat maps of string to string.
Numbers, booleans, arrays, and nested tables are not accepted — quote the value
and convert in the script:
[variables]
max_latency_ms = "800"
assert_faster_than(res, max_latency_ms.parse_int());
Hooks
setup and teardown accept a path, a table, or a list of either:
setup = "./login.snag" # one script
setup = ["./login.snag", "./seed.snag"] # in order
teardown = { file = "./reset.snag", always = true } # table form
teardown = [{ file = "./check.snag", always = false }, "./reset.snag"]
| Field | Type | Default | Description |
|---|---|---|---|
file | path | required | Script path, resolved like file on a test |
always | bool | true | Teardown only: false skips the script when the test failed |
Suite hooks wrap test hooks. Setup runs suite-first, teardown unwinds test-first:
suite setup → test setup → the test → test teardown → suite teardown
Hook scripts share the test's scope and variables — a let at the top level of
a setup script is readable from the test. See
Setup and teardown for the full ordering,
including the script-level fn setup / fn teardown and on_teardown(...).
Paths
file is resolved relative to the directory containing the manifest, never
relative to the shell's working directory. . components are stripped for
display; .. is preserved as written.
file = "./login.snag" # → tests/login.snag
file = "scripts/login.snag" # → tests/scripts/login.snag
file = "../shared/ping.snag" # → tests/../shared/ping.snag
A file that does not exist is not a manifest error: the test runs and fails
with script … does not exist, so one broken path does not take down the run.
Validation
Enforced at load time, before anything executes:
| Rule | Failure |
|---|---|
Unknown fields are rejected (deny_unknown_fields) | Error: parsing suite <path>, exit 2 |
id and file must be present | Error: parsing suite <path>, exit 2 |
timeout must be a valid duration | Error: parsing suite <path>, exit 2 |
| Ids must be unique within a file | duplicate test id 'x' in <path>, exit 2 |
always may only appear on teardown | `always` is only valid on teardown, not setup, exit 2 |
Common rejected typos: tag (should be tags), script (should be file),
parallel-safe (should be parallel_safe), variable (should be variables).
:::note The parse error does not include the TOML detail
Today the message is only Error: parsing suite <path> — the underlying reason
is not printed. To narrow it down, validate the file with any TOML parser, or
comment out [[test]] blocks until snag list succeeds.
:::
Precedence
Variables — a test's key wins over the suite's key. Flat, per key, no deep merge:
[variables] → [test.variables]
Timeouts — the most specific setting wins, not the tightest one:
suite timeout → test timeout → --timeout
Each step overrides the one before it whatever its value, so --timeout 60s
relaxes a test declaring timeout = "1s".
Parallelism — parallel_safe = false always means the serial phase; -j
only sizes the pool for the parallel phase.
Resolved test
What the runner sees after a manifest is loaded, one per [[test]]:
| Property | Origin |
|---|---|
id | id |
name | name, else id |
tags | tags |
timeout | test timeout, else suite timeout |
parallel_safe | parallel_safe, else true |
script | file, joined to the manifest's directory and normalised |
vars | suite variables merged with test variables |
setup | suite setup followed by test setup, paths resolved |
teardown | test teardown followed by suite teardown, paths resolved |
suite_title | title |
suite_path | the manifest path as discovered |
| qualified id | <suite_path>::<id> |
The qualified id embeds the path as discovered: snag from the repository root
produces ./suite.toml::cart-create, while snag suite.toml produces
suite.toml::cart-create. Keep one invocation in CI if a dashboard keys on it.
Full example
title = "Checkout API"
timeout = "10s"
[variables]
base_url = "https://staging.api.example.com"
currency = "EUR"
tenant_id = "tenant-test-001"
# Runs alone, before every parallel test.
[[test]]
id = "reset-fixtures"
name = "fixtures are reset"
tags = ["fixtures"]
parallel_safe = false
timeout = "60s"
file = "./checkout/reset.snag"
[[test]]
id = "cart-create"
name = "POST /carts creates a cart"
tags = ["smoke", "checkout"]
file = "./checkout/cart_create.snag"
[[test]]
id = "cart-create-usd"
name = "POST /carts honours a non-default currency"
tags = ["checkout"]
file = "./checkout/cart_create.snag"
[test.variables]
currency = "USD"
[[test]]
id = "report-generate"
name = "report generation finishes"
tags = ["slow"]
timeout = "120s"
file = "./checkout/report.snag"
Note that cart-create and cart-create-usd share one script and differ only in
a variable — the cheapest way to cover a matrix.