Module tour
A map of the crate for someone about to change it. Each section lists the items that matter and the reason they exist.
src/main.rs
CLI definition and dispatch. Also the crate root, so it declares the modules.
| Item | Notes |
|---|---|
Cli | Top-level clap parser. Global options: --format, --color, --verbose, --quiet |
Command | Run, List, Check, Init, Revision, Update, Completions |
SelectionArgs | Flattened into all three selecting commands: paths, filter, tag, exclude, regex |
RunArgs | SelectionArgs plus jobs, fail_fast, timeout, retries, seed, dry_run, report |
ListArgs, CheckArgs, InitArgs | The other commands' arguments |
RevisionArgs, UpdateArgs | Release-manifest and self-update arguments |
Format, ColorChoice | Value enums for --format and --color |
Exit | Ok = 0, TestsFailed = 1, Error = 2, with From<Exit> for ExitCode |
Ctx | Presentation settings resolved once: format, color as a bool, verbosity, quiet |
implicit_run() | Inserts the run verb in front of a leading path argument |
is_broken_pipe() | Walks the error chain so snag list | head exits 0 |
cmd_run / cmd_list / cmd_check / cmd_init / cmd_completions | One handler per command |
cmd_list owns its own rendering — it does not go through report.rs, because
it describes tests rather than results.
cmd_init holds the two scaffold templates as const &str.
Change here when: adding a flag, a command, or altering how argv is rewritten.
src/discovery.rs
Turns paths into a filtered list of tests.
| Item | Notes |
|---|---|
discover() | The shared entry point. Errors when no suite files were found |
suite_files() | Paths/globs/directories → sorted, de-duplicated manifest paths |
is_glob() | True when the string contains *, ?, or [ |
is_suite_file() | suite.toml, snag.toml, *.snag.toml — applied only while walking |
walk_for_suites() | walkdir recursion, skipping dot-dirs and IGNORED_DIRS, no symlinks |
IGNORED_DIRS | target, node_modules, dist, build, .venv |
describe_search() | Builds the "looked for …" half of the not-found error |
Filter | Holds include, exclude, and tags; accepts() applies them in that order |
Matcher | Substrings(Vec<String>) or Regexes(RegexSet) |
Filter::accepts() matches against [test.name, test.id], which is why -k
finds either.
Change here when: adding a discovery convention, a filter dimension, or a new ignored directory.
src/manifest.rs
Schema and resolution.
| Item | Notes |
|---|---|
Manifest | title, variables, timeout, setup, teardown, tests (renamed from test). deny_unknown_fields |
TestDefinition | id, name, parallel_safe, tags, timeout, file, variables, setup, teardown. deny_unknown_fields |
HookSpec / HookEntry / HookTable | Untagged: a path, a { file, always } table, or a list of either |
Hook | Resolved hook — script plus always |
resolve_hooks() | Joins hook paths to the manifest directory; rejects always on setup |
Test | The resolved form the rest of the crate uses |
Test::qualified_id() | format!("{}::{}", suite_path.display(), id) |
load_suite() | Read → parse → resolve, rejecting duplicate ids |
normalize() | Drops . components, leaves .. alone |
default_title() | "Untitled suite" |
Variables are BTreeMap<String, String>, so ordering is deterministic and
values are always strings.
Change here when: adding a manifest field. Remember deny_unknown_fields
means old binaries reject manifests using a new field — a versioning
consideration, not just a schema one.
src/runner.rs
Execution.
| Item | Notes |
|---|---|
run() | Builds the reporter, shuffles, partitions, runs both phases, returns the exit |
check() | Compiles every script; prints per-test results; bails with a count |
compile() | Registers the same functions the runner does, then engine.compile() |
build_reporter() | Plain reporter, or a Multi when --report is given |
jobs() | 0 → available_parallelism(), else the requested count |
Event | Started(Test) / Finished(Box<Outcome>) |
execute_batch() | The worker pool and the event loop |
execute() | Retry wrapper; owns attempts and the total duration |
execute_once() | One attempt: build client and engine, compile, run setup/body/teardown, classify |
compile_hooks() / compile_file() | Compile hook and test scripts before anything runs |
run_test_body() | Setup scripts → fn setup() → the script, all sharing one Scope |
run_teardown() | on_teardown callbacks last-first → fn teardown() → teardown scripts |
call_hook_fn() | eval_ast(false), rewind_scope(false) so fn setup() leaves its variables behind |
defines() | Whether an AST declares a zero-argument function by that name |
Failure | TimedOut or Error(String) |
classify() | Distinguishes a timeout from a genuine script failure |
phase_failure() | Same classification, prefixed with the phase and script |
shuffle() | Fisher-Yates over a seeded xorshift64 — deterministic, no rng dependency |
PROGRESS_INTERVAL | 2_000 operations between deadline checks |
The deadline behind on_progress lives in a shared cell so the teardown phase
can be handed a fresh budget — cleanup after a timed-out test would otherwise
abort on its first operation.
build_reporter() is where --format human --report FILE becomes JSON in the
file: a human report in a file would be useless to a tool.
Change here when: touching scheduling, retries, timeouts, or cancellation.
execute_batch has a dedicated test module — extend it alongside any change.
src/report.rs
Statuses, the reporter trait, and the five formats.
| Item | Notes |
|---|---|
Status | Passed, Failed, TimedOut, Skipped, plus as_str() and is_failure() |
Outcome | Test, status, message, duration, attempts, captured output |
Summary | Four counters plus duration; record(), total(), is_green() |
Reporter | Four methods, all defaulting to no-ops |
Multi | Broadcasts every event to a Vec<Box<dyn Reporter>> |
reporter_for() | Factory from Format to a boxed reporter |
Human | Streaming lines, a failure digest, and the summary |
Jsonl | One object per test, flushed; summary object last |
Json | Buffers, emits snag.run/v1 in run_finished |
TeamCity | Service messages; tc_escape() handles the spec's escapes |
Junit | Buffers, emits XML; xml_escape() also strips illegal control characters |
outcome_json() / summary_json() | Shared by Jsonl and Json, so the two cannot drift |
fmt_duration() | <1s → 123ms, otherwise 1.50s |
Change here when: adding a format or a field. Adding a field to
outcome_json() updates JSON and JSONL together; JUnit and TeamCity need their
own edit.
src/scripting_registration.rs
The script-visible surface.
| Item | Notes |
|---|---|
OutputSink | Arc<Mutex<Vec<String>>>, one per test |
new_sink() / push() | Create and append, ignoring a poisoned lock |
ReqBuilder | Method, URL, headers, optional body, and a cloned client |
Response | Status, text, headers, duration |
register_http() | Verbs, builder methods, send, accessors, field, basic |
register_debug() | on_print, on_debug, print_response |
register_assertions() | Every assert_*; eq_result() builds the message |
register_env() | env, env_or, sleep_ms, fail |
register_teardown() | Both on_teardown overloads, pushing onto the queue |
TeardownCallback / TeardownQueue | Rc<RefCell<Vec<_>>> — a FnPtr belongs to its engine's thread |
dig() | Backs field(); walks maps and arrays by dotted path |
json_to_dynamic() / dynamic_to_json_string() | serde bridges for res.json() and .json(value) |
truncate() | Caps bodies in messages at 500 chars, appending the byte total |
Builder methods take ReqBuilder by value and return it, which is what makes
get(url).header(…).bearer(…).send() chain in Rhai.
register_type_with_name::<ReqBuilder>("Request") exists so error messages say
Request, not a Rust path.
Change here when: adding a script function. See extending the script API.
Dependencies
| Crate | Used for |
|---|---|
clap + clap_complete | CLI parsing and completions |
anyhow | Error propagation with context |
thiserror | Declared; error types are currently anyhow-based |
serde + toml + serde_json | Manifest parsing and report serialisation |
humantime + humantime-serde | Duration parsing on the CLI and in manifests |
rhai | The scripting engine (serde and serde_json features) |
reqwest (blocking) | HTTP |
regex | --regex filters, via RegexSet |
glob + walkdir | Path expansion and directory walking |
base64 | basic() |
src/revision.rs
Builds the release manifest. See revision.json.
| Item | Notes |
|---|---|
VERSION / COMMIT / TARGET | Compile-time constants; the last two come from build.rs |
TARGETS | The release matrix. Must stay in step with the workflow build matrix |
Revision / Platform | The serialised shape. platforms is a BTreeMap, so output is byte-stable |
latest_url() / tag_url() | Manifest URLs. latest_url is version-free, so it resolves without knowing the tag |
load() | Reads a manifest from a file path or an http(s) URL |
next_build() / resolve_build() | The build counter, and the --build / --previous / --offline precedence |
src/update.rs
snag update and the startup notice. See updating.
| Item | Notes |
|---|---|
parse_version() / is_newer() | Numeric major.minor.patch compare. Unparseable never counts as newer |
Cached | The check cache: checked_at, version, tag |
cache_path() / read_cache() / write_cache() | XDG_CACHE_HOME or LOCALAPPDATA; written via temp file + rename |
is_stale() | Ages an entry out after CHECK_TTL. Saturating, so a backwards clock cannot panic |
roll_with() / roll() | The NOTICE_CHANCE sampling, split from the clock so it is testable |
enabled() | Every gate on the notice: opt-out env, format, quiet, stderr TTY, command |
start_check() | Prints from cache, then refreshes on a detached thread. Never blocks the run |
verify() | Runs the download's --version and compares. The only integrity check there is |
install() | Atomic rename on Unix; move-aside-and-restore on Windows |
denied() | Whether the failure was a permission error, which is the only one that earns the sudo hint |
Where to make a change
| Goal | File |
|---|---|
| New CLI flag | main.rs (and the consuming module) |
| New manifest field | manifest.rs, then wherever Test is consumed |
| New discovery rule | discovery.rs |
| New script function | scripting_registration.rs |
| New assertion | scripting_registration.rs, register_assertions |
| New output format | report.rs plus the Format enum in main.rs |
| Scheduling / retries / timeouts | runner.rs |
| New release platform | TARGETS in revision.rs and the workflow build matrices |
| Update behaviour or the startup notice | update.rs |