diff --git a/AGENTS.md b/AGENTS.md index 02b41c1..3847ebe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,8 @@ studio/ ├── src/ │ ├── main.zig # Model, Msg, update, app wiring │ ├── app.native # the view (declarative markup) -│ ├── ocpp/ # pure, headless OCPP engine (types, parser, …) +│ ├── ocpp/ # pure, headless OCPP engine (types, parser, detection, …) +│ │ └── conformance/ # vendored shared-contract fixtures + goldens + harness │ └── tests.zig # headless view/model tests ├── scripts/smoke.sh # portable automation smoke test (Xvfb in CI) ├── docs/adr/ # architecture decision records diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index a63749f..fba8db9 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -8,8 +8,8 @@ ## Active milestone -**S2 — Detection + conformance (0.1.0): in progress.** S1 — Engine core is -complete (see [ROADMAP.md](ROADMAP.md)). +**S2 — Detection + conformance (0.1.0): complete.** Next up: **S3 — Inspector UI** +(see [ROADMAP.md](ROADMAP.md)). ## What's done @@ -53,22 +53,26 @@ conformance contract (behavior, not source) and tested via correlates end to end (one completed session, transactionId 100001); engine tests green headlessly on macOS + Linux. +### S2 — Detection + conformance ✅ + +The full 16-rule OCPP 1.6J failure taxonomy in `src/ocpp/detection.zig`, mirroring +the toolkit's `detection.ts`, plus a harness that locks Studio's output to the +toolkit's: + +- **Failure model + foundational rules** (#19) — the `Failure` / `FailureCode` / + `FailureSeverity` model and rules 1–3. +- **Protocol & transaction rules** (#20) — rules 4–10. +- **Timing & anomaly rules** (#21) — rules 11–16. +- **Conformance harness** (#22) — 15 vendored scenario traces + goldens + (`src/ocpp/conformance/`, `contract-v1`, generated from the toolkit) and a + `native test` gate asserting Studio's de-duplicated, sorted `FailureCode` set + matches each golden. + +**Exit criteria met:** 15/15 scenarios match the locked goldens. + ## What's in progress -**S2 — Detection + conformance (0.1.0).** Building the 16-rule failure taxonomy -in Zig, mirroring the toolkit's `detection.ts`: - -- **Failure model + foundational rules** (#19) — the `Failure` / `FailureCode` - (16-code taxonomy) / `FailureSeverity` model, the `detection.zig` orchestrator - with per-code severity + remediation metadata, and rules 1–3 - (FAILED_AUTHORIZATION, CONNECTOR_FAULT, STATION_OFFLINE_DURING_SESSION). -- **Protocol & transaction rules** (#20) — rules 4–10: TIMEOUT_NO_HEARTBEAT, - METER_VALUE_GAP, INVALID_STOP_REASON, UNEXPECTED_START, - STATUS_TRANSITION_VIOLATION, DIAGNOSTICS_FAILURE, FIRMWARE_UPDATE_FAILURE. -- **Timing & anomaly rules** (#21) — rules 11–16: SUSPICIOUS_SESSION_DURATION, - SLOW_RESPONSE, HEARTBEAT_INTERVAL_VIOLATION, METER_VALUE_ANOMALY, - UNRESPONSIVE_CSMS, REPEATED_BOOT_NOTIFICATION. All 16 rules now run. -- Next: the conformance harness over the 15 shared scenarios (#22). +- Nothing in flight — S2 is closed; S3 is next. ## What's next @@ -86,8 +90,8 @@ search / filter, handling traces far larger than a browser tab can hold. | --- | --- | | `repo` (tooling, CI) | ✅ done for S0 | | `docs` (docs, ADRs) | ✅ done for S0 | -| `ocpp` (engine) | 🚧 detection in progress (S2) | +| `ocpp` (engine) | ✅ done for S2 | | `ui` (native views) | ⬜ placeholder (S3) | | `capture` (live proxy) | ⬜ not started (S5) | | `cli` (headless) | ⬜ not started (S4) | -| `conformance` | ⬜ not started (S2) | +| `conformance` | ✅ done for S2 (15/15, `contract-v1`) | diff --git a/ROADMAP.md b/ROADMAP.md index 438bf2b..c80289c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -10,8 +10,8 @@ the stable part. | --- | --- | --- | --- | | **S0** | 0.0.x | Foundation | ✅ Complete | | **S1** | 0.1.0 | Engine core | ✅ Complete | -| **S2** | 0.1.0 | Detection + conformance | Next up | -| **S3** | 0.2.0 | Inspector UI | Planned | +| **S2** | 0.1.0 | Detection + conformance | ✅ Complete | +| **S3** | 0.2.0 | Inspector UI | Next up | | **S4** | 0.3.0 | Analysis parity+ | Planned | | **S5** | 0.4.0 | Live capture ⭐ | Planned | | **S6** | 1.0.0 | 1.0 polish | Planned | @@ -32,11 +32,13 @@ inference), and session correlation by transaction id. **Exit criteria met:** th vendored `normal-session` fixture parses and correlates end to end; engine tests run green headlessly (`native test -Dplatform=null`) on macOS + Linux. -## S2 — Detection + conformance (0.1.0) +## S2 — Detection + conformance (0.1.0) ✅ -The full OCPP 1.6J failure taxonomy in Zig, plus the conformance harness that -runs the shared scenario fixtures and checks Studio's detected failures against -locked goldens. **Exit:** the shared scenarios match, byte for byte. +The full OCPP 1.6J failure taxonomy in Zig — all 16 detection rules — plus the +conformance harness that runs the 15 shared scenario fixtures and checks Studio's +detected failures against locked goldens generated from the toolkit. +**Exit criteria met:** 15/15 scenarios match the `contract-v1` goldens under +`native test`. ## S3 — Inspector UI (0.2.0) diff --git a/src/ocpp/conformance/README.md b/src/ocpp/conformance/README.md new file mode 100644 index 0000000..f2627b3 --- /dev/null +++ b/src/ocpp/conformance/README.md @@ -0,0 +1,44 @@ +# Conformance contract (`contract-v1`) + +Studio and the TypeScript toolkit are **independent implementations** of the same +OCPP analysis behavior (ADR-0001). This directory is the machine-checked contract +between them: the shared scenarios and the failure codes each must detect. + +- **`fixtures/.json`** — the 15 shared scenario **traces** (the input). +- **`goldens/.json`** — each scenario's expected **de-duplicated, sorted + `FailureCode` set** (the output to match), as a JSON array of wire codes. +- **`harness.zig`** — runs, under `native test`, the full engine + (`parseTrace → buildSessionTimeline → detectFailures`) over every fixture and + asserts its detected code set equals the golden — the same comparison the + toolkit's `evaluateScenario` makes. A missing or extra code fails the scenario + by name. + +## Generated, not hand-authored + +Both the fixtures and the goldens are exported from the toolkit — the source of +truth — so a golden can never drift from what the reference implementation +actually detects. They are regenerated (not edited by hand) from the toolkit's +`scenarios` and `evaluateScenario`: + +```js +// run against a built @ocpp-debugkit/toolkit +import { scenarios } from '@ocpp-debugkit/toolkit/scenarios'; +import { evaluateScenario } from '@ocpp-debugkit/toolkit'; +for (const s of scenarios) { + const detected = [...new Set(evaluateScenario(s).failures.map((f) => f.code))].sort(); + // fixtures/.json <- JSON.stringify(s.trace) + // goldens/.json <- JSON.stringify(detected) +} +``` + +## Why it lives under `src/` + +The Native SDK zero-config build (ADR-0004) embeds test data with `@embedFile`, +which resolves within the source module root. Keeping the vendored contract here +lets the harness embed it directly — no runtime file I/O, fully hermetic. + +## Versioning + +Tagged **`contract-v1`** (OCPP 1.6J). When the contract changes — new rules, +new scenarios, or OCPP 2.0.1 — regenerate against the matching toolkit release +and bump the tag. diff --git a/src/ocpp/conformance/fixtures/connector-fault.json b/src/ocpp/conformance/fixtures/connector-fault.json new file mode 100644 index 0000000..3d11757 --- /dev/null +++ b/src/ocpp/conformance/fixtures/connector-fault.json @@ -0,0 +1,271 @@ +{ + "traceId": "fixture-connector-fault", + "metadata": { + "stationId": "CS-SYNTHETIC-003", + "ocppVersion": "1.6", + "source": "synthetic-fixture", + "description": "Connector fault during active session: station boots, transaction starts, connector faults mid-charging, transaction stops with fault reason. Expects CONNECTOR_FAULT failure." + }, + "events": [ + { + "timestamp": "2024-01-15T12:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-003", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T12:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T12:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T12:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T12:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T12:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "StatusNotification", + { + "connectorId": 1, + "status": "Preparing", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T12:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + {} + ] + }, + { + "timestamp": "2024-01-15T12:02:15.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-002" + } + ] + }, + { + "timestamp": "2024-01-15T12:02:15.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + { + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2024-12-31T23:59:59.000Z" + } + } + ] + }, + { + "timestamp": "2024-01-15T12:02:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-002", + "meterStart": 0, + "timestamp": "2024-01-15T12:02:30.000Z" + } + ] + }, + { + "timestamp": "2024-01-15T12:02:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + { + "transactionId": 100002, + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T12:02:31.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "StatusNotification", + { + "connectorId": 1, + "status": "Charging", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T12:02:31.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + {} + ] + }, + { + "timestamp": "2024-01-15T12:07:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T12:07:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T12:07:30.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T12:15:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-007", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100002, + "meterValue": [ + { + "timestamp": "2024-01-15T12:15:00.000Z", + "sampledValue": [ + { + "value": "3500", + "measurand": "Energy.Active.Import.Register", + "unit": "Wh" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T12:15:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-007", + {} + ] + }, + { + "timestamp": "2024-01-15T12:18:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-008", + "StatusNotification", + { + "connectorId": 1, + "status": "Faulted", + "errorCode": "ConnectorLockFailure", + "info": "Connector lock mechanism failure detected" + } + ] + }, + { + "timestamp": "2024-01-15T12:18:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-008", + {} + ] + }, + { + "timestamp": "2024-01-15T12:18:05.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-009", + "StopTransaction", + { + "transactionId": 100002, + "idTag": "SYNTHETIC-TAG-002", + "meterStop": 3500, + "timestamp": "2024-01-15T12:18:05.000Z", + "reason": "Other" + } + ] + }, + { + "timestamp": "2024-01-15T12:18:05.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-009", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/diagnostics-failure.json b/src/ocpp/conformance/fixtures/diagnostics-failure.json new file mode 100644 index 0000000..a4649af --- /dev/null +++ b/src/ocpp/conformance/fixtures/diagnostics-failure.json @@ -0,0 +1,104 @@ +{ + "traceId": "scenario-diagnostics-failure", + "metadata": { + "stationId": "CS-SYNTHETIC-010", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Station reports a diagnostics failure via DiagnosticsStatusNotification with status DiagnosisFailed." + }, + "events": [ + { + "timestamp": "2024-01-15T06:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-010", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T06:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T06:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T06:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T06:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T06:07:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T06:07:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T06:07:30.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T06:12:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "DiagnosticsStatusNotification", + { + "status": "DiagnosisFailed" + } + ] + }, + { + "timestamp": "2024-01-15T06:12:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + {} + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/failed-auth.json b/src/ocpp/conformance/fixtures/failed-auth.json new file mode 100644 index 0000000..3ca2add --- /dev/null +++ b/src/ocpp/conformance/fixtures/failed-auth.json @@ -0,0 +1,184 @@ +{ + "traceId": "fixture-failed-auth", + "metadata": { + "stationId": "CS-SYNTHETIC-002", + "ocppVersion": "1.6", + "source": "synthetic-fixture", + "description": "Failed authorization: station boots, connector prepares, idTag is rejected by CSMS. StartTransaction is not attempted. Expects FAILED_AUTHORIZATION failure." + }, + "events": [ + { + "timestamp": "2024-01-15T11:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-002", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T11:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T11:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T11:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T11:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T11:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "StatusNotification", + { + "connectorId": 1, + "status": "Preparing", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T11:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + {} + ] + }, + { + "timestamp": "2024-01-15T11:02:15.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-INVALID" + } + ] + }, + { + "timestamp": "2024-01-15T11:02:15.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + { + "idTagInfo": { + "status": "Invalid" + } + } + ] + }, + { + "timestamp": "2024-01-15T11:02:20.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-INVALID" + } + ] + }, + { + "timestamp": "2024-01-15T11:02:20.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + { + "idTagInfo": { + "status": "Invalid" + } + } + ] + }, + { + "timestamp": "2024-01-15T11:02:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-INVALID" + } + ] + }, + { + "timestamp": "2024-01-15T11:02:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + { + "idTagInfo": { + "status": "Invalid" + } + } + ] + }, + { + "timestamp": "2024-01-15T11:02:40.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-007", + "StatusNotification", + { + "connectorId": 1, + "status": "Faulted", + "errorCode": "OtherError", + "info": "Authorization failed after 3 attempts" + } + ] + }, + { + "timestamp": "2024-01-15T11:02:40.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-007", + {} + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/heartbeat-irregular.json b/src/ocpp/conformance/fixtures/heartbeat-irregular.json new file mode 100644 index 0000000..3dc952f --- /dev/null +++ b/src/ocpp/conformance/fixtures/heartbeat-irregular.json @@ -0,0 +1,102 @@ +{ + "traceId": "scenario-heartbeat-irregular", + "metadata": { + "stationId": "CS-SYNTHETIC-014", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Heartbeats sent at irregular intervals — 300s expected, 600s gap observed." + }, + "events": [ + { + "timestamp": "2024-01-15T12:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-014", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T12:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T12:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T12:05:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T12:05:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T12:05:00.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T12:15:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-2", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T12:15:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-2", + { + "currentTime": "2024-01-15T12:15:00.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T12:25:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-3", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T12:25:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-3", + { + "currentTime": "2024-01-15T12:25:00.500Z" + } + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/invalid-stop-reason.json b/src/ocpp/conformance/fixtures/invalid-stop-reason.json new file mode 100644 index 0000000..0129e2e --- /dev/null +++ b/src/ocpp/conformance/fixtures/invalid-stop-reason.json @@ -0,0 +1,247 @@ +{ + "traceId": "scenario-invalid-stop-reason", + "metadata": { + "stationId": "CS-SYNTHETIC-007", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "StopTransaction with reason \"PowerSurge\" — not a valid OCPP 1.6 stop reason code." + }, + "events": [ + { + "timestamp": "2024-01-15T20:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-007", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T20:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T20:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T20:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T20:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T20:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-007" + } + ] + }, + { + "timestamp": "2024-01-15T20:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + { + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2024-12-31T23:59:59.000Z" + } + } + ] + }, + { + "timestamp": "2024-01-15T20:02:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-007", + "meterStart": 0, + "timestamp": "2024-01-15T20:02:30.000Z" + } + ] + }, + { + "timestamp": "2024-01-15T20:02:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + { + "transactionId": 100007, + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T20:02:31.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "StatusNotification", + { + "connectorId": 1, + "status": "Charging", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T20:02:31.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + {} + ] + }, + { + "timestamp": "2024-01-15T20:07:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T20:07:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T20:07:30.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T20:15:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100007, + "meterValue": [ + { + "timestamp": "2024-01-15T20:15:00.000Z", + "sampledValue": [ + { + "value": "5000", + "measurand": "Energy.Active.Import.Register", + "unit": "Wh" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T20:15:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + {} + ] + }, + { + "timestamp": "2024-01-15T20:30:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-007", + "StopTransaction", + { + "transactionId": 100007, + "idTag": "SYNTHETIC-TAG-007", + "meterStop": 5000, + "timestamp": "2024-01-15T20:30:00.000Z", + "reason": "PowerSurge" + } + ] + }, + { + "timestamp": "2024-01-15T20:30:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-007", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T20:30:01.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-008", + "StatusNotification", + { + "connectorId": 1, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T20:30:01.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-008", + {} + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/meter-anomaly.json b/src/ocpp/conformance/fixtures/meter-anomaly.json new file mode 100644 index 0000000..146f18a --- /dev/null +++ b/src/ocpp/conformance/fixtures/meter-anomaly.json @@ -0,0 +1,203 @@ +{ + "traceId": "scenario-meter-anomaly", + "metadata": { + "stationId": "CS-SYNTHETIC-012", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Meter reading decreases from 5000 to 3000 during charging — non-monotonic anomaly." + }, + "events": [ + { + "timestamp": "2024-01-15T14:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-012", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T14:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T14:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T14:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-012" + } + ] + }, + { + "timestamp": "2024-01-15T14:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T14:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-012", + "meterStart": 0 + } + ] + }, + { + "timestamp": "2024-01-15T14:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + { + "idTagInfo": { + "status": "Accepted" + }, + "transactionId": 100012 + } + ] + }, + { + "timestamp": "2024-01-15T14:05:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100012, + "meterValue": [ + { + "sampledValue": [ + { + "value": "5000" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T14:05:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + {} + ] + }, + { + "timestamp": "2024-01-15T14:06:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100012, + "meterValue": [ + { + "sampledValue": [ + { + "value": "3000" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T14:06:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + {} + ] + }, + { + "timestamp": "2024-01-15T14:10:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T14:10:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + { + "currentTime": "2024-01-15T14:10:00.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T14:30:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-007", + "StopTransaction", + { + "transactionId": 100012, + "idTag": "SYNTHETIC-TAG-012", + "meterStop": 3000, + "reason": "EVDisconnected" + } + ] + }, + { + "timestamp": "2024-01-15T14:30:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-007", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/meter-value-gap.json b/src/ocpp/conformance/fixtures/meter-value-gap.json new file mode 100644 index 0000000..64bded6 --- /dev/null +++ b/src/ocpp/conformance/fixtures/meter-value-gap.json @@ -0,0 +1,213 @@ +{ + "traceId": "scenario-meter-value-gap", + "metadata": { + "stationId": "CS-SYNTHETIC-006", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Transaction completes without any MeterValues — metering data is missing." + }, + "events": [ + { + "timestamp": "2024-01-15T18:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-006", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T18:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T18:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T18:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T18:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T18:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-006" + } + ] + }, + { + "timestamp": "2024-01-15T18:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + { + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2024-12-31T23:59:59.000Z" + } + } + ] + }, + { + "timestamp": "2024-01-15T18:02:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-006", + "meterStart": 0, + "timestamp": "2024-01-15T18:02:30.000Z" + } + ] + }, + { + "timestamp": "2024-01-15T18:02:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + { + "transactionId": 100006, + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T18:02:31.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "StatusNotification", + { + "connectorId": 1, + "status": "Charging", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T18:02:31.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + {} + ] + }, + { + "timestamp": "2024-01-15T18:07:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T18:07:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T18:07:30.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T18:30:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "StopTransaction", + { + "transactionId": 100006, + "idTag": "SYNTHETIC-TAG-006", + "meterStop": 0, + "timestamp": "2024-01-15T18:30:00.000Z", + "reason": "EVDisconnected" + } + ] + }, + { + "timestamp": "2024-01-15T18:30:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T18:30:01.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-007", + "StatusNotification", + { + "connectorId": 1, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T18:30:01.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-007", + {} + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/normal-session.json b/src/ocpp/conformance/fixtures/normal-session.json new file mode 100644 index 0000000..f28edd6 --- /dev/null +++ b/src/ocpp/conformance/fixtures/normal-session.json @@ -0,0 +1,305 @@ +{ + "traceId": "fixture-normal-session", + "metadata": { + "stationId": "CS-SYNTHETIC-001", + "ocppVersion": "1.6", + "source": "synthetic-fixture", + "description": "Normal charging session: boot → authorize → start transaction → meter values → stop transaction. No failures expected." + }, + "events": [ + { + "timestamp": "2024-01-15T10:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-001", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T10:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T10:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T10:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T10:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T10:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "StatusNotification", + { + "connectorId": 1, + "status": "Preparing", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T10:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + {} + ] + }, + { + "timestamp": "2024-01-15T10:02:15.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-001" + } + ] + }, + { + "timestamp": "2024-01-15T10:02:15.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + { + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2024-12-31T23:59:59.000Z", + "parentIdTag": "SYNTHETIC-PARENT-001" + } + } + ] + }, + { + "timestamp": "2024-01-15T10:02:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-001", + "meterStart": 0, + "timestamp": "2024-01-15T10:02:30.000Z" + } + ] + }, + { + "timestamp": "2024-01-15T10:02:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + { + "transactionId": 100001, + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T10:02:31.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "StatusNotification", + { + "connectorId": 1, + "status": "Charging", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T10:02:31.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + {} + ] + }, + { + "timestamp": "2024-01-15T10:07:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T10:07:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T10:07:30.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T10:15:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-007", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100001, + "meterValue": [ + { + "timestamp": "2024-01-15T10:15:00.000Z", + "sampledValue": [ + { + "value": "5000", + "measurand": "Energy.Active.Import.Register", + "unit": "Wh" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T10:15:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-007", + {} + ] + }, + { + "timestamp": "2024-01-15T10:30:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-008", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100001, + "meterValue": [ + { + "timestamp": "2024-01-15T10:30:00.000Z", + "sampledValue": [ + { + "value": "10000", + "measurand": "Energy.Active.Import.Register", + "unit": "Wh" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T10:30:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-008", + {} + ] + }, + { + "timestamp": "2024-01-15T10:35:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-009", + "StopTransaction", + { + "transactionId": 100001, + "idTag": "SYNTHETIC-TAG-001", + "meterStop": 10000, + "timestamp": "2024-01-15T10:35:00.000Z", + "reason": "EVDisconnected" + } + ] + }, + { + "timestamp": "2024-01-15T10:35:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-009", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T10:35:01.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-010", + "StatusNotification", + { + "connectorId": 1, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T10:35:01.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-010", + {} + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/short-session.json b/src/ocpp/conformance/fixtures/short-session.json new file mode 100644 index 0000000..7ec2375 --- /dev/null +++ b/src/ocpp/conformance/fixtures/short-session.json @@ -0,0 +1,172 @@ +{ + "traceId": "scenario-short-session", + "metadata": { + "stationId": "CS-SYNTHETIC-013", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Transaction starts and stops within 10 seconds — suspiciously short." + }, + "events": [ + { + "timestamp": "2024-01-15T09:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-013", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T09:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T09:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T09:00:01.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-013" + } + ] + }, + { + "timestamp": "2024-01-15T09:00:01.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T09:00:02.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-013", + "meterStart": 0 + } + ] + }, + { + "timestamp": "2024-01-15T09:00:02.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + { + "idTagInfo": { + "status": "Accepted" + }, + "transactionId": 100013 + } + ] + }, + { + "timestamp": "2024-01-15T09:00:03.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100013, + "meterValue": [ + { + "sampledValue": [ + { + "value": "100" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T09:00:03.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + {} + ] + }, + { + "timestamp": "2024-01-15T09:00:04.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T09:00:04.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + { + "currentTime": "2024-01-15T09:00:04.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T09:00:10.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "StopTransaction", + { + "transactionId": 100013, + "idTag": "SYNTHETIC-TAG-013", + "meterStop": 100, + "reason": "EVDisconnected" + } + ] + }, + { + "timestamp": "2024-01-15T09:00:10.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/slow-csms-response.json b/src/ocpp/conformance/fixtures/slow-csms-response.json new file mode 100644 index 0000000..dafdda1 --- /dev/null +++ b/src/ocpp/conformance/fixtures/slow-csms-response.json @@ -0,0 +1,60 @@ +{ + "traceId": "scenario-slow-csms-response", + "metadata": { + "stationId": "CS-SYNTHETIC-011", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "CSMS responds slowly to BootNotification — 15s response gap." + }, + "events": [ + { + "timestamp": "2024-01-15T06:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-011", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T06:00:15.000Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T06:00:15.000Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T06:00:16.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T06:00:16.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + { + "currentTime": "2024-01-15T06:00:16.500Z" + } + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/station-offline.json b/src/ocpp/conformance/fixtures/station-offline.json new file mode 100644 index 0000000..923b193 --- /dev/null +++ b/src/ocpp/conformance/fixtures/station-offline.json @@ -0,0 +1,195 @@ +{ + "traceId": "scenario-station-offline", + "metadata": { + "stationId": "CS-SYNTHETIC-004", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Station offline during active session — StartTransaction with no StopTransaction." + }, + "events": [ + { + "timestamp": "2024-01-15T14:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-004", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T14:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T14:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T14:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T14:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T14:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-004" + } + ] + }, + { + "timestamp": "2024-01-15T14:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + { + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2024-12-31T23:59:59.000Z" + } + } + ] + }, + { + "timestamp": "2024-01-15T14:02:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-004", + "meterStart": 0, + "timestamp": "2024-01-15T14:02:30.000Z" + } + ] + }, + { + "timestamp": "2024-01-15T14:02:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + { + "transactionId": 100004, + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T14:02:31.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "StatusNotification", + { + "connectorId": 1, + "status": "Charging", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T14:02:31.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + {} + ] + }, + { + "timestamp": "2024-01-15T14:07:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T14:07:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T14:07:30.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T14:15:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100004, + "meterValue": [ + { + "timestamp": "2024-01-15T14:15:00.000Z", + "sampledValue": [ + { + "value": "2000", + "measurand": "Energy.Active.Import.Register", + "unit": "Wh" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T14:15:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + {} + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/status-transition-violation.json b/src/ocpp/conformance/fixtures/status-transition-violation.json new file mode 100644 index 0000000..c301735 --- /dev/null +++ b/src/ocpp/conformance/fixtures/status-transition-violation.json @@ -0,0 +1,129 @@ +{ + "traceId": "scenario-status-transition-violation", + "metadata": { + "stationId": "CS-SYNTHETIC-009", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Connector status jumps from Available to Finishing without going through Preparing/Charging — invalid OCPP 1.6 transition." + }, + "events": [ + { + "timestamp": "2024-01-15T08:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-009", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T08:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T08:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T08:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T08:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T08:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "StatusNotification", + { + "connectorId": 1, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T08:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + {} + ] + }, + { + "timestamp": "2024-01-15T08:07:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T08:07:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T08:07:30.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T08:12:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "StatusNotification", + { + "connectorId": 1, + "status": "Finishing", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T08:12:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + {} + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/unexpected-start.json b/src/ocpp/conformance/fixtures/unexpected-start.json new file mode 100644 index 0000000..59c0e5e --- /dev/null +++ b/src/ocpp/conformance/fixtures/unexpected-start.json @@ -0,0 +1,126 @@ +{ + "traceId": "scenario-unexpected-start", + "metadata": { + "stationId": "CS-SYNTHETIC-008", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Station sends StartTransaction without BootNotification or Authorize — transaction started without proper initialization." + }, + "events": [ + { + "timestamp": "2024-01-15T22:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-008", + "meterStart": 0, + "timestamp": "2024-01-15T22:00:00.000Z" + } + ] + }, + { + "timestamp": "2024-01-15T22:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "transactionId": 100008, + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T22:00:01.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 1, + "status": "Charging", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T22:00:01.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T22:00:10.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100008, + "meterValue": [ + { + "timestamp": "2024-01-15T22:00:10.000Z", + "sampledValue": [ + { + "value": "100", + "measurand": "Energy.Active.Import.Register", + "unit": "Wh" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T22:00:10.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + {} + ] + }, + { + "timestamp": "2024-01-15T22:01:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "StopTransaction", + { + "transactionId": 100008, + "idTag": "SYNTHETIC-TAG-008", + "meterStop": 100, + "timestamp": "2024-01-15T22:01:30.000Z", + "reason": "EVDisconnected" + } + ] + }, + { + "timestamp": "2024-01-15T22:01:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/unexpected-stop-reason.json b/src/ocpp/conformance/fixtures/unexpected-stop-reason.json new file mode 100644 index 0000000..b953363 --- /dev/null +++ b/src/ocpp/conformance/fixtures/unexpected-stop-reason.json @@ -0,0 +1,247 @@ +{ + "traceId": "scenario-unexpected-stop-reason", + "metadata": { + "stationId": "CS-SYNTHETIC-005", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "Stop transaction with unexpected stop reason — parser/timeline validation only." + }, + "events": [ + { + "timestamp": "2024-01-15T16:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-005", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T16:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { + "currentTime": "2024-01-15T16:00:00.500Z", + "interval": 300, + "status": "Accepted" + } + ] + }, + { + "timestamp": "2024-01-15T16:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "StatusNotification", + { + "connectorId": 0, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T16:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + {} + ] + }, + { + "timestamp": "2024-01-15T16:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "Authorize", + { + "idTag": "SYNTHETIC-TAG-005" + } + ] + }, + { + "timestamp": "2024-01-15T16:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-003", + { + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2024-12-31T23:59:59.000Z" + } + } + ] + }, + { + "timestamp": "2024-01-15T16:02:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "StartTransaction", + { + "connectorId": 1, + "idTag": "SYNTHETIC-TAG-005", + "meterStart": 0, + "timestamp": "2024-01-15T16:02:30.000Z" + } + ] + }, + { + "timestamp": "2024-01-15T16:02:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-004", + { + "transactionId": 100005, + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T16:02:31.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-005", + "StatusNotification", + { + "connectorId": 1, + "status": "Charging", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T16:02:31.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-005", + {} + ] + }, + { + "timestamp": "2024-01-15T16:07:30.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-hb-1", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T16:07:30.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-hb-1", + { + "currentTime": "2024-01-15T16:07:30.500Z" + } + ] + }, + { + "timestamp": "2024-01-15T16:20:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "MeterValues", + { + "connectorId": 1, + "transactionId": 100005, + "meterValue": [ + { + "timestamp": "2024-01-15T16:20:00.000Z", + "sampledValue": [ + { + "value": "4000", + "measurand": "Energy.Active.Import.Register", + "unit": "Wh" + } + ] + } + ] + } + ] + }, + { + "timestamp": "2024-01-15T16:20:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-006", + {} + ] + }, + { + "timestamp": "2024-01-15T16:25:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-007", + "StopTransaction", + { + "transactionId": 100005, + "idTag": "SYNTHETIC-TAG-005", + "meterStop": 4000, + "timestamp": "2024-01-15T16:25:00.000Z", + "reason": "Other" + } + ] + }, + { + "timestamp": "2024-01-15T16:25:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-007", + { + "idTagInfo": { + "status": "Accepted" + } + } + ] + }, + { + "timestamp": "2024-01-15T16:25:01.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-008", + "StatusNotification", + { + "connectorId": 1, + "status": "Available", + "errorCode": "NoError" + } + ] + }, + { + "timestamp": "2024-01-15T16:25:01.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-008", + {} + ] + } + ] +} diff --git a/src/ocpp/conformance/fixtures/unresponsive-csms.json b/src/ocpp/conformance/fixtures/unresponsive-csms.json new file mode 100644 index 0000000..3e34923 --- /dev/null +++ b/src/ocpp/conformance/fixtures/unresponsive-csms.json @@ -0,0 +1,47 @@ +{ + "traceId": "scenario-unresponsive-csms", + "metadata": { + "stationId": "CS-SYNTHETIC-015", + "ocppVersion": "1.6", + "source": "synthetic-scenario", + "description": "CSMS never responds to BootNotification — unresponsive CSMS." + }, + "events": [ + { + "timestamp": "2024-01-15T08:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "SyntheticVendor", + "chargePointModel": "SM-100", + "chargePointSerialNumber": "CS-SYNTHETIC-015", + "firmwareVersion": "1.0.0" + } + ] + }, + { + "timestamp": "2024-01-15T08:00:05.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-002", + "Heartbeat", + {} + ] + }, + { + "timestamp": "2024-01-15T08:00:05.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + { + "currentTime": "2024-01-15T08:00:05.500Z" + } + ] + } + ] +} diff --git a/src/ocpp/conformance/goldens/connector-fault.json b/src/ocpp/conformance/goldens/connector-fault.json new file mode 100644 index 0000000..0763085 --- /dev/null +++ b/src/ocpp/conformance/goldens/connector-fault.json @@ -0,0 +1 @@ +["CONNECTOR_FAULT"] diff --git a/src/ocpp/conformance/goldens/diagnostics-failure.json b/src/ocpp/conformance/goldens/diagnostics-failure.json new file mode 100644 index 0000000..e062642 --- /dev/null +++ b/src/ocpp/conformance/goldens/diagnostics-failure.json @@ -0,0 +1 @@ +["DIAGNOSTICS_FAILURE"] diff --git a/src/ocpp/conformance/goldens/failed-auth.json b/src/ocpp/conformance/goldens/failed-auth.json new file mode 100644 index 0000000..50b085c --- /dev/null +++ b/src/ocpp/conformance/goldens/failed-auth.json @@ -0,0 +1 @@ +["FAILED_AUTHORIZATION"] diff --git a/src/ocpp/conformance/goldens/heartbeat-irregular.json b/src/ocpp/conformance/goldens/heartbeat-irregular.json new file mode 100644 index 0000000..8908b7f --- /dev/null +++ b/src/ocpp/conformance/goldens/heartbeat-irregular.json @@ -0,0 +1 @@ +["HEARTBEAT_INTERVAL_VIOLATION"] diff --git a/src/ocpp/conformance/goldens/invalid-stop-reason.json b/src/ocpp/conformance/goldens/invalid-stop-reason.json new file mode 100644 index 0000000..67c3fac --- /dev/null +++ b/src/ocpp/conformance/goldens/invalid-stop-reason.json @@ -0,0 +1 @@ +["INVALID_STOP_REASON"] diff --git a/src/ocpp/conformance/goldens/meter-anomaly.json b/src/ocpp/conformance/goldens/meter-anomaly.json new file mode 100644 index 0000000..d95f1c1 --- /dev/null +++ b/src/ocpp/conformance/goldens/meter-anomaly.json @@ -0,0 +1 @@ +["METER_VALUE_ANOMALY"] diff --git a/src/ocpp/conformance/goldens/meter-value-gap.json b/src/ocpp/conformance/goldens/meter-value-gap.json new file mode 100644 index 0000000..29a3646 --- /dev/null +++ b/src/ocpp/conformance/goldens/meter-value-gap.json @@ -0,0 +1 @@ +["METER_VALUE_GAP"] diff --git a/src/ocpp/conformance/goldens/normal-session.json b/src/ocpp/conformance/goldens/normal-session.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/ocpp/conformance/goldens/normal-session.json @@ -0,0 +1 @@ +[] diff --git a/src/ocpp/conformance/goldens/short-session.json b/src/ocpp/conformance/goldens/short-session.json new file mode 100644 index 0000000..419a57e --- /dev/null +++ b/src/ocpp/conformance/goldens/short-session.json @@ -0,0 +1 @@ +["SUSPICIOUS_SESSION_DURATION"] diff --git a/src/ocpp/conformance/goldens/slow-csms-response.json b/src/ocpp/conformance/goldens/slow-csms-response.json new file mode 100644 index 0000000..81f4181 --- /dev/null +++ b/src/ocpp/conformance/goldens/slow-csms-response.json @@ -0,0 +1 @@ +["SLOW_RESPONSE"] diff --git a/src/ocpp/conformance/goldens/station-offline.json b/src/ocpp/conformance/goldens/station-offline.json new file mode 100644 index 0000000..35efa2d --- /dev/null +++ b/src/ocpp/conformance/goldens/station-offline.json @@ -0,0 +1 @@ +["STATION_OFFLINE_DURING_SESSION"] diff --git a/src/ocpp/conformance/goldens/status-transition-violation.json b/src/ocpp/conformance/goldens/status-transition-violation.json new file mode 100644 index 0000000..c1ffad5 --- /dev/null +++ b/src/ocpp/conformance/goldens/status-transition-violation.json @@ -0,0 +1 @@ +["STATUS_TRANSITION_VIOLATION"] diff --git a/src/ocpp/conformance/goldens/unexpected-start.json b/src/ocpp/conformance/goldens/unexpected-start.json new file mode 100644 index 0000000..23771a5 --- /dev/null +++ b/src/ocpp/conformance/goldens/unexpected-start.json @@ -0,0 +1 @@ +["UNEXPECTED_START"] diff --git a/src/ocpp/conformance/goldens/unexpected-stop-reason.json b/src/ocpp/conformance/goldens/unexpected-stop-reason.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/ocpp/conformance/goldens/unexpected-stop-reason.json @@ -0,0 +1 @@ +[] diff --git a/src/ocpp/conformance/goldens/unresponsive-csms.json b/src/ocpp/conformance/goldens/unresponsive-csms.json new file mode 100644 index 0000000..ab6935f --- /dev/null +++ b/src/ocpp/conformance/goldens/unresponsive-csms.json @@ -0,0 +1 @@ +["UNRESPONSIVE_CSMS"] diff --git a/src/ocpp/conformance/harness.zig b/src/ocpp/conformance/harness.zig new file mode 100644 index 0000000..9e0b667 --- /dev/null +++ b/src/ocpp/conformance/harness.zig @@ -0,0 +1,104 @@ +//! Conformance harness — pins Studio's detection to the toolkit's. +//! +//! For each of the 15 shared scenarios, the vendored **trace** runs through the +//! full engine (`parseTrace → buildSessionTimeline → detectFailures`) and its +//! de-duplicated, sorted `FailureCode` set is compared to the locked **golden** +//! — the same comparison semantics as the toolkit's `evaluateScenario`. +//! +//! Fixtures and goldens under `fixtures/` and `goldens/` are generated from the +//! toolkit (the source of truth), tagged `contract-v1`; they are not authored by +//! hand. See `README.md` in this directory. This layout lives under `src/` so the +//! zero-config build can `@embedFile` it (ADR-0004). + +const std = @import("std"); +const parser = @import("../parser.zig"); +const timeline = @import("../timeline.zig"); +const detection = @import("../detection.zig"); + +/// The 15 shared scenarios (the toolkit's `scenarioNames`). +const scenario_names = [_][]const u8{ + "normal-session", + "failed-auth", + "connector-fault", + "station-offline", + "unexpected-stop-reason", + "meter-value-gap", + "invalid-stop-reason", + "unexpected-start", + "status-transition-violation", + "diagnostics-failure", + "slow-csms-response", + "meter-anomaly", + "short-session", + "heartbeat-irregular", + "unresponsive-csms", +}; + +test "conformance: detected failure codes match the locked goldens" { + var any_failed = false; + inline for (scenario_names) |name| { + runScenario( + name, + @embedFile("fixtures/" ++ name ++ ".json"), + @embedFile("goldens/" ++ name ++ ".json"), + ) catch { + any_failed = true; + }; + } + try std.testing.expect(!any_failed); +} + +fn runScenario(name: []const u8, trace_json: []const u8, golden_json: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const a = arena.allocator(); + + // Full engine pipeline over the vendored trace. + const parsed = try parser.parseTrace(a, trace_json); + const sessions = try timeline.buildSessionTimeline(a, parsed.events); + const failures = try detection.detectFailures(a, parsed.events, sessions); + + // De-duplicate + sort the detected failure codes (evaluateScenario semantics). + var seen = std.StringHashMap(void).init(a); + var detected: std.ArrayList([]const u8) = .empty; + for (failures) |f| { + const code = f.code.toWire(); + const gop = try seen.getOrPut(code); + if (!gop.found_existing) try detected.append(a, code); + } + std.mem.sort([]const u8, detected.items, {}, lessStr); + + // The golden is a sorted JSON array of wire codes; sort again defensively. + const golden = try std.json.parseFromSliceLeaky([][]const u8, a, golden_json, .{ .allocate = .alloc_always }); + std.mem.sort([]const u8, golden, {}, lessStr); + + if (!equalCodes(golden, detected.items)) { + std.debug.print("conformance mismatch [{s}]\n expected: ", .{name}); + printCodes(golden); + std.debug.print("\n detected: ", .{}); + printCodes(detected.items); + std.debug.print("\n", .{}); + return error.ConformanceMismatch; + } +} + +fn equalCodes(a: []const []const u8, b: []const []const u8) bool { + if (a.len != b.len) return false; + for (a, b) |x, y| { + if (!std.mem.eql(u8, x, y)) return false; + } + return true; +} + +fn lessStr(_: void, a: []const u8, b: []const u8) bool { + return std.mem.order(u8, a, b) == .lt; +} + +fn printCodes(codes: []const []const u8) void { + std.debug.print("[", .{}); + for (codes, 0..) |c, i| { + if (i > 0) std.debug.print(", ", .{}); + std.debug.print("{s}", .{c}); + } + std.debug.print("]", .{}); +} diff --git a/src/ocpp/ocpp.zig b/src/ocpp/ocpp.zig index 37cf79b..5594399 100644 --- a/src/ocpp/ocpp.zig +++ b/src/ocpp/ocpp.zig @@ -16,4 +16,5 @@ test { _ = parser; _ = timeline; _ = detection; + _ = @import("conformance/harness.zig"); }