product: maestro audience: test-developer authority: normative
Standalone / Composite Test Pattern
The problem
A manufacturing test suite often grows to dozens of steps. Running the full suite every time is slow, but running only part of it risks skipping necessary hardware initialisation (setup) and cleanup (teardown). The typical result is either one huge monolithic YAML file, or fragile partial tests that leave hardware in an unknown state.
The solution
Split the suite into focused sub-sequence files. Each file:
- Has its own
setup:andteardown:so it can be run standalone. - Contains only the
steps:that belong to its responsibility.
Then create a top-level composite file that has its own setup: and
teardown: and calls each sub-sequence in order.
When a sub-sequence is called from the composite file, the engine executes only
its steps: — the sub-sequence's own setup: and teardown: are ignored. The
composite file's setup/teardown handle the full lifecycle.
This gives you both:
| Run mode | What executes |
|---|---|
power-tests.yaml standalone |
its own setup: → its steps: → its teardown: |
full-suite.yaml composite |
composite setup: → all sub-sequence steps: → composite teardown: |
Engine behaviour (normative)
When a YAML file is invoked via
type: sequence, the engine executes onlysteps:. Thesetup:andteardown:blocks in the child file are silently ignored.
This is verified by Sequence_SetupAndTeardownInChildYaml_AreIgnored_OnlyStepsExecute
in WorkflowEngine.Api.Tests/Integration/NestedSequenceTests.cs.
File layout
tests/
setup-common.yaml # optional: shared hardware init as a sub-sequence
power-tests.yaml # runnable standalone OR called from full-suite
comms-tests.yaml # runnable standalone OR called from full-suite
functional-tests.yaml # runnable standalone OR called from full-suite
full-suite.yaml # composite — calls all three in sequence
Sub-sequence file structure
Each sub-sequence file is a complete, valid test definition. It contains
setup: and teardown: so it can be run independently from the Maestro UI:
# power-tests.yaml
test:
name: "Power Rail Tests"
version: "1.0.0"
setup:
- name: "Power on DUT"
runner: dotnet
runner_type: net10.0
assembly: "BoardTests.dll"
class: "BoardTests.Power"
method: "PowerOn"
post_execution_action: terminate-on-fail
steps:
- name: "Measure 3V3 rail"
runner: dotnet
runner_type: net10.0
assembly: "BoardTests.dll"
class: "BoardTests.Power"
method: "Measure3V3"
measurement:
name: "V3V3"
value: "{{voltage}}"
low_limit: 3.135
high_limit: 3.465
unit: "V"
post_execution_action: terminate-on-fail
- name: "Measure 5V rail"
runner: dotnet
runner_type: net10.0
assembly: "BoardTests.dll"
class: "BoardTests.Power"
method: "Measure5V"
measurement:
name: "V5V"
value: "{{voltage}}"
low_limit: 4.75
high_limit: 5.25
unit: "V"
teardown:
- name: "Power off DUT"
runner: dotnet
runner_type: net10.0
assembly: "BoardTests.dll"
class: "BoardTests.Power"
method: "PowerOff"
run_on_abort: true
Composite file structure
The composite file provides the overarching setup/teardown and calls each sub-sequence as a step. The sub-sequences' own setup/teardown are not executed:
# full-suite.yaml
test:
name: "Full Board Test Suite"
version: "1.0.0"
setup:
- name: "Power on DUT"
runner: dotnet
runner_type: net10.0
assembly: "BoardTests.dll"
class: "BoardTests.Power"
method: "PowerOn"
post_execution_action: terminate-on-fail
- name: "Connect instruments"
runner: dotnet
runner_type: net10.0
assembly: "BoardTests.dll"
class: "BoardTests.Instruments"
method: "ConnectAll"
post_execution_action: terminate-on-fail
steps:
- name: "Power Rail Tests"
type: sequence
sequence: "tests/power-tests.yaml"
post_execution_action: terminate-on-fail
- name: "Comms Tests"
type: sequence
sequence: "tests/comms-tests.yaml"
post_execution_action: terminate-on-fail
- name: "Functional Tests"
type: sequence
sequence: "tests/functional-tests.yaml"
teardown:
- name: "Disconnect instruments"
runner: dotnet
runner_type: net10.0
assembly: "BoardTests.dll"
class: "BoardTests.Instruments"
method: "DisconnectAll"
run_on_abort: true
- name: "Power off DUT"
runner: dotnet
runner_type: net10.0
assembly: "BoardTests.dll"
class: "BoardTests.Power"
method: "PowerOff"
run_on_abort: true
Test Monitor — identifying composite files
In the Maestro Test Monitor (/test-monitor), the Test File dropdown
automatically marks any YAML file that contains at least one type: sequence
step with a ★ prefix:
★ full-suite ← composite file (calls sub-sequences)
comms-tests ← sub-sequence (no sequence steps)
functional-tests ← sub-sequence (no sequence steps)
power-tests ← sub-sequence (no sequence steps)
This is a visual hint only — the engine treats all YAML files identically. If multiple files in a package each call sub-sequences, all of them will be starred. There is no concept of a single "root" enforced by Maestro; the star simply reflects that the file orchestrates other files.
What the operator sees
When running power-tests.yaml standalone:
[PASS] Power on DUT ← setup
[PASS] Measure 3V3 rail ← steps
[PASS] Measure 5V rail ← steps
[PASS] Power off DUT ← teardown
When running full-suite.yaml:
[PASS] Power on DUT ← composite setup
[PASS] Connect instruments ← composite setup
[PASS] Measure 3V3 rail ← power-tests steps (its setup/teardown skipped)
[PASS] Measure 5V rail ← power-tests steps
[PASS] ...comms steps...
[PASS] ...functional steps...
[PASS] Disconnect instruments ← composite teardown
[PASS] Power off DUT ← composite teardown
Teardown and abort safety
Because sub-sequence teardowns are ignored in the composite run, the composite
file must mark all cleanup steps with run_on_abort: true. This guarantees
power-off and instrument release happen even if a step aborts mid-suite.
When running a sub-sequence standalone, its own teardown runs unconditionally
(Maestro's teardown: always executes regardless of the main steps verdict —
see yaml/schema.md).
Variable scope
All sub-sequences share the same variable scope as the composite test. A
variable set in the setup: phase or in an earlier sub-sequence is visible to
all later sub-sequences. Use parameters: and outputs: on the type: sequence
step to make data flow explicit when needed.
- name: "Power Rail Tests"
type: sequence
sequence: "tests/power-tests.yaml"
outputs:
supply_voltage: "{{measured_supply}}" # expose a child variable to the parent scope
- name: "Comms Tests"
type: sequence
sequence: "tests/comms-tests.yaml"
parameters:
expected_supply: "{{supply_voltage}}" # pass parent variable into next sub-sequence
Checklist
- Each sub-sequence file has
setup:andteardown:so it runs cleanly standalone. - Each sub-sequence
teardown:marks all cleanup stepsrun_on_abort: true. - The composite file's
teardown:marks all cleanup stepsrun_on_abort: true. - The composite
setup:haspost_execution_action: terminate-on-failon all init steps so partial initialisation does not reach the test steps. -
test.versionis bumped in every file before deploying.