product: assert audience: test-developer authority: normative
Runtime Control Fields
enabled — static disable
false → step IS immediately SKIPPED without executing.
- name: "Legacy step"
type: mock
enabled: false
precondition — conditional execution
A DynamicExpresso boolean expression evaluated at runtime.
false → SKIPPED. Exception during evaluation → ABORTED.
- name: "Calibrate"
type: mock
precondition: "runCalibration == true"
Supported syntax:
variable == true/variable == falsetemperature > 25a == true && b < 3skipCal == true || forceRun == true
cfg.*, exec.*, and mes.* MUST NOT be used in precondition: expressions.
These namespaces use dot notation which DynamicExpresso interprets as C# member access.
To branch on a cfg.* value, read it into a bare variable in a preceding step.
force — verdict override
- name: "Noisy measurement"
type: mock
force: "pass"
| Value | Effect |
|---|---|
pass |
Force PASS (VerdictForced = true) |
fail |
Force FAIL |
skip |
Do not execute; SKIPPED |
run |
Execute even if precondition is false |
retry — retry on failure
- name: "Flaky instrument read"
runner: dotnet
runner_type: net10.0
assembly: "Tests.dll"
class: "Tests.Instruments"
method: "ReadDMM"
retry:
count: 3 # 3 retries → 4 total attempts
delay_ms: 500
repeat — condition-based loop
Two built-in variables are set before each iteration:
| Variable | Type | Description |
|---|---|---|
__iteration__ |
int | Zero-based counter (0, 1, 2, …) |
__cycle__ |
int | One-based counter (1, 2, 3, …) — use this for human-readable names |
max_iterations IS required as a safety cap.
- name: "Sweep 5 points"
runner: python
runner_type: python3.11
module: "sweep"
function: "measure_point"
parameters:
step: "{{__iteration__}}"
repeat:
condition: "__iteration__ < 4" # iterations 0–4 = 5 total
max_iterations: 10
Looping a sub-sequence
repeat works on type: sequence steps. __iteration__ and __cycle__ are
available inside the child YAML through the shared variable scope. Each iteration is a new
invocation of the file, so it starts from the file's declared defaults, not from the previous
iteration's values (see Variable System).
Use __cycle__ to produce readable measurement names like VOUT_CYCLE1, VOUT_CYCLE2.
Every sub-sequence file MUST have a
test:root element withname:andversion:. The format is identical to a top-level test file; the engine ignoressetup:andteardown:blocks inside sub-sequences. Only thesteps:list is executed inline.This is intentional — see Standalone / Composite Test Pattern for how to use this behaviour to write sub-sequences that work both as independent tests and as reusable modules in a full suite.
# poe.yaml — top-level test
steps:
- name: "Power cycle"
type: sequence
sequence: "tests/poe-cycle.yaml"
repeat:
condition: "__iteration__ < 2" # 2 cycles
max_iterations: 5
post_execution_action: terminate-on-fail
# tests/poe-cycle.yaml — the reusable sub-sequence
test:
name: "PoE power cycle (sub-sequence)"
version: "1.0.0"
steps:
- name: "[Cycle {{__cycle__}}] Enable detection"
runner: dotnet
runner_type: net10.0
assembly: "regressiontests.dll"
class: "regressiontests.Poe"
method: "EnableDetectionAndClassification"
post_execution_action: terminate-on-fail
- name: "[Cycle {{__cycle__}}] Read registers"
runner: dotnet
runner_type: net10.0
assembly: "regressiontests.dll"
class: "regressiontests.Poe"
method: "DumpRegisters"
outputs:
opmode: "{{opmode_ch1}}"
vin: "{{vin}}"
measurements:
- { name: "OPMODE_CYCLE{{__cycle__}}", type: string, value: "{{opmode_ch1}}" }
- { name: "VIN_CYCLE{{__cycle__}}", value: "{{vin}}", unit: "V", operator: log }
timeout_ms — per-attempt wall-clock timeout
Hardware steps MUST always carry timeout_ms. An instrument that stops responding
will otherwise block the runner indefinitely.
- name: "Flash firmware"
runner: dotnet
runner_type: net10.0
assembly: "FlashTools.dll"
class: "FlashTools.Programmer"
method: "Flash"
timeout_ms: 30000
log — per-step log output control
Controls whether a step's output (the engine start/complete lines and the runner's
stdout/stderr) is written to the live monitor and the persisted result log. The step
node and its verdict/measurements always remain visible — log only governs log lines.
| Value | Behaviour |
|---|---|
always |
Log always — the default when omitted. |
on_fail |
Only log if the step FAILs or ABORTs. |
on_pass |
Only log if the step PASSes. |
never |
Never log — suppress this step's output from screen and result log entirely. |
- name: "Voltage rail check"
runner: dotnet
assembly: "PowerTests.dll"
class: "MyTests.PowerTests"
method: "MeasureVoltage"
log: on_fail # only surfaces if the rail check fails
- name: "Heartbeat ping"
runner: dotnet
assembly: "Tests.dll"
class: "Tests.Comms"
method: "Ping"
log: never # high-frequency step, no informational value
For on_fail/on_pass the verdict is only known once the step finishes, so those steps do
not stream live during execution — their log lines appear (or not) the moment the step
completes.
Propagation into sub-sequences
A non-always log on a step that calls a sub-sequence becomes the effective default for
every step inside that sub-sequence, at any depth — unless a descendant sets its own
log. This silences an entire sub-flow without annotating each child:
# Calls a noisy calibration sub-sequence but only keeps its log if it fails.
- name: "Calibrate (quiet unless it fails)"
sequence: "tests/calibrate.yaml"
log: on_fail # propagates to every step in calibrate.yaml
run_on_abort — guaranteed cleanup
true → the step IS executed even when the test is aborting. MUST be set on all
power-down, disconnect, and instrument-release steps.
- name: "Power down (always runs)"
runner: dotnet
runner_type: net10.0
assembly: "Tests.dll"
class: "Tests.Cleanup"
method: "PowerDown"
run_on_abort: true
timeout_ms: 10000 # recommended: bounds the cleanup if the instrument hangs
A test is aborting after any of:
- an external abort —
abort_test, the UI Abort button,debug_abort; - a step that ends ABORTED — a runtime error, a
timeout_msexpiry, the operator pressing a prompt's abort button, or a prompt/runner call cancelled by an external abort; - a
terminate-on-fail/terminate-alwaysaction firing.
While the test is aborting:
| Step | What happens |
|---|---|
setup: / steps: without run_on_abort |
SKIPPED ("Skipped: test is aborting"), recorded in the report |
setup: / steps: with run_on_abort: true |
Executed, result recorded in the report |
every teardown: step |
Executed, result recorded in the report |
sub-sequence children with run_on_abort: true |
Executed when the abort lands inside that sub-sequence |
All of this happens before the execution is marked ABORTED (external abort or an ABORTED step) or FAIL (terminate action).
Cleanup steps run under their own, fresh cancellation — the abort does not cancel them.
A run_on_abort: true step is not cancelled even when the abort arrives while it is
already running; a teardown step without it is interrupted only if it was already running
when the abort arrived, and the rest of teardown then still runs. Cleanup steps are
bounded instead by their own timeout_ms. A cleanup step with no timeout_ms is bounded by the station setting
AbortCleanupTimeoutMs (default 60 000 ms), counted from the moment the abort was
requested; set timeout_ms on cleanup steps rather than relying on it.
post_execution_action
| Value | Effect |
|---|---|
continue |
Default. Proceed regardless of verdict |
terminate-on-fail |
Stop the test if this step FAILs or ABORTs |
terminate-always |
Stop the test after this step regardless of verdict |
MUST be set to terminate-on-fail on all power-on, connect, and init steps.
requires — shared-instrument locking (parallel stations)
A list of named shared resources the step needs exclusive access to. When the station
runs more than one test at a time (MaxParallelExecutions > 1), concurrent tests share the
bench instruments; requires: tells the engine to serialise access to each named resource.
The whole set is acquired atomically before the step runs and released after.
- name: "Measure 3V3 Rail"
runner: dotnet
runner_type: net10.0
assembly: "PowerTests.dll"
class: "MyTests.PowerTests"
method: "MeasureVoltage"
requires: [dmm, mux] # exclusive DMM + mux for the duration of this step
measurement:
name: "RAIL_3V3"
value: "{{result}}"
low_limit: 3.135
high_limit: 3.465
unit: "V"
Rules:
- Names are free-form — use one consistent name per physical instrument across the
whole station (e.g.
dmm,psu,mux,scope). - The set is acquired in a fixed order, so declaring
requires:can never deadlock. - On a single-execution station (
MaxParallelExecutions = 1, the default) this has no effect — but it is always safe to declare, and makes the package correct on a parallel station. - Any step that touches a shared instrument MUST declare it. Omitting
requires:is the common cause of corrupted measurements when two tests run at once.