]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
test(ocpp-server): add OCPP 1.6 mock server and extend reservation coverage (#2047)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Mon, 27 Jul 2026 12:53:40 +0000 (14:53 +0200)
committerGitHub <noreply@github.com>
Mon, 27 Jul 2026 12:53:40 +0000 (14:53 +0200)
commit82ae6d1a78470403022fbb31dac35dfd46e91c85
tree6b54417cb6d137340847a4f184bbf7a6b334b6c5
parentdb46f9f59757a0c5ca41a723208f0dac3fec0adb
test(ocpp-server): add OCPP 1.6 mock server and extend reservation coverage (#2047)

* test(ocpp-server): add OCPP 1.6 mock server and extend reservation coverage

- Add server16.py: standalone OCPP 1.6 mock server mirroring server.py
  architecture (AuthConfig, ServerConfig, ChargePoint, on_connect, main)
- Add test_server16.py: 59 tests covering handlers, outgoing commands,
  signed meter values, timer, on_connect and main (~96% branch coverage)
- Add _send_reserve_now/_send_cancel_reservation to server.py (OCPP 2.0.1)
  with matching ServerConfig fields and DEFAULT_RESERVATION_* constants
- Extend test_server.py: tests for ReserveNow/CancelReservation commands,
  entries in EXPECTED_OUTGOING_COMMANDS and FAILURE_PATH_CASES, strengthen
  timestamp assertions (fromisoformat + tzinfo check)
- Update pyproject.toml: server16 run task, typecheck and coverage source
- Add tools/http_broadcast_timing.py and tools/reused_ws_request_id.py:
  ad-hoc diagnostic scripts for UI-server fixes #2037 and #2033

* chore(serena): update project knowledge base

* revert: remove tools/ from tracked files (kept locally)

* fix(ocpp-server): address review findings M1-M3 and m1-m8

M1 - test_boot_status_sequence_parsed: reuse _patch_main (add
**args_overrides), capture ServerConfig via side_effect, assert
boot_sequence == (pending, accepted)

M2 - DRY: extract check_positive_number to _common.py; update
typecheck task, coverage source, and mypy override to include it

M3 - README: rename to 'OCPP Mock Servers', add Running the Servers
section, add full OCPP 1.6 server documentation (20 CLI flags,
supported commands, transaction tracking)

m1 - remove dead DEFAULT_VENDOR_ID constant (server16.py)

m2 - align --reservation-id flag (server.py: --reserve-id ->
--reservation-id) and fix coupled Namespace in test_server.py

m3 - _call_and_log: log response.status on failure (server.py)

m4 - pyproject.toml: add test_server16 to mypy disallow_untyped_defs
override

m5 - add type hints to on_start_transaction, on_stop_transaction,
on_status_notification handlers (server16.py)

m6 - add test_windows_handler_schedules_via_call_soon_threadsafe
(test_server16.py)

m7 - document intentional 1.6 command scope in _COMMAND_HANDLERS

m8 - remove dead AuthMode.offline member (server.py)

INFO - boot_index: clarify shared-across-all-stations behavior
INFO - magic number 128: add Unix convention comment
INFO - argparse: 'OCPP2 Server' -> 'OCPP 2.0.1 Server' (server.py)
INFO - _parse_commands: raw_entry/entry variable naming (server.py)

Coverage tests: test_custom_auth_config, token vide x2,
test_meter_values_with_signed_meter_value (test_server16.py)

Quality gates: 335 passed, format/lint/typecheck clean, 94.78%

* fix(ocpp-server): address post-review findings N1-N6

N1/M2 - server16.py: import check_positive_number from _common, remove
local duplicate (567-576); _common.py now has a single consumer

N1/m1 - server16.py: remove dead DEFAULT_VENDOR_ID constant

N1/m5 - server16.py: add type hints to on_start_transaction
(connector_id, meter_start: int; id_tag: str), on_stop_transaction
(meter_stop, transaction_id: int), on_status_notification
(connector_id: int; error_code, status: str)

N1/m7 - server16.py: document intentional subset scope above
_COMMAND_HANDLERS with runtime behavior note

N1/INFO - server16.py: add Unix 128+signal comment before sys.exit;
expand boot_index one-liner to full NOTE (shared across ALL stations,
not per-station)

N2 - test_server.py: extend _patch_main with **args_overrides (same
pattern as test_server16.py); add test_boot_status_sequence_parsed in
TestMainGracefulShutdown — covers server.py:1134-1149

N3 - test_server16.py: add test_boot_status_single_value — covers
server16.py:786-787 (elif --boot-status single value branch)

N4 - pyproject.toml: update description to cover both 1.6 and 2.0.1

N5 - README.md: remove duplicated sentence at line 38

N6 - test_server16.py: fix RuntimeWarning (coroutine never awaited)
in test_commands_sequence_scheduled_on_connect — mock_cp.send_commands
was AsyncMock, its coroutine was passed to mocked create_task unrun;
replace with MagicMock() to prevent the orphan coroutine

Quality gates: 337 passed (0 warnings), format/lint/typecheck clean,
coverage 95.95% (+1.17pp)

* test(charging-station): relax template file count assertion

Replace strictEqual(15) with ok(count >= 15) so adding new station
templates does not require updating the hardcoded count.

* fix(ocpp-server): address round-3 review findings

MAJOR-1 - README.md 2.0.1: add ReserveNow and CancelReservation to
Outgoing Commands list; add --reservation-id, --reserve-id-token,
--reserve-evse-id flags to Command-specific options; add example command

MAJOR-2a - test_server.py: add caplog assertions to 4 signed MeterValue
tests (started/updated/ended/meter_values) — verify _log_signed_meter_values
actually logs, not just that the handler returns correct type

MAJOR-2b - test_server.py: rewrite test_unsupported_command_logs_warning
with caplog + assert 'not supported' in log output

MAJOR-2c - test_server.py: align 7 empty-response handlers on equality
pattern (== EmptyResult()) on top of isinstance — matches test_server16

MINOR-1 - _common.py: add generic parse_commands(str, type[ActionT])
factored from the two identical _parse_commands; both servers now use
a one-line wrapper; remove import math from server16/server (now in
_common only); coverage 96.76% (+0.81pp)

MINOR-2 - server.py: add intentional-subset scope comment above
_COMMAND_HANDLERS (mirrors server16.py:451-453)

MINOR-3 - server.py:342: (kwargs.get('evse') or {}).get('id', 0)
prevents AttributeError when payload carries evse=null explicitly;
add regression test test_transaction_event_started_null_evse_defaults_to_zero

MINOR-4 - test_server.py: port 3 missing tests from test_server16:
  - test_unexpected_error_is_caught (covers server.py:782-783)
  - test_commands_sequence_scheduled_on_connect (covers server.py:866)
  - test_boot_status_single_value via main() (covers server.py:1151)

MINOR-5 - README.md 1.6: enumerate valid --trigger-message types
(StatusNotification, BootNotification, Heartbeat, MeterValues,
FirmwareStatusNotification, DiagnosticsStatusNotification,
LogStatusNotification, SignChargePointCertificate)

Quality gates: 341 passed (0 warnings), format/lint/typecheck clean,
coverage 96.76% (+0.81pp from 95.95%)

* fix(ocpp-server): address round-4 review findings

MAJOR-A - README.md 2.0.1: replace invalid --trigger-message value
SignCertificate with the 11 exact MessageTriggerEnumType members
(SignChargingStationCertificate/SignV2GCertificate/SignCombinedCertificate
+ TransactionEvent + PublishFirmwareStatusNotification); the 1.6 list was
fixed in round-3, this is its 2.0.1 counterpart

MAJOR-B - test_server.py: add TestLogSignedMeterValues (3 tests) adapted
to 2.0.1 semantics (signed_meter_value field, not format=SignedData);
covers the previously-uncovered unsigned branch server.py:97->95

MINOR-C - test_server.py + test_server16.py: 6 error-handling tests
(timeout/ocpp_error/unexpected) upgraded from swallowing-only to caplog
assertions on level ERROR + message substring

MINOR-D - DRY: extract negotiate_subprotocol + install_signal_handlers_and_wait
to _common.py (version-agnostic lifecycle, logger passed explicitly to
preserve observability); both servers use them; remove import signal from
both servers; retarget 4 signal.signal patches to _common. Instance methods
(_call_and_log/_send_command/...) and DEFAULT_RESERVATION_* constants left
in place (OCPP coupling / locality cost > DRY gain for a test mock)

MINOR-E - _common.py to 100%: add non-numeric-delay and empty-entry tests
for _parse_commands in both suites

INFO MORT-1 - remove unreachable dead code (if not boot_sequence) in both
servers' main()

INFO-D - server.py: handle_connection_closed(self) -> None annotation

Quality gates: 348 passed (0 warnings), format/lint/typecheck clean,
coverage 97.51% (+0.75pp); _common.py 100%

* test(ocpp-server): close round-5 parity + config findings

MINOR parity - test_server.py: assert len(config.charge_points) == 0 on
both on_connect rejection tests (missing subprotocol / protocol mismatch),
mirroring test_server16.py; guards against a regression registering a
ChargePoint on a rejected connection in 2.0.1

MINOR parity - test_server.py: strengthen Heartbeat test_returns_current_time
with a freshness window (delta < 60s) + import timezone, mirroring the 1.6
suite (was isinstance + tzinfo only)

INFO I4 - pyproject.toml: mypy python_version 3.14 -> 3.12 to match the
requires-python floor (>=3.12); mypy stays clean (verified live)

INFO I5 - README.md: add the --boot-status shorthand sentence to the 1.6
section for parity with 2.0.1 (identical behavior)

Deliberately NOT changed (accepted debt, evidence-based):
- DRY mixin for send_command/send_commands/handle_connection_closed: moving
  them to _common would emit lifecycle logs under _common instead of
  server/server16 (silent observability regression) for ~24 lines in a mock
- mypy test override kept: real guard (test files carry untyped defs)
- docstrings on _common helpers: 0/4 documented, keeping style consistent

Quality gates: 348 passed, format/lint/typecheck clean (python 3.12),
coverage 97.51%

* test(ocpp-server): close round-6 parity + hygiene findings

MINOR-1 - test_server16.py: add test_parent_id_tag_absent_by_default,
mirroring 2.0.1 test_authorize_no_enrichment_by_default; asserts
parent_id_tag is absent from id_tag_info under the default AuthConfig
(closes the last inter-suite rigor asymmetry)

MINOR-2 - server.py: harmonize _call_and_log docstring with server16.py
(both now '...based on its status')

MINOR-3 - test_server.py: add test_parse_set_variable_specs_skips_empty_entries
and test_parse_get_variable_specs_invalid_no_dot, covering the two
previously-uncovered branches of _parse_variable_specs (empty-entry skip,
require_value=False missing-dot); server.py 875/885 now covered

I1 - server.py + server16.py: derive --auth-mode choices/default from the
local AuthMode StrEnum ([mode.value for mode in AuthMode] / AuthMode.normal.value)
instead of string literals (AGENTS.md: avoid string literals when an
enumeration exists); order/content identical (verified live), no behavior change

Quality gates: 351 passed, format/lint/typecheck clean (python 3.12),
coverage 97.88% (server.py 98%)

* test(ocpp-server): close round-7 style + DRY findings

- test_server.py: fix stale docstring (20 -> 24 _send_* methods)
- server.py/server16.py: --auth-mode type=str -> type=AuthMode to match
  the sibling enum-typed CLI args (--boot-status, --reset-type, ...); keep
  choices for unchanged help/UX
- server.py/server16.py: extract DEFAULT_WHITELIST/DEFAULT_BLACKLIST module
  constants (single source of truth; removes __init__/argparse duplication)
- test_server.py/test_server16.py: TestHandlerCoverage now asserts @on
  registration (_on_action) so an undecorated handler is caught

* test(ocpp-server): close round-8 findings (harmonize auth-mode, DRY, handler routing)

- _common.py: host DEFAULT_WHITELIST/DEFAULT_BLACKLIST as the single source
  of truth; server.py and server16.py now import them (removes cross-file
  literal duplication)
- server.py/server16.py: drop now-dead choices=list(AuthMode) from --auth-mode
  and move valid values into the help text, matching the sibling enum-typed
  args (--boot-status, --reset-type, ...); uniform invalid-value error message
- server.py/server16.py: mode=args.auth_mode (args value is already an AuthMode
  via type=AuthMode; drop redundant re-wrap)
- test_server.py/test_server16.py: TestHandlerCoverage now asserts each handler
  is registered for the CORRECT OCPP action (_on_action == expected), not merely
  decorated with some @on

* test(ocpp-server): address bot review comments on server16

- test_server16.py: use supported CSMS->CS commands (TriggerMessage/Reset)
  instead of ClearCache/Heartbeat in the send_commands sequencing and
  _parse_commands tests, so the fixtures reflect commands actually in
  _COMMAND_HANDLERS rather than unsupported outgoing actions
- server.py/server16.py: replace the empty `except KeyboardInterrupt: pass`
  with contextlib.suppress(KeyboardInterrupt) (clears the empty-except finding
  without a comment; both servers for parity)
- server.py/server16.py: _resolve_auth_status returns default_status via an
  unconditional trailing return instead of a wildcard `case _`, making the
  control flow unambiguous to static analysis (both servers for parity)
.serena/project.yml
tests/charging-station/TemplateValidation.test.ts
tests/ocpp-server/README.md
tests/ocpp-server/_common.py [new file with mode: 0644]
tests/ocpp-server/pyproject.toml
tests/ocpp-server/server.py
tests/ocpp-server/server16.py [new file with mode: 0644]
tests/ocpp-server/test_server.py
tests/ocpp-server/test_server16.py [new file with mode: 0644]