diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index 1135c94..ae72155 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -204,6 +204,7 @@ integration examples, and contributor onboarding. - ✅ Rich scenario assertions — 8 assertion types, `evaluateScenario()` (Issue #86) - ✅ Assert-based scenarios — 5 new scenarios (15 total) + `compareScenarioReports()` (Issue #87) - ✅ CLI: ci + anonymize + diff commands (Issue #88) +- ✅ Integration examples — 4 example projects (Issue #89) ## What's Next diff --git a/README.md b/README.md index 4a1a6df..ec1b9c7 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,14 @@ import { generateMarkdownReport } from '@ocpp-debugkit/toolkit/reporter'; See the [Migration Guide](./docs/migration.md) if you are upgrading from the old multi-package layout. +### Examples + +See the [`examples/`](./examples/) directory for integration examples: +- `simple-trace/` — Parse, detect, and report +- `simple-csms/` — CSMS validation mock +- `simulator-output/` — JSONL processing + HTML report +- `ci-example/` — GitHub Actions CI integration + ### Web App Visit [ocppdebugkit.com/inspector](https://ocppdebugkit.com/inspector) to diff --git a/eslint.config.js b/eslint.config.js index 1df37cb..5ed22e4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -24,6 +24,7 @@ export default tseslint.config( '**/coverage/**', '**/node_modules/**', '**/tests/external-fixture/**', + 'examples/**', ], }, ); diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..9396a2c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,23 @@ +# Examples + +Integration examples showing how to use `@ocpp-debugkit/toolkit` in your projects. + +## Available Examples + +| Directory | Description | +|-----------|-------------| +| [`simple-trace/`](./simple-trace/) | Parse a trace, detect failures, generate a Markdown report | +| [`simple-csms/`](./simple-csms/) | Validate incoming traces (CSMS mock), check for failures | +| [`simulator-output/`](./simulator-output/) | Process JSONL simulator output, generate HTML report | +| [`ci-example/`](./ci-example/) | Use `ocpp-debugkit ci` in GitHub Actions workflows | + +## Running + +Each example is a standalone project. `cd` into the directory and run: + +```bash +npm install +npm start +``` + +All trace data is fully synthetic — no real station identifiers or personal data. diff --git a/examples/ci-example/.github/workflows/ocpp-test.yml b/examples/ci-example/.github/workflows/ocpp-test.yml new file mode 100644 index 0000000..fa141d3 --- /dev/null +++ b/examples/ci-example/.github/workflows/ocpp-test.yml @@ -0,0 +1,23 @@ +name: OCPP Scenario Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + ocpp-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-node@v5 + with: + node-version: 24 + + - name: Install OCPP DebugKit CLI + run: npm install -g @ocpp-debugkit/toolkit + + - name: Run scenario tests + run: ocpp-debugkit ci --format json diff --git a/examples/ci-example/README.md b/examples/ci-example/README.md new file mode 100644 index 0000000..e4e6f14 --- /dev/null +++ b/examples/ci-example/README.md @@ -0,0 +1,39 @@ +# CI Integration Example + +Demonstrates how to use `ocpp-debugkit ci` in a GitHub Actions workflow to +run scenario tests as part of your CI pipeline. + +## Setup + +1. Install the CLI: + ```bash + npm install -g @ocpp-debugkit/toolkit + ``` + +2. Create scenario files in `scenarios/` (JSON files with the scenario format). + +3. Add the workflow file to `.github/workflows/`. + +## Workflow + +The included workflow (`.github/workflows/ocpp-test.yml`) runs: +```bash +ocpp-debugkit ci --format json +``` + +It exits 0 if all scenarios pass, 1 if any fail. The JSON output can be +parsed by CI tooling for dashboards or notifications. + +## Scenario File Format + +```json +{ + "name": "my-scenario", + "description": "Custom scenario for CI testing", + "trace": { ... }, + "expectedFailures": ["FAILED_AUTHORIZATION"], + "assertions": [ + { "type": "event_order", "params": { "actions": ["BootNotification", "Authorize"] } } + ] +} +``` diff --git a/examples/ci-example/scenarios/example-fail.json b/examples/ci-example/scenarios/example-fail.json new file mode 100644 index 0000000..1c6796c --- /dev/null +++ b/examples/ci-example/scenarios/example-fail.json @@ -0,0 +1,38 @@ +{ + "name": "example-fail", + "description": "Example scenario that detects a failed authorization.", + "trace": { + "traceId": "ci-example-fail", + "metadata": { + "stationId": "CS-CI-EXAMPLE-002", + "ocppVersion": "1.6", + "source": "synthetic-example" + }, + "events": [ + { + "timestamp": "2024-01-15T09:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [2, "m1", "BootNotification", { "chargePointSerialNumber": "CS-CI-002" }] + }, + { + "timestamp": "2024-01-15T09:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "m1", { "status": "Accepted", "interval": 300 }] + }, + { + "timestamp": "2024-01-15T09:00:01.000Z", + "direction": "CS_TO_CSMS", + "message": [2, "m2", "Authorize", { "idTag": "BAD-CI-TAG" }] + }, + { + "timestamp": "2024-01-15T09:00:01.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "m2", { "idTagInfo": { "status": "Invalid" } }] + } + ] + }, + "expectedFailures": ["FAILED_AUTHORIZATION"], + "assertions": [ + { "type": "failure_count", "params": { "code": "FAILED_AUTHORIZATION", "min": 1 } } + ] +} diff --git a/examples/ci-example/scenarios/example-pass.json b/examples/ci-example/scenarios/example-pass.json new file mode 100644 index 0000000..90d833e --- /dev/null +++ b/examples/ci-example/scenarios/example-pass.json @@ -0,0 +1,39 @@ +{ + "name": "example-pass", + "description": "Example scenario that should pass in CI.", + "trace": { + "traceId": "ci-example-pass", + "metadata": { + "stationId": "CS-CI-EXAMPLE-001", + "ocppVersion": "1.6", + "source": "synthetic-example" + }, + "events": [ + { + "timestamp": "2024-01-15T08:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [2, "m1", "BootNotification", { "chargePointSerialNumber": "CS-CI-001" }] + }, + { + "timestamp": "2024-01-15T08:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "m1", { "status": "Accepted", "interval": 300 }] + }, + { + "timestamp": "2024-01-15T08:00:01.000Z", + "direction": "CS_TO_CSMS", + "message": [2, "m2", "Heartbeat", {}] + }, + { + "timestamp": "2024-01-15T08:00:01.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "m2", { "currentTime": "2024-01-15T08:00:01.500Z" }] + } + ] + }, + "expectedFailures": [], + "assertions": [ + { "type": "event_order", "params": { "actions": ["BootNotification", "Heartbeat"] } }, + { "type": "no_failures", "params": {} } + ] +} diff --git a/examples/simple-csms/README.md b/examples/simple-csms/README.md new file mode 100644 index 0000000..8f55c54 --- /dev/null +++ b/examples/simple-csms/README.md @@ -0,0 +1,18 @@ +# Simple CSMS Example + +Demonstrates how a CSMS mock could use @ocpp-debugkit/toolkit to validate +incoming traces and check for failures. + +## Run + +```bash +npm install +npm start +``` + +## What it does + +1. Parses a trace with validation issues +2. Validates each OCPP message structurally +3. Detects failures +4. Reports validation errors and detected failures diff --git a/examples/simple-csms/index.js b/examples/simple-csms/index.js new file mode 100644 index 0000000..d74203c --- /dev/null +++ b/examples/simple-csms/index.js @@ -0,0 +1,31 @@ +import { + parseTrace, + validateMessages, + detectFailures, + buildSessionTimeline, +} from '@ocpp-debugkit/toolkit/core'; +import { readFileSync } from 'node:fs'; + +const content = readFileSync('./sample-trace.json', 'utf8'); +const { events } = parseTrace(content); + +// Validate each message +const validations = validateMessages(events); +let invalidCount = 0; +for (let i = 0; i < events.length; i++) { + const v = validations[i]; + if (v && !v.valid) { + invalidCount++; + console.log(`Event ${i}: INVALID - ${v.errors.join(', ')}`); + } +} +console.log(`\n${events.length - invalidCount}/${events.length} messages valid`); + +// Detect failures +const sessions = buildSessionTimeline(events); +const failures = detectFailures(events, sessions); +console.log(`\nDetected ${failures.length} failure(s):`); +for (const f of failures) { + console.log(` [${f.severity.toUpperCase()}] ${f.code}`); + console.log(` ${f.description}`); +} diff --git a/examples/simple-csms/package.json b/examples/simple-csms/package.json new file mode 100644 index 0000000..76b6315 --- /dev/null +++ b/examples/simple-csms/package.json @@ -0,0 +1,12 @@ +{ + "name": "ocpp-debugkit-simple-csms-example", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@ocpp-debugkit/toolkit": "^0.2.1" + } +} diff --git a/examples/simple-csms/sample-trace.json b/examples/simple-csms/sample-trace.json new file mode 100644 index 0000000..04ca03e --- /dev/null +++ b/examples/simple-csms/sample-trace.json @@ -0,0 +1,43 @@ +{ + "traceId": "example-csms-validation", + "metadata": { + "stationId": "CS-EXAMPLE-002", + "ocppVersion": "1.6", + "source": "synthetic-example" + }, + "events": [ + { + "timestamp": "2024-01-15T14:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "ExampleVendor", + "chargePointModel": "EX-100", + "chargePointSerialNumber": "CS-EXAMPLE-002" + } + ] + }, + { + "timestamp": "2024-01-15T14:00:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-001", + { "status": "Accepted", "interval": 300, "currentTime": "2024-01-15T14:00:00.500Z" } + ] + }, + { + "timestamp": "2024-01-15T14:01:00.000Z", + "direction": "CS_TO_CSMS", + "message": [2, "msg-002", "Authorize", { "idTag": "BAD-EXAMPLE-TAG" }] + }, + { + "timestamp": "2024-01-15T14:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "msg-002", { "idTagInfo": { "status": "Invalid" } }] + } + ] +} diff --git a/examples/simple-trace/README.md b/examples/simple-trace/README.md new file mode 100644 index 0000000..c3f19e9 --- /dev/null +++ b/examples/simple-trace/README.md @@ -0,0 +1,19 @@ +# Simple Trace Example + +Demonstrates basic usage of @ocpp-debugkit/toolkit: parse a trace, detect +failures, and generate a Markdown report. + +## Run + +```bash +npm install +npm start +``` + +## What it does + +1. Parses a synthetic OCPP trace file +2. Builds session timeline +3. Detects failures +4. Prints a summary to stdout +5. Generates a Markdown report diff --git a/examples/simple-trace/index.js b/examples/simple-trace/index.js new file mode 100644 index 0000000..9ca43d3 --- /dev/null +++ b/examples/simple-trace/index.js @@ -0,0 +1,38 @@ +import { + parseTrace, + buildSessionTimeline, + detectFailures, + summarizeSessions, +} from '@ocpp-debugkit/toolkit/core'; +import { generateMarkdownReport } from '@ocpp-debugkit/toolkit/reporter'; +import { readFileSync } from 'node:fs'; + +// Parse the trace +const content = readFileSync('./sample-trace.json', 'utf8'); +const { events, warnings } = parseTrace(content); + +console.log(`Parsed ${events.length} events (${warnings.length} warnings)`); + +// Build session timeline +const sessions = buildSessionTimeline(events); +console.log(`Found ${sessions.length} session(s)`); + +// Detect failures +const failures = detectFailures(events, sessions); +console.log(`Detected ${failures.length} failure(s)`); +for (const f of failures) { + console.log(` [${f.severity}] ${f.code}: ${f.description}`); +} + +// Generate summary +const summaries = summarizeSessions(sessions, failures); +for (const s of summaries) { + console.log( + ` Session ${s.sessionId}: ${s.status}, ${s.eventCount} events, ${s.failureCount} failures`, + ); +} + +// Generate Markdown report +const report = generateMarkdownReport({ events, sessions, failures, summaries, warnings }); +console.log('\n--- Markdown Report (first 500 chars) ---'); +console.log(report.slice(0, 500)); diff --git a/examples/simple-trace/package.json b/examples/simple-trace/package.json new file mode 100644 index 0000000..9c93a70 --- /dev/null +++ b/examples/simple-trace/package.json @@ -0,0 +1,12 @@ +{ + "name": "ocpp-debugkit-simple-trace-example", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@ocpp-debugkit/toolkit": "^0.2.1" + } +} diff --git a/examples/simple-trace/sample-trace.json b/examples/simple-trace/sample-trace.json new file mode 100644 index 0000000..0db3dc3 --- /dev/null +++ b/examples/simple-trace/sample-trace.json @@ -0,0 +1,119 @@ +{ + "traceId": "example-simple-trace", + "metadata": { + "stationId": "CS-EXAMPLE-001", + "ocppVersion": "1.6", + "source": "synthetic-example", + "description": "Synthetic trace for the simple-trace example." + }, + "events": [ + { + "timestamp": "2024-01-15T10:00:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-001", + "BootNotification", + { + "chargePointVendor": "ExampleVendor", + "chargePointModel": "EX-100", + "chargePointSerialNumber": "CS-EXAMPLE-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", "Authorize", { "idTag": "EXAMPLE-TAG-001" }] + }, + { + "timestamp": "2024-01-15T10:01:00.500Z", + "direction": "CSMS_TO_CS", + "message": [ + 3, + "msg-002", + { "idTagInfo": { "status": "Accepted", "expiryDate": "2024-12-31T23:59:59.000Z" } } + ] + }, + { + "timestamp": "2024-01-15T10:02:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-003", + "StartTransaction", + { + "connectorId": 1, + "idTag": "EXAMPLE-TAG-001", + "meterStart": 0, + "timestamp": "2024-01-15T10:02:00.000Z" + } + ] + }, + { + "timestamp": "2024-01-15T10:02:00.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "msg-003", { "transactionId": 200001, "idTagInfo": { "status": "Accepted" } }] + }, + { + "timestamp": "2024-01-15T10:03:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-004", + "MeterValues", + { + "connectorId": 1, + "transactionId": 200001, + "meterValue": [{ "sampledValue": [{ "value": "5000" }] }] + } + ] + }, + { + "timestamp": "2024-01-15T10:03:00.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "msg-004", {}] + }, + { + "timestamp": "2024-01-15T10:05:00.000Z", + "direction": "CS_TO_CSMS", + "message": [2, "msg-005", "Heartbeat", {}] + }, + { + "timestamp": "2024-01-15T10:05:00.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "msg-005", { "currentTime": "2024-01-15T10:05:00.500Z" }] + }, + { + "timestamp": "2024-01-15T10:30:00.000Z", + "direction": "CS_TO_CSMS", + "message": [ + 2, + "msg-006", + "StopTransaction", + { + "transactionId": 200001, + "idTag": "EXAMPLE-TAG-001", + "meterStop": 5000, + "timestamp": "2024-01-15T10:30:00.000Z", + "reason": "EVDisconnected" + } + ] + }, + { + "timestamp": "2024-01-15T10:30:00.500Z", + "direction": "CSMS_TO_CS", + "message": [3, "msg-006", { "idTagInfo": { "status": "Accepted" } }] + } + ] +} diff --git a/examples/simulator-output/README.md b/examples/simulator-output/README.md new file mode 100644 index 0000000..62ef07c --- /dev/null +++ b/examples/simulator-output/README.md @@ -0,0 +1,18 @@ +# Simulator Output Example + +Demonstrates processing OCPP simulator output in JSONL format: parse a JSONL +trace, detect failures, and generate an HTML report. + +## Run + +```bash +npm install +npm start +``` + +## What it does + +1. Parses a JSONL trace (one event per line) +2. Detects failures +3. Generates a self-contained HTML report +4. Writes the report to `report.html` diff --git a/examples/simulator-output/index.js b/examples/simulator-output/index.js new file mode 100644 index 0000000..bed66ac --- /dev/null +++ b/examples/simulator-output/index.js @@ -0,0 +1,26 @@ +import { + parseTrace, + detectFailures, + buildSessionTimeline, + summarizeSessions, +} from '@ocpp-debugkit/toolkit/core'; +import { generateHtmlReport } from '@ocpp-debugkit/toolkit/reporter'; +import { readFileSync, writeFileSync } from 'node:fs'; + +// Read JSONL trace (one event per line) +const content = readFileSync('./sample-trace.jsonl', 'utf8'); +const { events, warnings } = parseTrace(content); + +console.log(`Parsed ${events.length} events from JSONL (${warnings.length} warnings)`); + +// Detect failures +const sessions = buildSessionTimeline(events); +const failures = detectFailures(events, sessions); +console.log(`Detected ${failures.length} failure(s)`); + +// Generate HTML report +const summaries = summarizeSessions(sessions, failures); +const html = generateHtmlReport({ events, sessions, failures, summaries, warnings }); + +writeFileSync('./report.html', html, 'utf8'); +console.log('HTML report written to report.html'); diff --git a/examples/simulator-output/package.json b/examples/simulator-output/package.json new file mode 100644 index 0000000..7d77c2d --- /dev/null +++ b/examples/simulator-output/package.json @@ -0,0 +1,12 @@ +{ + "name": "ocpp-debugkit-simulator-output-example", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "@ocpp-debugkit/toolkit": "^0.2.1" + } +} diff --git a/examples/simulator-output/sample-trace.jsonl b/examples/simulator-output/sample-trace.jsonl new file mode 100644 index 0000000..9348736 --- /dev/null +++ b/examples/simulator-output/sample-trace.jsonl @@ -0,0 +1,14 @@ +{"timestamp":"2024-01-15T06:00:00.000Z","direction":"CS_TO_CSMS","message":[2,"sim-001","BootNotification",{"chargePointVendor":"SimVendor","chargePointModel":"SIM-200","chargePointSerialNumber":"CS-SIM-001"}]} +{"timestamp":"2024-01-15T06:00:00.500Z","direction":"CSMS_TO_CS","message":[3,"sim-001",{"status":"Accepted","interval":60,"currentTime":"2024-01-15T06:00:00.500Z"}]} +{"timestamp":"2024-01-15T06:01:00.000Z","direction":"CS_TO_CSMS","message":[2,"sim-002","Heartbeat",{}]} +{"timestamp":"2024-01-15T06:01:00.500Z","direction":"CSMS_TO_CS","message":[3,"sim-002",{"currentTime":"2024-01-15T06:01:00.500Z"}]} +{"timestamp":"2024-01-15T06:02:00.000Z","direction":"CS_TO_CSMS","message":[2,"sim-003","Authorize",{"idTag":"SIM-TAG-001"}]} +{"timestamp":"2024-01-15T06:02:00.500Z","direction":"CSMS_TO_CS","message":[3,"sim-003",{"idTagInfo":{"status":"Accepted"}}]} +{"timestamp":"2024-01-15T06:03:00.000Z","direction":"CS_TO_CSMS","message":[2,"sim-004","StartTransaction",{"connectorId":1,"idTag":"SIM-TAG-001","meterStart":0}]} +{"timestamp":"2024-01-15T06:03:00.500Z","direction":"CSMS_TO_CS","message":[3,"sim-004",{"idTagInfo":{"status":"Accepted"},"transactionId":300001}]} +{"timestamp":"2024-01-15T06:05:00.000Z","direction":"CS_TO_CSMS","message":[2,"sim-005","MeterValues",{"connectorId":1,"transactionId":300001,"meterValue":[{"sampledValue":[{"value":"2500"}]}]}]} +{"timestamp":"2024-01-15T06:05:00.500Z","direction":"CSMS_TO_CS","message":[3,"sim-005",{}]} +{"timestamp":"2024-01-15T06:06:00.000Z","direction":"CS_TO_CSMS","message":[2,"sim-006","Heartbeat",{}]} +{"timestamp":"2024-01-15T06:06:00.500Z","direction":"CSMS_TO_CS","message":[3,"sim-006",{"currentTime":"2024-01-15T06:06:00.500Z"}]} +{"timestamp":"2024-01-15T06:20:00.000Z","direction":"CS_TO_CSMS","message":[2,"sim-007","StopTransaction",{"transactionId":300001,"idTag":"SIM-TAG-001","meterStop":2500,"reason":"EVDisconnected"}]} +{"timestamp":"2024-01-15T06:20:00.500Z","direction":"CSMS_TO_CS","message":[3,"sim-007",{"idTagInfo":{"status":"Accepted"}}]}