Skip to main content

What Snag is

Snag is a regression test runner for HTTP APIs. A suite is declared in TOML, the assertions live in Rhai scripts, and the results come out in whatever format the consumer speaks — a terminal, a CI report, or an IDE test tree.

It is a single Rust binary. There is no runtime to install next to it, no project scaffolding, and no configuration file beyond the suite itself.

snag # run every suite found under the current directory
snag demo/suite.toml # run one suite (the `run` verb is implicit)
snag list --long # discover tests without executing them
snag check # parse manifests and compile scripts only
snag init # scaffold a suite.toml plus an example script

The mental model

Three concepts, and they map one-to-one onto files on disk.

ConceptOn diskJob
Suitesuite.toml, snag.toml, or *.snag.tomlDeclares variables, defaults, and the list of tests
Testone [[test]] tableNames a script, plus its tags, timeout, and variables
Scripta .snag file (Rhai)Performs requests and asserts what must be true

A test is identified by its qualified id — the suite path and the test id joined by ::, for example demo/suite.toml::json-api. That is the string reports use, and the one to reach for when a failure needs to be traced back to a file.

A complete example

suite.toml
title = "Snag Demo Regression Suite"
timeout = "10s"

[variables]
base_url = "https://example.com"

[[test]]
id = "example-responds"
name = "example.com answers 200"
tags = ["network", "smoke"]
file = "./example_test.snag"
example_test.snag
let res = get(base_url)
.header("accept", "text/html")
.send();

assert_status(res, 200);
assert_body_contains(res, "Example Domain");
assert_faster_than(res, 5000);

print(`served in ${res.duration_ms}ms`);
$ snag
running 1 test across 1 suite
PASS example.com answers 200 [212ms]

test result: ok. 1 passed; 0 failed; 0 timed out; 0 skipped; finished in 213ms

Suite variables land in the script's scope as constants, so base_url is just an identifier — not a lookup, not a template placeholder.

What makes it different

Assertions are a real language. Rhai is a scripting language, so a test can chain requests, branch on a status, loop over a list, and compute a value before asserting on it. Matcher-based tools force that logic back into config; Snag does not.

Failure output is the point. An assertion failure carries the expected and actual value, the script line and column, and up to 500 characters of the response body. Anything the script printed is captured per test and replayed under the failure — never interleaved with another test's output.

Timeouts bound both halves. A per-test timeout is installed on the HTTP client and on the interpreter's progress hook, so a hung socket and a runaway while true loop both end the test rather than the run.

One discovery path. run, list, and check share a single discovery and filtering implementation, so the set of tests you list is exactly the set that runs.

Reading order

If you are here to write tests

  1. Install Snag
  2. Quickstart — a green run in about a minute
  3. Your first suite, built from scratch
  4. Writing scripts and the script API reference

If you are here to work on Snag

  1. Architecture — how a run is executed
  2. Module tour — what lives where
  3. Extending the script API
  4. Testing and contributing

Status and scope

Snag is at version 0.1.1. What is documented here is what the binary does today. Deliberately out of scope so far: response schema validation, recorded fixtures and replay, cookie jars and redirect policy control, and any form of distributed execution.