Omit unit on numeric measurements |
Always include unit — it appears in reports |
Use {{test.variable}} (legacy Scriban syntax) |
Use {{variable}} (current engine syntax) |
Leave low_limit > high_limit |
Verify limits are in correct order |
Forget max_iterations on repeat: |
Always set a safety cap |
Use precondition syntax inside {{}} templates |
Templates are for substitution; preconditions use bare variable names |
| Create steps without measurements when the step measures something |
Always attach a measurement: block for traceable results |
Skip post_execution_action: terminate-on-fail on critical setup steps |
Mark power-on, connection, and init steps as terminate-on-fail |
Use type: mock in production tests |
Mock is for development only; use runner: dotnet or runner: python |
| Hard-code measurement values in gRPC runner steps |
Return values from your test function and use {{var}} templates |
Put pass/fail limit logic in Python (if v < lo or v > hi: raise ...) |
Python returns the raw value; declare low_limit/high_limit in the YAML measurement: block — limits in Python are invisible to the report engine, yield stats, and Cp/Cpk. validate.py will reject this pattern |
Forget run_on_abort: true on cleanup steps |
Always mark power-down and instrument-release steps |
| Write one large method that connects, measures, and disconnects |
Split into one method per YAML step — see sdk/design-principle.md |
| Hard-code instrument addresses in YAML |
Store in Station Config; reference via {{cfg.AGENT_HOST}} |
| Scatter magic strings through MSTest methods |
Declare constants at the top with cfg.*/exec.* comments — see sdk/mstest-debugging.md |
Declare pipPackages but forget to vendor wheels for production |
Vendor wheels in wheels/ — see sdk/python-dependencies.md |
Use pip install inside test code at runtime |
Pre-install all dependencies; runtime pip calls are fragile |
| Import a third-party module without testing locally first |
Run python -c "import mypackage" in the runner container first |
| Read a channel value without resolving the net name |
Always resolve the fully-qualified net name via GetNamesAsync() |
| Drive a channel without configuring its direction |
Always call Channels.ConfigureAsync() with the correct Direction first |
Treat Resources.GetValueAsync return as a number |
GetValueAsync always returns string — parse explicitly |
Forget .Wait() / .Result on async AccordionQ2Client calls |
All AccordionQ2Client methods are async; call .Wait() or .Result in synchronous code |
Use Thread.Sleep inside a test method to wait for hardware to settle |
Use a type: delay YAML step — keeps delay visible in results and tuneable without a rebuild |
Omit timeout_ms from hardware runner steps |
Set timeout_ms on every runner: dotnet / runner: python step that communicates with an instrument |
Omit requires: on a step that uses a shared instrument |
Declare requires: [dmm, ...] for every shared instrument the step touches — without it, two tests on a parallel station can use the instrument at once and corrupt each other's measurements. Harmless on single-execution stations, so always declare it |
| Write one large monolithic YAML test file |
Split into focused sub-sequence files, each with its own setup:/teardown:, then compose them with a top-level file — see yaml/standalone-composite-pattern.md |
Omit run_on_abort: true on cleanup in the composite teardown |
When sub-sequences are called from a composite file their teardowns are skipped; the composite's teardown: must mark all cleanup steps run_on_abort: true |