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
1,190 changes: 1,065 additions & 125 deletions .fusa-reqs.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,13 @@ add_test(NAME rcp_relay COMMAND test_relay)
add_executable(test_cli test_cli.cpp)
target_link_libraries(test_cli PRIVATE rcp::rcp Catch2::Catch2WithMain)
add_test(NAME rcp_cli COMMAND test_cli)

# ── Transport / bridge stubs + remaining modules ─────────────────────────────

foreach(mod
canbr ddsbr doipbr grpcbridge linbr mqttbr restbridge someipbr udsbr
tsn mdns powerstate tls)
add_executable(test_${mod} test_${mod}.cpp)
target_link_libraries(test_${mod} PRIVATE rcp::rcp Catch2::Catch2WithMain)
add_test(NAME rcp_${mod} COMMAND test_${mod})
endforeach()
56 changes: 53 additions & 3 deletions tests/test_admin.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
// fusa:req REQ-ADMIN-001
// fusa:req REQ-ADMIN-002
// fusa:req REQ-ADMIN-003
// fusa:test REQ-ADMIN-001
// fusa:test REQ-ADMIN-002
// fusa:test REQ-ADMIN-003
// fusa:test REQ-ADMIN-004
// fusa:test REQ-ADMIN-005
// fusa:test REQ-ADMIN-006
// fusa:test REQ-ADMIN-007
// fusa:test REQ-ADMIN-008
#include <catch2/catch_test_macros.hpp>

#include "rcp/admin.hpp"
#include "rcp/mock.hpp"

#include <atomic>
#include <thread>
#include <vector>

using namespace rcp;

TEST_CASE("admin: zones lists registered controllers", "[admin]") {
Expand Down Expand Up @@ -59,3 +68,44 @@ TEST_CASE("admin: multiple subscribers all receive events", "[admin]") {
REQUIRE(count_a == 1);
REQUIRE(count_b == 1);
}

TEST_CASE("admin: event delivers correct type and zone", "[admin][REQ-ADMIN-008]") {
mock::Registry reg;
admin::AdminServer srv(reg);

admin::Event got{};
srv.subscribe([&](const admin::Event& ev) { got = ev; });
srv.emit({admin::EventType::StatusUpdate, Zone::RearLeft, {}});

REQUIRE(got.type == admin::EventType::StatusUpdate);
REQUIRE(got.zone == Zone::RearLeft);
}

TEST_CASE("admin: concurrent record_counter and emit are thread-safe",
"[admin][REQ-ADMIN-004][REQ-ADMIN-005]") {
mock::Registry reg;
admin::AdminServer srv(reg);

std::atomic<int> events{0};
srv.subscribe([&](const admin::Event&) { events.fetch_add(1); });

constexpr int kThreads = 8;
constexpr int kPerThread = 1000;
std::vector<std::thread> ts;
for (int t = 0; t < kThreads; ++t) {
ts.emplace_back([&] {
for (int i = 0; i < kPerThread; ++i) {
srv.record_counter("rcp.commands.total", "zone=\"FrontLeft\"", 1.0);
srv.emit({admin::EventType::StatusUpdate, Zone::FrontLeft, {}});
}
});
}
for (auto& th : ts) th.join();

// record_counter accumulates every delta exactly once (REQ-ADMIN-004) and the
// server tolerates concurrent mutation without data races (REQ-ADMIN-005).
REQUIRE(events.load() == kThreads * kPerThread);
auto text = srv.metrics_text();
REQUIRE(text.find("rcp.commands.total") != std::string::npos);
REQUIRE(text.find(std::to_string(kThreads * kPerThread)) != std::string::npos);
}
72 changes: 67 additions & 5 deletions tests/test_authz.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
// fusa:req REQ-AUTH-001
// fusa:req REQ-AUTH-002
// fusa:req REQ-AUTH-003
// fusa:req REQ-AUTH-004
// fusa:test REQ-AUTH-001
// fusa:test REQ-AUTH-002
// fusa:test REQ-AUTH-003
// fusa:test REQ-AUTH-004
// fusa:test REQ-AUTH-005
// fusa:test REQ-AUTH-006
// fusa:test REQ-AUTH-007
// fusa:test REQ-AUTH-008
#include <catch2/catch_test_macros.hpp>

#include "rcp/authz.hpp"
#include "rcp/mock.hpp"

#include <atomic>
#include <thread>
#include <vector>

using namespace rcp;

static std::shared_ptr<mock::Controller> make_ctrl(Zone z = Zone::FrontLeft) {
Expand Down Expand Up @@ -91,9 +99,63 @@ TEST_CASE("authz: identity_fn overrides set_identity", "[authz]") {
REQUIRE_FALSE(ctrl->send(Context{}, cmd, resp));
}

TEST_CASE("authz: zone() delegates to inner", "[authz]") {
TEST_CASE("authz: zone() delegates to inner", "[authz][REQ-AUTH-005]") {
auto policy = std::make_shared<authz::AccessPolicy>();
auto inner = make_ctrl(Zone::RearLeft);
auto ctrl = authz::new_controller(inner, policy);
REQUIRE(ctrl->zone() == Zone::RearLeft);
}

TEST_CASE("authz: subscribe() delegates to inner", "[authz][REQ-AUTH-006]") {
auto policy = std::make_shared<authz::AccessPolicy>();
auto inner = make_ctrl(Zone::FrontLeft);
auto ctrl = authz::new_controller(inner, policy);

std::shared_ptr<StatusChannel> ch;
auto ec = ctrl->subscribe(Context{}, ch);
REQUIRE_FALSE(ec);
REQUIRE(ch != nullptr);
REQUIRE_FALSE(ctrl->close());
}

TEST_CASE("authz: close() delegates to inner", "[authz][REQ-AUTH-007]") {
auto policy = std::make_shared<authz::AccessPolicy>();
auto inner = make_ctrl(Zone::FrontLeft);
auto ctrl = authz::new_controller(inner, policy);

REQUIRE_FALSE(ctrl->close());
// After the inner controller is closed, sends through it report ErrClosed.
Command cmd;
cmd.zone = Zone::FrontLeft;
Response resp;
REQUIRE(inner->send(Context{}, cmd, resp) == ErrClosed);
}

TEST_CASE("authz: ErrForbidden is a distinct error code", "[authz][REQ-AUTH-008]") {
REQUIRE(authz::ErrForbidden);
REQUIRE(authz::ErrForbidden != ErrClosed);
REQUIRE(authz::ErrForbidden != ErrBusy);
REQUIRE(std::string(authz::ErrForbidden.category().name()) == "rcp.authz");
}

TEST_CASE("authz: AccessPolicy permits concurrently without data races",
"[authz][REQ-AUTH-004]") {
auto policy = std::make_shared<authz::AccessPolicy>();
policy->allow({"alice", {Zone::FrontLeft}, {CommandType::Set}});

std::atomic<int> permits{0};
std::vector<std::thread> ts;
for (int t = 0; t < 8; ++t) {
ts.emplace_back([&] {
for (int i = 0; i < 5000; ++i) {
// Concurrent readers (permit) interleaved with a writer (allow).
if (policy->permit("alice", Zone::FrontLeft, CommandType::Set))
permits.fetch_add(1, std::memory_order_relaxed);
if ((i & 0x3ff) == 0)
policy->allow({"tmp", {Zone::Central}, {}});
}
});
}
for (auto& th : ts) th.join();
REQUIRE(permits.load() == 8 * 5000);
}
48 changes: 48 additions & 0 deletions tests/test_canbr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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 <catch2/catch_test_macros.hpp>

#include "rcp/canbr.hpp"

#include <system_error>

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<StatusChannel> 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());
}
11 changes: 8 additions & 3 deletions tests/test_capi.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// fusa:req REQ-CAPI-001
// fusa:req REQ-CAPI-002
// fusa:req REQ-CAPI-003
// fusa:test REQ-CAPI-001
// fusa:test REQ-CAPI-002
// fusa:test REQ-CAPI-003
// fusa:test REQ-CAPI-004
// fusa:test REQ-CAPI-005
// fusa:test REQ-CAPI-006
// fusa:test REQ-CAPI-007
// fusa:test REQ-CAPI-008
#include <catch2/catch_test_macros.hpp>

#include "rcp/capi_impl.hpp"
Expand Down
8 changes: 4 additions & 4 deletions tests/test_cli.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// fusa:req REQ-CLI-001
// fusa:req REQ-CLI-002
// fusa:req REQ-CLI-003
// fusa:req REQ-CLI-004
// fusa:test REQ-CLI-001
// fusa:test REQ-CLI-002
// fusa:test REQ-CLI-003
// fusa:test REQ-CLI-004

// RELAY CLI conformance tests (spec §11 command surface, §12 JSON documents).
//
Expand Down
25 changes: 22 additions & 3 deletions tests/test_config.cpp
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// fusa:req REQ-CFG-001
// fusa:req REQ-CFG-002
// fusa:req REQ-CFG-003
// fusa:test REQ-CFG-001
// fusa:test REQ-CFG-002
// fusa:test REQ-CFG-003
// fusa:test REQ-CFG-004
// fusa:test REQ-CFG-005
// fusa:test REQ-CFG-006
#include <catch2/catch_test_macros.hpp>

#include "rcp/config.hpp"
#include "rcp/mock.hpp"
#include "rcp/proxy.hpp"

#include <stdexcept>
#include <string>

using namespace rcp;

TEST_CASE("config: parse_json two zones", "[config]") {
Expand Down Expand Up @@ -61,3 +67,16 @@ TEST_CASE("config: load duplicate zone returns ErrAlreadyExists", "[config]") {
auto ec = config::load(json, preg);
REQUIRE(ec == ErrAlreadyExists);
}

TEST_CASE("config: ParseError is a std::runtime_error subclass", "[config][REQ-CFG-006]") {
// Catchable as std::runtime_error (and thus std::exception) and carries a message.
bool caught = false;
try {
config::parse_json(R"({ "zones": [{ "zone": "Nope" }] })");
} catch (const std::runtime_error& e) {
caught = true;
REQUIRE(std::string(e.what()).find("Nope") != std::string::npos);
}
REQUIRE(caught);
REQUIRE(std::is_base_of<std::runtime_error, config::ParseError>::value);
}
48 changes: 48 additions & 0 deletions tests/test_ddsbr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// fusa:test REQ-DDS-001
// fusa:test REQ-DDS-002
// fusa:test REQ-DDS-003
// fusa:test REQ-DDS-004

// ddsbr protocol-bridge stub conformance tests.
//
// The DdsController 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 <catch2/catch_test_macros.hpp>

#include "rcp/ddsbr.hpp"

#include <system_error>

using namespace rcp;

namespace {
const std::error_code kUnsupported =
std::make_error_code(std::errc::function_not_supported);
} // namespace

TEST_CASE("ddsbr: send returns function_not_supported when stub", "[ddsbr][REQ-DDS-001]") {
auto ctrl = ddsbr::new_controller(Zone::FrontLeft, ddsbr::Config{});
Command cmd;
cmd.zone = Zone::FrontLeft;
cmd.type = CommandType::Set;
Response resp;
REQUIRE(ctrl->send(Context{}, cmd, resp) == kUnsupported);
}

TEST_CASE("ddsbr: zone returns configured zone", "[ddsbr][REQ-DDS-002]") {
auto ctrl = ddsbr::new_controller(Zone::RearRight, ddsbr::Config{});
REQUIRE(ctrl->zone() == Zone::RearRight);
}

TEST_CASE("ddsbr: subscribe returns function_not_supported when stub", "[ddsbr][REQ-DDS-003]") {
auto ctrl = ddsbr::new_controller(Zone::Central, ddsbr::Config{});
std::shared_ptr<StatusChannel> ch;
REQUIRE(ctrl->subscribe(Context{}, ch) == kUnsupported);
}

TEST_CASE("ddsbr: close returns no error", "[ddsbr][REQ-DDS-004]") {
auto ctrl = ddsbr::new_controller(Zone::FrontRight, ddsbr::Config{});
REQUIRE_FALSE(ctrl->close());
}
48 changes: 48 additions & 0 deletions tests/test_doipbr.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// fusa:test REQ-DOIP-001
// fusa:test REQ-DOIP-002
// fusa:test REQ-DOIP-003
// fusa:test REQ-DOIP-004

// doipbr protocol-bridge stub conformance tests.
//
// The DoIpController 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 <catch2/catch_test_macros.hpp>

#include "rcp/doipbr.hpp"

#include <system_error>

using namespace rcp;

namespace {
const std::error_code kUnsupported =
std::make_error_code(std::errc::function_not_supported);
} // namespace

TEST_CASE("doipbr: send returns function_not_supported when stub", "[doipbr][REQ-DOIP-001]") {
auto ctrl = doipbr::new_controller(Zone::FrontLeft, doipbr::Config{});
Command cmd;
cmd.zone = Zone::FrontLeft;
cmd.type = CommandType::Set;
Response resp;
REQUIRE(ctrl->send(Context{}, cmd, resp) == kUnsupported);
}

TEST_CASE("doipbr: zone returns configured zone", "[doipbr][REQ-DOIP-002]") {
auto ctrl = doipbr::new_controller(Zone::RearRight, doipbr::Config{});
REQUIRE(ctrl->zone() == Zone::RearRight);
}

TEST_CASE("doipbr: subscribe returns function_not_supported when stub", "[doipbr][REQ-DOIP-003]") {
auto ctrl = doipbr::new_controller(Zone::Central, doipbr::Config{});
std::shared_ptr<StatusChannel> ch;
REQUIRE(ctrl->subscribe(Context{}, ch) == kUnsupported);
}

TEST_CASE("doipbr: close returns no error", "[doipbr][REQ-DOIP-004]") {
auto ctrl = doipbr::new_controller(Zone::FrontRight, doipbr::Config{});
REQUIRE_FALSE(ctrl->close());
}
Loading
Loading