From 0c66d62ab24f655709fe4c95b566394034b499b1 Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Tue, 28 Jul 2026 20:46:29 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20v2.17.0=20=E2=80=94=20Deprecation=20Swe?= =?UTF-8?q?ep=20(=C2=A7Phase=2016=20milestone=2061)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the seven satellite packages the Satellite Package Disposition table already calls DEPRECATE with no TC18 analog: canbr.hpp, linbr.hpp (CAN/LIN are native RC Server endpoint types as of v2.7.0, not bridge targets — the bridge direction these two assumed is inverted by the real protocol), federation.hpp (multi-HPC lease-based zone forwarding has no analog; the spec's real multi-client arbitration is the root-client/ discovery-stream-claim mechanism, a different architecture), proxy.hpp (TC18 has no proxy-node concept — a client addresses a server directly), zonegroup.hpp (the spec's real way to batch targets is packing several ACF messages with different byte_bus_ids into one AVTPDU, not fan-out to N zones), prioqueue.hpp (its Priority enum is removed; the spec's real priority concept is a fixed server-side execution-priority ordering across request kinds), and firmware.hpp (OTA has no analog and is left an application-layer concern). Each header's own dedicated test file is deleted alongside it, and tests/CMakeLists.txt is updated to match: canbr/ linbr are pulled out of the shared bridge-stub foreach loop (the seven ADAPTed bridges sharing that loop are untouched), and the five standalone CMake blocks for federation/proxy/zonegroup/prioqueue/firmware are removed. tests/test_config.cpp borrowed proxy.hpp purely as a generic empty rcp::Registry test double (legacy_mock::Registry always pre-populates all 5 zones) — no ProxyController-specific behavior was ever under test. It now defines its own minimal EmptyRegistry double locally instead, so the config::load coverage (and its REQ-CFG-* traceability) survives without reaching back into a deleted package; rcp/config.hpp itself, and its own still-open rebind onto the new manifest schema, are untouched. .fusa-reqs.json is pruned of the 44 requirement entries backing the deleted packages (REQ-CAN-001..004 and REQ-LIN-001..004 — canbr.hpp's and linbr.hpp's own req prefixes, distinct from the native endpoints' REQ- CANEP-*/REQ-LINEP-* — plus REQ-PROXY-*, REQ-FED-*, REQ-ZG-*, REQ-PQ-*, and REQ-FW-*), since cpfusa trace --req-coverage 100 treats any requirement id with no matching fusa:req/fusa:test source annotation as an uncovered gap. This is a narrow prune of exactly what this milestone's own deletions orphaned, not the fuller regeneration milestone #62 (Certification Refresh) still owns against the complete v2.x requirement set. version.hpp and CMakeLists.txt PROJECT VERSION move to 2.17.0. Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 352 ------------------------------------- CMakeLists.txt | 2 +- ROADMAP.md | 72 ++++++++ include/rcp/canbr.hpp | 54 ------ include/rcp/federation.hpp | 146 --------------- include/rcp/firmware.hpp | 254 -------------------------- include/rcp/linbr.hpp | 52 ------ include/rcp/prioqueue.hpp | 173 ------------------ include/rcp/proxy.hpp | 145 --------------- include/rcp/version.hpp | 2 +- include/rcp/zonegroup.hpp | 138 --------------- tests/CMakeLists.txt | 22 +-- tests/test_canbr.cpp | 48 ----- tests/test_config.cpp | 75 +++++++- tests/test_federation.cpp | 118 ------------- tests/test_firmware.cpp | 108 ------------ tests/test_linbr.cpp | 48 ----- tests/test_prioqueue.cpp | 118 ------------- tests/test_proxy.cpp | 108 ------------ tests/test_zonegroup.cpp | 116 ------------ 20 files changed, 143 insertions(+), 2008 deletions(-) delete mode 100644 include/rcp/canbr.hpp delete mode 100644 include/rcp/federation.hpp delete mode 100644 include/rcp/firmware.hpp delete mode 100644 include/rcp/linbr.hpp delete mode 100644 include/rcp/prioqueue.hpp delete mode 100644 include/rcp/proxy.hpp delete mode 100644 include/rcp/zonegroup.hpp delete mode 100644 tests/test_canbr.cpp delete mode 100644 tests/test_federation.cpp delete mode 100644 tests/test_firmware.cpp delete mode 100644 tests/test_linbr.cpp delete mode 100644 tests/test_prioqueue.cpp delete mode 100644 tests/test_proxy.cpp delete mode 100644 tests/test_zonegroup.cpp diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 07d4b61..ded0e8f 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -1200,70 +1200,6 @@ "level": "ASIL-B", "asil": "ASIL-B" }, - { - "id": "REQ-PQ-001", - "title": "Higher priority dispatched first", - "text": "prioqueue::Controller shall dispatch Priority::Critical before Priority::High before Priority::Normal when multiple commands are queued.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PQ-002", - "title": "FIFO within same priority", - "text": "Commands with equal priority shall be dispatched in the order they were submitted (first-in, first-out).", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PQ-003", - "title": "Deadline respected per entry", - "text": "If a queued command's context deadline expires before dispatch, send() shall return ErrTimeout.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PQ-004", - "title": "done() context rejected", - "text": "send() shall return ErrTimeout immediately if the provided context is already done.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PQ-005", - "title": "Zone mismatch rejected", - "text": "send() shall return ErrZoneMismatch if cmd.zone differs from the inner controller's zone.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PQ-006", - "title": "Subscribe delegates to inner", - "text": "subscribe() shall delegate directly to the inner controller.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PQ-007", - "title": "Close stops dispatch thread", - "text": "close() shall stop the dispatch thread and cause subsequent send() calls to return ErrClosed.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PQ-008", - "title": "send() after close returns ErrClosed", - "text": "Any send() call after close() shall return ErrClosed.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, { "id": "REQ-RL-001", "title": "Manager::admit admits while the domain's bucket has tokens", @@ -1816,166 +1752,6 @@ "level": "SL-2", "asil": "ASIL-B" }, - { - "id": "REQ-FW-001", - "title": "FirmwareSession begins in Idle state", - "text": "A freshly constructed FirmwareSession shall report SessionState::Idle.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FW-002", - "title": "initiate transitions to Initiated", - "text": "FirmwareSession::initiate() shall transition the session to SessionState::Initiated on success.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FW-003", - "title": "transfer requires Initiated state", - "text": "FirmwareSession::transfer() shall return FirmwareErrc::bad_state if the session is not in Initiated state.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FW-004", - "title": "verify requires Transferring state", - "text": "FirmwareSession::verify() shall return FirmwareErrc::bad_state if the session is not in Transferring state.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FW-005", - "title": "activate requires Verifying state", - "text": "FirmwareSession::activate() shall return FirmwareErrc::bad_state if the session is not in Verifying state.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FW-006", - "title": "transfer chunks firmware in Config::chunk_size segments", - "text": "FirmwareSession::transfer() shall send the image in segments not exceeding Config::chunk_size bytes.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FW-007", - "title": "rollback resets session to Idle", - "text": "FirmwareSession::rollback() shall reset the session state to Idle on success.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FW-008", - "title": "transfer progress callback is called per chunk", - "text": "If a ProgressCallback is supplied, FirmwareSession::transfer() shall invoke it once for each chunk successfully delivered.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-ZG-001", - "title": "ZoneGroup::all() contains all five zones", - "text": "ZoneGroup::all() shall return a group containing Zone::FrontLeft, FrontRight, RearLeft, RearRight, and Central.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-ZG-002", - "title": "send_group dispatches to every zone concurrently", - "text": "GroupRegistry::send_group() shall dispatch the command to all zones in the group using independent asynchronous tasks.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-ZG-003", - "title": "GroupResponse::ok() is false if any zone fails", - "text": "GroupResponse::ok() shall return false if any per-zone result has a non-zero error_code.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-ZG-004", - "title": "GroupResponse::errors() lists failing zones", - "text": "GroupResponse::errors() shall return the set of zones for which the error_code is non-zero.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-ZG-005", - "title": "ZoneGroup is a value type", - "text": "ZoneGroup shall be copyable and default-constructible.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-ZG-006", - "title": "send_group uses caller context deadline", - "text": "GroupRegistry::send_group() shall forward the caller's Context (including deadline) to each zone controller.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PROXY-001", - "title": "ProxyController enforces latency budget", - "text": "ProxyController::send() shall derive a tighter deadline equal to min(caller deadline, now + latency_budget) and forward that to the upstream controller.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-PROXY-002", - "title": "ProxyRegistry::lookup returns proxy-wrapped controller", - "text": "Controllers added via ProxyRegistry::add_route() shall be wrapped in a ProxyController before being returned by lookup().", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-PROXY-003", - "title": "ProxyRegistry::deregister closes upstream", - "text": "ProxyRegistry::deregister() shall call close() on the removed controller.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-PROXY-004", - "title": "ProxyRegistry returns ErrNotFound for unknown zones", - "text": "ProxyRegistry::lookup() shall return ErrNotFound for zones not registered via add_route().", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-PROXY-005", - "title": "ProxyRegistry close is idempotent", - "text": "Calling ProxyRegistry::close() more than once shall return no error on subsequent calls.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-PROXY-006", - "title": "ProxyController zone() matches upstream", - "text": "ProxyController::zone() shall return the same zone as the wrapped upstream controller.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, { "id": "REQ-RED-001", "title": "RedundantController routes sends to primary", @@ -2040,70 +1816,6 @@ "level": "ASIL-A", "asil": "ASIL-A" }, - { - "id": "REQ-FED-001", - "title": "FederatedRegistry prefers local over remote lease", - "text": "When both a local controller and a remote lease exist for the same zone, FederatedRegistry::lookup() shall return the local controller.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FED-002", - "title": "Expired lease returns ErrNotFound", - "text": "FederatedRegistry::lookup() shall return ErrNotFound for a zone whose only entry is an expired Lease.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FED-003", - "title": "revoke_lease removes lease immediately", - "text": "FederatedRegistry::revoke_lease() shall remove the lease so that subsequent lookup() calls return ErrNotFound.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FED-004", - "title": "add_lease stores lease for valid zone", - "text": "FederatedRegistry::add_lease() shall store the lease so that lookup() returns the remote controller until the lease expires.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FED-005", - "title": "local_id identifies the HPC node", - "text": "FederatedRegistry::local_id() shall return the identifier supplied at construction.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-FED-006", - "title": "close() closes local and remote controllers", - "text": "FederatedRegistry::close() shall close all local controllers and all remote_ctrl handles in active leases.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FED-007", - "title": "closed registry returns ErrClosed on lookup", - "text": "After FederatedRegistry::close(), lookup() shall return ErrClosed.", - "standard": "iso26262", - "level": "ASIL-B", - "asil": "ASIL-B" - }, - { - "id": "REQ-FED-008", - "title": "register_ctrl returns ErrAlreadyExists for duplicate zone", - "text": "FederatedRegistry::register_ctrl() shall return ErrAlreadyExists if a controller for that zone is already registered locally.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, { "id": "REQ-OBS-001", "title": "ObservingClient records a span per request", @@ -2488,38 +2200,6 @@ "level": "ASIL-A", "asil": "ASIL-A" }, - { - "id": "REQ-CAN-001", - "title": "CanController::send returns function_not_supported when stub", - "text": "CanController::send() shall return std::errc::function_not_supported until a SocketCAN adapter is linked.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-CAN-002", - "title": "CanController::zone returns configured zone", - "text": "CanController::zone() shall return the zone supplied at construction.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-CAN-003", - "title": "CanController::subscribe returns function_not_supported when stub", - "text": "CanController::subscribe() shall return std::errc::function_not_supported until a SocketCAN adapter is linked.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-CAN-004", - "title": "CanController::close returns no error", - "text": "CanController::close() shall return a zero error_code.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, { "id": "REQ-DDS-001", "title": "DdsBridge::request returns function_not_supported when stub", @@ -2584,38 +2264,6 @@ "level": "ASIL-A", "asil": "ASIL-A" }, - { - "id": "REQ-LIN-001", - "title": "LinController::send returns function_not_supported when stub", - "text": "LinController::send() shall return std::errc::function_not_supported until a LIN driver adapter is linked.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-LIN-002", - "title": "LinController::zone returns configured zone", - "text": "LinController::zone() shall return the zone supplied at construction.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-LIN-003", - "title": "LinController::subscribe returns function_not_supported when stub", - "text": "LinController::subscribe() shall return std::errc::function_not_supported until a LIN adapter is linked.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, - { - "id": "REQ-LIN-004", - "title": "LinController::close returns no error", - "text": "LinController::close() shall return a zero error_code.", - "standard": "iso26262", - "level": "ASIL-A", - "asil": "ASIL-A" - }, { "id": "REQ-UDS-001", "title": "UdsBridge::request returns function_not_supported when stub", diff --git a/CMakeLists.txt b/CMakeLists.txt index eb332ad..373826e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) project(cpp-rcp - VERSION 2.16.0 + VERSION 2.17.0 DESCRIPTION "Remote Control Protocol for automotive zonal architecture (C++)" HOMEPAGE_URL "https://github.com/SoundMatt/cpp-RCP" LANGUAGES CXX diff --git a/ROADMAP.md b/ROADMAP.md index 5dd049f..fb4daaf 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1374,6 +1374,78 @@ landed and nothing else in the tree still depends on these headers: `canbr.hpp`, `linbr.hpp`, `federation.hpp`, `proxy.hpp`, `zonegroup.hpp`, `prioqueue.hpp`, `firmware.hpp`. +**Done (v2.17.0):** All seven headers named in this milestone's scope — +`include/rcp/canbr.hpp`, `linbr.hpp`, `federation.hpp`, `proxy.hpp`, +`zonegroup.hpp`, `prioqueue.hpp`, `firmware.hpp` — are deleted outright, +along with their dedicated test files (`tests/test_canbr.cpp`, +`test_linbr.cpp`, `test_federation.cpp`, `test_proxy.cpp`, +`test_zonegroup.cpp`, `test_prioqueue.cpp`, `test_firmware.cpp`), matching +the DEPRECATE call and per-package justification already recorded in the +Satellite Package Disposition table. Before deleting, grepping the tree for +`#include` references to each of the seven confirmed zero remaining +consumers outside the header/test pair being removed; `linbr.hpp` and +`prioqueue.hpp` were each still named in passing inside doc comments in +`rcp/lin.hpp`, `rcp/ratelimit.hpp`, and `rcp/tsn.hpp` (explaining, by name, +what those ADAPTed packages replaced) and `rcp/legacy_mock.hpp` (a +historical list of this file's old dependents) — none of these are +`#include`s, so they were left as the same kind of point-in-time +archaeological record every prior REPLACE/ADAPT milestone's own comments +already are, rather than rewritten. + +`tests/CMakeLists.txt` is updated to match: `canbr`/`linbr` are pulled out +of the shared bridge-stub `foreach` loop (the seven ADAPTed bridges sharing +that loop — `ddsbr`/`doipbr`/`grpcbridge`/`mqttbr`/`restbridge`/ +`someipbr`/`udsbr` — are untouched), and the five standalone +`add_executable`/`target_link_libraries`/`add_test` blocks for +`federation`/`proxy`/`zonegroup`/`prioqueue`/`firmware` are deleted. + +One real coupling had to be resolved rather than just deleted around: +`tests/test_config.cpp` pulled in `rcp/proxy.hpp` purely to get an +`rcp::Registry` implementation that starts empty (`legacy_mock::Registry` +always pre-populates all 5 zones, so it can't observe `config::load` +registering a zone from a clean slate) — no `ProxyController` latency-budget +behavior was ever under test there. Rather than drop the two `config::load` +test cases (and their `REQ-CFG-*` coverage) or reach back into `proxy.hpp`, +`test_config.cpp` now defines its own minimal `EmptyRegistry` test double +locally and uses that instead; `rcp/config.hpp` itself, and the fact that +its own rebind onto the new server/endpoint manifest schema remains a +separate, still-open item per the Satellite Package Disposition table's +`config.hpp` entry, are both otherwise untouched by this milestone. + +`.fusa-reqs.json` is pruned of the 44 requirement entries that back the +deleted packages — `REQ-CANBR`/`REQ-LINBR` didn't end up being the real +prefixes canbr.hpp/linbr.hpp used (they were `REQ-CAN-001..004` and +`REQ-LIN-001..004`, distinct from the native CAN/LIN endpoints' own +`REQ-CANEP-*`/`REQ-LINEP-*`), plus `REQ-PROXY-001..006`, +`REQ-FED-001..008`, `REQ-ZG-001..006`, `REQ-PQ-001..008`, and +`REQ-FW-001..008`. This was necessary within this milestone rather than +deferred to #62: `cpfusa trace --req-coverage 100` (the same gate CI's +`cpfusa-trace` job enforces) treats any requirement id present in +`.fusa-reqs.json` with no corresponding `fusa:req`/`fusa:test` source +annotation as an uncovered gap, and removing the seven headers' annotations +without pruning their matching entries dropped measured coverage to 90.9%. +No other safety artifact (`.fusa-hara.json`, `.fusa-problems.json`, +`.fusa-iec62443.json`, `fmea.csv`/`fmea.json`) references any of these 44 +ids, so none of them needed a corresponding edit. This is a narrowly-scoped +prune of exactly the entries this milestone's own deletions orphaned — it +is not the full regeneration against a new v2.x requirement set that +milestone #62 still owns, and does not touch the pre-replacement +`REQ-ZONE-*`/`REQ-CMD-*`/`REQ-STATUS-*` groups #62's own scope text calls +out for retirement. + +`version.hpp`/`CMakeLists.txt` are bumped to 2.17.0. Full local build +(clang/gcc, C++17) and `ctest` (53/53 tests, down from 60 — the seven +removed suites) pass; `cpfusa check`/`lint`/`cyber`/`trace --req-coverage +100` all pass with 0 errors (only the same pre-existing warnings already +present before this change); RELAY `conform --strict` against the built +`cpp-rcp` CLI still passes unchanged, since this milestone touches no +wire-format or CLI-surface code. Milestone #62 (Certification Refresh, +v2.18.0) is next: it still owns the full `.fusa-*`/`fmea.*`/`HARA.md`/ +`TARA-ANALYSIS.md`/`CYBERSECURITY.md`/`tla/`/`AUDIT_PACK.md` regeneration +against the complete v2.x requirement set, including retiring the older +pre-replacement `REQ-ZONE-*`/`REQ-CMD-*`/`REQ-STATUS-*` groups that this +milestone did not touch. + ### 62. Certification Refresh (v2.18.0) - Regenerate `.fusa-reqs.json`, `.fusa-hara.json`, `HARA.md`, `TARA-ANALYSIS.md`, diff --git a/include/rcp/canbr.hpp b/include/rcp/canbr.hpp deleted file mode 100644 index 95b3522..0000000 --- a/include/rcp/canbr.hpp +++ /dev/null @@ -1,54 +0,0 @@ -// fusa:req REQ-CAN-001 -// fusa:req REQ-CAN-002 -// fusa:req REQ-CAN-003 -// fusa:req REQ-CAN-004 - -// CAN / CAN-FD protocol bridge interface stub (v0.34.0). -// -// Maps RCP commands to CAN frames via SocketCAN (Linux) or a hardware CAN -// driver. All methods return errc::function_not_supported until the adapter -// is linked. -#pragma once - -#include "rcp.hpp" - -#include -#include - -namespace rcp { -namespace canbr { - -struct Config { - uint32_t can_id_base{0x100}; // base arbitration ID - bool fd_mode{false}; // CAN-FD frames - std::chrono::milliseconds timeout{100}; -}; - -class CanController final : public rcp::Controller { -public: - CanController(Zone zone, Config cfg) : zone_(zone), cfg_(std::move(cfg)) {} - - Zone zone() const noexcept override { return zone_; } - - std::error_code send(const rcp::Context&, const Command&, Response&) override { - return std::make_error_code(std::errc::function_not_supported); - } - - std::error_code subscribe(const rcp::Context&, - std::shared_ptr&) override { - return std::make_error_code(std::errc::function_not_supported); - } - - std::error_code close() override { return {}; } - -private: - Zone zone_; - Config cfg_; -}; - -inline std::shared_ptr new_controller(Zone zone, Config cfg) { - return std::make_shared(zone, std::move(cfg)); -} - -} // namespace canbr -} // namespace rcp diff --git a/include/rcp/federation.hpp b/include/rcp/federation.hpp deleted file mode 100644 index 9472104..0000000 --- a/include/rcp/federation.hpp +++ /dev/null @@ -1,146 +0,0 @@ -// fusa:req REQ-FED-001 -// fusa:req REQ-FED-002 -// fusa:req REQ-FED-003 -// fusa:req REQ-FED-004 -// fusa:req REQ-FED-005 -// fusa:req REQ-FED-006 -// fusa:req REQ-FED-007 -// fusa:req REQ-FED-008 - -// Multi-HPC federation: cross-HPC zone forwarding with lease-based ownership -// (v0.24.0). -// -// FederatedRegistry is an rcp::Registry whose lookup() transparently forwards -// to a remote HPC when the zone is not locally owned. Lease ownership is -// time-bounded; an expired lease causes lookup() to return ErrNotFound until -// refreshed by the owning HPC. -#pragma once - -#include "rcp.hpp" - -#include -#include -#include -#include -#include -#include -#include - -namespace rcp { -namespace federation { - -// ── HpcId ───────────────────────────────────────────────────────────────────── - -using HpcId = std::string; - -// ── Lease ───────────────────────────────────────────────────────────────────── - -struct Lease { - HpcId owner; - std::chrono::steady_clock::time_point expires_at; - std::shared_ptr remote_ctrl; // forwarding handle - - bool expired() const noexcept { - return std::chrono::steady_clock::now() >= expires_at; - } -}; - -// ── FederatedRegistry ───────────────────────────────────────────────────────── - -class FederatedRegistry final : public rcp::Registry { -public: - // local_id identifies this HPC node. - explicit FederatedRegistry(HpcId local_id) : local_id_(std::move(local_id)) {} - - // register_ctrl adds a locally-owned zone controller. - std::error_code register_ctrl(std::shared_ptr ctrl) override { - std::unique_lock lk(mu_); - if (closed_) return ErrClosed; - if (local_.count(ctrl->zone())) return ErrAlreadyExists; - local_[ctrl->zone()] = std::move(ctrl); - return {}; - } - - std::error_code deregister(Zone z) override { - std::unique_lock lk(mu_); - auto it = local_.find(z); - if (it == local_.end()) return ErrNotFound; - auto c = it->second; - local_.erase(it); - lk.unlock(); - return c->close(); - } - - // add_lease publishes a remote-HPC lease for a zone. - // The FederatedRegistry will forward commands through the lease's - // remote_ctrl until the lease expires. - std::error_code add_lease(Zone zone, Lease lease) { - std::unique_lock lk(mu_); - if (closed_) return ErrClosed; - leases_[zone] = std::move(lease); - return {}; - } - - // revoke_lease removes an active lease (called by the owning HPC on shutdown). - std::error_code revoke_lease(Zone zone) { - std::unique_lock lk(mu_); - leases_.erase(zone); - return {}; - } - - std::error_code lookup(Zone z, std::shared_ptr& out) override { - std::shared_lock lk(mu_); - if (closed_) return ErrClosed; - - // Prefer local ownership. - auto lit = local_.find(z); - if (lit != local_.end()) { out = lit->second; return {}; } - - // Fall through to remote lease. - auto it = leases_.find(z); - if (it == leases_.end()) return ErrNotFound; - if (it->second.expired()) return ErrNotFound; - out = it->second.remote_ctrl; - return {}; - } - - std::vector> controllers() override { - std::shared_lock lk(mu_); - std::vector> out; - out.reserve(local_.size()); - for (auto& kv : local_) out.push_back(kv.second); - return out; - } - - std::error_code close() override { - std::unique_lock lk(mu_); - if (closed_) return {}; - closed_ = true; - auto local = std::move(local_); - auto leases = std::move(leases_); - lk.unlock(); - for (auto& kv : local) { auto ec = kv.second->close(); (void)ec; } - for (auto& kv : leases) { - if (kv.second.remote_ctrl) { - auto ec = kv.second.remote_ctrl->close(); (void)ec; - } - } - return {}; - } - - const HpcId& local_id() const noexcept { return local_id_; } - -private: - HpcId local_id_; - mutable std::shared_mutex mu_; - std::map> local_; - std::map leases_; - bool closed_ = false; -}; - -inline std::unique_ptr new_registry(HpcId local_id) { - return std::make_unique(std::move(local_id)); -} - -} // namespace federation -} // namespace rcp diff --git a/include/rcp/firmware.hpp b/include/rcp/firmware.hpp deleted file mode 100644 index f10bc84..0000000 --- a/include/rcp/firmware.hpp +++ /dev/null @@ -1,254 +0,0 @@ -// fusa:req REQ-FW-001 -// fusa:req REQ-FW-002 -// fusa:req REQ-FW-003 -// fusa:req REQ-FW-004 -// fusa:req REQ-FW-005 -// fusa:req REQ-FW-006 -// fusa:req REQ-FW-007 -// fusa:req REQ-FW-008 - -// Zone controller OTA firmware update (v0.20.0). -// -// FirmwareSession manages a multi-command exchange: -// Initiate → Transfer (N chunks) → Verify → Activate → Reset -// -// SHA-256 integrity is computed over the full image before the Verify step. -// Rollback is available via firmware::rollback() if activation fails. -// -// Requires that CommandType::Update is defined in rcp.hpp (added in v0.20.0). -// The actual Update command byte is 0x07 — see rcp.hpp for the enum value. -#pragma once - -#include "rcp.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace rcp { -namespace firmware { - -// ── Config ──────────────────────────────────────────────────────────────────── - -struct Config { - size_t chunk_size{4096}; // bytes per Transfer command - int retries {3}; // per-chunk retry count - std::chrono::milliseconds chunk_timeout{500}; - std::chrono::milliseconds verify_timeout{5000}; -}; - -// ── SessionState ────────────────────────────────────────────────────────────── - -enum class SessionState : uint8_t { - Idle = 0, - Initiated = 1, - Transferring = 2, - Verifying = 3, - Activated = 4, - Failed = 5, -}; - -// ── FirmwareErrc ────────────────────────────────────────────────────────────── - -enum class FirmwareErrc : int { - bad_state = 1, - verify_failed = 2, - transfer_error = 3, - rollback_failed = 4, -}; - -inline const std::error_category& firmware_category() noexcept { - struct Cat : std::error_category { - const char* name() const noexcept override { return "rcp.firmware"; } - std::string message(int ev) const override { - switch (static_cast(ev)) { - case FirmwareErrc::bad_state: return "firmware: invalid session state"; - case FirmwareErrc::verify_failed: return "firmware: image verification failed"; - case FirmwareErrc::transfer_error: return "firmware: chunk transfer error"; - case FirmwareErrc::rollback_failed: return "firmware: rollback failed"; - default: return "firmware: unknown error"; - } - } - }; - static Cat instance; - return instance; -} - -inline std::error_code make_error_code(FirmwareErrc e) noexcept { - return {static_cast(e), firmware_category()}; -} - -// ── Progress callback ───────────────────────────────────────────────────────── - -struct Progress { - size_t bytes_sent; - size_t total_bytes; - size_t chunk_index; - size_t total_chunks; -}; - -using ProgressCallback = std::function; - -// ── FirmwareSession ─────────────────────────────────────────────────────────── - -class FirmwareSession { -public: - explicit FirmwareSession(std::shared_ptr ctrl, - Config cfg = {}) - : ctrl_(std::move(ctrl)), cfg_(cfg) {} - - SessionState state() const noexcept { - return state_.load(std::memory_order_acquire); - } - - // initiate starts an OTA session on the zone controller. - std::error_code initiate(const Context& ctx, const std::string& version) { - if (state_.load() != SessionState::Idle) - return make_error_code(FirmwareErrc::bad_state); - - Command cmd; - cmd.zone = ctrl_->zone(); - cmd.type = CommandType::Update; - cmd.priority = Priority::High; - // Payload: 1-byte subcommand 0x01 (Initiate) + version string - cmd.payload.push_back(0x01); - cmd.payload.insert(cmd.payload.end(), version.begin(), version.end()); - - Response resp; - auto ec = ctrl_->send(ctx, cmd, resp); - if (ec) return ec; - if (resp.status != ResponseStatus::OK) - return make_error_code(FirmwareErrc::bad_state); - - state_.store(SessionState::Initiated); - return {}; - } - - // transfer sends the firmware image in chunks. - std::error_code transfer(const Context& ctx, - const std::vector& image, - ProgressCallback progress_cb = {}) { - if (state_.load() != SessionState::Initiated) - return make_error_code(FirmwareErrc::bad_state); - state_.store(SessionState::Transferring); - - size_t total = image.size(); - size_t chunks = (total + cfg_.chunk_size - 1) / cfg_.chunk_size; - - for (size_t i = 0; i < chunks; ++i) { - size_t offset = i * cfg_.chunk_size; - size_t len = std::min(cfg_.chunk_size, total - offset); - - Command cmd; - cmd.zone = ctrl_->zone(); - cmd.type = CommandType::Update; - cmd.priority = Priority::High; - // Payload: 0x02 (Transfer) + 4-byte chunk index + chunk data - cmd.payload.push_back(0x02); - auto idx = static_cast(i); - cmd.payload.push_back(static_cast(idx >> 24)); - cmd.payload.push_back(static_cast(idx >> 16)); - cmd.payload.push_back(static_cast(idx >> 8)); - cmd.payload.push_back(static_cast(idx)); - cmd.payload.insert(cmd.payload.end(), - image.begin() + static_cast(offset), - image.begin() + static_cast(offset + len)); - - std::error_code ec; - for (int attempt = 0; attempt <= cfg_.retries; ++attempt) { - auto chunk_ctx = Context::with_timeout(cfg_.chunk_timeout); - Response resp; - ec = ctrl_->send(chunk_ctx, cmd, resp); - if (!ec && resp.status == ResponseStatus::OK) break; - if (attempt == cfg_.retries) { - state_.store(SessionState::Failed); - return ec ? ec : make_error_code(FirmwareErrc::transfer_error); - } - } - - if (progress_cb) { - progress_cb({offset + len, total, i + 1, chunks}); - } - } - - return {}; - } - - // verify triggers the zone controller to validate the received image. - std::error_code verify(const Context& ctx) { - if (state_.load() != SessionState::Transferring) - return make_error_code(FirmwareErrc::bad_state); - state_.store(SessionState::Verifying); - - Command cmd; - cmd.zone = ctrl_->zone(); - cmd.type = CommandType::Update; - cmd.priority = Priority::High; - cmd.payload = {0x03}; // Verify subcommand - - auto vctx = Context::with_timeout(cfg_.verify_timeout); - Response resp; - auto ec = ctrl_->send(vctx, cmd, resp); - if (ec) { state_.store(SessionState::Failed); return ec; } - if (resp.status != ResponseStatus::OK) { - state_.store(SessionState::Failed); - return make_error_code(FirmwareErrc::verify_failed); - } - return {}; - } - - // activate instructs the zone controller to boot the new image on next reset. - std::error_code activate(const Context& ctx) { - if (state_.load() != SessionState::Verifying) - return make_error_code(FirmwareErrc::bad_state); - - Command cmd; - cmd.zone = ctrl_->zone(); - cmd.type = CommandType::Update; - cmd.priority = Priority::High; - cmd.payload = {0x04}; // Activate subcommand - Response resp; - auto ec = ctrl_->send(ctx, cmd, resp); - if (ec) { state_.store(SessionState::Failed); return ec; } - if (resp.status != ResponseStatus::OK) { - state_.store(SessionState::Failed); - return make_error_code(FirmwareErrc::verify_failed); - } - state_.store(SessionState::Activated); - return {}; - } - - // rollback asks the zone controller to revert to the previous image. - std::error_code rollback(const Context& ctx) { - Command cmd; - cmd.zone = ctrl_->zone(); - cmd.type = CommandType::Update; - cmd.priority = Priority::High; - cmd.payload = {0x05}; // Rollback subcommand - Response resp; - auto ec = ctrl_->send(ctx, cmd, resp); - if (!ec && resp.status == ResponseStatus::OK) { - state_.store(SessionState::Idle); - return {}; - } - return ec ? ec : make_error_code(FirmwareErrc::rollback_failed); - } - -private: - std::shared_ptr ctrl_; - Config cfg_; - std::atomic state_{SessionState::Idle}; -}; - -} // namespace firmware -} // namespace rcp - -namespace std { -template <> -struct is_error_code_enum : true_type {}; -} // namespace std diff --git a/include/rcp/linbr.hpp b/include/rcp/linbr.hpp deleted file mode 100644 index 5310502..0000000 --- a/include/rcp/linbr.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// fusa:req REQ-LIN-001 -// fusa:req REQ-LIN-002 -// fusa:req REQ-LIN-003 -// fusa:req REQ-LIN-004 - -// LIN (Local Interconnect Network) bus bridge interface stub (v0.37.0). -// -// Maps RCP commands to LIN master-frame requests via a SocketCAN LIN driver -// or dedicated LIN hardware API. All methods return errc::function_not_supported. -#pragma once - -#include "rcp.hpp" - -#include -#include - -namespace rcp { -namespace linbr { - -struct Config { - uint8_t frame_id{0x10}; - std::chrono::milliseconds timeout{50}; -}; - -class LinController final : public rcp::Controller { -public: - LinController(Zone zone, Config cfg) : zone_(zone), cfg_(std::move(cfg)) {} - - Zone zone() const noexcept override { return zone_; } - - std::error_code send(const rcp::Context&, const Command&, Response&) override { - return std::make_error_code(std::errc::function_not_supported); - } - - std::error_code subscribe(const rcp::Context&, - std::shared_ptr&) override { - return std::make_error_code(std::errc::function_not_supported); - } - - std::error_code close() override { return {}; } - -private: - Zone zone_; - Config cfg_; -}; - -inline std::shared_ptr new_controller(Zone zone, Config cfg) { - return std::make_shared(zone, std::move(cfg)); -} - -} // namespace linbr -} // namespace rcp diff --git a/include/rcp/prioqueue.hpp b/include/rcp/prioqueue.hpp deleted file mode 100644 index fc5e360..0000000 --- a/include/rcp/prioqueue.hpp +++ /dev/null @@ -1,173 +0,0 @@ -// fusa:req REQ-PQ-001 -// fusa:req REQ-PQ-002 -// fusa:req REQ-PQ-003 -// fusa:req REQ-PQ-004 -// fusa:req REQ-PQ-005 -// fusa:req REQ-PQ-006 -// fusa:req REQ-PQ-007 -// fusa:req REQ-PQ-008 - -// Per-zone priority queue that serialises concurrent command senders while -// honouring Priority::Critical > Priority::High > Priority::Normal. -// -// Commands at equal priority are dispatched FIFO. Critical commands always -// pre-empt queued Normal and High commands, ensuring watchdog kicks and -// safety-critical actuation are never head-of-line blocked by lower-priority -// traffic. -// -// A single dispatch thread issues inner send() calls one at a time. -#pragma once - -#include "rcp.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace rcp { -namespace prioqueue { - -// ── Internal queue entry ────────────────────────────────────────────────────── - -struct Entry { - Context ctx; - Command cmd; - uint64_t seq = 0; - - struct Result { - std::error_code ec; - Response resp; - }; - std::shared_ptr> promise; // set by dispatch thread -}; - -struct EntryOrder { - bool operator()(const std::shared_ptr& a, - const std::shared_ptr& b) const noexcept { - // Higher priority wins; equal priority: lower seq first (FIFO). - if (a->cmd.priority != b->cmd.priority) - return a->cmd.priority < b->cmd.priority; - return a->seq > b->seq; - } -}; - -// ── Controller ──────────────────────────────────────────────────────────────── - -// Controller wraps any rcp::Controller and serialises Send calls through a -// priority queue. A single background thread dispatches commands so the -// inner controller never sees concurrent sends. -class Controller final : public rcp::Controller { -public: - explicit Controller(std::shared_ptr inner) - : inner_(std::move(inner)) { - dispatch_thread_ = std::thread([this]{ dispatch(); }); - } - - ~Controller() override { - closed_.store(true, std::memory_order_release); - cv_.notify_all(); - if (dispatch_thread_.joinable()) dispatch_thread_.join(); - (void)close(); - } - - Zone zone() const noexcept override { return inner_->zone(); } - - std::error_code send(const rcp::Context& ctx, - const Command& cmd, - Response& out) override { - if (closed_.load(std::memory_order_acquire)) - return ErrClosed; - - auto e = std::make_shared(); - e->ctx = ctx; - e->cmd = cmd; - e->seq = ++seq_; - e->promise = std::make_shared>(); - auto future = e->promise->get_future(); - - { - std::lock_guard lk(mu_); - queue_.push(std::move(e)); - } - cv_.notify_one(); - - // Wait for dispatch or context expiry. - if (ctx.deadline()) { - auto status = future.wait_until(*ctx.deadline()); - if (status == std::future_status::timeout) { - // Leave the entry in the queue; dispatch will find ctx done. - return ErrTimeout; - } - } else { - future.wait(); - } - - auto result = future.get(); - if (result.ec) return result.ec; - out = std::move(result.resp); - return {}; - } - - std::error_code subscribe(const rcp::Context& ctx, - std::shared_ptr& out) override { - return inner_->subscribe(ctx, out); - } - - std::error_code close() override { - closed_.store(true, std::memory_order_release); - cv_.notify_all(); - return inner_->close(); - } - -private: - std::shared_ptr inner_; - std::atomic closed_{false}; - std::atomic seq_{0}; - - mutable std::mutex mu_; - std::condition_variable cv_; - std::priority_queue, - std::vector>, - EntryOrder> queue_; - std::thread dispatch_thread_; - - void dispatch() { - while (true) { - std::shared_ptr e; - { - std::unique_lock lk(mu_); - cv_.wait(lk, [&]{ return closed_.load() || !queue_.empty(); }); - if (closed_.load() && queue_.empty()) break; - if (queue_.empty()) continue; - e = queue_.top(); - queue_.pop(); - } - if (e->ctx.done()) { - e->promise->set_value({ErrTimeout, {}}); - continue; - } - Entry::Result r; - r.ec = inner_->send(e->ctx, e->cmd, r.resp); - e->promise->set_value(std::move(r)); - } - // Drain remaining entries with ErrClosed. - std::unique_lock lk(mu_); - while (!queue_.empty()) { - auto top = queue_.top(); - queue_.pop(); - try { top->promise->set_value({ErrClosed, {}}); } catch (...) {} - } - } -}; - -inline std::shared_ptr new_controller(std::shared_ptr inner) { - return std::make_shared(std::move(inner)); -} - -} // namespace prioqueue -} // namespace rcp diff --git a/include/rcp/proxy.hpp b/include/rcp/proxy.hpp deleted file mode 100644 index 9b85191..0000000 --- a/include/rcp/proxy.hpp +++ /dev/null @@ -1,145 +0,0 @@ -// fusa:req REQ-PROXY-001 -// fusa:req REQ-PROXY-002 -// fusa:req REQ-PROXY-003 -// fusa:req REQ-PROXY-004 -// fusa:req REQ-PROXY-005 -// fusa:req REQ-PROXY-006 - -// Transparent zone proxy for cascaded zonal topologies (v0.22.0). -// -// ProxyController forwards commands through an upstream proxy hop. -// A latency budget is enforced at the proxy boundary: if the budget -// is exceeded the command returns ErrTimeout rather than propagating -// a slow response to the caller. -// -// Route table: zone → upstream Controller -// The ProxyRegistry builds routes from a zone-to-Controller map and -// exposes a standard rcp::Registry interface. -#pragma once - -#include "rcp.hpp" - -#include -#include -#include -#include -#include -#include - -namespace rcp { -namespace proxy { - -// ── Config ──────────────────────────────────────────────────────────────────── - -struct Config { - std::chrono::milliseconds latency_budget{50}; // max added hop latency -}; - -// ── ProxyController ─────────────────────────────────────────────────────────── - -// ProxyController wraps an upstream Controller and enforces a latency budget. -// If the adjusted context deadline (now + budget) is tighter than the caller's -// original deadline, the tighter deadline is used to prevent budget overrun. -class ProxyController final : public rcp::Controller { -public: - ProxyController(std::shared_ptr upstream, Config cfg = {}) - : upstream_(std::move(upstream)), cfg_(cfg) {} - - Zone zone() const noexcept override { return upstream_->zone(); } - - std::error_code send(const rcp::Context& ctx, - const Command& cmd, - Response& out) override { - // Derive a tighter deadline from the budget. - auto budget_deadline = std::chrono::steady_clock::now() + cfg_.latency_budget; - Context proxy_ctx; - if (ctx.deadline()) { - if (*ctx.deadline() <= budget_deadline) - proxy_ctx = ctx; - else - proxy_ctx = Context::with_deadline(budget_deadline); - } else { - proxy_ctx = Context::with_deadline(budget_deadline); - } - return upstream_->send(proxy_ctx, cmd, out); - } - - std::error_code subscribe(const rcp::Context& ctx, - std::shared_ptr& out) override { - return upstream_->subscribe(ctx, out); - } - - std::error_code close() override { return upstream_->close(); } - -private: - std::shared_ptr upstream_; - Config cfg_; -}; - -// ── ProxyRegistry ───────────────────────────────────────────────────────────── - -class ProxyRegistry final : public rcp::Registry { -public: - std::error_code add_route(std::shared_ptr upstream, - Config cfg = {}) { - return register_ctrl( - std::make_shared(std::move(upstream), cfg)); - } - - std::error_code register_ctrl(std::shared_ptr ctrl) override { - std::unique_lock lk(mu_); - if (closed_) return ErrClosed; - if (ctrls_.count(ctrl->zone())) return ErrAlreadyExists; - ctrls_[ctrl->zone()] = std::move(ctrl); - return {}; - } - - std::error_code deregister(Zone z) override { - std::unique_lock lk(mu_); - auto it = ctrls_.find(z); - if (it == ctrls_.end()) return ErrNotFound; - auto c = it->second; - ctrls_.erase(it); - lk.unlock(); - return c->close(); - } - - std::error_code lookup(Zone z, std::shared_ptr& out) override { - std::shared_lock lk(mu_); - if (closed_) return ErrClosed; - auto it = ctrls_.find(z); - if (it == ctrls_.end()) return ErrNotFound; - out = it->second; - return {}; - } - - std::vector> controllers() override { - std::shared_lock lk(mu_); - std::vector> out; - out.reserve(ctrls_.size()); - for (auto& kv : ctrls_) out.push_back(kv.second); - return out; - } - - std::error_code close() override { - std::unique_lock lk(mu_); - if (closed_) return {}; - closed_ = true; - auto local = std::move(ctrls_); - lk.unlock(); - for (auto& kv : local) { auto ec = kv.second->close(); (void)ec; } - return {}; - } - -private: - mutable std::shared_mutex mu_; - std::map> ctrls_; - bool closed_ = false; -}; - -inline std::unique_ptr new_registry() { - return std::make_unique(); -} - -} // namespace proxy -} // namespace rcp diff --git a/include/rcp/version.hpp b/include/rcp/version.hpp index 907a27b..a4f921b 100644 --- a/include/rcp/version.hpp +++ b/include/rcp/version.hpp @@ -6,6 +6,6 @@ namespace rcp { // Semantic version of the cpp-RCP implementation (matches the latest git tag). -constexpr std::string_view kVersion = "2.16.0"; +constexpr std::string_view kVersion = "2.17.0"; } // namespace rcp diff --git a/include/rcp/zonegroup.hpp b/include/rcp/zonegroup.hpp deleted file mode 100644 index a8e77b1..0000000 --- a/include/rcp/zonegroup.hpp +++ /dev/null @@ -1,138 +0,0 @@ -// fusa:req REQ-ZG-001 -// fusa:req REQ-ZG-002 -// fusa:req REQ-ZG-003 -// fusa:req REQ-ZG-004 -// fusa:req REQ-ZG-005 -// fusa:req REQ-ZG-006 - -// Atomic multi-zone command broadcast with typed zone group sets (v0.21.0). -// -// ZoneGroup is a value type enumerating a subset of zones. -// GroupRegistry::send_group() dispatches one command to every zone in the -// group concurrently and collects per-zone responses into a GroupResponse. -#pragma once - -#include "rcp.hpp" - -#include -#include -#include -#include -#include - -namespace rcp { -namespace zonegroup { - -// ── ZoneGroup ───────────────────────────────────────────────────────────────── - -class ZoneGroup { -public: - ZoneGroup() = default; - - ZoneGroup(std::initializer_list zones) - : zones_(zones.begin(), zones.end()) {} - - static ZoneGroup all() { - return ZoneGroup{Zone::FrontLeft, Zone::FrontRight, - Zone::RearLeft, Zone::RearRight, - Zone::Central}; - } - - static ZoneGroup rear() { - return ZoneGroup{Zone::RearLeft, Zone::RearRight}; - } - - static ZoneGroup front() { - return ZoneGroup{Zone::FrontLeft, Zone::FrontRight}; - } - - const std::vector& zones() const noexcept { return zones_; } - bool empty() const noexcept { return zones_.empty(); } - size_t size() const noexcept { return zones_.size(); } - - void add(Zone z) { zones_.push_back(z); } - -private: - std::vector zones_; -}; - -// ── GroupResponse ───────────────────────────────────────────────────────────── - -struct ZoneResult { - Response response; - std::error_code error; -}; - -struct GroupResponse { - std::map results; - - // ok returns true if every zone response succeeded. - bool ok() const { - for (auto& kv : results) { - if (kv.second.error || kv.second.response.status != ResponseStatus::OK) - return false; - } - return true; - } - - // errors returns zones that failed. - std::vector errors() const { - std::vector out; - for (auto& kv : results) { - if (kv.second.error) out.push_back(kv.first); - } - return out; - } -}; - -// ── GroupRegistry ───────────────────────────────────────────────────────────── - -// GroupRegistry wraps any rcp::Registry and adds send_group() broadcast. -class GroupRegistry { -public: - explicit GroupRegistry(rcp::Registry& reg) : reg_(reg) {} - - // send_group dispatches cmd to every zone in group concurrently. - // Uses the shared context deadline for all parallel sends. - // Priority::Critical commands ignore any ErrBusy backpressure. - GroupResponse send_group(const Context& ctx, - const ZoneGroup& group, - const Command& cmd) { - GroupResponse result; - - // Launch one future per zone. - std::vector>> futures; - futures.reserve(group.size()); - - for (Zone z : group.zones()) { - futures.emplace_back(z, std::async(std::launch::async, - [this, &ctx, &cmd, z]() -> ZoneResult { - std::shared_ptr ctrl; - auto ec = reg_.lookup(z, ctrl); - if (ec) return {Response{}, ec}; - Command zone_cmd = cmd; - zone_cmd.zone = z; - zone_cmd.id = 0; // registry assigns id - Response resp; - ec = ctrl->send(ctx, zone_cmd, resp); - return {resp, ec}; - })); - } - - for (auto& [z, fut] : futures) { - result.results[z] = fut.get(); - } - return result; - } - -private: - rcp::Registry& reg_; -}; - -// Named group constants. -inline ZoneGroup group_all() { return ZoneGroup::all(); } -inline ZoneGroup group_rear() { return ZoneGroup::rear(); } -inline ZoneGroup group_front() { return ZoneGroup::front(); } - -} // namespace zonegroup -} // namespace rcp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9003aac..e0d9b2b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -86,10 +86,6 @@ add_executable(test_wakeup test_wakeup.cpp) target_link_libraries(test_wakeup PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_wakeup COMMAND test_wakeup) -add_executable(test_prioqueue test_prioqueue.cpp) -target_link_libraries(test_prioqueue PRIVATE rcp::rcp Catch2::Catch2WithMain) -add_test(NAME rcp_prioqueue COMMAND test_prioqueue) - add_executable(test_ratelimit test_ratelimit.cpp) target_link_libraries(test_ratelimit PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_ratelimit COMMAND test_ratelimit) @@ -124,26 +120,10 @@ add_executable(test_authz test_authz.cpp) target_link_libraries(test_authz PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_authz COMMAND test_authz) -add_executable(test_firmware test_firmware.cpp) -target_link_libraries(test_firmware PRIVATE rcp::rcp Catch2::Catch2WithMain) -add_test(NAME rcp_firmware COMMAND test_firmware) - -add_executable(test_zonegroup test_zonegroup.cpp) -target_link_libraries(test_zonegroup PRIVATE rcp::rcp Catch2::Catch2WithMain) -add_test(NAME rcp_zonegroup COMMAND test_zonegroup) - -add_executable(test_proxy test_proxy.cpp) -target_link_libraries(test_proxy PRIVATE rcp::rcp Catch2::Catch2WithMain) -add_test(NAME rcp_proxy COMMAND test_proxy) - add_executable(test_redundancy test_redundancy.cpp) target_link_libraries(test_redundancy PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_redundancy COMMAND test_redundancy) -add_executable(test_federation test_federation.cpp) -target_link_libraries(test_federation PRIVATE rcp::rcp Catch2::Catch2WithMain) -add_test(NAME rcp_federation COMMAND test_federation) - add_executable(test_observe test_observe.cpp) target_link_libraries(test_observe PRIVATE rcp::rcp Catch2::Catch2WithMain) add_test(NAME rcp_observe COMMAND test_observe) @@ -192,7 +172,7 @@ add_test(NAME rcp_cli COMMAND test_cli) # ── Transport / bridge stubs + remaining modules ───────────────────────────── foreach(mod - canbr ddsbr doipbr grpcbridge linbr mqttbr restbridge someipbr udsbr + ddsbr doipbr grpcbridge mqttbr restbridge someipbr udsbr tsn mdns powerstate tls udp) add_executable(test_${mod} test_${mod}.cpp) target_link_libraries(test_${mod} PRIVATE rcp::rcp Catch2::Catch2WithMain) diff --git a/tests/test_canbr.cpp b/tests/test_canbr.cpp deleted file mode 100644 index ed38e3b..0000000 --- a/tests/test_canbr.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// fusa:test REQ-CAN-001 -// fusa:test REQ-CAN-002 -// fusa:test REQ-CAN-003 -// fusa:test REQ-CAN-004 - -// canbr protocol-bridge stub conformance tests. -// -// The CanController is a compile-time interface stub: until a concrete backend is -// linked, send()/subscribe() return std::errc::function_not_supported, zone() -// reports the configured zone, and close() succeeds. These tests pin that -// contract so callers get a well-defined error rather than undefined behaviour. -#include - -#include "rcp/canbr.hpp" - -#include - -using namespace rcp; - -namespace { -const std::error_code kUnsupported = - std::make_error_code(std::errc::function_not_supported); -} // namespace - -TEST_CASE("canbr: send returns function_not_supported when stub", "[canbr][REQ-CAN-001]") { - auto ctrl = canbr::new_controller(Zone::FrontLeft, canbr::Config{}); - Command cmd; - cmd.zone = Zone::FrontLeft; - cmd.type = CommandType::Set; - Response resp; - REQUIRE(ctrl->send(Context{}, cmd, resp) == kUnsupported); -} - -TEST_CASE("canbr: zone returns configured zone", "[canbr][REQ-CAN-002]") { - auto ctrl = canbr::new_controller(Zone::RearRight, canbr::Config{}); - REQUIRE(ctrl->zone() == Zone::RearRight); -} - -TEST_CASE("canbr: subscribe returns function_not_supported when stub", "[canbr][REQ-CAN-003]") { - auto ctrl = canbr::new_controller(Zone::Central, canbr::Config{}); - std::shared_ptr ch; - REQUIRE(ctrl->subscribe(Context{}, ch) == kUnsupported); -} - -TEST_CASE("canbr: close returns no error", "[canbr][REQ-CAN-004]") { - auto ctrl = canbr::new_controller(Zone::FrontRight, canbr::Config{}); - REQUIRE_FALSE(ctrl->close()); -} diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 026bd80..9ce492f 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -8,13 +8,75 @@ #include "rcp/config.hpp" #include "rcp/legacy_mock.hpp" -#include "rcp/proxy.hpp" +#include #include #include using namespace rcp; +namespace { + +// A minimal empty-starting rcp::Registry test double. +// +// legacy_mock::Registry always pre-populates all 5 zones (see its own +// constructor), so it can't be used to observe "did config::load actually +// register this zone" starting from a clean slate. Milestone 61's +// deprecation sweep (ROADMAP.md, v2.17.0) removed rcp/proxy.hpp, which +// these tests previously borrowed purely as a generic empty rcp::Registry +// implementation — no proxy-specific behavior was ever under test here. +// This local double replaces that borrowed dependency directly instead of +// pulling proxy.hpp's concept back in; rcp/config.hpp itself is otherwise +// unchanged, since its own rebind onto the new server/endpoint model +// remains a separate, still-open item (see the Satellite Package +// Disposition table's `config.hpp` entry). +class EmptyRegistry final : public rcp::Registry { +public: + std::error_code register_ctrl(std::shared_ptr ctrl) override { + if (closed_) return ErrClosed; + if (ctrls_.count(ctrl->zone())) return ErrAlreadyExists; + ctrls_[ctrl->zone()] = std::move(ctrl); + return {}; + } + + std::error_code deregister(Zone z) override { + auto it = ctrls_.find(z); + if (it == ctrls_.end()) return ErrNotFound; + auto ctrl = it->second; + ctrls_.erase(it); + return ctrl->close(); + } + + std::error_code lookup(Zone z, std::shared_ptr& out) override { + if (closed_) return ErrClosed; + auto it = ctrls_.find(z); + if (it == ctrls_.end()) return ErrNotFound; + out = it->second; + return {}; + } + + std::vector> controllers() override { + std::vector> out; + out.reserve(ctrls_.size()); + for (auto& kv : ctrls_) out.push_back(kv.second); + return out; + } + + std::error_code close() override { + if (closed_) return {}; + closed_ = true; + auto local = std::move(ctrls_); + for (auto& kv : local) (void)kv.second->close(); + return {}; + } + +private: + std::map> ctrls_; + bool closed_ = false; +}; + +} // namespace + TEST_CASE("config: parse_json two zones", "[config]") { const std::string json = R"({ "zones": [ @@ -43,11 +105,10 @@ TEST_CASE("config: load registers controllers", "[config]") { ] })"; - legacy_mock::Registry reg; - // Remove pre-registered zones first (legacy_mock::Registry pre-populates all 5) - // We test that load succeeds when zones are not yet registered. - // Use a proxy registry which starts empty. - proxy::ProxyRegistry preg; + // legacy_mock::Registry pre-populates all 5 zones, so it can't show + // config::load actually registering a new one; use the empty test + // double instead (see the EmptyRegistry comment above). + EmptyRegistry preg; REQUIRE_FALSE(config::load(json, preg)); std::shared_ptr ctrl; @@ -63,7 +124,7 @@ TEST_CASE("config: load duplicate zone returns ErrAlreadyExists", "[config]") { ] })"; - proxy::ProxyRegistry preg; + EmptyRegistry preg; auto ec = config::load(json, preg); REQUIRE(ec == ErrAlreadyExists); } diff --git a/tests/test_federation.cpp b/tests/test_federation.cpp deleted file mode 100644 index 20579e2..0000000 --- a/tests/test_federation.cpp +++ /dev/null @@ -1,118 +0,0 @@ -// fusa:test REQ-FED-001 -// fusa:test REQ-FED-002 -// fusa:test REQ-FED-003 -// fusa:test REQ-FED-004 -// fusa:test REQ-FED-005 -// fusa:test REQ-FED-006 -// fusa:test REQ-FED-007 -// fusa:test REQ-FED-008 -#include - -#include "rcp/federation.hpp" -#include "rcp/legacy_mock.hpp" - -using namespace rcp; - -TEST_CASE("federation: local controller preferred over lease", "[federation]") { - auto reg = federation::new_registry("hpc-a"); - auto local_ctrl = std::make_shared(Zone::FrontLeft); - REQUIRE_FALSE(reg->register_ctrl(local_ctrl)); - - // Add a lease for the same zone — local should win - auto remote_ctrl = std::make_shared(Zone::FrontLeft); - federation::Lease lease; - lease.owner = "hpc-b"; - lease.expires_at = std::chrono::steady_clock::now() + std::chrono::seconds(60); - lease.remote_ctrl = remote_ctrl; - REQUIRE_FALSE(reg->add_lease(Zone::FrontLeft, lease)); - - std::shared_ptr ctrl; - REQUIRE_FALSE(reg->lookup(Zone::FrontLeft, ctrl)); - REQUIRE(ctrl.get() == local_ctrl.get()); // local wins -} - -TEST_CASE("federation: remote lease used when no local", "[federation][REQ-FED-004]") { - auto reg = federation::new_registry("hpc-a"); - - auto remote_ctrl = std::make_shared(Zone::RearLeft); - federation::Lease lease; - lease.owner = "hpc-b"; - lease.expires_at = std::chrono::steady_clock::now() + std::chrono::seconds(60); - lease.remote_ctrl = remote_ctrl; - REQUIRE_FALSE(reg->add_lease(Zone::RearLeft, lease)); - - std::shared_ptr ctrl; - REQUIRE_FALSE(reg->lookup(Zone::RearLeft, ctrl)); - REQUIRE(ctrl.get() == remote_ctrl.get()); -} - -TEST_CASE("federation: expired lease returns ErrNotFound", "[federation]") { - auto reg = federation::new_registry("hpc-a"); - - auto remote_ctrl = std::make_shared(Zone::Central); - federation::Lease lease; - lease.owner = "hpc-b"; - lease.expires_at = std::chrono::steady_clock::now() - std::chrono::seconds(1); // expired - lease.remote_ctrl = remote_ctrl; - REQUIRE_FALSE(reg->add_lease(Zone::Central, lease)); - - std::shared_ptr ctrl; - auto ec = reg->lookup(Zone::Central, ctrl); - REQUIRE(ec == ErrNotFound); -} - -TEST_CASE("federation: revoke_lease removes lease", "[federation]") { - auto reg = federation::new_registry("hpc-a"); - - auto remote_ctrl = std::make_shared(Zone::FrontRight); - federation::Lease lease; - lease.owner = "hpc-b"; - lease.expires_at = std::chrono::steady_clock::now() + std::chrono::seconds(60); - lease.remote_ctrl = remote_ctrl; - REQUIRE_FALSE(reg->add_lease(Zone::FrontRight, lease)); - REQUIRE_FALSE(reg->revoke_lease(Zone::FrontRight)); - - std::shared_ptr ctrl; - REQUIRE(reg->lookup(Zone::FrontRight, ctrl) == ErrNotFound); -} - -TEST_CASE("federation: local_id is preserved", "[federation][REQ-FED-005]") { - auto reg = federation::new_registry("hpc-main"); - REQUIRE(reg->local_id() == "hpc-main"); -} - -TEST_CASE("federation: register_ctrl rejects duplicate zone", "[federation][REQ-FED-008]") { - auto reg = federation::new_registry("hpc-a"); - REQUIRE_FALSE(reg->register_ctrl(std::make_shared(Zone::Central))); - auto ec = reg->register_ctrl(std::make_shared(Zone::Central)); - REQUIRE(ec == ErrAlreadyExists); -} - -TEST_CASE("federation: close closes local and remote controllers", "[federation][REQ-FED-006]") { - auto reg = federation::new_registry("hpc-a"); - - auto local_ctrl = std::make_shared(Zone::FrontLeft); - REQUIRE_FALSE(reg->register_ctrl(local_ctrl)); - - auto remote_ctrl = std::make_shared(Zone::RearLeft); - federation::Lease lease; - lease.owner = "hpc-b"; - lease.expires_at = std::chrono::steady_clock::now() + std::chrono::seconds(60); - lease.remote_ctrl = remote_ctrl; - REQUIRE_FALSE(reg->add_lease(Zone::RearLeft, lease)); - - REQUIRE_FALSE(reg->close()); - - // Both the local and the leased remote controller are now closed. - Command cmd; cmd.zone = Zone::FrontLeft; Response resp; - REQUIRE(local_ctrl->send(Context{}, cmd, resp) == ErrClosed); - Command cmd2; cmd2.zone = Zone::RearLeft; Response resp2; - REQUIRE(remote_ctrl->send(Context{}, cmd2, resp2) == ErrClosed); -} - -TEST_CASE("federation: closed registry returns ErrClosed on lookup", "[federation][REQ-FED-007]") { - auto reg = federation::new_registry("hpc-a"); - REQUIRE_FALSE(reg->close()); - std::shared_ptr ctrl; - REQUIRE(reg->lookup(Zone::FrontLeft, ctrl) == ErrClosed); -} diff --git a/tests/test_firmware.cpp b/tests/test_firmware.cpp deleted file mode 100644 index fd88f55..0000000 --- a/tests/test_firmware.cpp +++ /dev/null @@ -1,108 +0,0 @@ -// fusa:test REQ-FW-001 -// fusa:test REQ-FW-002 -// fusa:test REQ-FW-003 -// fusa:test REQ-FW-004 -// fusa:test REQ-FW-005 -// fusa:test REQ-FW-006 -// fusa:test REQ-FW-007 -// fusa:test REQ-FW-008 -#include - -#include "rcp/firmware.hpp" -#include "rcp/legacy_mock.hpp" - -#include -#include - -using namespace rcp; - -TEST_CASE("firmware: state machine starts Idle", "[firmware]") { - auto ctrl = std::make_shared(Zone::FrontLeft); - firmware::FirmwareSession session(ctrl); - REQUIRE(session.state() == firmware::SessionState::Idle); -} - -TEST_CASE("firmware: initiate transitions to Initiated", "[firmware]") { - auto ctrl = std::make_shared(Zone::FrontLeft); - firmware::FirmwareSession session(ctrl); - REQUIRE_FALSE(session.initiate(Context{}, "1.2.3")); - REQUIRE(session.state() == firmware::SessionState::Initiated); -} - -TEST_CASE("firmware: double initiate fails", "[firmware]") { - auto ctrl = std::make_shared(Zone::FrontLeft); - firmware::FirmwareSession session(ctrl); - REQUIRE_FALSE(session.initiate(Context{}, "1.0")); - auto ec = session.initiate(Context{}, "1.0"); - REQUIRE(ec == firmware::make_error_code(firmware::FirmwareErrc::bad_state)); -} - -TEST_CASE("firmware: transfer requires Initiated state", "[firmware]") { - auto ctrl = std::make_shared(Zone::FrontLeft); - firmware::FirmwareSession session(ctrl); - std::vector image(8192, 0xAA); - auto ec = session.transfer(Context{}, image); - REQUIRE(ec == firmware::make_error_code(firmware::FirmwareErrc::bad_state)); -} - -TEST_CASE("firmware: full happy path", "[firmware]") { - auto ctrl = std::make_shared(Zone::FrontLeft); - firmware::FirmwareSession session(ctrl); - - REQUIRE_FALSE(session.initiate(Context{}, "2.0.0")); - REQUIRE(session.state() == firmware::SessionState::Initiated); - - std::vector image(4096, 0xFF); - size_t progress_calls = 0; - REQUIRE_FALSE(session.transfer(Context{}, image, [&](const firmware::Progress& p){ - ++progress_calls; - (void)p; - })); - REQUIRE(progress_calls == 1); // 4096 bytes / 4096 chunk_size = 1 chunk - - REQUIRE_FALSE(session.verify(Context{})); - REQUIRE(session.state() == firmware::SessionState::Verifying); - - REQUIRE_FALSE(session.activate(Context{})); - REQUIRE(session.state() == firmware::SessionState::Activated); -} - -TEST_CASE("firmware: transfer chunks image in chunk_size segments with per-chunk progress", - "[firmware][REQ-FW-006][REQ-FW-008]") { - auto ctrl = std::make_shared(Zone::FrontLeft); - firmware::Config cfg; - cfg.chunk_size = 1024; - firmware::FirmwareSession session(ctrl, cfg); - - REQUIRE_FALSE(session.initiate(Context{}, "3.0.0")); - - // 4096 bytes / 1024 chunk_size = exactly 4 chunks. - std::vector image(4096, 0x5A); - size_t calls = 0; - size_t last_total_chunks = 0; - size_t last_chunk_index = 0; - REQUIRE_FALSE(session.transfer(Context{}, image, [&](const firmware::Progress& p) { - ++calls; - last_total_chunks = p.total_chunks; - last_chunk_index = p.chunk_index; - REQUIRE(p.total_bytes == 4096); - })); - REQUIRE(calls == 4); // one progress callback per chunk (REQ-FW-008) - REQUIRE(last_total_chunks == 4); // image split into chunk_size segments (REQ-FW-006) - REQUIRE(last_chunk_index == 4); - - // A trailing partial chunk is still counted: 4097 bytes -> 5 chunks. - REQUIRE_FALSE(session.verify(Context{})); - REQUIRE_FALSE(session.activate(Context{})); -} - -TEST_CASE("firmware: rollback resets to Idle", "[firmware][REQ-FW-007]") { - auto ctrl = std::make_shared(Zone::FrontLeft); - firmware::FirmwareSession session(ctrl); - REQUIRE_FALSE(session.initiate(Context{}, "1.0")); - REQUIRE_FALSE(session.transfer(Context{}, std::vector(1, 0))); - REQUIRE_FALSE(session.verify(Context{})); - REQUIRE_FALSE(session.activate(Context{})); - REQUIRE_FALSE(session.rollback(Context{})); - REQUIRE(session.state() == firmware::SessionState::Idle); -} diff --git a/tests/test_linbr.cpp b/tests/test_linbr.cpp deleted file mode 100644 index ec11aff..0000000 --- a/tests/test_linbr.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// fusa:test REQ-LIN-001 -// fusa:test REQ-LIN-002 -// fusa:test REQ-LIN-003 -// fusa:test REQ-LIN-004 - -// linbr protocol-bridge stub conformance tests. -// -// The LinController is a compile-time interface stub: until a concrete backend is -// linked, send()/subscribe() return std::errc::function_not_supported, zone() -// reports the configured zone, and close() succeeds. These tests pin that -// contract so callers get a well-defined error rather than undefined behaviour. -#include - -#include "rcp/linbr.hpp" - -#include - -using namespace rcp; - -namespace { -const std::error_code kUnsupported = - std::make_error_code(std::errc::function_not_supported); -} // namespace - -TEST_CASE("linbr: send returns function_not_supported when stub", "[linbr][REQ-LIN-001]") { - auto ctrl = linbr::new_controller(Zone::FrontLeft, linbr::Config{}); - Command cmd; - cmd.zone = Zone::FrontLeft; - cmd.type = CommandType::Set; - Response resp; - REQUIRE(ctrl->send(Context{}, cmd, resp) == kUnsupported); -} - -TEST_CASE("linbr: zone returns configured zone", "[linbr][REQ-LIN-002]") { - auto ctrl = linbr::new_controller(Zone::RearRight, linbr::Config{}); - REQUIRE(ctrl->zone() == Zone::RearRight); -} - -TEST_CASE("linbr: subscribe returns function_not_supported when stub", "[linbr][REQ-LIN-003]") { - auto ctrl = linbr::new_controller(Zone::Central, linbr::Config{}); - std::shared_ptr ch; - REQUIRE(ctrl->subscribe(Context{}, ch) == kUnsupported); -} - -TEST_CASE("linbr: close returns no error", "[linbr][REQ-LIN-004]") { - auto ctrl = linbr::new_controller(Zone::FrontRight, linbr::Config{}); - REQUIRE_FALSE(ctrl->close()); -} diff --git a/tests/test_prioqueue.cpp b/tests/test_prioqueue.cpp deleted file mode 100644 index 0678af3..0000000 --- a/tests/test_prioqueue.cpp +++ /dev/null @@ -1,118 +0,0 @@ -// fusa:test REQ-PQ-001 -// fusa:test REQ-PQ-002 -// fusa:test REQ-PQ-003 -// fusa:test REQ-PQ-004 -// fusa:test REQ-PQ-005 -// fusa:test REQ-PQ-006 -// fusa:test REQ-PQ-007 -// fusa:test REQ-PQ-008 - -#include -#include -#include - -using namespace rcp; - -static std::shared_ptr make_mock(Zone z = Zone::FrontLeft) { - auto ctrl = std::make_shared(z); - return ctrl; -} - -// ── Basic send ──────────────────────────────────────────────────────────────── - -TEST_CASE("PrioQueue::send forwards command to inner and returns OK", "[prioqueue][REQ-PQ-001]") { - auto inner = make_mock(); - prioqueue::Controller pq(inner); - - Command cmd; - cmd.zone = Zone::FrontLeft; - Response resp; - auto ec = pq.send(Context::background(), cmd, resp); - REQUIRE_FALSE(ec); - REQUIRE(resp.status == ResponseStatus::OK); -} - -TEST_CASE("PrioQueue::zone returns inner zone", "[prioqueue][REQ-PQ-002]") { - auto inner = make_mock(Zone::RearRight); - prioqueue::Controller pq(inner); - REQUIRE(pq.zone() == Zone::RearRight); -} - -// ── Priority ordering ───────────────────────────────────────────────────────── - -TEST_CASE("PrioQueue prioritises Critical > High > Normal", "[prioqueue][REQ-PQ-003]") { - auto inner = make_mock(); - - // Block the dispatch thread by holding the queue mutex — instead, test - // via concurrent sends and verify no reorder crash occurs. Full ordering - // test requires inspection of dispatch order which mock doesn't expose, - // so we verify all three priorities complete without error. - prioqueue::Controller pq(inner); - - std::vector threads; - for (auto pri : {Priority::Normal, Priority::High, Priority::Critical}) { - threads.emplace_back([&pq, pri]() mutable { - Command cmd; - cmd.zone = Zone::FrontLeft; - cmd.priority = pri; - Response resp; - auto ec = pq.send(Context::background(), cmd, resp); - (void)ec; - }); - } - for (auto& t : threads) t.join(); -} - -// ── Context deadline ────────────────────────────────────────────────────────── - -TEST_CASE("PrioQueue::send returns ErrTimeout when context already expired", "[prioqueue][REQ-PQ-004]") { - auto inner = make_mock(); - prioqueue::Controller pq(inner); - - auto ctx = Context::with_timeout(std::chrono::milliseconds(-1)); - Command cmd; - cmd.zone = Zone::FrontLeft; - Response resp; - auto ec = pq.send(ctx, cmd, resp); - REQUIRE(ec == ErrTimeout); -} - -// ── Zone mismatch ───────────────────────────────────────────────────────────── - -TEST_CASE("PrioQueue::send returns ErrZoneMismatch on wrong zone", "[prioqueue][REQ-PQ-005]") { - auto inner = make_mock(Zone::FrontLeft); - prioqueue::Controller pq(inner); - - Command cmd; - cmd.zone = Zone::RearLeft; // mismatched - Response resp; - auto ec = pq.send(Context::background(), cmd, resp); - REQUIRE(ec == ErrZoneMismatch); -} - -// ── Subscribe ───────────────────────────────────────────────────────────────── - -TEST_CASE("PrioQueue::subscribe delegates to inner", "[prioqueue][REQ-PQ-006]") { - auto inner = make_mock(); - prioqueue::Controller pq(inner); - - std::shared_ptr ch; - auto ec = pq.subscribe(Context::background(), ch); - REQUIRE_FALSE(ec); - REQUIRE(ch != nullptr); - ch->close(); -} - -// ── Close ───────────────────────────────────────────────────────────────────── - -TEST_CASE("PrioQueue::close stops accepting work", "[prioqueue][REQ-PQ-007][REQ-PQ-008]") { - auto inner = make_mock(); - prioqueue::Controller pq(inner); - REQUIRE_FALSE(pq.close()); - - Command cmd; - cmd.zone = Zone::FrontLeft; - Response resp; - auto ec = pq.send(Context::background(), cmd, resp); - REQUIRE(ec == ErrClosed); -} diff --git a/tests/test_proxy.cpp b/tests/test_proxy.cpp deleted file mode 100644 index 757f592..0000000 --- a/tests/test_proxy.cpp +++ /dev/null @@ -1,108 +0,0 @@ -// fusa:test REQ-PROXY-001 -// fusa:test REQ-PROXY-002 -// fusa:test REQ-PROXY-003 -// fusa:test REQ-PROXY-004 -// fusa:test REQ-PROXY-005 -// fusa:test REQ-PROXY-006 -#include - -#include "rcp/legacy_mock.hpp" -#include "rcp/proxy.hpp" - -#include - -using namespace rcp; - -TEST_CASE("proxy: ProxyController forwards command within budget", "[proxy][REQ-PROXY-001]") { - auto inner = std::make_shared(Zone::FrontLeft); - proxy::ProxyController pc(inner); // default 50ms budget — not exceeded - - Command cmd; - cmd.zone = Zone::FrontLeft; - cmd.type = CommandType::Get; - Response resp; - REQUIRE_FALSE(pc.send(Context{}, cmd, resp)); - REQUIRE(resp.status == ResponseStatus::OK); -} - -TEST_CASE("proxy: zero latency budget makes the hop time out", "[proxy][REQ-PROXY-001]") { - auto inner = std::make_shared(Zone::FrontLeft); - proxy::Config cfg; - cfg.latency_budget = std::chrono::milliseconds(0); // deadline = now → already due - proxy::ProxyController pc(inner, cfg); - - Command cmd; - cmd.zone = Zone::FrontLeft; - cmd.type = CommandType::Get; - Response resp; - // The proxy derives a now+budget deadline and passes it down; the upstream - // observes the budget has elapsed and returns ErrTimeout instead of forwarding. - REQUIRE(pc.send(Context{}, cmd, resp) == ErrTimeout); -} - -TEST_CASE("proxy: ProxyController zone matches upstream", "[proxy][REQ-PROXY-006]") { - auto inner = std::make_shared(Zone::RearRight); - proxy::ProxyController pc(inner); - REQUIRE(pc.zone() == Zone::RearRight); -} - -TEST_CASE("proxy: ProxyRegistry lookup and send", "[proxy][REQ-PROXY-002]") { - auto reg = proxy::new_registry(); - auto inner = std::make_shared(Zone::RearLeft); - REQUIRE_FALSE(reg->add_route(inner)); - - std::shared_ptr ctrl; - REQUIRE_FALSE(reg->lookup(Zone::RearLeft, ctrl)); - - Command cmd; - cmd.zone = Zone::RearLeft; - cmd.type = CommandType::Get; - Response resp; - REQUIRE_FALSE(ctrl->send(Context{}, cmd, resp)); -} - -TEST_CASE("proxy: lookup unknown zone returns ErrNotFound", "[proxy][REQ-PROXY-004]") { - auto reg = proxy::new_registry(); - std::shared_ptr ctrl; - auto ec = reg->lookup(Zone::Central, ctrl); - REQUIRE(ec == ErrNotFound); -} - -TEST_CASE("proxy: deregister closes the upstream controller", "[proxy][REQ-PROXY-003]") { - auto reg = proxy::new_registry(); - auto inner = std::make_shared(Zone::RearLeft); - REQUIRE_FALSE(reg->add_route(inner)); - - REQUIRE_FALSE(reg->deregister(Zone::RearLeft)); - - // The upstream controller wrapped by the route is now closed. - Command cmd; cmd.zone = Zone::RearLeft; Response resp; - REQUIRE(inner->send(Context{}, cmd, resp) == ErrClosed); - - // And the route is gone. - std::shared_ptr ctrl; - REQUIRE(reg->lookup(Zone::RearLeft, ctrl) == ErrNotFound); -} - -TEST_CASE("proxy: ProxyRegistry close is idempotent", "[proxy][REQ-PROXY-005]") { - auto reg = proxy::new_registry(); - auto inner = std::make_shared(Zone::FrontRight); - REQUIRE_FALSE(reg->add_route(inner)); - REQUIRE_FALSE(reg->close()); - REQUIRE_FALSE(reg->close()); // second close is a no-op, still no error -} - -TEST_CASE("proxy: duplicate route returns ErrAlreadyExists", "[proxy]") { - auto reg = proxy::new_registry(); - auto a = std::make_shared(Zone::FrontLeft); - auto b = std::make_shared(Zone::FrontLeft); - REQUIRE_FALSE(reg->add_route(a)); - REQUIRE(reg->add_route(b) == ErrAlreadyExists); -} - -TEST_CASE("proxy: close closes all routes", "[proxy]") { - auto reg = proxy::new_registry(); - auto inner = std::make_shared(Zone::FrontRight); - REQUIRE_FALSE(reg->add_route(inner)); - REQUIRE_FALSE(reg->close()); -} diff --git a/tests/test_zonegroup.cpp b/tests/test_zonegroup.cpp deleted file mode 100644 index af50724..0000000 --- a/tests/test_zonegroup.cpp +++ /dev/null @@ -1,116 +0,0 @@ -// fusa:test REQ-ZG-001 -// fusa:test REQ-ZG-002 -// fusa:test REQ-ZG-003 -// fusa:test REQ-ZG-004 -// fusa:test REQ-ZG-005 -// fusa:test REQ-ZG-006 -#include - -#include "rcp/legacy_mock.hpp" -#include "rcp/proxy.hpp" -#include "rcp/zonegroup.hpp" - -#include -#include - -using namespace rcp; - -TEST_CASE("zonegroup: ZoneGroup::all() has 5 zones", "[zonegroup]") { - auto g = zonegroup::ZoneGroup::all(); - REQUIRE(g.size() == 5); -} - -TEST_CASE("zonegroup: ZoneGroup::rear() has 2 zones", "[zonegroup]") { - auto g = zonegroup::ZoneGroup::rear(); - REQUIRE(g.size() == 2); -} - -TEST_CASE("zonegroup: send_group dispatches to all zones", "[zonegroup]") { - legacy_mock::Registry reg; - - zonegroup::GroupRegistry gr(reg); - - Command cmd; - cmd.type = CommandType::Get; - - auto resp = gr.send_group(Context{}, zonegroup::ZoneGroup::all(), cmd); - REQUIRE(resp.ok()); - REQUIRE(resp.results.size() == 5); -} - -TEST_CASE("zonegroup: send_group returns results per zone", "[zonegroup]") { - legacy_mock::Registry reg; - zonegroup::GroupRegistry gr(reg); - - Command cmd; - cmd.type = CommandType::Set; - - auto resp = gr.send_group(Context{}, zonegroup::ZoneGroup::front(), cmd); - REQUIRE(resp.results.count(Zone::FrontLeft)); - REQUIRE(resp.results.count(Zone::FrontRight)); - REQUIRE(resp.errors().empty()); -} - -TEST_CASE("zonegroup: missing zone returns error in result", "[zonegroup]") { - legacy_mock::Registry reg; - // Deregister all zones to create an empty registry - auto ec = reg.close(); - (void)ec; - - // Fresh empty-ish registry: we need a test registry with no controllers - // Use a proxy registry with no routes - proxy::ProxyRegistry empty_reg; - zonegroup::GroupRegistry gr(empty_reg); - - Command cmd; - cmd.type = CommandType::Get; - - zonegroup::ZoneGroup g{Zone::FrontLeft}; - auto resp = gr.send_group(Context{}, g, cmd); - REQUIRE_FALSE(resp.ok()); -} - -TEST_CASE("zonegroup: errors() lists every failing zone", "[zonegroup][REQ-ZG-003][REQ-ZG-004]") { - proxy::ProxyRegistry empty_reg; // no routes → every lookup fails - zonegroup::GroupRegistry gr(empty_reg); - - Command cmd; cmd.type = CommandType::Get; - auto resp = gr.send_group(Context{}, zonegroup::ZoneGroup::front(), cmd); - - REQUIRE_FALSE(resp.ok()); - auto errs = resp.errors(); - REQUIRE(errs.size() == 2); - REQUIRE(std::find(errs.begin(), errs.end(), Zone::FrontLeft) != errs.end()); - REQUIRE(std::find(errs.begin(), errs.end(), Zone::FrontRight) != errs.end()); -} - -TEST_CASE("zonegroup: ZoneGroup is a copyable value type", "[zonegroup][REQ-ZG-005]") { - zonegroup::ZoneGroup a{Zone::FrontLeft, Zone::FrontRight}; - zonegroup::ZoneGroup b = a; // copy construct - b.add(Zone::Central); // mutate the copy only - - REQUIRE(a.size() == 2); // original is unaffected → value semantics - REQUIRE(b.size() == 3); - - zonegroup::ZoneGroup c; - c = a; // copy assign - REQUIRE(c.size() == 2); -} - -TEST_CASE("zonegroup: send_group honours the caller context deadline", - "[zonegroup][REQ-ZG-006]") { - legacy_mock::Registry reg; - zonegroup::GroupRegistry gr(reg); - - Command cmd; cmd.type = CommandType::Get; - // An already-expired deadline must propagate to every per-zone send. - auto ctx = Context::with_deadline(std::chrono::steady_clock::now() - - std::chrono::seconds(1)); - auto resp = gr.send_group(ctx, zonegroup::ZoneGroup::all(), cmd); - - REQUIRE_FALSE(resp.ok()); - REQUIRE(resp.results.size() == 5); - for (auto& kv : resp.results) { - REQUIRE(kv.second.error == ErrTimeout); // deadline observed downstream - } -}