Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions include/lin/master/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
#pragma once

#include <lin/lin.hpp>
#include <atomic>
#include <functional>
#include <memory>
#include <stop_token>
#include <vector>

namespace lin::master {
Expand Down Expand Up @@ -52,14 +52,14 @@ class Node {
// fusa:req REQ-MASTER-002
std::pair<Frame, std::error_code> 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<bool>& stop);

private:
std::shared_ptr<IMasterBus> bus_;
Expand Down
4 changes: 2 additions & 2 deletions src/lin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@ class LinAdapter : public relay::INode {
int depth = cfg.effective_depth(64);
auto out = std::make_shared<Chan<relay::Message>>(static_cast<std::size_t>(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);
Expand Down
8 changes: 4 additions & 4 deletions src/master/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ std::pair<Frame, std::error_code> 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<bool>& 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) {
Expand All @@ -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));
}
Expand Down
10 changes: 5 additions & 5 deletions tests/test_lin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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]") {
Expand Down
75 changes: 46 additions & 29 deletions tests/test_master.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,30 +22,30 @@ 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]") {
auto bus = Bus::create();
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]") {
auto bus = Bus::create();
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]") {
Expand All @@ -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<uint8_t>{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<bool> 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]") {
Expand All @@ -89,35 +88,33 @@ TEST_CASE("run iterates schedule and invokes callbacks", "[master][REQ-MASTER-00

std::atomic<int> frame_count{0};
std::atomic<int> error_count{0};
std::vector<uint8_t> 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<bool> 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}}));
Expand All @@ -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<bool> 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<bool> 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<int> frame_count{0};
node.on_frame([&](Frame) { frame_count++; });

std::atomic<bool> 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();
}
2 changes: 1 addition & 1 deletion tests/test_safety.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading