Reference
States and exit codes
A task ends in one of seven states, and the run's exit code is computed from them in one place. There are seven because "did not pass" has four different fixes: an assertion to look at, a server to fix, a guard that was false, or a run that ended before the task started.
The seven states
| State | Meaning | Counts as |
|---|---|---|
passed |
ran; every assertion evaluated and held | pass (passed (unverified) for a script with no step events) |
failed |
ran; an assertion did not hold, or was n/a |
fail |
errored |
could not run to completion | fail |
timed_out |
a deadline fired first | fail |
skipped |
a when guard was false, or an excluded tag |
neither |
blocked |
an upstream did not pass | neither |
not_run |
the run ended first | neither |
The summary line spells two of them with a space, timed out and not run. The event log,
JUnit and TAP use the underscore forms.
How a task reaches each state
A task passes when every step ran and every assertion held. Only then are its results published; a downstream task never sees a partial result from a task that did not pass.
A task fails when a step's assertion evaluated and did not hold, or evaluated to n/a
because it compared a timing phase that did not happen. The task stops at that step.
A task errors when a step could not complete: a connection refused, a TLS handshake the run
does not trust, a response that is not the JSON it claims to be, a process killed by a signal
vero did not send. A when guard that cannot be evaluated also errors the task, with the reason
when guard could not be evaluated: <guard>: <error>, because a guard that reads a result the
plan never declared is a mistake in the plan, not a condition that was false.
A task times out when a deadline fired while a step was running: the step's own, the task's,
the run's, or a signal. The reason names which: run timeout 300ms fired while it was running: deadline exceeded waiting for first byte, task timeout 200ms ..., cancelled (interrupt) ...,
finally timeout 30s .... A step cut off by a deadline did not observe an answer, so it is never
failed or errored.
A task is skipped when its own guard was false, when guard false: env.feature == "on", or
when its tag is excluded, excluded by tag slow; run with --include-tag slow to include it. A
skipped task did nothing wrong, and the run can still exit 0.
A task is blocked when an upstream task did not pass. The reason says which and why:
blocked by wrong, which failed(orerrored,timed out). The upstream's own code covers the run, so this adds nothing to the exit code.blocked by optional-feature, which was skipped (when guard false: env.feature == "on"). The run did not finish, so the exit code is 3. If this task is optional too, give it the same when guard so it is skipped in its own right. Nothing failed, but a task that was meant to run did not, and a green exit would hide that.blocked by nightly, which was excluded by tag slow; the run did not finish, so the exit code is 3. Run with --include-tag slow to run both.
A task whose own guard is false is skipped, never blocked, even when its upstream was skipped
too. That is the fix the second reason suggests.
A task is not run when the run ended before it started: not run: run timeout 300ms,
not run: cancelled (interrupt), or, after a reset hook that did not pass, not run: lifecycle reset step 1 failed: .... Nothing is known about it.
Exit codes
| Code | Meaning | When |
|---|---|---|
| 0 | every task passed, or was skipped by its own guard | |
| 1 | an assertion failed | a main task is failed |
| 2 | a task errored or timed out | a main task is errored or timed_out |
| 3 | the run did not finish: a task was blocked or not run, and nothing is known about it | a task is not_run, or a main task is blocked and the root of its chain was skipped or not run |
| 4 | every task passed but a teardown failed | a finally task is failed, errored or timed_out |
| 64 | the plan is invalid and nothing was sent | a load error, a usage error, or lints under --strict |
When several apply, the first in the order 64, 3, 2, 1, 4 wins: a run that did not finish
outranks one that failed, because an incomplete run may hide more failures. A finally task
blocked by a main task that failed adds nothing; the main task's code already says it. With
--repeat, the exit code is the worst over all runs. The exports never change it: JUnit, TAP and
the HTML report are written after the code is known, and a file that cannot be written raises the
code to at least 2.
The HTML report opens with the code and the meaning in the second column, in those words.
One run, every state
This plan reaches six of the seven states against the fixture server, with the feature flag
unset so optional-feature skips:
apiVersion: vero.plan/v1
kind: Plan
metadata: { name: states }
env:
baseUrl: http://127.0.0.1:18080
feature: ${FEATURE:-off}
lifecycle: { strategy: isolated }
tasks:
- name: health
steps:
- http: { method: GET, url: "{{ env.baseUrl }}/healthz" }
assert: [status == 200]
- name: optional-feature
when: ['env.feature == "on"']
steps:
- http: { method: GET, url: "{{ env.baseUrl }}/healthz" }
assert: [status == 200]
results: { seen: status }
- name: uses-it
steps:
- http: { method: GET, url: "{{ env.baseUrl }}/fixtures/status?code={{ tasks.optional-feature.results.seen }}" }
assert: [status == 200]
- name: wrong
steps:
- http: { method: GET, url: "{{ env.baseUrl }}/fixtures/status?code=503" }
assert: [status == 200]
results: { x: status }
- name: after-wrong
steps:
- http: { method: GET, url: "{{ env.baseUrl }}/fixtures/status?code=200", headers: { X-Prev: "{{ tasks.wrong.results.x }}" } }
assert: [status == 200]
- name: nobody
steps:
- http: { method: GET, url: "http://127.0.0.1:1/x" }
assert: [status == 200]
- name: slow-one
steps:
- http: { method: GET, url: "{{ env.baseUrl }}/fixtures/slow?ms=3000" }
timeout: 500ms
assert: [status == 200]
finally:
- name: cleanup
steps:
- http: { method: GET, url: "{{ env.baseUrl }}/fixtures/status?code=204" }
assert: [status == 204]
vero run prints the three failure blocks, the two blocked lines and the summary, and exits 3:
BLOCKED uses-it blocked by optional-feature, which was skipped (when guard false: env.feature == "on"). The run did not finish, so the exit code is 3. If this task is optional too, give it the same when guard so it is skipped in its own right
BLOCKED after-wrong blocked by wrong, which failed
2 passed 1 failed 1 errored 1 timed out 1 skipped 2 blocked 0 not run (8 tasks, 5 requests, 0.5s) teardown: 1 passed
Codes 1, 2 and 3 all apply here, and 3 wins. Give uses-it the same when guard and the run
exits 2, the errored and timed-out tasks outranking the failed one. With --timeout 300ms the
slow task is cut by the run instead of its own deadline; the states and the code are the same,
and the timing line says deadline hit waiting for first byte either way.
In JUnit and TAP
JUnit XML can say passed, failed, errored and skipped, and most CI systems read <skipped> as
green. So skipped, blocked and not_run all go out as <skipped> with a message naming the
cause: the guard, blocked by <task>, or not run: run ended first (<cause>). Wrong but labelled
beats wrong and silent, and the exit code does not read this file. One <testsuite> per run
carries the run id, the plan file, a UTC timestamp and three properties: vero.runId,
vero.summary (the summary line) and, after --only, vero.only (what was selected and left
out). A finally task's classname ends in .finally. From the run above:
<testcase name="optional-feature" classname="states.tasks" time="0.000">
<skipped message="when guard false: env.feature == "on""></skipped>
</testcase>
<testcase name="uses-it" classname="states.tasks" time="0.000">
<skipped message="blocked by optional-feature"></skipped>
</testcase>
<testcase name="wrong" classname="states.tasks" time="0.001">
<failure type="failed" message="assert #1 failed: status == 200">wrong › step 1 › assert #1 plan.yaml:27
status == 200 failed
 status = 503
</failure>
</testcase>
<testcase name="nobody" classname="states.tasks" time="0.000">
<error type="errored" message="step 1: Get "http://127.0.0.1:1/x": dial tcp 127.0.0.1:1: connect: connection refused">...</error>
</testcase>
<testcase name="slow-one" classname="states.tasks" time="0.501">
<error type="timed_out" message="step 1: deadline exceeded waiting for first byte">...</error>
</testcase>
<testcase name="cleanup" classname="states.finally" time="0.001"></testcase>
A passed (unverified) task is a plain pass with a <system-out> note, because JUnit has no
such state. The dialect is Jenkins and Surefire's, checked against their schema in the
repository's tests.
TAP version 14 has the same three labels after # SKIP, a not ok with a YAML block for each
failure, and finally <task> as the name of a teardown task:
TAP version 14
1..8
ok 1 - health
ok 2 - optional-feature # SKIP when guard false: env.feature == "on"
ok 3 - uses-it # SKIP blocked by optional-feature
not ok 4 - wrong
---
message: "assert #1 failed: status == 200"
severity: failed
detail: |
wrong › step 1 › assert #1 plan.yaml:27
status == 200 failed
status = 503
...
ok 5 - after-wrong # SKIP blocked by wrong
not ok 6 - nobody
---
message: "step 1: Get \"http://127.0.0.1:1/x\": dial tcp 127.0.0.1:1: connect: connection refused"
severity: errored
detail: |
step 1: Get "http://127.0.0.1:1/x": dial tcp 127.0.0.1:1: connect: connection refused
...
not ok 7 - slow-one
---
message: "step 1: deadline exceeded waiting for first byte"
severity: timed_out
detail: |
step 1: deadline exceeded waiting for first byte
...
ok 8 - finally cleanup
A # SKIP is an ok in TAP. The label is what keeps it from reading as a pass, and the exit
code, not the file, decides the build. With --repeat, each run becomes a subtest, # Subtest: run <id>, and the outer ok or not ok follows that run's exit code.
This page is docs/content/reference/states-and-exit-codes.md in the repository.