Skip to main content

Report formats reference

Every reporter renders the same three events — run started, test finished, run finished — plus test started where the format has a use for it.

FormatStreamsEmitted
humanyesPer test, plus a digest at the end
jsonlyesOne object per test, then a summary object
jsonnoOne document in run_finished
teamcityyesService messages per event
junitnoOne XML document in run_finished

Buffering matters when a run is interrupted: json and junit produce nothing if the process is killed mid-run, while jsonl and teamcity have already flushed what completed.

Statuses

ValueMeaningGreen?
passedScript finished without throwingyes
failedAssertion failed, script threw, or the script file was missingno
timed_outClient or interpreter deadline reachedno
skippedDry run, or drained after a fail-fast tripyes

Human

running 3 tests across 1 suite
PASS example.com answers 200 [212ms]
FAIL cart step 1 [28ms]
TIME report generation finishes [10.01s]
SKIP cart step 2 [0ms]

failures:

cart step 1 (./suite.toml::t1)
Runtime error: cart service unavailable (line 1, position 1)
| attempting cart creation

test result: FAILED. 0 passed; 1 failed; 1 timed out; 1 skipped; finished in 10.02s
ElementRule
Headerrunning N test(s) across M suite(s); suites are counted by distinct suite path. Suppressed by -q
MarkPASS (green), FAIL (red), TIME (red), SKIP (yellow)
Duration<ms>ms under one second, otherwise <s.ss>s
Retries (after N attempts) when attempts > 1
Captured outputNon-failing tests (passed and skipped) only with -v, prefixed |
DigestOne block per failure: name, qualified id, message lines, captured output
SummaryAlways printed; ok when green, FAILED otherwise

With -q, only failing tests print a line — the header and passing lines are suppressed, the digest and summary are not.

Color follows --color: auto (TTY detection), always, or never.

JSONL

One JSON object per line, flushed as each test finishes, then a summary object. Keys are alphabetical.

Test object

{"attempts":1,"duration_ms":7,"id":"demo/suite.toml::assertions","message":null,"name":"assertion helpers, no network","output":["running against local"],"script":"demo/assertions_test.snag","status":"passed","suite":"Snag Demo Regression Suite","suite_path":"demo/suite.toml","tags":["fast"],"test_id":"assertions"}
FieldTypeDescription
idstringQualified id, <suite_path>::<test_id>
test_idstringThe manifest id
namestringDisplay name
suitestringSuite title
suite_pathstringManifest path as discovered
scriptstringResolved script path
tagsarray of stringsManifest tags
statusstringpassed, failed, timed_out, skipped
duration_msintegerTotal duration, including every retry attempt
attemptsintegerAttempts made. 0 for skipped tests
messagestring or nullFailure message, timeout message, or skip reason
outputarray of stringsCaptured print / debug / print_response lines

Summary object

{"duration_ms":8,"failed":0,"passed":1,"skipped":0,"timed_out":0,"total":1,"type":"summary"}

Distinguished by "type": "summary" — the field exists only here, so select(.type != "summary") separates the two shapes.

FieldTypeDescription
typestringAlways summary
totalintegerSum of the four counters
passed / failed / timed_out / skippedintegerPer-status counts
duration_msintegerWall-clock duration of the whole run

JSON

One document, written after the run finishes.

{
"schema": "snag.run/v1",
"summary": {
"duration_ms": 7,
"failed": 0,
"passed": 1,
"skipped": 0,
"timed_out": 0,
"total": 1,
"type": "summary"
},
"tests": [
{
"attempts": 1,
"duration_ms": 7,
"id": "demo/suite.toml::assertions",
"message": null,
"name": "assertion helpers, no network",
"output": ["running against local"],
"script": "demo/assertions_test.snag",
"status": "passed",
"suite": "Snag Demo Regression Suite",
"suite_path": "demo/suite.toml",
"tags": ["fast"],
"test_id": "assertions"
}
]
}
FieldDescription
schemaFormat version, currently snag.run/v1. Branch on this rather than sniffing the shape
summaryThe same object the JSONL summary line carries
testsArray of test objects, in completion order

Output is pretty-printed. Keys are alphabetical within each object — stable, but generated rather than authored, so parse by key.

TeamCity

Service messages, flushed per event. Understood by TeamCity and by IntelliJ IDEA, which renders a native test tree.

##teamcity[testSuiteStarted name='Snag Demo Regression Suite']
##teamcity[testStarted name='assertion helpers, no network' locationHint='file://demo/assertions_test.snag']
##teamcity[testStdOut name='assertion helpers, no network' out='running against local']
##teamcity[testFinished name='assertion helpers, no network' duration='5']
##teamcity[testSuiteFinished name='snag']
EventMessage
Run startedtestSuiteStarted with the first test's suite title, or snag when empty
Test startedtestStarted with locationHint='file://<script>'
Captured outputOne testStdOut per line, before the status message
Failure / timeouttestFailed with the message
SkiptestIgnored with the message
Test finishedtestFinished with duration in milliseconds
Run finishedtestSuiteFinished name='snag'

Values are escaped per the TeamCity spec: '\|', newline → \|n, carriage return → \|r, \|\|\|, [\|[, ]\|].

Note that the closing testSuiteFinished always uses the literal name snag, while the opening message uses the suite title.

JUnit

One XML document, written after the run finishes.

<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="snag" tests="1" failures="0" skipped="0" time="0.005">
<testsuite name="snag" tests="1" failures="0" skipped="0" time="0.005">
<testcase classname="Snag Demo Regression Suite" name="assertion helpers, no network" time="0.005">
<system-out>running against local</system-out>
</testcase>
</testsuite>
</testsuites>
Element / attributeValue
testsuites / testsuite nameAlways snag; all tests go into one testsuite regardless of how many manifests ran
testssummary.total()
failuresfailed + timed_out — JUnit has no separate timeout concept
skippedsummary.skipped
timeSeconds, three decimals
testcase classnameThe suite title
testcase nameThe test name
testcase timeTest duration in seconds
<failure message="…" type="…">For failed and timed_out; type carries which
<skipped message="…"/>For skipped tests
<system-out>Captured output, newline-joined, present only when non-empty

Escaping: &, <, >, ", and ' become entities, and control characters that are illegal in XML 1.0 (other than tab, newline, carriage return) are replaced with a space — a binary-ish response body cannot produce an unparseable report.

list output

snag list emits its own shape — a description of tests, not results.

$ snag list demo/suite.toml -t fast -f jsonl
{"id":"demo/suite.toml::assertions","name":"assertion helpers, no network","parallel_safe":true,"script":"demo/assertions_test.snag","suite":"Snag Demo Regression Suite","suite_path":"demo/suite.toml","tags":["fast"],"test_id":"assertions","timeout_ms":10000}
FieldDescription
id, test_id, name, suite, suite_path, script, tagsAs in a test object
timeout_msResolved timeout in milliseconds, null when none applies
parallel_safeEffective value after defaults

-f jsonl prints one object per line; json, junit, and teamcity all print a pretty-printed JSON arraylist has no XML or service-message rendering. human prints names, or the indented block with --long.

Stability

  • Field names and the snag.run/v1 schema string are the contract; key order is alphabetical but not part of it.
  • New fields may be added within v1; consumers should ignore unknown keys.
  • The human format is for people. Parse jsonl or json instead.

See also