diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41e60f7..e12aa4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -377,10 +377,10 @@ jobs: - uses: actions/checkout@v4 - name: Build Docker image - run: docker build --target test -t cpp-lin-test . + run: docker build -f docker/Dockerfile --target test -t cpp-lin-test . - name: Smoke test - run all tests in container - run: docker run --rm cpp-lin-test --reporter compact + run: docker run --rm cpp-lin-test sarif: name: SARIF upload diff --git a/include/lin/master/node.hpp b/include/lin/master/node.hpp index cf1bc4d..24be6ed 100644 --- a/include/lin/master/node.hpp +++ b/include/lin/master/node.hpp @@ -18,9 +18,9 @@ #pragma once #include +#include #include #include -#include #include namespace lin::master { @@ -52,14 +52,14 @@ class Node { // fusa:req REQ-MASTER-002 std::pair send_header(uint8_t id); - // Executes the schedule table repeatedly until the stop_token is requested. + // Executes the schedule table repeatedly until stop is set to true. // Each slot transmits a header, waits for a slave response, then sleeps // for the slot's configured delay. Per-slot errors invoke on_error but do // not abort the schedule. // Returns an error immediately if the schedule is empty. // fusa:req REQ-MASTER-003 REQ-MASTER-004 REQ-MASTER-005 REQ-MASTER-006 // fusa:req REQ-MASTER-007 REQ-MASTER-008 REQ-MASTER-009 REQ-MASTER-013 - std::error_code run(std::stop_token token); + std::error_code run(const std::atomic& stop); private: std::shared_ptr bus_; diff --git a/src/lin.cpp b/src/lin.cpp index a2a68fd..257d6c4 100644 --- a/src/lin.cpp +++ b/src/lin.cpp @@ -134,11 +134,11 @@ class LinAdapter : public relay::INode { int depth = cfg.effective_depth(64); auto out = std::make_shared>(static_cast(depth)); - std::thread([this, frames = std::move(frames), out, + std::thread([this, frame_ch = std::move(frames), out, bp = cfg.back_pressure]() mutable { while (true) { - auto opt_f = frames->recv(); + auto opt_f = frame_ch->recv(); if (!opt_f) break; relay::Message msg = to_message(*opt_f); diff --git a/src/master/node.cpp b/src/master/node.cpp index 284866a..919221e 100644 --- a/src/master/node.cpp +++ b/src/master/node.cpp @@ -51,13 +51,13 @@ std::pair Node::send_header(uint8_t id) { // fusa:req REQ-MASTER-003 REQ-MASTER-004 REQ-MASTER-005 REQ-MASTER-006 // fusa:req REQ-MASTER-007 REQ-MASTER-008 REQ-MASTER-009 REQ-MASTER-013 -std::error_code Node::run(std::stop_token token) { +std::error_code Node::run(const std::atomic& stop) { if (schedule_.empty()) return relay::make_error_code(relay::Errc::payload_too_large); - while (!token.stop_requested()) { + while (!stop.load()) { for (const auto& slot : schedule_) { - if (token.stop_requested()) return {}; + if (stop.load()) return {}; auto [f, err] = bus_->send_header(slot.id); if (err) { @@ -69,7 +69,7 @@ std::error_code Node::run(std::stop_token token) { if (slot.delay_ms > 0) { auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(slot.delay_ms); - while (!token.stop_requested() && + while (!stop.load() && std::chrono::steady_clock::now() < deadline) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } diff --git a/tests/test_lin.cpp b/tests/test_lin.cpp index fdfc61f..52ee33c 100644 --- a/tests/test_lin.cpp +++ b/tests/test_lin.cpp @@ -80,11 +80,11 @@ TEST_CASE("protect_id: preserves lower 6 bits", "[lin][REQ-LIN-018]") { } TEST_CASE("protect_id: known vector ID=0x10", "[lin][REQ-LIN-004][REQ-LIN-005]") { - // ID=0x10 (16 decimal): bits 0-5 = 010000 - // P0 = 0^0^0^0 = 0 → bit 6 = 0 - // P1 = NOT(0^0^0^1) = NOT(1) = 0 → bit 7 = 0 - // PID = 0x10 | 0x00 | 0x00 = 0x10 - CHECK(protect_id(0x10) == 0x10); + // ID=0x10 (16 decimal): bits 0-5 = 010000 (bit4=1, rest 0) + // P0 = ID0^ID1^ID2^ID4 = 0^0^0^1 = 1 → bit 6 = 1 + // P1 = NOT(ID1^ID3^ID4^ID5) = NOT(0^0^1^0) = NOT(1) = 0 → bit 7 = 0 + // PID = 0x10 | 0x40 = 0x50 + CHECK(protect_id(0x10) == 0x50); } TEST_CASE("protect_id: known vector ID=0x00", "[lin][REQ-LIN-004][REQ-LIN-005]") { diff --git a/tests/test_master.cpp b/tests/test_master.cpp index 1a3aef5..23e3739 100644 --- a/tests/test_master.cpp +++ b/tests/test_master.cpp @@ -22,7 +22,7 @@ using namespace lin::master; TEST_CASE("Node is constructible from a bus", "[master][REQ-MASTER-001]") { auto bus = Bus::create(); Node node(bus); - bus->close(); + (void)bus->close(); } TEST_CASE("set_schedule rejects empty schedule", "[master][REQ-MASTER-010]") { @@ -30,7 +30,7 @@ TEST_CASE("set_schedule rejects empty schedule", "[master][REQ-MASTER-010]") { Node node(bus); auto err = node.set_schedule({}); CHECK(err); - bus->close(); + (void)bus->close(); } TEST_CASE("set_schedule rejects invalid frame ID", "[master][REQ-MASTER-011]") { @@ -38,14 +38,14 @@ TEST_CASE("set_schedule rejects invalid frame ID", "[master][REQ-MASTER-011]") { Node node(bus); auto err = node.set_schedule({{0x40, 0}}); // 0x40 > 0x3F CHECK(err); - bus->close(); + (void)bus->close(); } TEST_CASE("set_schedule accepts valid schedule", "[master][REQ-MASTER-012]") { auto bus = Bus::create(); Node node(bus); REQUIRE_FALSE(node.set_schedule({{0x10, 0}, {0x20, 0}})); - bus->close(); + (void)bus->close(); } TEST_CASE("set_schedule stores defensive copy", "[master][REQ-MASTER-012]") { @@ -55,28 +55,27 @@ TEST_CASE("set_schedule stores defensive copy", "[master][REQ-MASTER-012]") { REQUIRE_FALSE(node.set_schedule(sched)); sched[0].id = 0x20; // mutate caller's copy // node's schedule should still have 0x10 — verified by running - bus->close(); + (void)bus->close(); } TEST_CASE("send_header delegates to bus", "[master][REQ-MASTER-002]") { auto bus = Bus::create(); - bus->publish(0x10, {0xAA}); + (void)bus->publish(0x10, {0xAA}); Node node(bus); auto [f, err] = node.send_header(0x10); REQUIRE_FALSE(err); CHECK(f.id == 0x10); CHECK(f.data == std::vector{0xAA}); - bus->close(); + (void)bus->close(); } TEST_CASE("run returns error for empty schedule", "[master][REQ-MASTER-009]") { auto bus = Bus::create(); Node node(bus); - std::stop_source ss; - ss.request_stop(); - auto err = node.run(ss.get_token()); + std::atomic stop{true}; + auto err = node.run(stop); CHECK(err); // empty schedule - bus->close(); + (void)bus->close(); } TEST_CASE("run iterates schedule and invokes callbacks", "[master][REQ-MASTER-003][REQ-MASTER-004][REQ-MASTER-006][REQ-MASTER-007]") { @@ -89,35 +88,33 @@ TEST_CASE("run iterates schedule and invokes callbacks", "[master][REQ-MASTER-00 std::atomic frame_count{0}; std::atomic error_count{0}; - std::vector received_id; - node.on_frame([&](Frame f) { + node.on_frame([&](Frame) { frame_count++; - received_id.push_back(f.id); }); node.on_error([&](std::error_code) { error_count++; }); - std::stop_source ss; + std::atomic stop{false}; std::thread t([&]{ - node.run(ss.get_token()); + (void)node.run(stop); }); // Let it run a few iterations std::this_thread::sleep_for(std::chrono::milliseconds(20)); - ss.request_stop(); + stop.store(true); t.join(); CHECK(frame_count.load() >= 1); CHECK(error_count.load() >= 1); // 0x20 triggers error - bus->close(); + (void)bus->close(); } TEST_CASE("run continues after per-slot errors", "[master][REQ-MASTER-013]") { auto bus = Bus::create(); // 0x10 registered, 0x20 not - bus->publish(0x10, {0x01}); + (void)bus->publish(0x10, {0x01}); Node node(bus); REQUIRE_FALSE(node.set_schedule({{0x20, 0}, {0x10, 0}})); @@ -126,27 +123,47 @@ TEST_CASE("run continues after per-slot errors", "[master][REQ-MASTER-013]") { node.on_frame([&](Frame) { frame_count++; }); node.on_error([](std::error_code) {}); // absorb errors - std::stop_source ss; - std::thread t([&]{ node.run(ss.get_token()); }); + std::atomic stop{false}; + std::thread t([&]{ (void)node.run(stop); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); - ss.request_stop(); + stop.store(true); t.join(); // Should have received at least one frame (0x10) despite 0x20 errors CHECK(frame_count.load() >= 1); - bus->close(); + (void)bus->close(); } -TEST_CASE("run returns on stop token", "[master][REQ-MASTER-008]") { +TEST_CASE("run returns on stop", "[master][REQ-MASTER-008]") { auto bus = Bus::create(); - bus->publish(0x10, {0x01}); + (void)bus->publish(0x10, {0x01}); Node node(bus); REQUIRE_FALSE(node.set_schedule({{0x10, 0}})); - std::stop_source ss; - std::thread t([&]{ node.run(ss.get_token()); }); + std::atomic stop{false}; + std::thread t([&]{ (void)node.run(stop); }); std::this_thread::sleep_for(std::chrono::milliseconds(5)); - ss.request_stop(); + stop.store(true); t.join(); // must return - bus->close(); + (void)bus->close(); +} + +TEST_CASE("run with delay_ms respects timing", "[master][REQ-MASTER-005]") { + auto bus = Bus::create(); + (void)bus->publish(0x10, {0x01}); + Node node(bus); + REQUIRE_FALSE(node.set_schedule({{0x10, 5}})); // 5ms delay + + std::atomic frame_count{0}; + node.on_frame([&](Frame) { frame_count++; }); + + std::atomic stop{false}; + std::thread t([&]{ (void)node.run(stop); }); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + stop.store(true); + t.join(); + + // At 5ms/slot, ~25ms → expect 4–5 frames (timing-sensitive, allow >= 2) + CHECK(frame_count.load() >= 2); + (void)bus->close(); } diff --git a/tests/test_safety.cpp b/tests/test_safety.cpp index 0bc0cb4..f727f67 100644 --- a/tests/test_safety.cpp +++ b/tests/test_safety.cpp @@ -191,7 +191,7 @@ TEST_CASE("Protect is safe for concurrent calls", "[safety][REQ-SAFETY-014]") { CHECK(ok.load() == 400); } -TEST_CASE("crc16: known vector '123456789' → 0x29B1", "[safety][REQ-SAFETY-005]") { +TEST_CASE("crc16: known vector '123456789' -> 0x29B1", "[safety][REQ-SAFETY-005]") { const uint8_t data[] = {'1','2','3','4','5','6','7','8','9'}; CHECK(crc16(data, 9) == 0x29B1); }