diff --git a/.fusa.json b/.fusa.json index 1d2b3a2..ff5328d 100644 --- a/.fusa.json +++ b/.fusa.json @@ -3,7 +3,7 @@ "configVersion": "1.0", "project": { "name": "cpp-LIN", - "version": "0.1.0" + "version": "0.4.0" }, "sourceDirs": [ "src", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 053e593..ff248aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -248,7 +248,7 @@ jobs: - name: cpfusa init working-directory: cpp-LIN - run: ../cpp-FuSa/build/cpfusa init --name cpp-LIN --standard iso26262 --asil ASIL-B --project-version 0.1.0 --force || true + run: ../cpp-FuSa/build/cpfusa init --name cpp-LIN --standard iso26262 --asil ASIL-B --project-version 0.4.0 --force || true - name: cpfusa check working-directory: cpp-LIN @@ -448,7 +448,7 @@ jobs: - name: cpfusa init working-directory: cpp-LIN - run: ../cpp-FuSa/build/cpfusa init --name cpp-LIN --standard iso26262 --asil ASIL-B --project-version 0.1.0 --force || true + run: ../cpp-FuSa/build/cpfusa init --name cpp-LIN --standard iso26262 --asil ASIL-B --project-version 0.4.0 --force || true - name: Generate SARIF report working-directory: cpp-LIN diff --git a/CMakeLists.txt b/CMakeLists.txt index e69f502..387df6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.21) project(cpplin - VERSION 0.1.0 + VERSION 0.4.0 DESCRIPTION "C++ LIN bus library — RELAY-conformant, ISO 26262 ASIL-B" HOMEPAGE_URL "https://github.com/SoundMatt/cpp-LIN" LANGUAGES CXX @@ -34,6 +34,17 @@ endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") include(cmake/FetchDeps.cmake) +# ── Generated headers ───────────────────────────────────────────────────────── +# cli/version.hpp is generated from PROJECT_VERSION so the CLI's self-reported +# version (§12.1) can't drift from the project's own version a second time +# (see the v0.1.0-stale-for-two-releases fix). +set(CPPLIN_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cli/version.hpp.in + ${CPPLIN_GENERATED_DIR}/cli/version.hpp + @ONLY +) + # ── Library ─────────────────────────────────────────────────────────────────── add_library(cpplin_lib STATIC diff --git a/README.md b/README.md index f952427..c02b0f9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ of [go-LIN](https://github.com/SoundMatt/go-LIN) and is patterned after ### Features - **Virtual bus** — in-process, thread-safe LIN bus for testing and simulation +- **Mock module** — RELAY spec §13.7.1 canonical `mock` namespace (`lin::mock::Bus`) - **LDF parser** — parse LIN Description Files (`.ldf`), decode signals - **Master node** — schedule table runner with configurable slot timing - **Slave node** — publish response payloads for specific LIN frame IDs @@ -52,6 +53,7 @@ include/lin/ relay.hpp — RELAY v1.11 types (Protocol, Message, INode, …) lin.hpp — IBus, IMasterBus, Frame, Filter, free functions virtual/bus.hpp — In-process virtual LIN bus + mock/mock.hpp — RELAY spec §13.7.1 canonical `mock` module (alias of virtual::Bus) safety/e2e.hpp — E2E Protector / Receiver ldf/parser.hpp — LDF file parser / signal decoder master/node.hpp — Master schedule runner diff --git a/ROADMAP.md b/ROADMAP.md index 2782728..a510f07 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # Roadmap — cpp-LIN -## v0.1.0 — Initial Release (current) +## v0.1.0 — Initial Release - [x] Virtual bus with Classic and Enhanced checksum support - [x] Protected Identifier (PID) computation and verification @@ -8,13 +8,49 @@ - [x] Master node with schedule table runner (`std::stop_token`) - [x] Slave node with per-ID response registration - [x] E2E safety: Protector and Receiver (CRC-16/CCITT-FALSE, 10-byte header) -- [x] RELAY v1.11 adapter (`relay::INode`) +- [x] RELAY v1.10 adapter (`relay::INode`) - [x] CLI binary (`cpp-lin-cli`) for relay conform/interop - [x] ISO 26262 ASIL-B requirements traceability - [x] CI pipeline: build, test, coverage, sanitizers, RELAY conform, cpfusa - [x] Docker multi-stage image -## v0.2.0 — Enhanced Bus Features +## v0.2.0 — Full Safety & Cyber Pack + +- [x] TARA (ISO/SAE 21434, IEC 62443-4-1 SL-2): threats, controls, `tara.json` +- [x] FMEA (ISO 26262-9) linked to hazards, `fmea.json` +- [x] SAS (ISO 26262-6 §7.4): 9-module architecture specification +- [x] Boundary diagram (ISO 26262-6 §7.4.1): `boundary.mermaid` +- [x] Safety Manual (ISO 26262-6 §7, Part 10 §9): `SAFETY_MANUAL.md` +- [x] `SECURITY.md` + `INCIDENT-RESPONSE.md`: coordinated disclosure, ISN process +- [x] IEC 62443: expanded `.fusa-iec62443.json` security requirements +- [x] Full requirements traceability closure in `.fusa-reqs.json` +- [x] CI: `cpfusa lint`/`cyber` hard gates; ThreadSanitizer gate added + +## v0.3.0 — RELAY spec v1.11 conformance + +- [x] `kSpecVersion` bumped 1.10 → 1.11 (`relay.hpp`, `lin.hpp`) +- [x] CLI version/capabilities JSON report `spec_version` 1.11 +- [x] `RELAY_BUILD_CLI` CMake alias (§17.7, since removed — see v0.4.0) +- [x] Test vectors updated to spec_version 1.11 + +## v0.4.0 — RELAY ecosystem audit fixes (current) + +- [x] LIN-specific error category (`lin::Errc::invalid_frame`) distinct from + `ErrPayloadTooLarge` for out-of-range frame IDs (spec §5.3) +- [x] `Adapt()` subscription thread no longer captures a raw `this` — fixes a + use-after-free if the adapter is dropped before the bus is closed +- [x] `convert` writes the correct `ErrInvalidFrame` sentinel name to stderr +- [x] `capabilities` reports an actual transport backend (`virtual`), not the + protocol name +- [x] `version`/`capabilities`/`status` self-report the real release version + (driven by `PROJECT_VERSION`, not a second hard-coded literal) +- [x] `version`/`status` support `--format text|json` with exit `2` on an + unrecognized format or unknown argument (spec §11.1) +- [x] `tool` field lowercased to `cpp-lin` (spec §13.2 CLI-binary-name + convention), matching peers like `go-can` +- [x] `lin::mock` canonical module (spec §13.7.1), aliasing `lin::virt::Bus` + +## v0.5.0 — Enhanced Bus Features - [ ] Sleep/wake frame support (LIN 2.x go-to-sleep command) - [ ] Break field and sync field simulation in virtual bus @@ -22,19 +58,19 @@ - [ ] `IMasterBus::set_schedule` hot-swap without restarting `run()` - [ ] `IBus::unsubscribe` to cancel an active subscription -## v0.3.0 — Diagnostics +## v0.6.0 — Diagnostics - [ ] ISO 15765-3 / LIN Transport Layer (segmented diagnostic frames) - [ ] UDS (ISO 14229) over LIN TP adapter - [ ] Diagnostic session management in master node -## v0.4.0 — Hardware Abstraction +## v0.7.0 — Hardware Abstraction - [ ] Serial/UART hardware bus implementation (Linux `termios`) - [ ] SocketLIN adapter (if Linux kernel support is available) - [ ] PEAK LIN hardware adapter -## v0.5.0 — AUTOSAR-Lite +## v0.8.0 — AUTOSAR-Lite - [ ] LIN Interface (LINIf) / LIN Driver (LINDrv) API-compatible shim - [ ] COM-stack signal packing / unpacking with endianness support diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index fb824ea..ee7367b 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -1,3 +1,6 @@ add_executable(cpp-lin-cli main.cpp) target_link_libraries(cpp-lin-cli PRIVATE cpplin_lib) -target_include_directories(cpp-lin-cli PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(cpp-lin-cli PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CPPLIN_GENERATED_DIR}/cli +) diff --git a/cli/json.hpp b/cli/json.hpp index 727b9a5..1d6dbd8 100644 --- a/cli/json.hpp +++ b/cli/json.hpp @@ -8,6 +8,7 @@ #pragma once +#include "version.hpp" #include #include #include @@ -132,7 +133,11 @@ inline lin::Frame parse_frame_json(const std::string& json) { lin::Frame f{}; uint32_t id_val = 0; if (!detail::extract_u32(json, "id", id_val)) - throw std::runtime_error("ErrInvalidInput: missing or invalid 'id'"); + // Not a spec §5 sentinel: JSON that doesn't even parse into a + // lin.Frame never reaches lin::validate_frame(), so there is no + // ErrInvalidFrame to name here — describe the parse failure plainly + // instead of fabricating a sentinel-looking name that doesn't exist. + throw std::runtime_error("invalid input JSON: missing or invalid 'id' field"); f.id = static_cast(id_val); detail::extract_bytes(json, "data", f.data); uint32_t ct = 0; @@ -173,28 +178,41 @@ inline std::string message_to_json(const relay::Message& m) { // fusa:req REQ-CLI-001 inline std::string version_json() { return "{" - "\"tool\":\"cpp-LIN\"," + "\"tool\":\"cpp-lin\"," "\"protocol\":\"LIN\"," "\"protocol_int\":3," - "\"version\":\"0.1.0\"," + "\"version\":\"" + std::string(kToolVersion) + "\"," "\"spec_version\":\"1.11\"," "\"language\":\"cpp\"," "\"runtime\":\"c++17\"" "}"; } +// fusa:req REQ-CLI-001 +// Human-readable rendering of version_json() for `version --format text` (spec §11.1). +inline std::string version_text() { + std::ostringstream o; + o << "tool: cpp-lin\n" + << "protocol: LIN\n" + << "version: " << kToolVersion << "\n" + << "spec: 1.11\n" + << "language: cpp\n" + << "runtime: c++17\n"; + return o.str(); +} + // fusa:req REQ-CLI-002 inline std::string capabilities_json() { return "{" "\"kind\":\"capabilities\"," - "\"tool\":\"cpp-LIN\"," + "\"tool\":\"cpp-lin\"," "\"protocol\":\"LIN\"," "\"protocol_int\":3," - "\"version\":\"0.1.0\"," + "\"version\":\"" + std::string(kToolVersion) + "\"," "\"spec_version\":\"1.11\"," "\"commands\":[\"version\",\"capabilities\",\"status\",\"convert\"]," - "\"transports\":[\"LIN\"]," - "\"features\":[\"ldf\",\"e2e\",\"master\",\"slave\",\"virtual\"]," + "\"transports\":[\"virtual\"]," + "\"features\":[\"ldf\",\"e2e\",\"master\",\"slave\",\"virtual\",\"mock\"]," "\"interfaces\":[\"IBus\",\"IMasterBus\",\"INode\"]," "\"optional_interfaces\":[\"IHealthProvider\",\"IMetricsProvider\",\"IDrainer\"]," "\"adapt\":true" @@ -205,8 +223,8 @@ inline std::string capabilities_json() { inline std::string status_json() { return "{" "\"protocol\":\"LIN\"," - "\"tool\":\"cpp-LIN\"," - "\"version\":\"0.1.0\"," + "\"tool\":\"cpp-lin\"," + "\"version\":\"" + std::string(kToolVersion) + "\"," "\"healthy\":true," "\"connected\":false," "\"endpoint\":\"\"," @@ -214,4 +232,17 @@ inline std::string status_json() { "}"; } +// fusa:req REQ-CLI-003 +// Human-readable rendering of status_json() for `status --format text` (spec §11.1). +inline std::string status_text() { + std::ostringstream o; + o << "protocol: LIN\n" + << "tool: cpp-lin\n" + << "version: " << kToolVersion << "\n" + << "healthy: true\n" + << "connected: false\n" + << "endpoint: (none)\n"; + return o.str(); +} + } // namespace cli diff --git a/cli/main.cpp b/cli/main.cpp index 816cee3..2440f78 100644 --- a/cli/main.cpp +++ b/cli/main.cpp @@ -18,9 +18,43 @@ #include #include -static int cmd_version() { std::cout << cli::version_json() << "\n"; return 0; } +// Parses an optional "--format " flag from argv[2..argc) for commands +// that accept it (spec §11.1: `version [--format text|json]`, `status +// [--format text|json]`). Defaults `fmt` to "json". Any other argument +// (including an unrecognized --format value) is an invalid-args error: the +// caller is responsible for validating `fmt` against the command's allowed +// set and exiting 2 per spec §11.1/§11.3. +static bool parse_format_flag(int argc, char* argv[], std::string& fmt) { + fmt = "json"; + for (int i = 2; i < argc; ++i) { + std::string arg = argv[i]; + if (arg == "--format" && i + 1 < argc) { + fmt = argv[++i]; + } else { + std::cerr << "ErrInvalidArgs: unrecognized argument '" << arg << "'\n"; + return false; + } + } + return true; +} + +// fusa:req REQ-CLI-001 +static int cmd_version(const std::string& fmt) { + if (fmt == "json") { std::cout << cli::version_json() << "\n"; return 0; } + if (fmt == "text") { std::cout << cli::version_text(); return 0; } + std::cerr << "ErrInvalidArgs: unsupported --format '" << fmt << "' (want text|json)\n"; + return 2; +} + static int cmd_capabilities() { std::cout << cli::capabilities_json() << "\n"; return 0; } -static int cmd_status() { std::cout << cli::status_json() << "\n"; return 0; } + +// fusa:req REQ-CLI-003 +static int cmd_status(const std::string& fmt) { + if (fmt == "json") { std::cout << cli::status_json() << "\n"; return 0; } + if (fmt == "text") { std::cout << cli::status_text(); return 0; } + std::cerr << "ErrInvalidArgs: unsupported --format '" << fmt << "' (want text|json)\n"; + return 2; +} // fusa:req REQ-CLI-006 static int cmd_convert(const std::string& protocol) { @@ -44,7 +78,10 @@ static int cmd_convert(const std::string& protocol) { try { lin::validate_frame(f); } catch (const lin::ErrInvalidFrame& e) { - std::cerr << "ErrInvalidInput: " << e.what() << "\n"; + // spec §11.2: convert MUST write the sentinel error name (§5) to + // stderr — the thrown type here is lin::ErrInvalidFrame, so that is + // the name relay interop/relay conform expect, not a made-up one. + std::cerr << "ErrInvalidFrame: " << e.what() << "\n"; return 1; } @@ -64,9 +101,17 @@ int main(int argc, char* argv[]) { std::string cmd = argv[1]; - if (cmd == "version") return cmd_version(); + if (cmd == "version") { + std::string fmt; + if (!parse_format_flag(argc, argv, fmt)) return 2; + return cmd_version(fmt); + } if (cmd == "capabilities") return cmd_capabilities(); - if (cmd == "status") return cmd_status(); + if (cmd == "status") { + std::string fmt; + if (!parse_format_flag(argc, argv, fmt)) return 2; + return cmd_status(fmt); + } if (cmd == "convert") { std::string protocol; diff --git a/cli/version.hpp.in b/cli/version.hpp.in new file mode 100644 index 0000000..8c6ce84 --- /dev/null +++ b/cli/version.hpp.in @@ -0,0 +1,17 @@ +// Copyright (c) 2026 Matt Jones. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// cli/version.hpp.in — configure_file template for the CLI's self-reported +// version. Generated into ${CMAKE_CURRENT_BINARY_DIR}/version.hpp so the +// version string is driven by CMakeLists.txt's project(cpplin VERSION ...) +// rather than a second hard-coded literal (spec §12.1). + +#pragma once + +namespace cli { + +inline constexpr const char* kToolVersion = "@PROJECT_VERSION@"; + +} // namespace cli diff --git a/include/lin/lin.hpp b/include/lin/lin.hpp index 1610b1e..ba38d71 100644 --- a/include/lin/lin.hpp +++ b/include/lin/lin.hpp @@ -107,6 +107,23 @@ inline std::error_code ErrNotConnected() noexcept { return relay::ErrNotConne inline std::error_code ErrTimeout() noexcept { return relay::ErrTimeout(); } inline std::error_code ErrPayloadTooLarge() noexcept { return relay::ErrPayloadTooLarge(); } +// ── LIN-specific error category (spec §5.2, §5.3, §5.4) ────────────────────── +// +// A structurally invalid frame (bad ID bits, out-of-range ID) is distinct from +// a too-large payload (§5.3): "ErrInvalidFrame and ErrPayloadTooLarge are +// distinct ... ValidateFrame MUST return ErrInvalidFrame; it MUST NOT return +// ErrPayloadTooLarge." IBus/IMasterBus (§8.3) are LIN's own protocol interface, +// not the relay.Node interface, so — like CAN's ErrInvalidFrame (§5.4, "not a +// relay sentinel") — this code is not mapped to any relay::Errc value. + +// fusa:req REQ-LIN-015 +enum class Errc : int { + invalid_frame = 1, +}; + +const std::error_category& error_category() noexcept; +std::error_code make_error_code(Errc e) noexcept; + // ── Free functions ──────────────────────────────────────────────────────────── // Computes the Protected Identifier (PID) for a 6-bit LIN frame ID. @@ -205,3 +222,9 @@ using IDrainer = relay::IDrainer; std::unique_ptr adapt(std::shared_ptr bus); } // namespace lin + +// fusa:req REQ-LIN-015 — allow implicit construction of std::error_code from lin::Errc. +namespace std { +template<> +struct is_error_code_enum : true_type {}; +} // namespace std diff --git a/include/lin/mock/mock.hpp b/include/lin/mock/mock.hpp new file mode 100644 index 0000000..860125b --- /dev/null +++ b/include/lin/mock/mock.hpp @@ -0,0 +1,30 @@ +// Copyright (c) 2026 Matt Jones. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// mock/mock.hpp — the RELAY spec's canonical `mock` module (§13.7.1). +// +// §13.7.1 requires every implementation to provide, and name, a module +// literally called `mock`: "in-process mock implementation (§7 Form 2 New)". +// This is a distinct registry entry from `virtual` (§13.7.2, "in-process +// virtual bus/transport ... for bus protocols"), but cpp-LIN's virtual bus +// (lin::virt::Bus) already IS a full in-process IBus/IMasterBus +// implementation — it is used both for tests and as the documented +// development bus. Rather than duplicate that type, `mock` is provided as a +// thin alias so the canonical name is present without a second +// implementation to keep in sync. +// +// fusa:req REQ-LIN-011 REQ-LIN-012 + +#pragma once + +#include + +namespace lin::mock { + +// Bus is the RELAY-spec-canonical `mock` in-process implementation. +// Identical to lin::virt::Bus (§13.7.2) — see file header for rationale. +using Bus = lin::virt::Bus; + +} // namespace lin::mock diff --git a/src/lin.cpp b/src/lin.cpp index cf4d8fd..2bece40 100644 --- a/src/lin.cpp +++ b/src/lin.cpp @@ -65,6 +65,33 @@ void validate_frame(const Frame& f) { throw ErrInvalidFrame("diagnostic frame 0x" + [f]{ char buf[8]; std::snprintf(buf, sizeof(buf), "%02X", f.id); return std::string(buf); }() + " must use classic checksum"); } +// ── LIN error category ── REQ-LIN-015 ──────────────────────────────────────── + +namespace { + +class LinErrorCategory : public std::error_category { +public: + const char* name() const noexcept override { return "lin"; } + + std::string message(int ev) const override { + switch (static_cast(ev)) { + case Errc::invalid_frame: return "lin: invalid frame"; + default: return "lin: unknown error"; + } + } +}; + +} // anonymous namespace + +const std::error_category& error_category() noexcept { + static LinErrorCategory cat; + return cat; +} + +std::error_code make_error_code(Errc e) noexcept { + return {static_cast(e), error_category()}; +} + // ── to_message / from_message ── REQ-LIN-011 REQ-LIN-012 ──────────────────── relay::Message to_message(const Frame& f) { @@ -118,16 +145,22 @@ class LinAdapter : public relay::INode { relay::Protocol protocol() const noexcept override { return relay::Protocol::LIN; } // fusa:req REQ-ADAPT-002 REQ-ADAPT-003 + // + // relay::INode::send() (§10.1) is documented to return only ErrClosed, + // ErrNotConnected, ErrTimeout, or ErrPayloadTooLarge, so an invalid ID is + // approximated as ErrPayloadTooLarge here rather than lin::Errc::invalid_frame + // (which would be correct for IBus::publish() directly, but isn't one of + // the relay.Node sentinels). Delegate ID parsing to from_message() — the + // single source of truth for LIN ID parsing/validation — instead of + // re-implementing it inline. std::error_code send(relay::Message msg) override { - unsigned long long id_val{}; + Frame f; try { - id_val = std::stoull(msg.id); - } catch (...) { + f = from_message(msg); + } catch (const ErrInvalidFrame&) { return relay::make_error_code(relay::Errc::payload_too_large); } - if (id_val > kLINMaxID) - return relay::make_error_code(relay::Errc::payload_too_large); - return bus_->publish(static_cast(id_val), std::move(msg.payload)); + return bus_->publish(f.id, std::move(f.data)); } // fusa:req REQ-ADAPT-004 @@ -142,7 +175,15 @@ class LinAdapter : public relay::INode { int depth = cfg.effective_depth(64); auto out = std::make_shared>(static_cast(depth)); - std::thread([this, frame_ch = std::move(frames), out, + // REQ-ADAPT-004: per spec §10.5.2 point 7, the Seq counter is owned by + // the subscription goroutine/thread, not shared across subscriptions or + // Adapt() calls. Capturing it by shared_ptr (rather than the adapter's + // `this`) also means the thread never dereferences the LinAdapter once + // it may have been destroyed — the thread only touches frame_ch/out/seq, + // all of which it owns shared references to. + auto seq = std::make_shared>(0); + + std::thread([frame_ch = std::move(frames), out, seq, bp = cfg.back_pressure]() mutable { while (true) { @@ -151,7 +192,7 @@ class LinAdapter : public relay::INode { relay::Message msg = to_message(*opt_f); msg.timestamp = std::chrono::system_clock::now(); - msg.seq = ++seq_; + msg.seq = ++(*seq); switch (bp) { case relay::BackPressurePolicy::DropNewest: @@ -176,7 +217,6 @@ class LinAdapter : public relay::INode { private: std::shared_ptr bus_; - std::atomic seq_{0}; }; } // anonymous namespace diff --git a/src/master/node.cpp b/src/master/node.cpp index 919221e..a1dbf00 100644 --- a/src/master/node.cpp +++ b/src/master/node.cpp @@ -22,11 +22,11 @@ Node::Node(std::shared_ptr bus) // fusa:req REQ-MASTER-010 REQ-MASTER-011 REQ-MASTER-012 std::error_code Node::set_schedule(std::vector entries) { if (entries.empty()) - return relay::make_error_code(relay::Errc::payload_too_large); // empty schedule + return lin::make_error_code(lin::Errc::invalid_frame); // empty schedule for (const auto& e : entries) { if (e.id > kLINMaxID) - return relay::make_error_code(relay::Errc::payload_too_large); + return lin::make_error_code(lin::Errc::invalid_frame); } // defensive copy stored @@ -53,7 +53,7 @@ std::pair Node::send_header(uint8_t id) { // fusa:req REQ-MASTER-007 REQ-MASTER-008 REQ-MASTER-009 REQ-MASTER-013 std::error_code Node::run(const std::atomic& stop) { if (schedule_.empty()) - return relay::make_error_code(relay::Errc::payload_too_large); + return lin::make_error_code(lin::Errc::invalid_frame); while (!stop.load()) { for (const auto& slot : schedule_) { diff --git a/src/slave/node.cpp b/src/slave/node.cpp index baabe3c..8da4e08 100644 --- a/src/slave/node.cpp +++ b/src/slave/node.cpp @@ -18,8 +18,11 @@ Node::Node(std::shared_ptr bus) // fusa:req REQ-SLAVE-002 REQ-SLAVE-003 REQ-SLAVE-004 REQ-SLAVE-008 std::error_code Node::set_response(uint8_t id, std::vector data) { + // Same mis-mapping as virtual::Bus::do_publish()/send_header() and + // master::Node::set_schedule()/run() — out-of-range ID is a structural + // violation (§5.3 ErrInvalidFrame), not a payload-size violation. if (id > kLINMaxID) - return relay::make_error_code(relay::Errc::payload_too_large); + return lin::make_error_code(lin::Errc::invalid_frame); auto err = bus_->publish(id, data); if (err) return err; diff --git a/src/virtual/bus.cpp b/src/virtual/bus.cpp index fb99dac..2c7626a 100644 --- a/src/virtual/bus.cpp +++ b/src/virtual/bus.cpp @@ -33,7 +33,7 @@ std::shared_ptr Bus::create() { std::error_code Bus::do_publish(uint8_t id, std::vector data, ChecksumType ct) { if (id > kLINMaxID) { error_count_.fetch_add(1); - return relay::make_error_code(relay::Errc::payload_too_large); + return lin::make_error_code(lin::Errc::invalid_frame); } std::unique_lock lk(mu_); @@ -65,7 +65,7 @@ std::error_code Bus::publish_classic(uint8_t id, std::vector data) { std::pair Bus::send_header(uint8_t id) { if (id > kLINMaxID) { error_count_.fetch_add(1); - return {Frame{}, relay::make_error_code(relay::Errc::payload_too_large)}; + return {Frame{}, lin::make_error_code(lin::Errc::invalid_frame)}; } std::shared_lock lk(mu_); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d29c2f9..556d4b1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable(cpplin_tests test_master.cpp test_slave.cpp test_cli.cpp + test_mock.cpp ) target_link_libraries(cpplin_tests @@ -20,6 +21,7 @@ target_include_directories(cpplin_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ${CMAKE_CURRENT_SOURCE_DIR}/../cli + ${CPPLIN_GENERATED_DIR}/cli ) include(CTest) diff --git a/tests/test_cli.cpp b/tests/test_cli.cpp index 3772673..6ba9bad 100644 --- a/tests/test_cli.cpp +++ b/tests/test_cli.cpp @@ -63,32 +63,58 @@ TEST_CASE("message_to_json: timestamp is zero string", "[cli][REQ-CLI-005]") { TEST_CASE("version_json: contains expected fields", "[cli][REQ-CLI-001]") { auto json = version_json(); - CHECK(json.find("cpp-LIN") != std::string::npos); + CHECK(json.find("\"tool\":\"cpp-lin\"") != std::string::npos); CHECK(json.find("\"protocol\":\"LIN\"") != std::string::npos); CHECK(json.find("\"protocol_int\":3") != std::string::npos); CHECK(json.find("\"version\"") != std::string::npos); CHECK(json.find("\"spec_version\"") != std::string::npos); } +TEST_CASE("version_text: contains expected fields", "[cli][REQ-CLI-001]") { + auto text = version_text(); + CHECK(text.find("cpp-lin") != std::string::npos); + CHECK(text.find("LIN") != std::string::npos); + CHECK(text.find(kToolVersion) != std::string::npos); +} + // ── capabilities_json ── REQ-CLI-002 ───────────────────────────────────────── TEST_CASE("capabilities_json: contains expected fields", "[cli][REQ-CLI-002]") { auto json = capabilities_json(); CHECK(json.find("capabilities") != std::string::npos); + CHECK(json.find("\"tool\":\"cpp-lin\"") != std::string::npos); CHECK(json.find("\"protocol\":\"LIN\"") != std::string::npos); CHECK(json.find("\"protocol_int\":3") != std::string::npos); CHECK(json.find("convert") != std::string::npos); CHECK(json.find("version") != std::string::npos); } +TEST_CASE("capabilities_json: transports lists a transport backend, not the protocol", "[cli][REQ-CLI-002]") { + auto json = capabilities_json(); + CHECK(json.find("\"transports\":[\"virtual\"]") != std::string::npos); + CHECK(json.find("\"transports\":[\"LIN\"]") == std::string::npos); +} + +TEST_CASE("capabilities_json: features lists the mock module (spec 13.7.1)", "[cli][REQ-CLI-002]") { + auto json = capabilities_json(); + CHECK(json.find("\"mock\"") != std::string::npos); +} + // ── status_json ── REQ-CLI-003 ──────────────────────────────────────────────── TEST_CASE("status_json: contains expected fields", "[cli][REQ-CLI-003]") { auto json = status_json(); + CHECK(json.find("\"tool\":\"cpp-lin\"") != std::string::npos); CHECK(json.find("\"protocol\":\"LIN\"") != std::string::npos); CHECK(json.find("\"healthy\":true") != std::string::npos); } +TEST_CASE("status_text: contains expected fields", "[cli][REQ-CLI-003]") { + auto text = status_text(); + CHECK(text.find("cpp-lin") != std::string::npos); + CHECK(text.find("healthy") != std::string::npos); +} + // ── round-trip: parse → to_message → message_to_json ───────────────────────── TEST_CASE("RELAY vector: lin-frame enhanced checksum", "[cli][REQ-CLI-004][REQ-CLI-005]") { diff --git a/tests/test_lin.cpp b/tests/test_lin.cpp index 52ee33c..accc24f 100644 --- a/tests/test_lin.cpp +++ b/tests/test_lin.cpp @@ -270,6 +270,16 @@ TEST_CASE("ErrNoResponse is throwable", "[lin][REQ-LIN-014][REQ-LIN-021]") { REQUIRE_THROWS_AS(throw ErrNoResponse(), ErrNoResponse); } +TEST_CASE("lin::Errc::invalid_frame is distinct from every relay sentinel", "[lin][REQ-LIN-015]") { + auto err = make_error_code(Errc::invalid_frame); + CHECK(err); + CHECK(err != relay::ErrClosed()); + CHECK(err != relay::ErrNotConnected()); + CHECK(err != relay::ErrTimeout()); + CHECK(err != relay::ErrPayloadTooLarge()); + CHECK_FALSE(err.message().empty()); +} + TEST_CASE("kSpecVersion is non-empty", "[lin]") { CHECK(!std::string(kSpecVersion).empty()); } diff --git a/tests/test_mock.cpp b/tests/test_mock.cpp new file mode 100644 index 0000000..6aeddd6 --- /dev/null +++ b/tests/test_mock.cpp @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Matt Jones. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// fusa:test REQ-LIN-011 REQ-LIN-012 + +// Verifies the RELAY spec §13.7.1 canonical `mock` module is present and +// usable as a full IMasterBus implementation, same as lin::virt::Bus. + +#include +#include +#include + +using namespace lin; + +TEST_CASE("lin::mock::Bus is the same type as lin::virt::Bus", "[mock]") { + STATIC_CHECK(std::is_same_v); +} + +TEST_CASE("lin::mock::Bus is usable as a full IMasterBus", "[mock]") { + auto bus = mock::Bus::create(); + REQUIRE(bus != nullptr); + + auto err = bus->publish(0x10, {0xAA, 0xBB}); + CHECK_FALSE(err); + + auto [f, herr] = bus->send_header(0x10); + CHECK_FALSE(herr); + CHECK(f.id == 0x10); + CHECK(f.data == std::vector{0xAA, 0xBB}); + + (void)bus->close(); +}