diff --git a/cmake/libe3Tests.cmake b/cmake/libe3Tests.cmake index 03e9d10..a965142 100644 --- a/cmake/libe3Tests.cmake +++ b/cmake/libe3Tests.cmake @@ -16,6 +16,16 @@ target_include_directories(libe3_test_framework INTERFACE file(GLOB LIBE3_TEST_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "tests/*.cpp") +# Tests that hardcode the ASN.1 wire format: test_asn1_size exercises the +# APER encoder directly, and test_e2e_report_path configures its agent and +# fake-dApp peer with EncodingFormat::ASN1. On builds with +# LIBE3_ENABLE_ASN1=OFF the encoder factory cannot create an ASN.1 encoder, +# so these tests can only fail; exclude them instead of weakening them. +set(LIBE3_ASN1_ONLY_TESTS + asn1_size + e2e_report_path +) + foreach(test_src IN LISTS LIBE3_TEST_SOURCES) # Derive a target name from the source file name: tests/test_foo.cpp -> test_foo get_filename_component(test_name ${test_src} NAME_WE) @@ -27,6 +37,10 @@ foreach(test_src IN LISTS LIBE3_TEST_SOURCES) message(STATUS "Skipping test_json_encoder: JSON support disabled") continue() endif() + if(NOT LIBE3_ENABLE_ASN1 AND simple_name IN_LIST LIBE3_ASN1_ONLY_TESTS) + message(STATUS "Skipping test_${simple_name}: ASN.1 support disabled") + continue() + endif() add_executable(${target_name} "${CMAKE_CURRENT_SOURCE_DIR}/${test_src}") target_link_libraries(${target_name} diff --git a/include/libe3/e3_interface.hpp b/include/libe3/e3_interface.hpp index af00c61..66dc82d 100644 --- a/include/libe3/e3_interface.hpp +++ b/include/libe3/e3_interface.hpp @@ -208,6 +208,12 @@ class E3Interface { // Core components std::unique_ptr connector_; std::unique_ptr encoder_; + // Service Models registered with THIS interface. Owned per interface + // (not process-wide) so multiple agents in one process can host SMs + // with the same RAN function id, and stopping one agent cannot wipe + // another agent's registry. Only the RAN role registers SMs; on the + // dApp role this stays empty. + SmRegistry sm_registry_; // RAN-only state (nullptr when role==DAPP). std::unique_ptr subscription_manager_; // dApp-only state (nullptr when role==RAN). @@ -308,6 +314,17 @@ class E3Interface { void handle_release_message(const ReleaseMessage &release); void handle_dapp_disconnection(uint32_t dapp_id); + /** + * @brief Complete a setup REQ/REP exchange with a best-effort empty reply. + * + * Used whenever a received setup message cannot be answered with a real + * SetupResponse (undecodable bytes, wrong PDU type, response encode + * failure). A ZMQ REP socket must send exactly one reply per received + * request before it can receive again; bailing out without replying + * wedges the setup channel for every subsequent dApp until restart. + */ + void send_empty_setup_reply(); + // dApp-role handlers void handle_setup_response(const SetupResponse& response); void handle_subscription_response(const SubscriptionResponse& response); diff --git a/include/libe3/sm_interface.hpp b/include/libe3/sm_interface.hpp index f1f85a6..b3f66b5 100644 --- a/include/libe3/sm_interface.hpp +++ b/include/libe3/sm_interface.hpp @@ -212,15 +212,18 @@ using SmFactory = std::function()>; /** * @brief SM Registry for managing registered Service Models * - * This class provides a central registry for Service Models. - * It's used by the E3Agent to find and manage SMs. + * Each E3Interface owns one SmRegistry instance: the registry is scoped to + * its owning interface/agent, not to the process. Two agents in the same + * process (e.g. integration tests, multi-RAN deployments) can therefore + * register Service Models with the same RAN function id independently, and + * tearing one agent down only clears that agent's own SMs. */ class SmRegistry { public: - /** - * @brief Get the singleton instance - */ - static SmRegistry& instance(); + SmRegistry() = default; + ~SmRegistry() = default; + SmRegistry(const SmRegistry&) = delete; + SmRegistry& operator=(const SmRegistry&) = delete; /** * @brief Register a Service Model @@ -277,11 +280,6 @@ class SmRegistry { void clear(); private: - SmRegistry() = default; - ~SmRegistry() = default; - SmRegistry(const SmRegistry&) = delete; - SmRegistry& operator=(const SmRegistry&) = delete; - mutable std::mutex mutex_; std::unordered_map> sms_; std::unordered_map factories_; diff --git a/src/core/e3_interface.cpp b/src/core/e3_interface.cpp index d8e00dc..1c4e7f0 100644 --- a/src/core/e3_interface.cpp +++ b/src/core/e3_interface.cpp @@ -240,13 +240,12 @@ void E3Interface::stop() { } setup_complete_cv_.notify_all(); - // Clean up SM registry — only the RAN role owns SMs. Doing this - // unconditionally would wipe a sibling RAN-role E3Interface's registered - // SMs in a two-roles-in-one-process scenario (integration tests, the - // latency benchmark, multi-peer dApps colocated with a RAN). The dApp - // role never registers an SM, so there's nothing to clear. + // Clean up this interface's own SM registry — only the RAN role owns + // SMs (the dApp role never registers any, so its registry is empty). + // The registry is per-interface, so this cannot affect a sibling + // E3Interface's SMs in a multi-agent process. if (config_.role == E3Role::RAN) { - SmRegistry::instance().clear(); + sm_registry_.clear(); } // Dispose connector @@ -266,7 +265,7 @@ ErrorCode E3Interface::queue_outbound(Pdu pdu) { } std::vector E3Interface::get_available_ran_functions() const { - return SmRegistry::instance().get_available_ran_functions(); + return sm_registry_.get_available_ran_functions(); } ErrorCode E3Interface::register_sm(std::unique_ptr sm) { @@ -286,7 +285,7 @@ ErrorCode E3Interface::register_sm(std::unique_ptr sm) { } return queue_outbound(std::move(pdu)); }); - return SmRegistry::instance().register_sm(std::move(sm)); + return sm_registry_.register_sm(std::move(sm)); } void E3Interface::notify_dapp_status_changed() { @@ -302,7 +301,7 @@ void E3Interface::notify_dapp_status_changed() { void E3Interface::setup_loop_ran() { E3_LOG_INFO(LOG_TAG) << "Setup loop (RAN) started"; - auto available_ran_functions = SmRegistry::instance().get_available_ran_functions(); + auto available_ran_functions = sm_registry_.get_available_ran_functions(); while (!should_stop_.load()) { std::vector buffer; @@ -315,26 +314,32 @@ void E3Interface::setup_loop_ran() { E3_LOG_INFO(LOG_TAG) << "Setup request received: " << ret << " bytes"; - // Decode the setup request + // Decode the setup request. On any failure we must still complete + // the REQ/REP exchange (see send_empty_setup_reply) or the setup + // channel wedges for every subsequent dApp. auto decode_result = encoder_->decode(buffer.data(), static_cast(ret)); if (!decode_result) { - E3_LOG_ERROR(LOG_TAG) << "Failed to decode setup request; ret=" << ret; + E3_LOG_WARN(LOG_TAG) << "Undecodable setup message (" << ret + << " bytes, wrong encoding or garbage); replying empty"; + send_empty_setup_reply(); continue; } - + Pdu& pdu = *decode_result; if (pdu.type != PduType::SETUP_REQUEST) { - E3_LOG_ERROR(LOG_TAG) << "Unexpected PDU type in setup: " - << pdu_type_to_string(pdu.type); + E3_LOG_WARN(LOG_TAG) << "Unexpected PDU type in setup: " + << pdu_type_to_string(pdu.type) << "; replying empty"; + send_empty_setup_reply(); continue; } - + auto* request = std::get_if(&pdu.choice); if (!request) { - E3_LOG_ERROR(LOG_TAG) << "Failed to get SetupRequest from PDU"; + E3_LOG_WARN(LOG_TAG) << "Failed to get SetupRequest from PDU; replying empty"; + send_empty_setup_reply(); continue; } - + handle_setup_request(*request, pdu.message_id); } @@ -523,7 +528,7 @@ void E3Interface::sm_data_handler_loop() { } // Get SM for this RAN function - ServiceModel* sm = SmRegistry::instance().get_by_ran_function(ran_func); + ServiceModel* sm = sm_registry_.get_by_ran_function(ran_func); if (!sm || !sm->is_running()) { continue; @@ -568,13 +573,13 @@ void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t req // Create and send response // Get available RAN functions and convert to RanFunctionDef list - auto available_ran_function_ids = SmRegistry::instance().get_available_ran_functions(); + auto available_ran_function_ids = sm_registry_.get_available_ran_functions(); E3_LOG_DEBUG(LOG_TAG) << "Available RAN function ids count: " << available_ran_function_ids.size(); std::vector ran_function_list; for (auto id : available_ran_function_ids) { RanFunctionDef func; func.ran_function_identifier = id; - ServiceModel* sm = SmRegistry::instance().get_by_ran_function(id); + ServiceModel* sm = sm_registry_.get_by_ran_function(id); if (sm) { func.telemetry_identifier_list = sm->telemetry_ids(); func.control_identifier_list = sm->control_ids(); @@ -603,9 +608,11 @@ void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t req if (!encode_result) { E3_LOG_ERROR(LOG_TAG) << "Failed to encode setup response for request id " << request_message_id; + // Still complete the REQ/REP exchange so the setup channel survives. + send_empty_setup_reply(); return; } - + ErrorCode send_result = connector_->send_response(encode_result->buffer); if (send_result != ErrorCode::SUCCESS) { E3_LOG_ERROR(LOG_TAG) << "Failed to send setup response for request id " << request_message_id @@ -615,6 +622,23 @@ void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t req } } +void E3Interface::send_empty_setup_reply() { + // The RAN side of the setup channel is a ZMQ REP socket: it must send + // exactly one reply per received request before it can receive again. + // Returning without replying (undecodable request, wrong PDU type, + // response-encode failure) leaves the socket in the send state, where + // every later recv fails — all future dApp setups stall until the + // connector resets or the agent restarts. Completing the exchange with + // an empty frame keeps the channel alive; the peer treats the empty + // reply as a failed setup and retries. On connectors without a REP + // state machine (POSIX) the empty send is harmless. + ErrorCode rc = connector_->send_response({}); + if (rc != ErrorCode::SUCCESS) { + E3_LOG_WARN(LOG_TAG) << "Failed to send empty setup reply: " + << error_code_to_string(rc); + } +} + void E3Interface::handle_subscription_request(const SubscriptionRequest& request, uint32_t request_message_id) { E3_LOG_INFO(LOG_TAG) << "Handling subscription request from dApp " << request.dapp_identifier << " for RAN function " << request.ran_function_identifier; @@ -706,7 +730,7 @@ void E3Interface::handle_control_action(const DAppControlAction& action, uint32_ << " control " << action.control_identifier << " (" << action.action_data.size() << " bytes)"; - ServiceModel* sm = SmRegistry::instance().get_by_ran_function(action.ran_function_identifier); + ServiceModel* sm = sm_registry_.get_by_ran_function(action.ran_function_identifier); if (sm && sm->is_running()) { ErrorCode result = sm->handle_control_action(request_message_id, action); @@ -1137,13 +1161,13 @@ ErrorCode E3Interface::wait_for_setup(std::chrono::milliseconds timeout) { void E3Interface::on_sm_lifecycle_change(uint32_t ran_function_id, bool should_start) { if (should_start) { - ErrorCode result = SmRegistry::instance().start_sm(ran_function_id); + ErrorCode result = sm_registry_.start_sm(ran_function_id); if (result != ErrorCode::SUCCESS) { E3_LOG_ERROR(LOG_TAG) << "Failed to start SM for RAN function " << ran_function_id << ": " << error_code_to_string(result); } } else { - ErrorCode result = SmRegistry::instance().stop_sm(ran_function_id); + ErrorCode result = sm_registry_.stop_sm(ran_function_id); if (result != ErrorCode::SUCCESS) { E3_LOG_WARN(LOG_TAG) << "Failed to stop SM for RAN function " << ran_function_id << ": " << error_code_to_string(result); diff --git a/src/core/sm_registry.cpp b/src/core/sm_registry.cpp index af7ff02..50155e1 100644 --- a/src/core/sm_registry.cpp +++ b/src/core/sm_registry.cpp @@ -15,11 +15,6 @@ namespace { constexpr const char* LOG_TAG = "SmReg"; } -SmRegistry& SmRegistry::instance() { - static SmRegistry registry; - return registry; -} - ErrorCode SmRegistry::register_sm(std::unique_ptr sm) { if (!sm) { return ErrorCode::INVALID_PARAM; diff --git a/src/encoder/asn1_encoder.cpp b/src/encoder/asn1_encoder.cpp index da03e24..40c39be 100644 --- a/src/encoder/asn1_encoder.cpp +++ b/src/encoder/asn1_encoder.cpp @@ -219,9 +219,25 @@ E3_PDU* Asn1E3Encoder::pdu_to_asn1(const Pdu& pdu) const { ASN_SEQUENCE_ADD(&ran_func->controlIdentifierList, id); } - OCTET_STRING_fromBuf(&ran_func->ranFunctionData, - reinterpret_cast(func.ran_function_data.data()), - static_cast(func.ran_function_data.size())); + if (func.ran_function_data.empty()) { + // ranFunctionData is MANDATORY in the E3AP schema and + // constrained to OCTET STRING (SIZE (1..32768)) + // (messages/asn1/V1/e3ap-1.0.0.asn1, + // E3-RanFunctionDefinition): it can be neither omitted + // nor empty. An SM that provides no RAN-function data + // (ServiceModel::ran_function_data() defaults to {}) + // would otherwise violate the APER size constraint and + // abort the encode of the entire setupResponse, taking + // every other registered RAN function down with it. + // Substitute a 1-byte 0x00 placeholder instead. + static const char kPlaceholder = '\0'; + OCTET_STRING_fromBuf(&ran_func->ranFunctionData, + &kPlaceholder, 1); + } else { + OCTET_STRING_fromBuf(&ran_func->ranFunctionData, + reinterpret_cast(func.ran_function_data.data()), + static_cast(func.ran_function_data.size())); + } // Ensure ranFunctionList container is allocated (optional field) if (!asn1_pdu->msg.choice.setupResponse->ranFunctionList) { diff --git a/tests/test_asn1_size.cpp b/tests/test_asn1_size.cpp index 455cb67..9f3273a 100644 --- a/tests/test_asn1_size.cpp +++ b/tests/test_asn1_size.cpp @@ -225,6 +225,63 @@ TEST(Asn1Size_growsLinearlyWithPayload) { ASSERT_LE(delta, 2 * naive_payload_delta + ENVELOPE_OVERHEAD_MAX); } +/** + * A setupResponse that advertises an SM with EMPTY ran_function_data must + * still encode. The schema makes ranFunctionData a mandatory + * OCTET STRING (SIZE (1..32768)), so the encoder substitutes a 1-byte 0x00 + * placeholder; without that substitution the APER size constraint fails and + * the whole setupResponse encode aborts. The result must also decode, with + * the placeholder visible to the peer and every other field intact. + */ +TEST(Asn1_setupResponse_emptyRanFunctionData_stillEncodes) { + auto enc = make_encoder(); + + RanFunctionDef with_data; + with_data.ran_function_identifier = 1; + with_data.telemetry_identifier_list = {1, 2}; + with_data.control_identifier_list = {10}; + with_data.ran_function_data = {0xAB, 0xCD}; + + RanFunctionDef without_data; + without_data.ran_function_identifier = 2; + without_data.telemetry_identifier_list = {3}; + without_data.control_identifier_list = {20}; + // ran_function_data deliberately left empty (ServiceModel default) + + auto encoded = enc->encode_setup_response( + 7, // message_id + 42, // request_id + ResponseCode::POSITIVE, + std::string("1.0.0"), // e3ap_protocol_version + 3u, // dapp_identifier + "asn1-test-ran", // ran_identifier + {with_data, without_data}); + ASSERT_TRUE(encoded.has_value()); + + auto decoded = enc->decode(encoded->buffer.data(), encoded->buffer.size()); + ASSERT_TRUE(decoded.has_value()); + ASSERT_EQ(static_cast(decoded->type), + static_cast(PduType::SETUP_RESPONSE)); + + auto* resp = std::get_if(&decoded->choice); + ASSERT_TRUE(resp != nullptr); + ASSERT_EQ(static_cast(resp->response_code), + static_cast(ResponseCode::POSITIVE)); + ASSERT_EQ(resp->request_id, 42u); + ASSERT_EQ(resp->ran_function_list.size(), 2u); + + // SM with real data: payload round-trips byte-for-byte. + ASSERT_EQ(resp->ran_function_list[0].ran_function_identifier, 1u); + ASSERT_EQ(resp->ran_function_list[0].ran_function_data.size(), 2u); + ASSERT_EQ(static_cast(resp->ran_function_list[0].ran_function_data[0]), 0xAB); + ASSERT_EQ(static_cast(resp->ran_function_list[0].ran_function_data[1]), 0xCD); + + // SM without data: the wire carries the 1-byte 0x00 placeholder. + ASSERT_EQ(resp->ran_function_list[1].ran_function_identifier, 2u); + ASSERT_EQ(resp->ran_function_list[1].ran_function_data.size(), 1u); + ASSERT_EQ(static_cast(resp->ran_function_list[1].ran_function_data[0]), 0x00); +} + // --------------------------------------------------------------------------- int main() { diff --git a/tests/test_multi_agent_registry.cpp b/tests/test_multi_agent_registry.cpp new file mode 100644 index 0000000..500533f --- /dev/null +++ b/tests/test_multi_agent_registry.cpp @@ -0,0 +1,211 @@ +/** + * @file test_multi_agent_registry.cpp + * @brief Multi-instance correctness of the (now per-interface) SM registry. + * + * Historically SmRegistry was a process-wide singleton, which broke any + * process hosting more than one E3 agent: + * + * - registering an SM with the same RAN function id on a second agent + * failed with SM_ALREADY_REGISTERED even though the agents are + * completely independent peers on different endpoints, and + * + * - stopping/destroying one RAN-role agent cleared the shared registry, + * silently wiping the OTHER agent's Service Models. + * + * Properties verified with two real RAN-role E3Agents in one process (and + * a real dApp-role agent as the client): + * + * (1) Both agents can register an SM with the SAME ran_function_id. + * + * (2) Each agent advertises its own registration independently. + * + * (3) After one agent is stopped and destroyed, the surviving agent is + * still running, still has its SM registered, and still serves a + * full setup handshake advertising that RAN function to a dApp. + * + * The test uses whichever encoding the build provides (JSON preferred) so + * it runs on JSON-only, ASN.1-only, and dual-encoder builds. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "test_framework.hpp" +#include "libe3/libe3.hpp" +#include "libe3/e3_agent.hpp" +#include "libe3/sm_interface.hpp" +#include "libe3/types.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // getpid + +using namespace libe3; +using namespace std::chrono_literals; + +namespace { + +inline int error_to_int(ErrorCode e) { return static_cast(e); } + +/// Counter so each test instance uses distinct IPC namespaces — required +/// when running ctest -j to avoid socket-file collisions. +std::atomic g_seq{0}; + +std::string unique_ipc(const char* tag) { + std::ostringstream oss; + oss << "ipc:///tmp/dapps/multireg_test_" << getpid() << "_" + << g_seq.fetch_add(1) << "_" << tag; + return oss.str(); +} + +/// Pick an encoding that is compiled into this build (JSON preferred so the +/// test also runs on JSON-only builds). +EncodingFormat pick_encoding() { +#if defined(LIBE3_ENABLE_JSON) + return EncodingFormat::JSON; +#else + return EncodingFormat::ASN1; +#endif +} + +/// Minimal ServiceModel so register_sm() succeeds. +class RegistryTestSM : public ServiceModel { +public: + explicit RegistryTestSM(uint32_t ran_function_id) : id_(ran_function_id) {} + std::string name() const override { return "RegistryTestSM"; } + uint32_t version() const override { return 1; } + uint32_t ran_function_id() const override { return id_; } + std::vector telemetry_ids() const override { return {1}; } + std::vector control_ids() const override { return {10}; } + ErrorCode init() override { return ErrorCode::SUCCESS; } + void destroy() override { running_ = false; } + ErrorCode start() override { running_ = true; return ErrorCode::SUCCESS; } + void stop() override { running_ = false; } + bool is_running() const override { return running_; } + ErrorCode handle_control_action(uint32_t, const DAppControlAction&) override { + return ErrorCode::SUCCESS; + } +private: + uint32_t id_; + bool running_ = false; +}; + +struct RanEndpoints { + std::string setup; + std::string sub; + std::string pub; +}; + +E3Config make_ran_config(const RanEndpoints& ep, const char* ran_id, + EncodingFormat encoding) { + E3Config cfg; + cfg.role = E3Role::RAN; + cfg.link_layer = E3LinkLayer::ZMQ; + cfg.transport_layer = E3TransportLayer::IPC; + cfg.setup_endpoint = ep.setup; + cfg.subscriber_endpoint = ep.sub; + cfg.publisher_endpoint = ep.pub; + cfg.encoding = encoding; + cfg.log_level = 0; + cfg.ran_identifier = ran_id; + return cfg; +} + +} // namespace + +TEST(MultiAgent_sameRanFunctionId_independentRegistries) { + constexpr uint32_t RAN_FUNC_ID = 1; + const EncodingFormat encoding = pick_encoding(); + + const RanEndpoints ep1{unique_ipc("setup1"), unique_ipc("in1"), unique_ipc("out1")}; + const RanEndpoints ep2{unique_ipc("setup2"), unique_ipc("in2"), unique_ipc("out2")}; + + // Agent 1 hosts SM with ran_function_id 1. + auto agent1 = std::make_unique(make_ran_config(ep1, "multireg-ran-1", encoding)); + ASSERT_EQ(error_to_int(agent1->register_sm(std::make_unique(RAN_FUNC_ID))), + error_to_int(ErrorCode::SUCCESS)); + ASSERT_EQ(error_to_int(agent1->start()), error_to_int(ErrorCode::SUCCESS)); + + // Agent 2 hosts an SM with the SAME ran_function_id. With a process-wide + // registry this registration failed with SM_ALREADY_REGISTERED. + E3Agent agent2(make_ran_config(ep2, "multireg-ran-2", encoding)); + ASSERT_EQ(error_to_int(agent2.register_sm(std::make_unique(RAN_FUNC_ID))), + error_to_int(ErrorCode::SUCCESS)); + ASSERT_EQ(error_to_int(agent2.start()), error_to_int(ErrorCode::SUCCESS)); + + // Both agents advertise their own registration. + auto funcs1 = agent1->get_available_ran_functions(); + ASSERT_EQ(funcs1.size(), 1u); + ASSERT_EQ(funcs1[0], RAN_FUNC_ID); + auto funcs2 = agent2.get_available_ran_functions(); + ASSERT_EQ(funcs2.size(), 1u); + ASSERT_EQ(funcs2[0], RAN_FUNC_ID); + + // Stop and destroy agent 1. With a process-wide registry this cleared + // agent 2's SM as collateral damage. + agent1->stop(); + agent1.reset(); + + ASSERT_TRUE(agent2.is_running()); + auto funcs2_after = agent2.get_available_ran_functions(); + ASSERT_EQ(funcs2_after.size(), 1u); + ASSERT_EQ(funcs2_after[0], RAN_FUNC_ID); + + // Agent 2 must still SERVE: a real dApp-role agent performs the full + // setup handshake against it and must be offered RAN function 1. + E3Config dapp_cfg; + dapp_cfg.role = E3Role::DAPP; + dapp_cfg.link_layer = E3LinkLayer::ZMQ; + dapp_cfg.transport_layer = E3TransportLayer::IPC; + dapp_cfg.setup_endpoint = ep2.setup; + dapp_cfg.subscriber_endpoint = ep2.sub; + dapp_cfg.publisher_endpoint = ep2.pub; + dapp_cfg.encoding = encoding; + dapp_cfg.log_level = 0; + dapp_cfg.dapp_name = "multireg-dapp"; + dapp_cfg.dapp_version = "0.0.1"; + dapp_cfg.vendor = "test-vendor"; + + std::mutex resp_mu; + std::vector offered; + std::atomic got_positive{false}; + + E3Agent dapp(std::move(dapp_cfg)); + dapp.set_setup_response_handler([&](const SetupResponse& resp) { + std::lock_guard lk(resp_mu); + offered = resp.ran_function_list; + if (resp.response_code == ResponseCode::POSITIVE) { + got_positive.store(true); + } + }); + ASSERT_EQ(error_to_int(dapp.start()), error_to_int(ErrorCode::SUCCESS)); + ASSERT_EQ(error_to_int(dapp.wait_for_setup(10s)), + error_to_int(ErrorCode::SUCCESS)); + ASSERT_TRUE(got_positive.load()); + { + std::lock_guard lk(resp_mu); + bool found = false; + for (const auto& rf : offered) { + if (rf.ran_function_identifier == RAN_FUNC_ID) { + found = true; + } + } + ASSERT_TRUE(found); + } + + dapp.stop(); + agent2.stop(); +} + +// --------------------------------------------------------------------------- + +int main() { + return RUN_ALL_TESTS(); +} diff --git a/tests/test_setup_bad_request.cpp b/tests/test_setup_bad_request.cpp new file mode 100644 index 0000000..70bc7f1 --- /dev/null +++ b/tests/test_setup_bad_request.cpp @@ -0,0 +1,152 @@ +/** + * @file test_setup_bad_request.cpp + * @brief Regression test: a malformed setup request must not wedge the + * RAN's setup REP socket. + * + * The RAN side of the E3 setup channel is a ZMQ REP socket. The REP state + * machine requires exactly one reply per received request before the socket + * can receive again. If the agent bails out of the setup loop without + * replying (e.g. the incoming bytes do not decode — wrong encoding dialect, + * or plain garbage), the socket is stuck in the send state and every later + * setup request from any dApp stalls until the agent restarts. + * + * Properties verified, using a raw ZMQ REQ peer against a real E3Agent: + * + * (1) Sending undecodable bytes on the setup channel produces a reply + * (best-effort empty), i.e. the REQ/REP exchange always completes. + * + * (2) After the garbage exchange, a well-formed SetupRequest on the very + * same channel still receives a positive SetupResponse — the setup + * channel survived the malformed message. + * + * The test uses whichever encoding the build provides (JSON preferred) so + * it runs on JSON-only, ASN.1-only, and dual-encoder builds. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "test_framework.hpp" +#include "libe3/libe3.hpp" +#include "libe3/e3_agent.hpp" +#include "libe3/e3_encoder.hpp" +#include "libe3/types.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include // getpid + +using namespace libe3; +using namespace std::chrono_literals; + +namespace { + +inline int error_to_int(ErrorCode e) { return static_cast(e); } + +/// Counter so each test instance uses distinct IPC namespaces — required +/// when running ctest -j to avoid socket-file collisions. +std::atomic g_seq{0}; + +std::string unique_ipc(const char* tag) { + std::ostringstream oss; + oss << "ipc:///tmp/dapps/badsetup_test_" << getpid() << "_" + << g_seq.fetch_add(1) << "_" << tag; + return oss.str(); +} + +/// Pick an encoding that is compiled into this build (JSON preferred so the +/// test also runs on JSON-only builds). +EncodingFormat pick_encoding() { +#if defined(LIBE3_ENABLE_JSON) + return EncodingFormat::JSON; +#else + return EncodingFormat::ASN1; +#endif +} + +} // namespace + +TEST(SetupChannel_garbageRequest_repliesAndChannelSurvives) { + const std::string setup_ep = unique_ipc("setup"); + const std::string sub_ep = unique_ipc("inbound"); + const std::string pub_ep = unique_ipc("outbound"); + const EncodingFormat encoding = pick_encoding(); + + E3Config cfg; + cfg.role = E3Role::RAN; + cfg.link_layer = E3LinkLayer::ZMQ; + cfg.transport_layer = E3TransportLayer::IPC; + cfg.setup_endpoint = setup_ep; + cfg.subscriber_endpoint = sub_ep; + cfg.publisher_endpoint = pub_ep; + cfg.encoding = encoding; + cfg.log_level = 0; + cfg.ran_identifier = "badsetup-test"; + + E3Agent agent(std::move(cfg)); + ASSERT_EQ(error_to_int(agent.start()), error_to_int(ErrorCode::SUCCESS)); + ASSERT_TRUE(agent.is_running()); + + // The REP socket is bound synchronously inside start(); a short settle + // keeps the IPC connect race-free. + std::this_thread::sleep_for(100ms); + + void* ctx = zmq_ctx_new(); + ASSERT_TRUE(ctx != nullptr); + void* req = zmq_socket(ctx, ZMQ_REQ); + ASSERT_TRUE(req != nullptr); + int recv_timeout = 5000; // a wedged REP socket shows up as a timeout + zmq_setsockopt(req, ZMQ_RCVTIMEO, &recv_timeout, sizeof(recv_timeout)); + int linger = 0; + zmq_setsockopt(req, ZMQ_LINGER, &linger, sizeof(linger)); + ASSERT_EQ(zmq_connect(req, setup_ep.c_str()), 0); + + // (1) Garbage on the setup channel: a reply must still arrive (the + // agent sends a best-effort empty frame). Without the fix the agent + // never replies and this recv times out with -1/EAGAIN. + const char garbage[] = "definitely-not-an-e3ap-setup-request"; + ASSERT_GE(zmq_send(req, garbage, sizeof(garbage) - 1, 0), 0); + + uint8_t buf[4096]; + int n = zmq_recv(req, buf, sizeof(buf), 0); + ASSERT_GE(n, 0); + + // (2) The same channel must still serve a well-formed SetupRequest. + auto encoder = create_encoder(encoding); + ASSERT_TRUE(encoder != nullptr); + auto setup = encoder->encode_setup_request( + 42, "1.0.0", "badsetup-dapp", "0.0.1", "test-vendor"); + ASSERT_TRUE(setup.has_value()); + ASSERT_GE(zmq_send(req, setup->buffer.data(), setup->buffer.size(), 0), 0); + + n = zmq_recv(req, buf, sizeof(buf), 0); + ASSERT_GT(n, 0); + auto decoded = encoder->decode(buf, static_cast(n)); + ASSERT_TRUE(decoded.has_value()); + ASSERT_EQ(static_cast(decoded->type), + static_cast(PduType::SETUP_RESPONSE)); + auto* resp = std::get_if(&decoded->choice); + ASSERT_TRUE(resp != nullptr); + ASSERT_EQ(static_cast(resp->response_code), + static_cast(ResponseCode::POSITIVE)); + ASSERT_EQ(resp->request_id, 42u); + ASSERT_TRUE(resp->dapp_identifier.has_value()); + + zmq_close(req); + zmq_ctx_destroy(ctx); + agent.stop(); +} + +// --------------------------------------------------------------------------- + +int main() { + return RUN_ALL_TESTS(); +}