AOI Unit Tests
Validate Add-On Instruction logic in rungs.dev with YAML-based unit tests that drive inputs, advance scans, and assert outputs across single and multi-step scenarios.
Rungs uses YAML-based test cases to validate your AOI logic. Tests define inputs, advance the simulation, and check outputs — verifying behavior across single scans, multi-step sequences, and timed progressions.
Every test case begins with prescan, a preparation pass that puts stateful instructions into their startup state before the first test scan.
YAML Format
Each test file is a YAML array of test cases. Every test case has a name and one or more steps.
- name: motor starts on command
steps:
- in:
StartButton: 1
advance:
scans: 1
expect:
MotorRun: 1Step Fields
| Field | Required | Description |
|---|---|---|
in | No | Sets input tag values before execution |
advance | Yes | How far to advance the simulation |
hold | No* | Output tag values to verify after every scan |
expect | No* | Output tag values to verify after execution |
* Every step must check something: include hold, expect, or both. A phase where an
output should stay steady usually needs only hold — it already covers the final scan.
in — Set Inputs
Assigns values to input tags before the step executes. Only tags with usage: input are valid. Values must be numbers (0, 1, 3.14).
in:
StartButton: 1
Temperature: 72.5Omit in when testing default behavior:
- name: green light when idle
steps:
- advance:
scans: 1
expect:
GreenLight: 1advance — Progress the Simulation
Every step must specify how to advance. Two modes are available:
Scan-based — run a specific number of deterministic test scans:
advance:
scans: 1Test scans are 100 ms apart in simulation time unless the test case sets its own pace with
scanTime (see below). Unlike Start, tests do not wait for real time, so they run
quickly and produce the same timing on every run.
Time-based — specify a duration in milliseconds or seconds:
advance:
time: 500msadvance:
time: 3.1sTime values must be exact multiples of the test scan time. At the default 100 ms, the test
runner rejects values such as 250ms instead of silently rounding them to a different
duration.
| Duration | Scans (at 100 ms) |
|---|---|
100ms | 1 |
500ms | 5 |
1s | 10 |
3.1s | 31 |
1m | 600 |
1h | 36000 |
scanTime — Set the Test Scan Pace
A test case can declare how far apart its scans are, as a top-level field next to name:
- name: catches a fast pulse
scanTime: 20ms
steps:
- in:
StartButton: 1
advance:
time: 200ms
expect:
MotorRunning: 1Why change it? A real controller scans much faster than 100 ms. A finer scanTime checks
timing behavior more precisely — a transition that happens 100 ms late is invisible at the
default pace but plainly wrong at 20ms. Allowed values are whole milliseconds from 10ms
to 1000ms; when omitted, the default is 100 ms. Remember that scans-based advances mean
scans, so their duration changes with scanTime, while time-based advances always mean
the same elapsed time.
expect — Verify Outputs
Checks output tag values after the step finishes. Only tags with usage: output are valid. Structured types (TIMER, COUNTER) and array tags cannot be checked directly.
expect:
MotorRun: 1
FaultActive: 0hold — Verify Outputs Stay Steady
expect only checks outputs after the last scan of a step. If a step advances 50 scans and an output briefly drops in the middle, expect never sees it. hold closes that gap: it checks the listed outputs after every scan of the step, so a value that flickers mid-step fails the test.
- name: red phase holds for its full duration
steps:
- in:
Enable: 1
advance:
scans: 1
expect:
RedLight: 1
- advance:
time: 4900ms
hold:
RedLight: 1
GreenLight: 0
expect:
RedLight: 1The same rules as expect apply: output tags only, numeric values, no structured or array tags. A failure reports the exact scan where the value first went wrong, e.g. hold failed at step 2, scan 7/49 (t=4800ms): RedLight must remain 1 throughout this step; got 0.
Use hold for "stays at this value the whole time" and expect for "ends at this value". A step needs at least one of them; a steady phase is usually hold-only, because hold already checks the final scan.
Test Patterns
Single-Step Tests
Test combinatorial logic that resolves in one scan:
- name: both inputs required for series
steps:
- in:
InputA: 1
InputB: 1
advance:
scans: 1
expect:
SeriesResult: 1Multi-Step Tests
Verify stateful behavior by chaining steps. State persists across all steps in a test case — local variables, latches, and accumulators carry forward.
Latch persistence:
- name: latch persists after command released
steps:
- in:
LatchCmd: 1
advance:
scans: 1
expect:
LatchedOut: 1
- in:
LatchCmd: 0
advance:
scans: 1
expect:
LatchedOut: 1Fault seal-in and reset:
- name: clears fault on reset
steps:
- in:
OverloadTrip: 1
advance:
scans: 1
expect:
FaultActive: 1
- in:
OverloadTrip: 0
ResetFault: 1
advance:
scans: 1
expect:
FaultActive: 0Input Persistence
Input values set via in persist into subsequent steps until explicitly changed. If step 1 sets StartButton: 1, step 2 will still see StartButton: 1 unless overridden.
- name: motor stays running after start released
steps:
- in:
StartButton: 1
advance:
scans: 1
expect:
MotorRun: 1
- in:
StartButton: 0
advance:
scans: 1
expect:
MotorRun: 1Step 2 explicitly sets StartButton: 0 — without this, it would remain 1 from step 1.
Timer Tests
Use advance: time: to progress timers through their phases:
- name: timer completes after preset elapses
steps:
- in:
Enable: 1
advance:
time: 3.1s
expect:
TimerDone: 1
TimerTiming: 0The first active scan starts this TON at .ACC = 0; it does not add 100 ms immediately. The example uses 3.1s so a 3-second timer receives its start scan followed by 30 elapsed 100 ms intervals. Use time-based advances for timer tests and reserve scans: 1 for scan-order, edge, and reset checks.
Timer reset on disable (TON behavior):
- name: timer resets when enable drops
steps:
- in:
Enable: 1
advance:
time: 1s
expect:
TimerTiming: 1
- in:
Enable: 0
advance:
scans: 1
expect:
TimerTiming: 0
TimerAcc: 0Multi-Scan Accumulation
Advance multiple scans in a single step to test accumulating behavior:
- name: triggers high alarm above 80
steps:
- in:
FillCmd: 1
advance:
scans: 24
expect:
HighAlarm: 1Edge-Triggered Counters
LD counters (CTU/CTD) count on rising edges. Toggle inputs between steps to generate multiple counts:
- name: multiple pulses increment counter
steps:
- in:
CountUpPulse: 1
advance:
scans: 1
expect:
CountValue: 1
- in:
CountUpPulse: 0
advance:
scans: 1
expect:
CountValue: 1
- in:
CountUpPulse: 1
advance:
scans: 1
expect:
CountValue: 2Phased Sequences
Combine time and scan advances to test state machines with multiple phases:
- name: transitions to red after yellow
steps:
- in:
WalkRequest: 1
advance:
scans: 1
expect:
YellowLight: 1
- in:
WalkRequest: 0
advance:
time: 1s
expect:
RedLight: 1
WalkSign: 1Holding a Phase Steady
A phase that lasts many scans can hide a glitch: expect only samples the last scan, so an output that briefly drops mid-phase still passes. Give the transition its own short step with expect, then hold the rest of the phase. An input-driven change (like Enable going high) lands on that same scan, so one scan is enough. A timer-driven change needs a 2–3 scan window instead — correct programs can update the output on the scan the timer finishes or a scan or two later, depending on the order of their statements or rungs:
- name: red phase holds for its full duration
steps:
- in:
Enable: 1
advance:
scans: 1
expect:
RedLight: 1
- advance:
time: 4900ms
hold:
RedLight: 1
GreenLight: 0
AmberLight: 0
expect:
RedLight: 1Start the hold window after the transition step, and end it a scan before the next timer boundary. Scans inside the transition window can legally read either value, so a hold that overlaps it fails on correct programs — the failure would blame the program for something the test asked wrong.
Validation Rules
The editor validates tests in real-time and highlights errors inline.
Structural Rules
- Top level must be a YAML array
- Each item needs
name(string) andsteps(array);scanTimeis optional - Each step needs
advanceplus at least one ofholdorexpect - Only valid step keys:
in,advance,hold,expect
Tag Rules
invalues must reference tags withusage: inputexpectandholdvalues must reference tags withusage: output- Structured types (TIMER, COUNTER, FBD_TIMER, FBD_COUNTER) cannot appear in
expectorhold - Array tags cannot appear in
in,expect, orhold - All values must be finite numbers (no booleans, strings, or
NaN)
Time Format
Time strings are a number followed by a unit — ms, s, m (minutes), or h (hours):
✓ 100ms 2.5s 0.5s 1500ms 1m 1h
✗ 2 seconds 100 1dEditor Features
Autocomplete
Press Ctrl+Space or type : to trigger context-aware suggestions:
- Top level: test case snippets
- Inside a step:
in,advance,hold,expectkeys - Inside
in: input tag names from your AOI - Inside
expectorhold: output tag names from your AOI - Inside
advance:scansortime
Snippets
Full test case and step templates are available as snippets with tab stops for quick authoring.
Execution
Tests run via the Run Tests button in the toolbar. Each test case:
- Initializes tag state from default values
- For each step:
- Applies
invalues to the current state - Executes the AOI logic routine for the specified number of scans, checking
holdvalues after each scan - Compares output values against
expect
- Applies
- Reports pass/fail with duration and failure details
A safety limit of 10,000 total scan cycles prevents infinite loops.
Results
Running tests clears the previous logs and opens the status panel on All. Test results also appear under Tests; test-file validation errors appear under Errors.
Each line shows a symbol and clock time. Each completed test case also shows how long the
runner took to execute it. This wall-clock duration is separate from simulated test time,
which appears as t=... in time-based failure details. The clock times and durations below
are examples.
All tests passing:
◆ 12:34:56 Running tests for MotorControl_ST...
✓ 12:34:56 latches run on start (3ms)
✓ 12:34:56 motor stays running after start released (1ms)
✓ 12:34:56 stop has priority over start (1ms)
✓ 12:34:56 ── Summary ── 3 passed, 0 failed (3 total)With failures:
◆ 12:34:56 Running tests for Flasher...
✕ 12:34:56 keeps the output on for one second (4ms)
hold failed at step 3, scan 1/5 (t=1100ms): Out_Flash must remain 0 throughout this step; got 1 (previous scan at t=1000ms: Out_Flash=1)
expect failed at step 4 (after scan t=2300ms): Out_Seconds should be 1 at the end of this step; got 0 (inputs: In_Run=1; previous scan at t=2200ms: Out_Seconds=0)
✕ 12:34:56 ── Summary ── 0 passed, 1 failed (1 total)With validation errors:
◆ 12:34:56 Running tests for MyAOI...
● 12:34:56 (Line 4): Missing required property "advance" — use { scans: N } or { time: "Xms" }
● 12:34:56 (Line 8): Unknown tag "BadTag" — input must reference an existing input tag
✕ 12:34:56 ── Summary ── 0 passed, 1 failed (1 total)Result Types
| Symbol | Meaning |
|---|---|
| ◆ | Test run started |
| ✓ | Test passed, or every test in the summary passed |
| ✕ | Test failed, summary contains failures, or execution stopped |
| ● | Test-file validation error |
Failure Details
Each mismatch names the YAML check that failed:
holdreports the first bad scan for each output, including its position within the step.expectreports the bad value after the step's final scan.
Time-based test cases include the simulated time when the scan ran. Scan-only cases omit it. The final parentheses can show input values carried from earlier steps and the output's value after the previous scan. These details help distinguish an output that never changed from one that changed too early or too late.
hold failed at step 2, scan 7/49 (t=4800ms): RedLight must remain 1 throughout this step; got 0 (inputs: Enable=1; previous scan at t=4700ms: RedLight=1)If the same final-scan mismatch fails both hold and expect, it appears only once. If the
AOI fails to compile or a scan stops with an execution error, that error replaces the normal
mismatch details.
Introduction
Welcome to the rungs.dev documentation — a complete guide to building, simulating, testing, and sharing PLC programs in Ladder Logic and Structured Text in your browser.
Save, Export and Share
Three ways to take an Add-On Instruction out of Rungs Studio — save a .rungs file you can re-open, export an .L5X file for Studio 5000 Logix Designer, or share a public link.