Orcraft Statuses
Every component result carries a status saying how it went and descriptions saying why. This page
covers the statuses themselves and the mechanisms that set them - checkpoint,
handle_errors and the description fields - before
Orcraft Result Fields covers what else a
result can carry.
Statuses
| Status | Meaning |
|---|---|
PASS |
The component ran and everything it checked came out as expected. |
FAIL |
The component ran and something it checked did not come out as expected - a value, a timing, a state, whatever the test verifies; a returned value differing from the expected one is the common case. This is the component doing its job: a real finding about the system under test. |
ERROR |
An exception - an unexpected failure the component was not testing for. It could not reach a verdict, so there is no finding. |
ABORT |
The run was stopped from the controller before the component finished. Set by the framework alone; a suite only ever reads it. |
The distinction between FAIL and ERROR is the one that matters in practice.
FAIL is an expected kind of failure: the component compared what it expected with what
it observed, they differed, and it reported that - the system under test is wrong. ERROR is an
unexpected failure: something raised - a missing file, a dead connection, a bug in the test
itself - before a comparison could be made, so nothing has been established about the system under
test either way. A suite where half the results are errors has an infrastructure problem, not a
quality problem, and Orcraft counts the two separately so that difference is visible at a glance.
A result starts at PASS and is moved from there - by a failing checkpoint, by an
assignment to one of the descriptions, or by an uncaught exception, which is what produces
ERROR. A call to exit() counts as one: the component reports
"exit() called by user" as its error description, without a traceback.
An error ends the component it happened in, not the run around it. A case that errors is counted as one executed, errored case, and the test goes on with the next case - the cause may be specific to that case, or sporadic and gone by the next one. The Orcraft Setup, Teardown and Exit page's Calling exit() section lists what an error ends at every place it can happen.
Setting a status
A test rarely assigns status directly. Most of the time it states a finding and lets the
framework derive the status from it:
from orcraft import OrcraftStatus
...
def _run_case(self, case: OrcraftTestCase) -> OrcraftTestResult:
case.result = MyTestResult()
response = self.query_system(self.config.payload_size)
# ERROR: the case cannot produce a finding at all, so say why and stop.
if response is None:
case.result.status = OrcraftStatus.ERROR
case.result.error_description = "The system under test returned no response."
return case.result
case.result.measured_value = response.value
# FAIL: a failing checkpoint records the finding and carries on.
self.checkpoint(
response.value <= self.config.threshold,
f"Measured {response.value}, within the threshold.",
f"Measured {response.value}, above the threshold of {self.config.threshold}.",
)
# FAIL: assigning a failure description has the same effect as a failed checkpoint.
if response.code != 0:
case.result.failure_description = f"The system reported code {response.code}."
# Still PASS, but worth flagging.
if response.retries:
case.result.warning_description = f"Succeeded after {response.retries} retries."
return case.result
Each status has its own route:
| Status | How a test produces it |
|---|---|
PASS |
The value a result starts at. Nothing to set. |
FAIL |
A failing checkpoint, or assigning failure_description. |
ERROR |
An uncaught exception, a checkpoint with is_error=True, or assigning status together with error_description. |
Note what the example does not do: it never sets OrcraftStatus.FAIL by hand. Going
through a checkpoint or a description means the reason travels with the status, so the log and the
controller show why the case failed and not merely that it did. Assigning
failure_description on a passing result moves it to FAIL on its own, which is
why the two are never set together.
Only ERROR is worth assigning explicitly, and only when the case has to stop early - it is
the one status that says the finding is missing rather than negative.
Reading a status
Statuses compare as an enum, which is occasionally useful when a caller wants to branch on the outcome of a nested component rather than just propagate it:
flow_result = flow.run()
if flow_result.status is OrcraftStatus.ERROR:
Logger.log("Skipping verification - the flow could not complete.", color=Logger.Color.YELLOW)
return case.result
For the usual case - copy the outcome upward and stop if it failed - reach for
handle_errors instead of comparing
statuses by hand.
Checkpoints
checkpoint is the framework's assertion, available on every component. It takes a
condition and the message to log for each outcome:
self.checkpoint(
measured <= threshold,
f"Measured {measured}, within the threshold of {threshold}.",
f"Measured {measured}, above the threshold of {threshold}.",
details="Measured over the full run, not a single sample.",
)
| Argument | Meaning |
|---|---|
result |
The condition being asserted. |
pass_msg |
Logged when the condition holds. |
fail_msg |
Logged when it does not, and used as the failure description. |
details |
Optional extra context, logged alongside the outcome. |
is_error |
Treat a failed check as ERROR rather than FAIL, abandoning the surrounding scope. |
Both outcomes are logged, so the log records what was checked and not only what broke. This is what makes a passing Orcraft run readable as evidence rather than as silence.
A failing checkpoint sets the component to FAIL with the failure message as its
description, and then continues. That is deliberate: one case can report every problem it
found instead of only the first. Pass is_error=True when a failed check means the rest of
the work would be meaningless - a missing file, an application that did not start - and the component
is marked ERROR and the scope abandoned instead.
Inside a setup or teardown a failing
checkpoint is always treated as an error, whether or not is_error was passed. A lifecycle
assertion that does not hold means the work it was guarding should not proceed.
Warnings
A warning records something worth surfacing that does not make the result wrong - a retry that
succeeded, output that was unusual but valid, a slower run than expected. It is raised by assigning
warning_description on the result:
case.result.warning_description = f"Retried {retries} times before succeeding."
The status is unaffected - setting warning_description never
moves a result off PASS - but the case is counted as a case with
warnings, and that count aggregates up through the group and suite results. A run can therefore be
entirely green and still tell you where to look.
Propagating statuses
Components do not raise their way up the hierarchy - they return results, and the caller decides.
handle_errors copies status and descriptions from one result to another and returns
True when the caller should stop:
flow_result = flow.run()
if self.handle_errors(flow_result, case.result):
return case.result
This is the idiom to use after every flow call in a case and after every step call in a flow. It keeps the failure attached to the component that produced it while still surfacing it at every level above, so the controller can show which step of which flow of which case of which test failed - rather than a single stack trace at the top.
Signature
def handle_errors(self, source_result, target_result) -> bool
| Argument | Meaning |
|---|---|
source_result |
The result just produced by the nested component - a flow result in a case, a step result in a flow. |
target_result |
The result of the caller, which receives the outcome - case.result in a case, self.result in a flow. |
| Returns | True when the source status is FAIL or ERROR, meaning the caller should stop; False on PASS, including a pass with a warning. |
It copies four fields from source to target: status, failure_description,
error_description and warning_description. The copy is unconditional - the last
call wins. That is harmless for failures, because the idiom returns on the first one, but it means a
warning carried by an earlier flow is overwritten when a later flow passes cleanly. Read
warning_description off the earlier result before the next call if you need to keep it.
handle_errors is available on OrcraftTest and OrcraftTestFlow
- the two components that call something beneath them. A step runs nothing below it, and groups and
suites aggregate their children's results on their own, so none of them carries the method.