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: 1

Step Fields

FieldRequiredDescription
inNoSets input tag values before execution
advanceYesHow far to advance the simulation
holdNo*Output tag values to verify after every scan
expectNo*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.5

Omit in when testing default behavior:

- name: green light when idle
  steps:
    - advance:
        scans: 1
      expect:
        GreenLight: 1

advance — 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: 1

Test 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: 500ms
advance:
  time: 3.1s

Time 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.

DurationScans (at 100 ms)
100ms1
500ms5
1s10
3.1s31
1m600
1h36000

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: 1

Why 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: 0

hold — 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: 1

The 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: 1

Multi-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: 1

Fault 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: 0

Input 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: 1

Step 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: 0

The 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: 0

Multi-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: 1

Edge-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: 2

Phased 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: 1

Holding 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: 1

Start 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) and steps (array); scanTime is optional
  • Each step needs advance plus at least one of hold or expect
  • Only valid step keys: in, advance, hold, expect

Tag Rules

  • in values must reference tags with usage: input
  • expect and hold values must reference tags with usage: output
  • Structured types (TIMER, COUNTER, FBD_TIMER, FBD_COUNTER) cannot appear in expect or hold
  • Array tags cannot appear in in, expect, or hold
  • 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  1d

Editor Features

Autocomplete

Press Ctrl+Space or type : to trigger context-aware suggestions:

  • Top level: test case snippets
  • Inside a step: in, advance, hold, expect keys
  • Inside in: input tag names from your AOI
  • Inside expect or hold: output tag names from your AOI
  • Inside advance: scans or time

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:

  1. Initializes tag state from default values
  2. For each step:
    • Applies in values to the current state
    • Executes the AOI logic routine for the specified number of scans, checking hold values after each scan
    • Compares output values against expect
  3. 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

SymbolMeaning
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:

  • hold reports the first bad scan for each output, including its position within the step.
  • expect reports 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.

On this page