7.4 KiB
Test Scenario Schema & Runner — CLAUDE.md
Parent: ../CLAUDE.md
Overview
Automated PLC I/O validation through JSON-defined test scenarios. The test runner is Python 3.10+ and communicates with Ignition exclusively through the WebDev API. It never touches tags, files, or Modbus directly.
Scenario Schema
Every scenario JSON file has exactly five top-level sections.
{
"identity": {
"id": "PMP-101-START-001",
"version": "1.0.0",
"description": "Verify P-101 starts when start command is issued and permissives are met",
"author": "engineer@company.com",
"created": "2026-03-14",
"tags": ["pump", "start", "P-101", "plantpax"]
},
"device": {
"type": "Pump",
"udt_path": "[default]Devices/Pumps/P-101",
"plc_connection": "ControlLogix",
"related_devices": [
{
"type": "Valve",
"udt_path": "[default]Devices/Valves/XV-1001",
"role": "discharge_valve"
}
]
},
"initial_state": {
"tags": [
{
"path": "[default]Devices/Pumps/P-101/Status/Running",
"value": false
},
{
"path": "[default]Devices/Pumps/P-101/Status/Faulted",
"value": false
},
{
"path": "[default]Devices/Pumps/P-101/Status/Ready",
"value": true
},
{
"path": "[default]Devices/Valves/XV-1001/Status/FullOpen",
"value": true
}
]
},
"sequence": [
{
"step": 1,
"action": "write",
"description": "Issue start command",
"path": "[default]Devices/Pumps/P-101/Command/Start",
"value": true,
"delay_ms": 0
},
{
"step": 2,
"action": "wait",
"description": "Allow PLC scan time for logic execution",
"delay_ms": 2000
},
{
"step": 3,
"action": "read",
"description": "Check running status",
"path": "[default]Devices/Pumps/P-101/Status/Running",
"delay_ms": 0
}
],
"expected_outcomes": [
{
"path": "[default]Devices/Pumps/P-101/Status/Running",
"expected": true,
"tolerance": null,
"comparison": "equals",
"description": "Pump should be running after start command"
},
{
"path": "[default]Devices/Pumps/P-101/Status/Faulted",
"expected": false,
"tolerance": null,
"comparison": "equals",
"description": "Pump should not be faulted"
},
{
"path": "[default]Devices/Pumps/P-101/Status/Speed",
"expected": 60.0,
"tolerance": 2.0,
"comparison": "within_tolerance",
"description": "Pump speed should be near setpoint"
}
]
}
Section Details
identity
Required fields: id, version, description
Optional fields: author, created, tags
ID convention: {DEVICE_PREFIX}-{NUMBER}-{ACTION}-{SEQUENCE}
Example: PMP-101-START-001, VLV-1001-OPEN-002
Version follows semver. Bump on any change to sequence or expected outcomes.
device
Required fields: type, udt_path
Optional fields: plc_connection, related_devices
udt_path is the full Ignition tag path to the device UDT instance.
related_devices lists other devices involved in the test with their role.
initial_state
Array of {path, value} objects. These tags are written (via WebDev API)
before the test sequence begins. The runner writes all initial_state tags,
waits 500ms for propagation, then verifies they were set correctly.
sequence
Ordered array of steps. Each step has:
step(integer) — execution orderaction— one of:write,read,wait,assertdescription— human-readable step descriptionpath— tag path (required forwrite,read,assert)value— value to write (required forwrite)delay_ms— milliseconds to wait before executing this step
Actions:
write— writevaluetopathvia WebDev tagWriteread— readpathvia WebDev tagRead, store result for later assertionwait— pause execution fordelay_ms(no tag interaction)assert— immediately checkpathagainstvalue(mid-sequence validation)
expected_outcomes
Array of final assertions evaluated after the sequence completes. Each outcome has:
path— tag to checkexpected— expected valuetolerance— numeric tolerance (null for exact match)comparison— one of:equals,within_tolerance,greater_than,less_than,not_equalsdescription— what this assertion validates
Scenario File Rules
- One scenario per file. Named:
{identity.id}.json - Validate JSON before writing. Use sorted keys, 2-space indent.
- Store in
testing/scenarios/directory. - Back up before overwriting:
cp file.json file.json.bak
Test Runner
Architecture
runner/
├── main.py ← CLI entry point (Python 3.10+)
├── executor.py ← scenario execution engine
├── api_client.py ← WebDev API client wrapper
├── validator.py ← scenario JSON schema validation
├── reporter.py ← results formatting and output
└── config.py ← runner configuration (gateway URL, timeouts)
Execution Flow
- Load and validate scenario JSON against schema.
- Connect to Ignition WebDev API (health check first).
- Write
initial_statetags → wait 500ms → verify writes. - Execute
sequencesteps in order, respectingdelay_ms. - After sequence completes, evaluate
expected_outcomes. - Generate results report (JSON + human-readable summary).
API Client
The runner talks to Ignition through WebDev endpoints only:
# env: python3.10+ (external tooling — NOT Ignition)
class IgnitionAPIClient:
def __init__(self, base_url: str = "http://localhost:8088/system/webdev"):
self.base_url = base_url
def tag_write(self, path: str, value) -> dict:
"""POST /tagWrite {"path": path, "value": value}"""
def tag_read(self, path: str) -> dict:
"""GET /tagRead?path=<path>"""
def tag_browse(self, root: str = "") -> dict:
"""GET /tagBrowse?root=<root>"""
def health(self) -> dict:
"""GET /health"""
Results Format
{
"scenario_id": "PMP-101-START-001",
"scenario_version": "1.0.0",
"timestamp": "2026-03-14T10:30:00Z",
"duration_ms": 4523,
"result": "PASS",
"initial_state_verification": {
"status": "OK",
"details": []
},
"sequence_log": [
{
"step": 1,
"action": "write",
"path": "[default]Devices/Pumps/P-101/Command/Start",
"value": true,
"response": {"status": "ok"},
"elapsed_ms": 45
}
],
"outcomes": [
{
"path": "[default]Devices/Pumps/P-101/Status/Running",
"expected": true,
"actual": true,
"result": "PASS",
"description": "Pump should be running after start command"
}
],
"summary": {
"total_outcomes": 3,
"passed": 3,
"failed": 0
}
}
Results are stored in testing/results/ as {scenario_id}_{timestamp}.json.
File Structure
testing/
├── CLAUDE.md ← you are here
├── scenarios/
│ ├── PMP-101-START-001.json
│ ├── PMP-101-STOP-001.json
│ └── VLV-1001-OPEN-001.json
├── runner/
│ ├── main.py
│ ├── executor.py
│ ├── api_client.py
│ ├── validator.py
│ ├── reporter.py
│ └── config.py
└── results/
└── (timestamped result files)