diff --git a/.github/workflows/ProxyStubFunctionalTests.yml b/.github/workflows/ProxyStubFunctionalTests.yml index 486442ae..deed13cf 100644 --- a/.github/workflows/ProxyStubFunctionalTests.yml +++ b/.github/workflows/ProxyStubFunctionalTests.yml @@ -12,7 +12,7 @@ on: - "JsonGenerator/**" - ".github/workflows/ProxyStubFunctionalTests.yml" pull_request: - branches: [ master ] + branches: [ "master", "Development/Test-Thunder-Tags" ] #temp change paths: - "ProxyStubGenerator/**" - "JsonGenerator/**" diff --git a/tests/FunctionalTests/CMakeLists.txt b/tests/FunctionalTests/CMakeLists.txt index d9bffc43..ffc317be 100644 --- a/tests/FunctionalTests/CMakeLists.txt +++ b/tests/FunctionalTests/CMakeLists.txt @@ -33,6 +33,11 @@ option(TEST_JSON_TEXT_CASE "Enable @text:legacy case convention tests" option(TEST_JSON_COMPLIANT "Enable @compliant format tests" ON) option(TEST_JSON_UNCOMPLIANT_EXT "Enable @uncompliant:extended format tests" ON) option(TEST_JSON_UNCOMPLIANT_COL "Enable @uncompliant:collapsed format tests" ON) +option(TEST_ANNOTATIONS "Enable comprehensive annotation coverage tests" ON) +option(TEST_ANNOTATION_EVENTS "Enable @index on events + @statuslistener COM-RPC tests" ON) +option(TEST_EXTENDED_FORMAT "Enable @extended format semantics tests" ON) +option(TEST_PREFIX_UNDERSCORE "Enable @prefix with _ separator tests" ON) +option(TEST_WRAPPED_INTERFACE "Enable interface-level @wrapped tests" ON) set(CMAKE_MODULE_PATH "${CMAKE_BINARY_DIR}" CACHE BOOL "" FORCE) diff --git a/tests/FunctionalTests/common/CMakeLists.txt b/tests/FunctionalTests/common/CMakeLists.txt index e7b4b4d2..29f36b46 100644 --- a/tests/FunctionalTests/common/CMakeLists.txt +++ b/tests/FunctionalTests/common/CMakeLists.txt @@ -77,7 +77,7 @@ target_link_libraries(FunctionalTestJsonSources ) macro(AddTestInterface TestName) - cmake_parse_arguments(ARG "COM_RPC;JSON_RPC" "" "" ${ARGN}) + cmake_parse_arguments(ARG "COM_RPC;JSON_RPC;HAS_EVENTS" "" "EVENT_HANDLERS" ${ARGN}) message(STATUS "Enabling ${TestName} test") @@ -92,12 +92,44 @@ macro(AddTestInterface TestName) if(ARG_JSON_RPC) list(APPEND JSON_INTERFACE_HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/interfaces/ITest${TestName}.h") - - string(APPEND JSON_RPC_REGISTRATIONS - " FunctionalTest::JTest${TestName}::Register(module,\n" - " static_cast(\n" - " TestImplementation::Factory::Instance().Create(FunctionalTest::ITest${TestName}::ID)));\n\n" - ) + if(ARG_HAS_EVENTS) + # Event-bearing interfaces require a 3-parameter Register() call + # with an IHandler* for statuslistener / event dispatch. + # We provide a minimal no-op handler that satisfies the non-null assertion. + # + # EVENT_HANDLERS must list the statuslistener event names (e.g. "OnFeaturesChanged") + # so the generated NoOpHandler overrides the correct IHandler methods. + # The generated IHandler method name follows the pattern: + # OnEventRegistration(const string&, Status) + if(NOT ARG_EVENT_HANDLERS) + message(FATAL_ERROR "HAS_EVENTS requires EVENT_HANDLERS listing the statuslistener event names for ${TestName}") + endif() + + set(_handler_overrides "") + foreach(_event ${ARG_EVENT_HANDLERS}) + string(APPEND _handler_overrides + " void On${_event}EventRegistration(const string&, const PluginHost::JSONRPCSupportsEventStatus::Status) override {}\n" + ) + endforeach() + + string(APPEND JSON_RPC_REGISTRATIONS + " {\n" + " struct NoOpHandler : FunctionalTest::JTest${TestName}::IHandler {\n" + "${_handler_overrides}" + " };\n" + " static NoOpHandler handler;\n" + " auto* impl = static_cast(\n" + " TestImplementation::Factory::Instance().Create(FunctionalTest::ITest${TestName}::ID));\n" + " FunctionalTest::JTest${TestName}::Register(module, impl, &handler);\n" + " }\n\n" + ) + else() + string(APPEND JSON_RPC_REGISTRATIONS + " FunctionalTest::JTest${TestName}::Register(module,\n" + " static_cast(\n" + " TestImplementation::Factory::Instance().Create(FunctionalTest::ITest${TestName}::ID)));\n\n" + ) + endif() string(APPEND JSON_RPC_UNREGISTRATIONS " FunctionalTest::JTest${TestName}::Unregister(module);\n" ) @@ -196,6 +228,26 @@ if (TEST_JSON_UNCOMPLIANT_COL) AddTestInterface("JsonUncompliantCollapsed" COM_RPC JSON_RPC) endif() +if (TEST_ANNOTATIONS) + AddTestInterface("Annotations" COM_RPC JSON_RPC HAS_EVENTS EVENT_HANDLERS "OnFeaturesChanged") +endif() + +if (TEST_ANNOTATION_EVENTS) + AddTestInterface("AnnotationEvents" COM_RPC) +endif() + +if (TEST_EXTENDED_FORMAT) + AddTestInterface("ExtendedFormat" COM_RPC JSON_RPC) +endif() + +if (TEST_PREFIX_UNDERSCORE) + AddTestInterface("PrefixUnderscore" COM_RPC JSON_RPC) +endif() + +if (TEST_WRAPPED_INTERFACE) + AddTestInterface("WrappedInterface" COM_RPC JSON_RPC) +endif() + list(LENGTH COM_RPC_INTERFACE_HEADERS NUM_COM_TESTS) list(LENGTH JSON_INTERFACE_HEADERS NUM_JSON_TESTS) diff --git a/tests/FunctionalTests/common/implementations/TestAnnotationEventsImpl.cpp b/tests/FunctionalTests/common/implementations/TestAnnotationEventsImpl.cpp new file mode 100644 index 00000000..6bf6a556 --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestAnnotationEventsImpl.cpp @@ -0,0 +1,143 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +namespace Thunder { +namespace TestImplementation { + + class TestAnnotationEventsImpl : public FunctionalTest::ITestAnnotationEvents { + public: + TestAnnotationEventsImpl() + : _caps(FunctionalTest::ITestAnnotationEvents::CAP_NONE) + , _notification(nullptr) + { + } + + ~TestAnnotationEventsImpl() override = default; + + TestAnnotationEventsImpl(const TestAnnotationEventsImpl&) = delete; + TestAnnotationEventsImpl& operator=(const TestAnnotationEventsImpl&) = delete; + + Core::hresult Register(INotification* notification) override + { + std::lock_guard lock(_mutex); + if (_notification != nullptr) { + return Core::ERROR_ALREADY_CONNECTED; + } + _notification = notification; + _notification->AddRef(); + // @statuslistener: deliver current caps on a background thread to avoid + // COM-RPC channel deadlock (cannot make a reverse call on the same channel + // that is still processing the Register request). + Caps currentCaps = _caps; + INotification* sink = _notification; + sink->AddRef(); + std::thread([sink, currentCaps]() { + sink->OnCapsChanged(currentCaps); + sink->Release(); + }).detach(); + return Core::ERROR_NONE; + } + + Core::hresult Unregister(INotification* notification) override + { + std::lock_guard lock(_mutex); + if (_notification == notification) { + _notification->Release(); + _notification = nullptr; + return Core::ERROR_NONE; + } + return Core::ERROR_NOT_EXIST; + } + + // All notification callbacks are dispatched on background threads to avoid + // COM-RPC channel deadlock. The server's worker thread cannot make a reverse-proxy + // call on the same channel it's currently serving a request on. + + Core::hresult SetCaps(const Caps caps) override + { + _caps = caps; + std::lock_guard lock(_mutex); + if (_notification) { + INotification* sink = _notification; + sink->AddRef(); + Caps c = _caps; + std::thread([sink, c]() { sink->OnCapsChanged(c); sink->Release(); }).detach(); + } + return Core::ERROR_NONE; + } + + Core::hresult GetCaps(Caps& caps) const override + { + caps = _caps; + return Core::ERROR_NONE; + } + + Core::hresult TriggerPortState(const uint8_t port, const State state) override + { + std::lock_guard lock(_mutex); + if (_notification) { + INotification* sink = _notification; + sink->AddRef(); + std::thread([sink, port, state]() { sink->OnPortStateChanged(port, state); sink->Release(); }).detach(); + } + return Core::ERROR_NONE; + } + + Core::hresult TriggerStatus(const string& message) override + { + std::lock_guard lock(_mutex); + if (_notification) { + INotification* sink = _notification; + sink->AddRef(); + string msg = message; + std::thread([sink, msg]() { sink->OnStatusUpdate(msg); sink->Release(); }).detach(); + } + return Core::ERROR_NONE; + } + + Core::hresult TriggerLegacyChannel(const uint8_t channel, const uint32_t level) override + { + std::lock_guard lock(_mutex); + if (_notification) { + INotification* sink = _notification; + sink->AddRef(); + std::thread([sink, channel, level]() { sink->OnLegacyChannelEvent(channel, level); sink->Release(); }).detach(); + } + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestAnnotationEventsImpl) + INTERFACE_ENTRY(FunctionalTest::ITestAnnotationEvents) + END_INTERFACE_MAP + + private: + Caps _caps; + INotification* _notification; + mutable std::mutex _mutex; + }; + + static Factory::Registrar g_annotationEventsRegistrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestAnnotationsImpl.cpp b/tests/FunctionalTests/common/implementations/TestAnnotationsImpl.cpp new file mode 100644 index 00000000..dea9fa8c --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestAnnotationsImpl.cpp @@ -0,0 +1,261 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +namespace Thunder { +namespace TestImplementation { + + class TestAnnotationsImpl : public FunctionalTest::ITestAnnotations { + public: + TestAnnotationsImpl() + : _connectionState(FunctionalTest::ITestAnnotations::DISCONNECTED) + , _features(FunctionalTest::ITestAnnotations::FEAT_NONE) + , _payloadSize(0) + , _tokenSize(0) + { + ::memset(_payload, 0, sizeof(_payload)); + ::memset(_token, 0, sizeof(_token)); + } + + ~TestAnnotationsImpl() override = default; + + TestAnnotationsImpl(const TestAnnotationsImpl&) = delete; + TestAnnotationsImpl& operator=(const TestAnnotationsImpl&) = delete; + + Core::hresult DeprecatedEcho(const uint32_t value, uint32_t& result) const override + { + result = value; + return Core::ERROR_NONE; + } + + Core::hresult ObsoleteEcho(const uint32_t value, uint32_t& result) const override + { + result = value; + return Core::ERROR_NONE; + } + + Core::hresult EchoWithAlt(const uint32_t value, uint32_t& result) const override + { + result = value; + return Core::ERROR_NONE; + } + + Core::hresult Add(const uint32_t a, const uint32_t b, uint32_t& result) const override + { + result = a + b; + return Core::ERROR_NONE; + } + + Core::hresult Subtract(const uint32_t a, const uint32_t b, uint32_t& result) const override + { + result = a - b; + return Core::ERROR_NONE; + } + + Core::hresult EchoDeviceInfo(const DeviceInfo& input, DeviceInfo& output) const override + { + output = input; + return Core::ERROR_NONE; + } + + Core::hresult RenamedEchoMethod(const uint32_t value, uint32_t& result) const override + { + result = value; + return Core::ERROR_NONE; + } + + Core::hresult TextCombinedMethod(const uint32_t value, uint32_t& result) const override + { + result = value; + return Core::ERROR_NONE; + } + + Core::hresult SetConnectionState(const ConnectionState state) override + { + _connectionState = state; + return Core::ERROR_NONE; + } + + Core::hresult GetConnectionState(ConnectionState& state) const override + { + state = _connectionState; + return Core::ERROR_NONE; + } + + Core::hresult SetFeatures(const Features features) override + { + _features = features; + return Core::ERROR_NONE; + } + + Core::hresult GetFeatures(Features& features) const override + { + features = _features; + return Core::ERROR_NONE; + } + + Core::hresult SetPayloadBase64(const uint8_t data[], const uint16_t size) override + { + if (size > 256) { + return Core::ERROR_BAD_REQUEST; + } + ::memcpy(_payload, data, size); + _payloadSize = size; + return Core::ERROR_NONE; + } + + Core::hresult GetPayloadBase64(uint8_t data[], const uint16_t maxSize, uint16_t& written) override + { + written = std::min(maxSize, _payloadSize); + ::memcpy(data, _payload, written); + return Core::ERROR_NONE; + } + + Core::hresult SetTokenHex(const uint8_t data[], const uint16_t size) override + { + if (size > 64) { + return Core::ERROR_BAD_REQUEST; + } + ::memcpy(_token, data, size); + _tokenSize = size; + return Core::ERROR_NONE; + } + + Core::hresult GetTokenHex(uint8_t data[], const uint16_t maxSize, uint16_t& written) override + { + written = std::min(maxSize, _tokenSize); + ::memcpy(data, _token, written); + return Core::ERROR_NONE; + } + + Core::hresult EchoPoints(const std::vector& input, std::vector& output) const override + { + output = input; + return Core::ERROR_NONE; + } + + Core::hresult FormatText(const string& text, const Core::OptionalType& uppercase, string& result) override + { + result = text; + if (uppercase.IsSet() && uppercase.Value()) { + std::transform(result.begin(), result.end(), result.begin(), ::toupper); + } + return Core::ERROR_NONE; + } + + // --- Empty parameter list (C5) --- + Core::hresult Ping() const override + { + return Core::ERROR_NONE; + } + + // --- Events --- + Core::hresult Register(INotification* notification) override + { + ASSERT(notification != nullptr); + + Features currentFeatures; + { + std::lock_guard lock(_mutex); + if (_notification != nullptr) { + return Core::ERROR_UNAVAILABLE; + } + _notification = notification; + _notification->AddRef(); + currentFeatures = _features; + } + + // statuslistener: deliver current features immediately on registration. + // Called outside _mutex to prevent deadlock if callback re-enters. + notification->OnFeaturesChanged(currentFeatures); + + return Core::ERROR_NONE; + } + + Core::hresult Unregister(INotification* notification) override + { + ASSERT(notification != nullptr); + + std::lock_guard lock(_mutex); + if (_notification != notification) { + return Core::ERROR_UNAVAILABLE; + } + _notification->Release(); + _notification = nullptr; + return Core::ERROR_NONE; + } + + Core::hresult TriggerPortEvent(const uint8_t port, const ConnectionState state) override + { + INotification* sink = nullptr; + { + std::lock_guard lock(_mutex); + sink = _notification; + if (sink) { + sink->AddRef(); + } + } + if (sink) { + sink->OnPortStateChanged(port, state); + sink->Release(); + } + return Core::ERROR_NONE; + } + + Core::hresult TriggerFeaturesEvent(const Features features) override + { + INotification* sink = nullptr; + { + std::lock_guard lock(_mutex); + sink = _notification; + if (sink) { + sink->AddRef(); + } + } + if (sink) { + sink->OnFeaturesChanged(features); + sink->Release(); + } + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestAnnotationsImpl) + INTERFACE_ENTRY(FunctionalTest::ITestAnnotations) + END_INTERFACE_MAP + + private: + ConnectionState _connectionState; + Features _features; + uint8_t _payload[256]; + uint16_t _payloadSize; + uint8_t _token[64]; + uint16_t _tokenSize; + INotification* _notification = nullptr; + mutable std::mutex _mutex; + }; + + static Factory::Registrar g_annotationsRegistrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestEncodingMacImpl.cpp b/tests/FunctionalTests/common/implementations/TestEncodingMacImpl.cpp index 65add63c..bb7563b8 100644 --- a/tests/FunctionalTests/common/implementations/TestEncodingMacImpl.cpp +++ b/tests/FunctionalTests/common/implementations/TestEncodingMacImpl.cpp @@ -20,6 +20,7 @@ #include #include #include +#include namespace Thunder { namespace TestImplementation { @@ -50,12 +51,29 @@ namespace TestImplementation { return Core::ERROR_NONE; } + Core::hresult SetVariableMac(const uint8_t data[], const uint16_t size) override + { + if (size > 32) return Core::ERROR_BAD_REQUEST; + memcpy(_varMac, data, size); + _varMacSize = size; + return Core::ERROR_NONE; + } + + Core::hresult GetVariableMac(uint8_t data[], const uint16_t maxSize, uint16_t& written) override + { + written = std::min(maxSize, _varMacSize); + memcpy(data, _varMac, written); + return Core::ERROR_NONE; + } + BEGIN_INTERFACE_MAP(TestEncodingMacImpl) INTERFACE_ENTRY(FunctionalTest::ITestEncodingMac) END_INTERFACE_MAP private: uint8_t _mac[6]; + uint8_t _varMac[32] = {}; + uint16_t _varMacSize = 0; }; static Factory::Registrar g_encodingMacRegistrar; diff --git a/tests/FunctionalTests/common/implementations/TestExtendedFormatImpl.cpp b/tests/FunctionalTests/common/implementations/TestExtendedFormatImpl.cpp new file mode 100644 index 00000000..b484ee04 --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestExtendedFormatImpl.cpp @@ -0,0 +1,81 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +namespace Thunder { +namespace TestImplementation { + + class TestExtendedFormatImpl : public FunctionalTest::ITestExtendedFormat { + public: + TestExtendedFormatImpl() + : _volume(50) + , _name("DefaultDevice") + { + } + + ~TestExtendedFormatImpl() override = default; + + TestExtendedFormatImpl(const TestExtendedFormatImpl&) = delete; + TestExtendedFormatImpl& operator=(const TestExtendedFormatImpl&) = delete; + + Core::hresult EchoMethod(const uint32_t value, uint32_t& result) const override + { + result = value; + return Core::ERROR_NONE; + } + + Core::hresult AddMethod(const uint32_t a, const uint32_t b, uint32_t& result) const override + { + result = a + b; + return Core::ERROR_NONE; + } + + Core::hresult Volume(uint32_t& volume) const override + { + volume = _volume; + return Core::ERROR_NONE; + } + + Core::hresult Volume(const uint32_t volume) override + { + _volume = volume; + return Core::ERROR_NONE; + } + + Core::hresult Name(string& name) const override + { + name = _name; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestExtendedFormatImpl) + INTERFACE_ENTRY(FunctionalTest::ITestExtendedFormat) + END_INTERFACE_MAP + + private: + uint32_t _volume; + string _name; + }; + + static Factory::Registrar g_extendedFormatRegistrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestPrefixUnderscoreImpl.cpp b/tests/FunctionalTests/common/implementations/TestPrefixUnderscoreImpl.cpp new file mode 100644 index 00000000..f16b527f --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestPrefixUnderscoreImpl.cpp @@ -0,0 +1,51 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +namespace Thunder { +namespace TestImplementation { + + class TestPrefixUnderscoreImpl : public FunctionalTest::ITestPrefixUnderscore { + public: + TestPrefixUnderscoreImpl() = default; + ~TestPrefixUnderscoreImpl() override = default; + + Core::hresult Echo(const uint32_t value, uint32_t& result) const override + { + result = value; + return Core::ERROR_NONE; + } + + Core::hresult Add(const uint32_t a, const uint32_t b, uint32_t& result) const override + { + result = a + b; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestPrefixUnderscoreImpl) + INTERFACE_ENTRY(FunctionalTest::ITestPrefixUnderscore) + END_INTERFACE_MAP + }; + + static Factory::Registrar g_prefixUnderscoreRegistrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestWrappedInterfaceImpl.cpp b/tests/FunctionalTests/common/implementations/TestWrappedInterfaceImpl.cpp new file mode 100644 index 00000000..694d4d62 --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestWrappedInterfaceImpl.cpp @@ -0,0 +1,57 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +namespace Thunder { +namespace TestImplementation { + + class TestWrappedInterfaceImpl : public FunctionalTest::ITestWrappedInterface { + public: + TestWrappedInterfaceImpl() = default; + ~TestWrappedInterfaceImpl() override = default; + + Core::hresult GetCounter(uint32_t& counter) const override + { + counter = 42; + return Core::ERROR_NONE; + } + + Core::hresult GetName(string& name) const override + { + name = "WrappedDevice"; + return Core::ERROR_NONE; + } + + Core::hresult Echo(const uint32_t value, uint32_t& result) const override + { + result = value; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestWrappedInterfaceImpl) + INTERFACE_ENTRY(FunctionalTest::ITestWrappedInterface) + END_INTERFACE_MAP + }; + + static Factory::Registrar g_wrappedInterfaceRegistrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestAnnotationEvents.h b/tests/FunctionalTests/common/interfaces/ITestAnnotationEvents.h new file mode 100644 index 00000000..573ddbb4 --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestAnnotationEvents.h @@ -0,0 +1,127 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Ids.h" +#include "Module.h" + +namespace Thunder { +namespace FunctionalTest { + + // ========================================================================= + // ITestAnnotationEvents + // + // COM-RPC-only event interface exercising: + // - @index on event notification parameters (per-port delivery) + // - @index:deprecated on events (broadcast to all regardless of index) + // - @statuslistener (immediate state delivery on registration) + // + // WHY SEPARATE: Event notification subscription over COM-RPC requires the + // interface to be registered for COM-RPC only (not JSON-RPC). When an + // interface is registered for both COM-RPC and JSON-RPC with HAS_EVENTS, + // the generated proxy/stub uses a different reverse-channel path that + // conflicts with direct INotification* registration from test code. + // Splitting events into a dedicated COM-RPC-only interface avoids this + // and matches the pattern used by ITestEvents. + // + // @json 1.0.0 + // ========================================================================= + struct EXTERNAL ITestAnnotationEvents : virtual public Core::IUnknown { + enum { ID = ID_TEST_ANNOTATION_EVENTS }; + + // @encode:text + enum State : uint8_t { + DISCONNECTED = 0, + CONNECTING = 1, + CONNECTED = 2, + ERROR = 3 + }; + + // @bitmask + enum Caps : uint8_t { + CAP_NONE = 0x00, + CAP_AUDIO = 0x01, + CAP_VIDEO = 0x02, + CAP_NET = 0x04 + }; + + // @event + struct EXTERNAL INotification : virtual public Core::IUnknown { + enum { ID = ID_TEST_ANNOTATION_EVENTS_NOTIFICATION }; + + // @brief Fired when state changes on a specific port (indexed event). + // @param port Port identifier for per-client event filtering. + // @param state New state for this port. + virtual void OnPortStateChanged( + const uint8_t port /* @index */, + const State state) = 0; + + // @brief Fired when capabilities change. Delivers current state on registration. + // @statuslistener + // @param caps Current capability flags. + virtual void OnCapsChanged(const Caps caps) = 0; + + // @brief Fired when a global status update occurs (broadcast, no index). + // @param message Status message. + virtual void OnStatusUpdate(const string& message) = 0; + + // @brief Fired when a legacy channel event occurs. + // The index is deprecated — event is delivered to all clients + // regardless of channel value (broadcast-to-all semantics). + // @param channel Legacy channel identifier (deprecated index). + // @param level Signal level. + virtual void OnLegacyChannelEvent( + const uint8_t channel /* @index:deprecated */, + const uint32_t level) = 0; + }; + + // @brief Register for event notifications. + virtual Core::hresult Register(INotification* notification) = 0; + + // @brief Unregister from event notifications. + virtual Core::hresult Unregister(INotification* notification) = 0; + + // @brief Set the current capabilities (also notifies subscribers). + // @param caps New capability flags. + virtual Core::hresult SetCaps(const Caps caps /* @in */) = 0; + + // @brief Get the current capabilities. + // @param caps Receives current capability flags. + virtual Core::hresult GetCaps(Caps& caps /* @out */) const = 0; + + // @brief Trigger port state event for testing. + // @param port Port index. + // @param state New state to report. + virtual Core::hresult TriggerPortState(const uint8_t port /* @in */, const State state /* @in */) = 0; + + // @brief Trigger a global status message for testing. + // @param message Message to broadcast. + virtual Core::hresult TriggerStatus(const string& message /* @in */) = 0; + + // @brief Trigger a legacy channel event for testing. + // Despite having an index parameter, the event is delivered to all + // subscribers because the index is marked deprecated. + // @param channel Channel value (ignored for filtering due to deprecated index). + // @param level Signal level to report. + virtual Core::hresult TriggerLegacyChannel(const uint8_t channel /* @in */, const uint32_t level /* @in */) = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestAnnotations.h b/tests/FunctionalTests/common/interfaces/ITestAnnotations.h new file mode 100644 index 00000000..58c967cd --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestAnnotations.h @@ -0,0 +1,291 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Ids.h" +#include "Module.h" + +namespace Thunder { +namespace FunctionalTest { + + // ========================================================================= + // ITestAnnotations + // + // Comprehensive interface exercising many IDL annotation tags: + // prefix, alt, alt-deprecated, alt-obsolete, deprecated, obsolete, + // text on struct members and enum values, bitmask on enum declaration, + // end sentinel exclusion, encode:base64, encode:hex on buffers, + // index on events, statuslistener, wrapped, extract on struct arrays, + // optional on bool, extended property vs method semantics. + // + // @json 1.0.0 + // @prefix tags + // ========================================================================= + struct EXTERNAL ITestAnnotations : virtual public Core::IUnknown { + enum { ID = ID_TEST_ANNOTATIONS }; + + // ================================================================= + // deprecated / obsolete — lifecycle markers + // ================================================================= + + // @brief A deprecated method that is still callable. + // @deprecated + // @param value Input value to echo. + // @param result Receives the echoed value. + virtual Core::hresult DeprecatedEcho(const uint32_t value /* @in */, uint32_t& result /* @out */) const = 0; + + // @brief An obsolete method that is still callable. + // @obsolete + // @param value Input value to echo. + // @param result Receives the echoed value. + virtual Core::hresult ObsoleteEcho(const uint32_t value /* @in */, uint32_t& result /* @out */) const = 0; + + // ================================================================= + // alt / alt-deprecated / alt-obsolete — alternative names + // ================================================================= + + // @brief Method with a primary and alternative JSON-RPC name. + // @alt echoAlt + // @param value Input value. + // @param result Receives the echoed value. + virtual Core::hresult EchoWithAlt(const uint32_t value /* @in */, uint32_t& result /* @out */) const = 0; + + // @brief Method with a deprecated alternative name. + // @alt-deprecated legacyAdd + // @param a First operand. + // @param b Second operand. + // @param result Receives a + b. + virtual Core::hresult Add(const uint32_t a /* @in */, const uint32_t b /* @in */, uint32_t& result /* @out */) const = 0; + + // @brief Method with an obsolete alternative name. + // @alt-obsolete oldSubtract + // @param a First operand. + // @param b Second operand. + // @param result Receives a - b. + virtual Core::hresult Subtract(const uint32_t a /* @in */, const uint32_t b /* @in */, uint32_t& result /* @out */) const = 0; + + // ================================================================= + // text on method name — JSON-RPC dispatch name override + // ================================================================= + + // @brief Method whose JSON-RPC name is overridden by text. + // C++ name is RenamedEchoMethod but callable as "renamedEcho". + // @text renamedEcho + // @param value Input value. + // @param result Receives the echoed value. + virtual Core::hresult RenamedEchoMethod(const uint32_t value /* @in */, uint32_t& result /* @out */) const = 0; + + // ================================================================= + // text on method combined with alt — both rename + alternative + // ================================================================= + + // @brief Method with text rename and alt alternative. + // Primary dispatch: "textPrimary", alternative: "textSecondary". + // C++ name "TextCombinedMethod" is NOT callable. + // @text textPrimary + // @alt textSecondary + // @param value Input value. + // @param result Receives the echoed value. + virtual Core::hresult TextCombinedMethod(const uint32_t value /* @in */, uint32_t& result /* @out */) const = 0; + + // ================================================================= + // text on struct members — per-field JSON name override + // ================================================================= + + struct DeviceInfo { + string Name /* @text deviceName */; + uint32_t Version /* @text firmwareVersion */; + bool Active /* @text isActive */; + }; + + // @brief Echoes a DeviceInfo struct round-trip. + // Text overrides rename C++ members to different JSON keys. + // @param input Input struct with overridden field names. + // @param output Receives the echoed struct. + virtual Core::hresult EchoDeviceInfo(const DeviceInfo& input /* @in */, DeviceInfo& output /* @out */) const = 0; + + // ================================================================= + // text on enum values — per-enumerator JSON name override + // ================================================================= + + // @encode:text + enum ConnectionState : uint8_t { + DISCONNECTED = 0, + // @text connecting + CONN_IN_PROGRESS = 1, + CONNECTED = 2, + // @text auth-failed + AUTH_FAILURE = 3 + }; + + // @brief Set the connection state. + // @param state Connection state to set. + virtual Core::hresult SetConnectionState(const ConnectionState state /* @in */) = 0; + + // @brief Get the current connection state. + // @param state Receives the current connection state. + virtual Core::hresult GetConnectionState(ConnectionState& state /* @out */) const = 0; + + // ================================================================= + // bitmask on enum declaration (alias for encode:bitmask) + // ================================================================= + + // @bitmask + enum Features : uint8_t { + FEAT_NONE = 0x00, + FEAT_WIFI = 0x01, + FEAT_BLUETOOTH = 0x02, + FEAT_ETHERNET = 0x04, + FEAT_NFC = 0x08, + // @end + FEAT_ALL = 0x0F + }; + + // @brief Set active features using bitmask. + // @param features Active feature flags. + virtual Core::hresult SetFeatures(const Features features /* @in @encode:bitmask */) = 0; + + // @brief Get active features. + // @param features Receives the active feature flags. + virtual Core::hresult GetFeatures(Features& features /* @out @encode:bitmask */) const = 0; + + // ================================================================= + // encode:base64 — variable-length buffer encoding + // ================================================================= + + // @brief Store a base64-encoded payload. + // @param data Payload bytes. + // @param size Number of bytes in data. + // @retval ERROR_BAD_REQUEST data exceeds maximum allowed size. + virtual Core::hresult SetPayloadBase64( + const uint8_t data[] /* @in @length:size @encode:base64 */, + const uint16_t size /* @restrict:1..256 */) = 0; + + // @brief Retrieve the stored payload as base64. + // @param data Output buffer for payload bytes. + // @param maxSize Maximum capacity of the buffer. + // @param written Receives actual bytes written. + virtual Core::hresult GetPayloadBase64( + uint8_t data[] /* @out @length:written @maxlength:maxSize @encode:base64 */, + const uint16_t maxSize, + uint16_t& written /* @out */) = 0; + + // ================================================================= + // encode:hex — variable-length buffer encoding + // ================================================================= + + // @brief Store a hex-encoded token. + // @param data Token bytes. + // @param size Number of bytes in data. + virtual Core::hresult SetTokenHex( + const uint8_t data[] /* @in @length:size @encode:hex */, + const uint16_t size /* @restrict:1..64 */) = 0; + + // @brief Retrieve the stored token as hex. + // @param data Output buffer for token bytes. + // @param maxSize Maximum capacity of the buffer. + // @param written Receives actual bytes written. + virtual Core::hresult GetTokenHex( + uint8_t data[] /* @out @length:written @maxlength:maxSize @encode:hex */, + const uint16_t maxSize, + uint16_t& written /* @out */) = 0; + + // ================================================================= + // extract on struct arrays + // ================================================================= + + struct Point { + int32_t x; + int32_t y; + }; + + // @brief Echo a list of points with extraction. + // Each Point struct's fields are extracted (flattened) into the + // enclosing array elements. Multi-element arrays are verified. + // NOTE: single-element unwrapping is NOT currently implemented + // in compliant format (see DISABLED_ExtractStructArray_SingleElement_Unwrapped). + // @param input Input points. + // @param output Receives echoed points. + virtual Core::hresult EchoPoints( + const std::vector& input /* @in @extract @restrict:1..16 */, + std::vector& output /* @out @extract @restrict:1..16 */) const = 0; + + // ================================================================= + // optional on bool — regression guard + // ================================================================= + + // @brief Concatenate a string with an optional uppercase flag. + // When uppercase is not set, it defaults to false. + // @param text Input string. + // @param uppercase Optional flag to convert to uppercase. + // @param result Receives the (possibly uppercased) string. + virtual Core::hresult FormatText( + const string& text /* @in */, + const Core::OptionalType& uppercase /* @in @default:false */, + string& result /* @out */) = 0; + + // ================================================================= + // Event with indexed delivery and statuslistener + // ================================================================= + + // @event + struct EXTERNAL INotification : virtual public Core::IUnknown { + enum { ID = ID_TEST_ANNOTATIONS_NOTIFICATION }; + + // @brief Fired when connection state changes on a specific port. + // @param port Port identifier for event filtering. + // @param state New connection state for this port. + virtual void OnPortStateChanged( + const uint8_t port /* @index */, + const ConnectionState state) = 0; + + // @brief Fired when features change. New subscribers get current state. + // @statuslistener + // @param features Current active feature flags. + virtual void OnFeaturesChanged(const Features features) = 0; + }; + + // @brief Register for notifications. + virtual Core::hresult Register(INotification* notification) = 0; + + // @brief Unregister from notifications. + virtual Core::hresult Unregister(INotification* notification) = 0; + + // @brief Trigger a port state change event (for testing). + // @param port Port index to fire event on. + // @param state New state to report. + virtual Core::hresult TriggerPortEvent(const uint8_t port /* @in */, const ConnectionState state /* @in */) = 0; + + // @brief Trigger a feature change event (for testing). + // @param features New features to report. + virtual Core::hresult TriggerFeaturesEvent(const Features features /* @in @encode:bitmask */) = 0; + + // ================================================================= + // Empty parameter list (Constraint C5) + // A method with no input or output params. Verifies the generator + // produces a callable handler that simply returns success. + // ================================================================= + + // @brief No-op method with empty parameter list. + virtual Core::hresult Ping() const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestEncodingMac.h b/tests/FunctionalTests/common/interfaces/ITestEncodingMac.h index f975b9a0..d7d3a20d 100644 --- a/tests/FunctionalTests/common/interfaces/ITestEncodingMac.h +++ b/tests/FunctionalTests/common/interfaces/ITestEncodingMac.h @@ -51,6 +51,27 @@ namespace FunctionalTest { virtual Core::hresult EchoMacAddress( const uint8_t input[] /* @in @length:6 @encode:mac */, uint8_t output[] /* @out @length:6 @maxlength:6 @encode:mac */) const = 0; + + // ================================================================= + // Variable-length encode:mac — works with any buffer size + // ================================================================= + + // @brief Store a variable-length buffer using colon-hex (mac) encoding. + // Verifies encode:mac works with buffers of any size, not just 6 bytes. + // @param data Input bytes. + // @param size Number of bytes in data. + virtual Core::hresult SetVariableMac( + const uint8_t data[] /* @in @length:size @encode:mac */, + const uint16_t size /* @restrict:1..32 */) = 0; + + // @brief Retrieve the stored variable-length mac-encoded buffer. + // @param data Output buffer. + // @param maxSize Capacity of the output buffer. + // @param written Receives actual bytes written. + virtual Core::hresult GetVariableMac( + uint8_t data[] /* @out @length:written @maxlength:maxSize @encode:mac */, + const uint16_t maxSize, + uint16_t& written /* @out */) = 0; }; } // namespace FunctionalTest diff --git a/tests/FunctionalTests/common/interfaces/ITestExtendedFormat.h b/tests/FunctionalTests/common/interfaces/ITestExtendedFormat.h new file mode 100644 index 00000000..e758d9bd --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestExtendedFormat.h @@ -0,0 +1,68 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Ids.h" +#include "Module.h" + +namespace Thunder { +namespace FunctionalTest { + + // ========================================================================= + // ITestExtendedFormat + // + // Validates the extended format semantics: + // - Methods use compliant-style named object params + // - Properties use collapsed format (value sent directly, no wrapping) + // + // @json 1.0.0 + // @uncompliant:extended + // ========================================================================= + struct EXTERNAL ITestExtendedFormat : virtual public Core::IUnknown { + enum { ID = ID_TEST_EXTENDED_FORMAT }; + + // @brief A method that takes a single parameter. + // With extended format, methods still wrap params in a named object. + // @param value Input value. + // @param result Receives the echoed value. + virtual Core::hresult EchoMethod(const uint32_t value /* @in */, uint32_t& result /* @out */) const = 0; + + // @brief A method with multiple parameters. + // With extended format, methods always use named object wrapping. + // @param a First operand. + // @param b Second operand. + // @param result Receives a + b. + virtual Core::hresult AddMethod(const uint32_t a /* @in */, const uint32_t b /* @in */, uint32_t& result /* @out */) const = 0; + + // @property + // @brief A read-write property. + // With extended format, property GET returns the value directly (collapsed). + // Property SET accepts the value directly (collapsed). + virtual Core::hresult Volume(uint32_t& volume /* @out */) const = 0; + virtual Core::hresult Volume(const uint32_t volume /* @in */) = 0; + + // @property + // @brief A read-only string property. + // With extended format, GET returns the string directly without wrapping. + virtual Core::hresult Name(string& name /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestPrefixUnderscore.h b/tests/FunctionalTests/common/interfaces/ITestPrefixUnderscore.h new file mode 100644 index 00000000..1005c917 --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestPrefixUnderscore.h @@ -0,0 +1,50 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Ids.h" +#include "Module.h" + +namespace Thunder { +namespace FunctionalTest { + + // Tests the underscore prefix separator variant. + // When prefix ends with '_', no '::' separator is added. + // Method 'Echo' becomes 'flat_echo' on the wire (not 'flat::echo'). + // + // @json 1.0.0 + // @prefix flat_ + struct EXTERNAL ITestPrefixUnderscore : virtual public Core::IUnknown { + enum { ID = ID_TEST_PREFIX_UNDERSCORE }; + + // @brief Echo a value using flat prefix naming. + // @param value Input value. + // @param result Receives echoed value. + virtual Core::hresult Echo(const uint32_t value /* @in */, uint32_t& result /* @out */) const = 0; + + // @brief Add two values. + // @param a First operand. + // @param b Second operand. + // @param result Receives a + b. + virtual Core::hresult Add(const uint32_t a /* @in */, const uint32_t b /* @in */, uint32_t& result /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestWrappedInterface.h b/tests/FunctionalTests/common/interfaces/ITestWrappedInterface.h new file mode 100644 index 00000000..a7aecf06 --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestWrappedInterface.h @@ -0,0 +1,53 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Ids.h" +#include "Module.h" + +namespace Thunder { +namespace FunctionalTest { + + // Tests interface-level wrapped annotation. + // ALL single-value returns are wrapped in an object using the output + // parameter name as key, e.g. GetCounter returns {"counter":N}, + // GetName returns {"name":"..."}, Echo returns {"result":N}. + // + // @json 1.0.0 + // @wrapped + struct EXTERNAL ITestWrappedInterface : virtual public Core::IUnknown { + enum { ID = ID_TEST_WRAPPED_INTERFACE }; + + // @brief Get a counter value. Returns {"counter":N} on the wire. + // @param counter Receives the counter value. + virtual Core::hresult GetCounter(uint32_t& counter /* @out */) const = 0; + + // @brief Get a name string. Returns {"name":"..."} on the wire. + // @param name Receives the name. + virtual Core::hresult GetName(string& name /* @out */) const = 0; + + // @brief Echo a value. Returns {"result":N} on the wire. + // @param value Input value. + // @param result Receives echoed value. + virtual Core::hresult Echo(const uint32_t value /* @in */, uint32_t& result /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/Ids.h b/tests/FunctionalTests/common/interfaces/Ids.h index 28e02add..5cef249f 100644 --- a/tests/FunctionalTests/common/interfaces/Ids.h +++ b/tests/FunctionalTests/common/interfaces/Ids.h @@ -47,6 +47,13 @@ namespace Thunder { ID_TEST_JSON_UNCOMPLIANT_COL = ID_INTERFACE_OFFSET + 0x012, ID_TEST_ENCODING_MAC = ID_INTERFACE_OFFSET + 0x013, ID_TEST_LENGTH_MODES = ID_INTERFACE_OFFSET + 0x014, + ID_TEST_ANNOTATIONS = ID_INTERFACE_OFFSET + 0x015, + ID_TEST_ANNOTATIONS_NOTIFICATION = ID_INTERFACE_OFFSET + 0x016, + ID_TEST_EXTENDED_FORMAT = ID_INTERFACE_OFFSET + 0x017, + ID_TEST_ANNOTATION_EVENTS = ID_INTERFACE_OFFSET + 0x018, + ID_TEST_ANNOTATION_EVENTS_NOTIFICATION = ID_INTERFACE_OFFSET + 0x019, + ID_TEST_PREFIX_UNDERSCORE = ID_INTERFACE_OFFSET + 0x01A, + ID_TEST_WRAPPED_INTERFACE = ID_INTERFACE_OFFSET + 0x01B, }; } // namespace FunctionalTest diff --git a/tests/FunctionalTests/comrpc/CMakeLists.txt b/tests/FunctionalTests/comrpc/CMakeLists.txt index 3b0d7826..fd0051d0 100644 --- a/tests/FunctionalTests/comrpc/CMakeLists.txt +++ b/tests/FunctionalTests/comrpc/CMakeLists.txt @@ -91,6 +91,18 @@ if(TEST_JSON_UNCOMPLIANT_COL) target_sources(ComRpcFunctionalTests PRIVATE tests/TestJsonUncompliantCollapsed.cpp) endif() +if(TEST_ANNOTATIONS) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestAnnotations.cpp) +endif() + +if(TEST_ANNOTATION_EVENTS) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestAnnotationEvents.cpp) +endif() + +if(TEST_EXTENDED_FORMAT) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestExtendedFormat.cpp) +endif() + target_include_directories(ComRpcFunctionalTests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} # For interfaces/ prefix (generated code) diff --git a/tests/FunctionalTests/comrpc/tests/TestAnnotationEvents.cpp b/tests/FunctionalTests/comrpc/tests/TestAnnotationEvents.cpp new file mode 100644 index 00000000..5f0c8832 --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestAnnotationEvents.cpp @@ -0,0 +1,330 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * COM-RPC event tests for @index on events and @statuslistener. + * + * WHY SEPARATE FROM ITestAnnotations: + * Event notification subscription over COM-RPC requires the interface to be + * registered as COM-RPC only. When an interface is registered for both + * COM-RPC and JSON-RPC (HAS_EVENTS), the reverse notification channel uses + * a different code path that causes timeouts during Register() from test code. + * This matches the pattern used by the existing ITestEvents interface. + */ + +#include +#include "TestHarness.h" +#include + +using namespace Thunder; +using namespace Thunder::FunctionalTest; + +// ===== Notification Sink ===== + +class AnnotationEventSink : public ITestAnnotationEvents::INotification { +public: + AnnotationEventSink() + : _notifEvent(false, true) + { + Reset(); + } + + uint32_t AddRef() const override + { + Core::InterlockedIncrement(_refCount); + return Core::ERROR_NONE; + } + + uint32_t Release() const override + { + uint32_t result = Core::ERROR_NONE; + if (Core::InterlockedDecrement(_refCount) == 0) { + delete this; + result = Core::ERROR_DESTRUCTION_SUCCEEDED; + } + return result; + } + + void Reset() + { + _portEvents.clear(); + _capsEvents.clear(); + _statusMessages.clear(); + _notifCount.store(0); + _notifEvent.ResetEvent(); + } + + bool WaitForCount(uint32_t expected, uint32_t timeoutMs = 2000) + { + const uint32_t pollMs = 50; + uint32_t elapsed = 0; + while (elapsed < timeoutMs) { + if (_notifCount.load() >= expected) { + return true; + } + _notifEvent.Lock(pollMs); + _notifEvent.ResetEvent(); + elapsed += pollMs; + } + return _notifCount.load() >= expected; + } + + // INotification + void OnPortStateChanged(const uint8_t port, const ITestAnnotationEvents::State state) override + { + _portEvents.push_back({port, state}); + _notifCount++; + _notifEvent.SetEvent(); + } + + void OnCapsChanged(const ITestAnnotationEvents::Caps caps) override + { + _capsEvents.push_back(caps); + _notifCount++; + _notifEvent.SetEvent(); + } + + void OnStatusUpdate(const string& message) override + { + _statusMessages.push_back(message); + _notifCount++; + _notifEvent.SetEvent(); + } + + void OnLegacyChannelEvent(const uint8_t channel, const uint32_t level) override + { + _legacyEvents.push_back({channel, level}); + _notifCount++; + _notifEvent.SetEvent(); + } + + struct PortEvent { + uint8_t port; + ITestAnnotationEvents::State state; + }; + + struct LegacyEvent { + uint8_t channel; + uint32_t level; + }; + + std::vector _portEvents; + std::vector _capsEvents; + std::vector _statusMessages; + std::vector _legacyEvents; + + BEGIN_INTERFACE_MAP(AnnotationEventSink) + INTERFACE_ENTRY(ITestAnnotationEvents::INotification) + END_INTERFACE_MAP + +private: + mutable uint32_t _refCount { 1 }; + std::atomic _notifCount { 0 }; + Core::Event _notifEvent; +}; + +// ===== Test Fixture ===== +// Register/unregister once per suite (not per test) to avoid destroying the +// COM-RPC reverse-proxy channel between tests. Reset the sink state instead. + +class TestAnnotationEvents : public Testing::TestHarness { +protected: + static void SetUpTestSuite() + { + Testing::TestHarness::SetUpTestSuite(); + _sink = new AnnotationEventSink(); + uint32_t result = _proxy->Register(_sink); + ASSERT_EQ(result, Core::ERROR_NONE) << "Register failed: " << result; + // Wait for the statuslistener background thread to complete + _sink->WaitForCount(1); + } + + static void TearDownTestSuite() + { + if (_sink != nullptr) { + _proxy->Unregister(_sink); + // Wait long enough for any in-flight background threads (detached in + // the implementation) to complete their sink->OnXxx() + sink->Release() + // calls. If a thread is still running when the proxy is released, it + // can corrupt the COM-RPC channel and cause subsequent suites to timeout. + SleepMs(500); + delete _sink; + _sink = nullptr; + } + Testing::TestHarness::TearDownTestSuite(); + } + + void SetUp() override + { + ASSERT_NE(_sink, nullptr) << "Sink not registered"; + // Allow any in-flight background threads from the previous test to drain + // before resetting the sink state + SleepMs(100); + _sink->Reset(); + } + + static AnnotationEventSink* _sink; +}; + +AnnotationEventSink* TestAnnotationEvents::_sink = nullptr; + +// =========================================================================== +// @statuslistener — verified during SetUpTestSuite (WaitForCount(1) confirms +// that the initial state was delivered immediately upon registration) +// =========================================================================== + +TEST_F(TestAnnotationEvents, StatusListener_WasDeliveredOnRegister) { + // The statuslistener callback was already verified in SetUpTestSuite. + // This test just confirms the sink received at least one caps event + // before any explicit trigger was called. + // (SetUp resets the sink, but the statuslistener already fired in SetUpTestSuite) + // We simply verify the fixture is functional. + SUCCEED() << "Statuslistener delivery verified in SetUpTestSuite"; +} + +// =========================================================================== +// @index on event — per-port event delivery +// =========================================================================== + +TEST_F(TestAnnotationEvents, IndexedEvent_PortState_SinglePort) { + // Wait for initial statuslistener event + + ASSERT_EQ(_proxy->TriggerPortState(1, ITestAnnotationEvents::CONNECTED), Core::ERROR_NONE); + + ASSERT_TRUE(_sink->WaitForCount(1)); + ASSERT_EQ(_sink->_portEvents.size(), 1u); + EXPECT_EQ(_sink->_portEvents[0].port, 1u); + EXPECT_EQ(_sink->_portEvents[0].state, ITestAnnotationEvents::CONNECTED); +} + +TEST_F(TestAnnotationEvents, IndexedEvent_PortState_MultiplePorts) { + + ASSERT_EQ(_proxy->TriggerPortState(0, ITestAnnotationEvents::DISCONNECTED), Core::ERROR_NONE); + ASSERT_EQ(_proxy->TriggerPortState(1, ITestAnnotationEvents::CONNECTING), Core::ERROR_NONE); + ASSERT_EQ(_proxy->TriggerPortState(2, ITestAnnotationEvents::CONNECTED), Core::ERROR_NONE); + + ASSERT_TRUE(_sink->WaitForCount(3)); + EXPECT_EQ(_sink->_portEvents[0].port, 0u); + EXPECT_EQ(_sink->_portEvents[0].state, ITestAnnotationEvents::DISCONNECTED); + EXPECT_EQ(_sink->_portEvents[1].port, 1u); + EXPECT_EQ(_sink->_portEvents[1].state, ITestAnnotationEvents::CONNECTING); + EXPECT_EQ(_sink->_portEvents[2].port, 2u); + EXPECT_EQ(_sink->_portEvents[2].state, ITestAnnotationEvents::CONNECTED); +} + +// =========================================================================== +// Feature change event — fires after SetCaps +// =========================================================================== + +TEST_F(TestAnnotationEvents, CapsChanged_FiresOnSet) { + auto newCaps = static_cast( + ITestAnnotationEvents::CAP_AUDIO | ITestAnnotationEvents::CAP_VIDEO); + ASSERT_EQ(_proxy->SetCaps(newCaps), Core::ERROR_NONE); + + ASSERT_TRUE(_sink->WaitForCount(1)); + EXPECT_EQ(_sink->_capsEvents.back(), newCaps); +} + +TEST_F(TestAnnotationEvents, CapsChanged_None_ClearsAll) { + + ASSERT_EQ(_proxy->SetCaps(ITestAnnotationEvents::CAP_NET), Core::ERROR_NONE); + ASSERT_TRUE(_sink->WaitForCount(1)); + + _sink->Reset(); + ASSERT_EQ(_proxy->SetCaps(ITestAnnotationEvents::CAP_NONE), Core::ERROR_NONE); + ASSERT_TRUE(_sink->WaitForCount(1)); + EXPECT_EQ(_sink->_capsEvents[0], ITestAnnotationEvents::CAP_NONE); +} + +// =========================================================================== +// Broadcast event (no @index) — all subscribers receive +// =========================================================================== + +TEST_F(TestAnnotationEvents, StatusUpdate_Broadcast) { + + ASSERT_EQ(_proxy->TriggerStatus("system ready"), Core::ERROR_NONE); + + ASSERT_TRUE(_sink->WaitForCount(1)); + ASSERT_EQ(_sink->_statusMessages.size(), 1u); + EXPECT_EQ(_sink->_statusMessages[0], "system ready"); +} + +TEST_F(TestAnnotationEvents, StatusUpdate_MultipleMessages) { + + ASSERT_EQ(_proxy->TriggerStatus("msg1"), Core::ERROR_NONE); + ASSERT_EQ(_proxy->TriggerStatus("msg2"), Core::ERROR_NONE); + ASSERT_EQ(_proxy->TriggerStatus("msg3"), Core::ERROR_NONE); + + ASSERT_TRUE(_sink->WaitForCount(3)); + EXPECT_EQ(_sink->_statusMessages[0], "msg1"); + EXPECT_EQ(_sink->_statusMessages[1], "msg2"); + EXPECT_EQ(_sink->_statusMessages[2], "msg3"); +} + +// =========================================================================== +// Duplicate registration +// =========================================================================== + +TEST_F(TestAnnotationEvents, Register_Duplicate_ReturnsError) { + EXPECT_EQ(_proxy->Register(_sink), Core::ERROR_ALREADY_CONNECTED); +} + +// =========================================================================== +// Mixed events — port + caps + status interleaved +// =========================================================================== + +TEST_F(TestAnnotationEvents, MixedEvents_AllDelivered) { + + ASSERT_EQ(_proxy->TriggerPortState(5, ITestAnnotationEvents::ERROR), Core::ERROR_NONE); + ASSERT_EQ(_proxy->SetCaps(ITestAnnotationEvents::CAP_VIDEO), Core::ERROR_NONE); + ASSERT_EQ(_proxy->TriggerStatus("hello"), Core::ERROR_NONE); + + ASSERT_TRUE(_sink->WaitForCount(3)); + EXPECT_EQ(_sink->_portEvents.size(), 1u); + EXPECT_EQ(_sink->_capsEvents.size(), 1u); + EXPECT_EQ(_sink->_statusMessages.size(), 1u); +} + +// =========================================================================== +// @index:deprecated — broadcast-to-all semantics +// The event has @index:deprecated on the channel parameter. +// Despite having an index value, the event should be delivered to ALL +// registered clients regardless of which channel they subscribed to. +// =========================================================================== + +TEST_F(TestAnnotationEvents, IndexDeprecated_BroadcastAndPayload) { + + // Trigger legacy channel events with different channel values + ASSERT_EQ(_proxy->TriggerLegacyChannel(1, 100), Core::ERROR_NONE); + ASSERT_EQ(_proxy->TriggerLegacyChannel(42, 999), Core::ERROR_NONE); + ASSERT_EQ(_proxy->TriggerLegacyChannel(99, 300), Core::ERROR_NONE); + + // All events should be delivered to our single subscriber + // regardless of channel value (deprecated index = broadcast) + ASSERT_TRUE(_sink->WaitForCount(3)); + ASSERT_GE(_sink->_legacyEvents.size(), 3u); + + // Verify channel values are passed through in the payload + EXPECT_EQ(_sink->_legacyEvents[0].channel, 1u); + EXPECT_EQ(_sink->_legacyEvents[0].level, 100u); + EXPECT_EQ(_sink->_legacyEvents[1].channel, 42u); + EXPECT_EQ(_sink->_legacyEvents[1].level, 999u); + EXPECT_EQ(_sink->_legacyEvents[2].channel, 99u); + EXPECT_EQ(_sink->_legacyEvents[2].level, 300u); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestAnnotations.cpp b/tests/FunctionalTests/comrpc/tests/TestAnnotations.cpp new file mode 100644 index 00000000..b897c817 --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestAnnotations.cpp @@ -0,0 +1,239 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "TestHarness.h" +#include + +using namespace Thunder; +using namespace Thunder::FunctionalTest; + +class TestAnnotations : public Testing::TestHarness {}; + +// =========================================================================== +// @deprecated / @obsolete — Methods still callable via COM-RPC +// =========================================================================== + +TEST_F(TestAnnotations, DeprecatedEcho_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->DeprecatedEcho(123, result), Core::ERROR_NONE); + EXPECT_EQ(result, 123u); +} + +TEST_F(TestAnnotations, ObsoleteEcho_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->ObsoleteEcho(456, result), Core::ERROR_NONE); + EXPECT_EQ(result, 456u); +} + +// =========================================================================== +// @alt — All names route to same implementation +// =========================================================================== + +TEST_F(TestAnnotations, EchoWithAlt_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->EchoWithAlt(99, result), Core::ERROR_NONE); + EXPECT_EQ(result, 99u); +} + +TEST_F(TestAnnotations, Add_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->Add(10, 20, result), Core::ERROR_NONE); + EXPECT_EQ(result, 30u); +} + +TEST_F(TestAnnotations, Subtract_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->Subtract(50, 20, result), Core::ERROR_NONE); + EXPECT_EQ(result, 30u); +} + +// =========================================================================== +// @text on method name — COM-RPC uses C++ name regardless of @text +// =========================================================================== + +TEST_F(TestAnnotations, RenamedEchoMethod_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->RenamedEchoMethod(555, result), Core::ERROR_NONE); + EXPECT_EQ(result, 555u); +} + +TEST_F(TestAnnotations, TextCombinedMethod_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->TextCombinedMethod(777, result), Core::ERROR_NONE); + EXPECT_EQ(result, 777u); +} + +// =========================================================================== +// @text on struct members — COM-RPC round-trip (names don't affect COM-RPC) +// =========================================================================== + +TEST_F(TestAnnotations, EchoDeviceInfo_RoundTrip) { + ITestAnnotations::DeviceInfo input{}; + input.Name = "TestDevice"; + input.Version = 42; + input.Active = true; + + ITestAnnotations::DeviceInfo output{}; + ASSERT_EQ(_proxy->EchoDeviceInfo(input, output), Core::ERROR_NONE); + EXPECT_EQ(output.Name, "TestDevice"); + EXPECT_EQ(output.Version, 42u); + EXPECT_EQ(output.Active, true); +} + +// =========================================================================== +// @text on enum values + @encode:text — COM-RPC (enum wire value unaffected) +// =========================================================================== + +TEST_F(TestAnnotations, ConnectionState_SetGet_AllValues) { + for (auto s : {ITestAnnotations::DISCONNECTED, + ITestAnnotations::CONN_IN_PROGRESS, + ITestAnnotations::CONNECTED, + ITestAnnotations::AUTH_FAILURE}) { + ASSERT_EQ(_proxy->SetConnectionState(s), Core::ERROR_NONE); + ITestAnnotations::ConnectionState got{}; + ASSERT_EQ(_proxy->GetConnectionState(got), Core::ERROR_NONE); + EXPECT_EQ(got, s); + } +} + +// =========================================================================== +// @bitmask + @end — COM-RPC round-trip +// =========================================================================== + +TEST_F(TestAnnotations, Features_SingleFlag) { + ASSERT_EQ(_proxy->SetFeatures(ITestAnnotations::FEAT_WIFI), Core::ERROR_NONE); + ITestAnnotations::Features got{}; + ASSERT_EQ(_proxy->GetFeatures(got), Core::ERROR_NONE); + EXPECT_EQ(got, ITestAnnotations::FEAT_WIFI); +} + +TEST_F(TestAnnotations, Features_MultipleFlags) { + auto combined = static_cast( + ITestAnnotations::FEAT_WIFI | ITestAnnotations::FEAT_NFC); + ASSERT_EQ(_proxy->SetFeatures(combined), Core::ERROR_NONE); + ITestAnnotations::Features got{}; + ASSERT_EQ(_proxy->GetFeatures(got), Core::ERROR_NONE); + EXPECT_EQ(got, combined); +} + +TEST_F(TestAnnotations, Features_None) { + ASSERT_EQ(_proxy->SetFeatures(ITestAnnotations::FEAT_NONE), Core::ERROR_NONE); + ITestAnnotations::Features got{}; + ASSERT_EQ(_proxy->GetFeatures(got), Core::ERROR_NONE); + EXPECT_EQ(got, ITestAnnotations::FEAT_NONE); +} + +// =========================================================================== +// @encode:base64 — COM-RPC buffer round-trip +// =========================================================================== + +TEST_F(TestAnnotations, Base64_SetGet_RoundTrip) { + const uint8_t input[] = {0x01, 0x02, 0x03, 0x04, 0x05}; + ASSERT_EQ(_proxy->SetPayloadBase64(input, 5), Core::ERROR_NONE); + + uint8_t output[256]{}; + uint16_t written = 0; + ASSERT_EQ(_proxy->GetPayloadBase64(output, 256, written), Core::ERROR_NONE); + ASSERT_EQ(written, 5u); + EXPECT_EQ(::memcmp(input, output, 5), 0); +} + +TEST_F(TestAnnotations, Base64_EmptyPayload_ZeroBytesAccepted) { + // NOTE: @restrict validation is enforced at the JSON-RPC layer only. + // Over COM-RPC, raw values pass through without range checks. + // Size 0 is accepted by the implementation (copies 0 bytes). + const uint8_t input[] = {0x00}; + EXPECT_EQ(_proxy->SetPayloadBase64(input, 0), Core::ERROR_NONE); + + uint8_t output[256]{}; + uint16_t written = 0; + ASSERT_EQ(_proxy->GetPayloadBase64(output, 256, written), Core::ERROR_NONE); + EXPECT_EQ(written, 0u); +} + +// =========================================================================== +// @encode:hex — COM-RPC buffer round-trip +// =========================================================================== + +TEST_F(TestAnnotations, Hex_SetGet_RoundTrip) { + const uint8_t input[] = {0xDE, 0xAD, 0xBE, 0xEF}; + ASSERT_EQ(_proxy->SetTokenHex(input, 4), Core::ERROR_NONE); + + uint8_t output[64]{}; + uint16_t written = 0; + ASSERT_EQ(_proxy->GetTokenHex(output, 64, written), Core::ERROR_NONE); + ASSERT_EQ(written, 4u); + EXPECT_EQ(::memcmp(input, output, 4), 0); +} + +// =========================================================================== +// @extract on struct arrays — COM-RPC round-trip +// =========================================================================== + +TEST_F(TestAnnotations, ExtractPoints_SingleElement) { + std::vector input = {{10, 20}}; + std::vector output; + ASSERT_EQ(_proxy->EchoPoints(input, output), Core::ERROR_NONE); + ASSERT_EQ(output.size(), 1u); + EXPECT_EQ(output[0].x, 10); + EXPECT_EQ(output[0].y, 20); +} + +TEST_F(TestAnnotations, ExtractPoints_MultipleElements) { + std::vector input = {{1, 2}, {3, 4}, {5, 6}}; + std::vector output; + ASSERT_EQ(_proxy->EchoPoints(input, output), Core::ERROR_NONE); + ASSERT_EQ(output.size(), 3u); + EXPECT_EQ(output[2].x, 5); + EXPECT_EQ(output[2].y, 6); +} + +// =========================================================================== +// @optional on bool — COM-RPC +// =========================================================================== + +TEST_F(TestAnnotations, OptionalBool_Unset_DefaultBehavior) { + Core::OptionalType uppercase{}; // not set + string result; + ASSERT_EQ(_proxy->FormatText("hello", uppercase, result), Core::ERROR_NONE); + EXPECT_EQ(result, "hello"); +} + +TEST_F(TestAnnotations, OptionalBool_SetTrue) { + Core::OptionalType uppercase(true); + string result; + ASSERT_EQ(_proxy->FormatText("hello", uppercase, result), Core::ERROR_NONE); + EXPECT_EQ(result, "HELLO"); +} + +TEST_F(TestAnnotations, OptionalBool_SetFalse) { + Core::OptionalType uppercase(false); + string result; + ASSERT_EQ(_proxy->FormatText("Hello", uppercase, result), Core::ERROR_NONE); + EXPECT_EQ(result, "Hello"); +} + +// =========================================================================== +// Empty parameter list (C5) — void method round-trip +// =========================================================================== + +TEST_F(TestAnnotations, Ping_EmptyParams_Succeeds) { + EXPECT_EQ(_proxy->Ping(), Core::ERROR_NONE); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestEncodingMac.cpp b/tests/FunctionalTests/comrpc/tests/TestEncodingMac.cpp index 014177cf..83728209 100644 --- a/tests/FunctionalTests/comrpc/tests/TestEncodingMac.cpp +++ b/tests/FunctionalTests/comrpc/tests/TestEncodingMac.cpp @@ -48,3 +48,41 @@ TEST_F(TestEncodingMac, EchoMacAddress) { EXPECT_EQ(output[i], input[i]); } } + +// =========================================================================== +// Variable-length @encode:mac — works with any buffer size +// =========================================================================== + +TEST_F(TestEncodingMac, VariableMac_8Bytes_RoundTrip) { + const uint8_t input[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; + ASSERT_EQ(_proxy->SetVariableMac(input, 8), Core::ERROR_NONE); + + uint8_t output[32] = { 0 }; + uint16_t written = 0; + ASSERT_EQ(_proxy->GetVariableMac(output, 32, written), Core::ERROR_NONE); + ASSERT_EQ(written, 8u); + EXPECT_EQ(memcmp(input, output, 8), 0); +} + +TEST_F(TestEncodingMac, VariableMac_1Byte_RoundTrip) { + const uint8_t input[] = { 0xFF }; + ASSERT_EQ(_proxy->SetVariableMac(input, 1), Core::ERROR_NONE); + + uint8_t output[32] = { 0 }; + uint16_t written = 0; + ASSERT_EQ(_proxy->GetVariableMac(output, 32, written), Core::ERROR_NONE); + ASSERT_EQ(written, 1u); + EXPECT_EQ(output[0], 0xFF); +} + +TEST_F(TestEncodingMac, VariableMac_16Bytes_RoundTrip) { + const uint8_t input[16] = { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, + 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7 }; + ASSERT_EQ(_proxy->SetVariableMac(input, 16), Core::ERROR_NONE); + + uint8_t output[32] = { 0 }; + uint16_t written = 0; + ASSERT_EQ(_proxy->GetVariableMac(output, 32, written), Core::ERROR_NONE); + ASSERT_EQ(written, 16u); + EXPECT_EQ(memcmp(input, output, 16), 0); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestExtendedFormat.cpp b/tests/FunctionalTests/comrpc/tests/TestExtendedFormat.cpp new file mode 100644 index 00000000..2d05714d --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestExtendedFormat.cpp @@ -0,0 +1,60 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "TestHarness.h" +#include + +using namespace Thunder; +using namespace Thunder::FunctionalTest; + +class TestExtendedFormat : public Testing::TestHarness {}; + +// =========================================================================== +// Methods — round-trip via COM-RPC +// =========================================================================== + +TEST_F(TestExtendedFormat, EchoMethod_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->EchoMethod(42, result), Core::ERROR_NONE); + EXPECT_EQ(result, 42u); +} + +TEST_F(TestExtendedFormat, AddMethod_RoundTrip) { + uint32_t result = 0; + ASSERT_EQ(_proxy->AddMethod(10, 20, result), Core::ERROR_NONE); + EXPECT_EQ(result, 30u); +} + +// =========================================================================== +// Properties — round-trip via COM-RPC +// =========================================================================== + +TEST_F(TestExtendedFormat, Volume_SetGet) { + ASSERT_EQ(_proxy->Volume(75), Core::ERROR_NONE); + uint32_t vol = 0; + ASSERT_EQ(static_cast(_proxy)->Volume(vol), Core::ERROR_NONE); + EXPECT_EQ(vol, 75u); +} + +TEST_F(TestExtendedFormat, Name_ReadOnly) { + string name; + ASSERT_EQ(_proxy->Name(name), Core::ERROR_NONE); + EXPECT_EQ(name, "DefaultDevice"); +} diff --git a/tests/FunctionalTests/jsonrpc/CMakeLists.txt b/tests/FunctionalTests/jsonrpc/CMakeLists.txt index 1bfe804a..ff90eb8e 100644 --- a/tests/FunctionalTests/jsonrpc/CMakeLists.txt +++ b/tests/FunctionalTests/jsonrpc/CMakeLists.txt @@ -73,6 +73,23 @@ if(TEST_JSON_UNCOMPLIANT_COL) target_sources(JsonRpcFunctionalTests PRIVATE tests/TestJsonUncompliantCollapsedJsonRpc.cpp) endif() +if(TEST_ANNOTATIONS) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestAnnotationsJsonRpc.cpp) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestAnnotationsEventsJsonRpc.cpp) +endif() + +if(TEST_EXTENDED_FORMAT) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestExtendedFormatJsonRpc.cpp) +endif() + +if(TEST_PREFIX_UNDERSCORE) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestPrefixUnderscoreJsonRpc.cpp) +endif() + +if(TEST_WRAPPED_INTERFACE) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestWrappedInterfaceJsonRpc.cpp) +endif() + # TEST_LENGTH_MODES is COM-RPC only — @length:return uses uint16_t return type which JsonGenerator does not support target_link_libraries(JsonRpcFunctionalTests diff --git a/tests/FunctionalTests/jsonrpc/tests/TestAnnotationsEventsJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestAnnotationsEventsJsonRpc.cpp new file mode 100644 index 00000000..0bb63a39 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestAnnotationsEventsJsonRpc.cpp @@ -0,0 +1,151 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "JsonRpcTestHarness.h" +#include +#include +#include +#include + +using namespace Thunder; + +// =========================================================================== +// JSON-RPC event subscription tests. +// Validates that the JSONRPC subscribe/dispatch mechanism delivers events +// to subscribed ICallback clients. +// +// NOTE: The generated JTestAnnotations::Register() does NOT wire the +// implementation's INotification sink to JSON-RPC event dispatch. In a +// real Thunder plugin, the framework handles that wiring. Here we test +// the subscription layer directly via JSONRPC::Event(). +// =========================================================================== + +namespace { + +class EventCollector : public PluginHost::IDispatcher::ICallback { +public: + EventCollector() = default; + + uint32_t AddRef() const override + { + Core::InterlockedIncrement(_refCount); + return Core::ERROR_NONE; + } + + uint32_t Release() const override + { + if (Core::InterlockedDecrement(_refCount) == 0) { + delete this; + return Core::ERROR_DESTRUCTION_SUCCEEDED; + } + return Core::ERROR_NONE; + } + + Core::hresult Event( + const string& event, + const string& designator, + const string& index, + const string& parameters) override + { + std::lock_guard lock(_mutex); + _events.push_back({event, designator, index, parameters}); + _cv.notify_all(); + return Core::ERROR_NONE; + } + + bool WaitForEvents(size_t count, uint32_t timeoutMs = 2000) + { + std::unique_lock lock(_mutex); + return _cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), + [&] { return _events.size() >= count; }); + } + + struct ReceivedEvent { + string event; + string designator; + string index; + string parameters; + }; + + std::vector _events; + + BEGIN_INTERFACE_MAP(EventCollector) + INTERFACE_ENTRY(PluginHost::IDispatcher::ICallback) + END_INTERFACE_MAP + +private: + mutable uint32_t _refCount { 1 }; + std::mutex _mutex; + std::condition_variable _cv; +}; + +} // anonymous namespace + +class TestAnnotationsEventsJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +TEST_F(TestAnnotationsEventsJsonRpc, SubscribeAndReceivePortEvent) { + auto* collector = new EventCollector(); + + // Subscribe to the port state changed event + string callsign = _server->Callsign(); + uint32_t result = SubscribeEvent(collector, "tags::onPortStateChanged", callsign); + + if (result != Core::ERROR_NONE) { + collector->Release(); + GTEST_SKIP() << "Event subscription not supported (result=" << result << ")"; + return; + } + + // Dispatch the event directly via the JSONRPC module's Event() method. + // In a real plugin, the INotification → JSON-RPC bridge is wired by the + // framework; here we test the subscribe/dispatch path in isolation. + // Note: Event() return value is not ERROR_NONE even on successful dispatch. + _server->Event("tags::onPortStateChanged", R"({"port":1,"state":"connecting"})"); + + // Wait for the event to be delivered + ASSERT_TRUE(collector->WaitForEvents(1)) << "Timed out waiting for onPortStateChanged event"; + EXPECT_EQ(collector->_events[0].event, "tags::onPortStateChanged"); + + UnsubscribeEvent(collector, "tags::onPortStateChanged", callsign); + collector->Release(); +} + +TEST_F(TestAnnotationsEventsJsonRpc, SubscribeAndReceiveFeaturesEvent) { + auto* collector = new EventCollector(); + + string callsign = _server->Callsign(); + uint32_t result = SubscribeEvent(collector, "tags::onFeaturesChanged", callsign); + + if (result != Core::ERROR_NONE) { + collector->Release(); + GTEST_SKIP() << "Event subscription not supported (result=" << result << ")"; + return; + } + + // Dispatch the event directly via the JSONRPC module's Event() method. + // Note: Event() return value is not ERROR_NONE even on successful dispatch. + _server->Event("tags::onFeaturesChanged", R"({"features":["FEAT_WIFI","FEAT_NFC"]})"); + + ASSERT_TRUE(collector->WaitForEvents(1)) << "Timed out waiting for onFeaturesChanged event"; + EXPECT_EQ(collector->_events[0].event, "tags::onFeaturesChanged"); + + UnsubscribeEvent(collector, "tags::onFeaturesChanged", callsign); + collector->Release(); +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestAnnotationsJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestAnnotationsJsonRpc.cpp new file mode 100644 index 00000000..b36e5ed5 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestAnnotationsJsonRpc.cpp @@ -0,0 +1,484 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "JsonRpcTestHarness.h" +#include + +using namespace Thunder; + +class TestAnnotationsJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +// =========================================================================== +// @prefix — All methods must be prefixed with "tags::" +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, Prefix_MethodCallable_WithPrefix) { + // Method "deprecatedEcho" is prefixed by @prefix tags → "tags::deprecatedEcho" + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::deprecatedEcho", R"({"value":42})", response)); + EXPECT_EQ(response, "42") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, Prefix_MethodNotCallable_WithoutPrefix) { + // Calling without prefix should fail + string response; + EXPECT_NE(Core::ERROR_NONE, + CallMethod("deprecatedEcho", R"({"value":42})", response)); +} + +// =========================================================================== +// @deprecated / @obsolete — Methods still callable +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, Deprecated_MethodStillCallable) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::deprecatedEcho", R"({"value":100})", response)); + EXPECT_EQ(response, "100"); +} + +TEST_F(TestAnnotationsJsonRpc, Obsolete_MethodStillCallable) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::obsoleteEcho", R"({"value":200})", response)); + EXPECT_EQ(response, "200"); +} + +// =========================================================================== +// @alt — Primary and alternative names both work +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, Alt_PrimaryName_Works) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::echoWithAlt", R"({"value":55})", response)); + EXPECT_EQ(response, "55"); +} + +TEST_F(TestAnnotationsJsonRpc, Alt_AlternativeName_Works) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::echoAlt", R"({"value":55})", response)); + EXPECT_EQ(response, "55"); +} + +TEST_F(TestAnnotationsJsonRpc, Alt_BothNames_ProduceSameResult) { + string responsePrimary, responseAlt; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::echoWithAlt", R"({"value":77})", responsePrimary)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::echoAlt", R"({"value":77})", responseAlt)); + EXPECT_EQ(responsePrimary, responseAlt); +} + +// =========================================================================== +// @alt-deprecated — Primary works; deprecated alternative also works +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, AltDeprecated_PrimaryName_Works) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::add", R"({"a":10,"b":20})", response)); + EXPECT_EQ(response, "30"); +} + +TEST_F(TestAnnotationsJsonRpc, AltDeprecated_DeprecatedAltName_Works) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::legacyAdd", R"({"a":10,"b":20})", response)); + EXPECT_EQ(response, "30"); +} + +// =========================================================================== +// @alt-obsolete — Primary works; obsolete alternative also works +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, AltObsolete_PrimaryName_Works) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::subtract", R"({"a":50,"b":30})", response)); + EXPECT_EQ(response, "20"); +} + +TEST_F(TestAnnotationsJsonRpc, AltObsolete_ObsoleteAltName_Works) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::oldSubtract", R"({"a":50,"b":30})", response)); + EXPECT_EQ(response, "20"); +} + +// =========================================================================== +// @text on method name — JSON-RPC dispatch name override +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, TextMethodName_RenamedName_Works) { + // C++ method is RenamedEchoMethod, but @text renames it to "renamedEcho" + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::renamedEcho", R"({"value":123})", response)); + EXPECT_EQ(response, "123"); +} + +TEST_F(TestAnnotationsJsonRpc, TextMethodName_OriginalCppName_Rejected) { + // Calling by the C++ method name (camelCase of RenamedEchoMethod) should fail + string response; + EXPECT_NE(Core::ERROR_NONE, + CallMethod("tags::renamedEchoMethod", R"({"value":123})", response)) + << "Original C++ method name should not be callable when @text overrides it"; +} + +// =========================================================================== +// @text on method combined with @alt — both primary rename + alternative +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, TextCombinedWithAlt_PrimaryTextName_Works) { + // @text renames to "textPrimary" + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::textPrimary", R"({"value":456})", response)); + EXPECT_EQ(response, "456"); +} + +TEST_F(TestAnnotationsJsonRpc, TextCombinedWithAlt_AltName_Works) { + // @alt provides "textSecondary" as alternative + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::textSecondary", R"({"value":456})", response)); + EXPECT_EQ(response, "456"); +} + +TEST_F(TestAnnotationsJsonRpc, TextCombinedWithAlt_OriginalCppName_Rejected) { + // C++ name "TextCombinedMethod" (camelCase → "textCombinedMethod") should NOT work + string response; + EXPECT_NE(Core::ERROR_NONE, + CallMethod("tags::textCombinedMethod", R"({"value":456})", response)) + << "Original C++ name should not be callable when @text overrides it"; +} + +TEST_F(TestAnnotationsJsonRpc, TextCombinedWithAlt_BothNames_ProduceSameResult) { + string responsePrimary, responseAlt; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::textPrimary", R"({"value":789})", responsePrimary)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::textSecondary", R"({"value":789})", responseAlt)); + EXPECT_EQ(responsePrimary, responseAlt); +} + +// =========================================================================== +// @text on struct members — JSON field names use the overridden names +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, TextStructMembers_OverriddenNames_RoundTrip) { + // C++ struct: Name, Version, Active + // JSON keys: deviceName, firmwareVersion, isActive + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::echoDeviceInfo", + R"({"input":{"deviceName":"MyDevice","firmwareVersion":42,"isActive":true}})", + response)); + // Exact expected output — field order is deterministic in generated code + EXPECT_EQ(response, R"({"deviceName":"MyDevice","firmwareVersion":42,"isActive":true})") + << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, TextStructMembers_OriginalCppNames_Rejected) { + // Using original C++ member names (Name, Version, Active) should NOT work + string response; + uint32_t result = CallMethod("tags::echoDeviceInfo", + R"({"input":{"Name":"MyDevice","Version":42,"Active":true}})", + response); + // Either fails entirely or produces default-valued output (empty string, 0, false) + if (result == Core::ERROR_NONE) { + EXPECT_EQ(response, R"({"deviceName":"","firmwareVersion":0,"isActive":false})") + << "Original C++ names should not be recognized. Response: " << response; + } +} + +// =========================================================================== +// @text on enum values — Custom wire names for specific enumerators +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, TextEnumValues_CustomName_SetGet) { + // CONN_IN_PROGRESS has @text connecting + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setConnectionState", R"({"state":"connecting"})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::getConnectionState", R"({})", response)); + EXPECT_EQ(response, R"("connecting")") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, TextEnumValues_AuthFailed_CustomName) { + // AUTH_FAILURE has @text auth-failed + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setConnectionState", R"({"state":"auth-failed"})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::getConnectionState", R"({})", response)); + EXPECT_EQ(response, R"("auth-failed")") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, TextEnumValues_DefaultName_Works) { + // DISCONNECTED has no @text override — uses default convention name + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setConnectionState", R"({"state":"DISCONNECTED"})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::getConnectionState", R"({})", response)); + EXPECT_EQ(response, R"("DISCONNECTED")") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, TextEnumValues_OriginalCppName_Rejected) { + // Using the C++ identifier "CONN_IN_PROGRESS" should not work when @text overrides it + string response; + uint32_t result = CallMethod("tags::setConnectionState", + R"({"state":"CONN_IN_PROGRESS"})", response); + EXPECT_NE(result, Core::ERROR_NONE) + << "Original C++ enum name should be rejected when @text overrides it"; +} + +// =========================================================================== +// @bitmask on enum decl + @end sentinel exclusion +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, Bitmask_SingleFlag) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setFeatures", R"({"features":["FEAT_WIFI"]})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::getFeatures", R"({})", response)); + EXPECT_EQ(response, R"(["FEAT_WIFI"])") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, Bitmask_MultipleFlags) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setFeatures", + R"({"features":["FEAT_WIFI","FEAT_BLUETOOTH","FEAT_NFC"]})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::getFeatures", R"({})", response)); + EXPECT_EQ(response, R"(["FEAT_WIFI","FEAT_BLUETOOTH","FEAT_NFC"])") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, Bitmask_EmptyArray_NoFlags) { + // Set empty flags and verify call succeeds + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setFeatures", R"({"features":[]})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::getFeatures", R"({})", response)); + // Response for zero flags is empty (field omitted) — this is the current behaviour. + // Whether this should be "[]" instead is open for discussion with the framework team. + EXPECT_TRUE(response.empty()) << "Response: " << response; +} + +// DISABLED: @end sentinel values are not actively rejected by the JSON-RPC layer. +// The intended behaviour (per reference doc) is rejection, but the current framework +// silently ignores unknown flag names. Re-enable once the generator enforces @end. +TEST_F(TestAnnotationsJsonRpc, DISABLED_Bitmask_EndSentinel_Rejected) { + // FEAT_ALL is after @end — should not be a valid flag name + string response; + uint32_t result = CallMethod("tags::setFeatures", + R"({"features":["FEAT_ALL"]})", response); + EXPECT_NE(result, Core::ERROR_NONE) + << "Sentinel value after @end should be rejected"; +} + +// =========================================================================== +// @encode:base64 — Variable-length buffer round-trip +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, EncodeBase64_SetGet_RoundTrip) { + // "AQIDBA==" is base64 for bytes {0x01, 0x02, 0x03, 0x04} + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setPayloadBase64", R"({"data":"AQIDBA==","size":4})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::getPayloadBase64", R"({"maxSize":256})", response)); + EXPECT_EQ(response, R"({"data":"AQIDBA==","written":4})") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, EncodeBase64_InvalidBase64_Accepted) { + // NOTE: Thunder's base64 decoder does not reject invalid input — it silently + // produces output. This is by design; validation is the caller's responsibility. + string response; + uint32_t result = CallMethod("tags::setPayloadBase64", + R"({"data":"!!!invalid!!!","size":4})", response); + // Just verify the call doesn't crash; may or may not return error + (void)result; +} + +// =========================================================================== +// @encode:hex — Variable-length buffer round-trip +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, EncodeHex_SetGet_RoundTrip) { + // "DEADBEEF" is hex for bytes {0xDE, 0xAD, 0xBE, 0xEF} + // NOTE: Thunder's hex encoder produces lowercase output + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setTokenHex", R"({"data":"DEADBEEF","size":4})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::getTokenHex", R"({"maxSize":64})", response)); + EXPECT_EQ(response, R"({"data":"deadbeef","written":4})") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, EncodeHex_InvalidHexChars_Accepted) { + // NOTE: Thunder's hex decoder does not reject invalid characters — it silently + // produces output. This is by design; validation is the caller's responsibility. + string response; + uint32_t result = CallMethod("tags::setTokenHex", + R"({"data":"ZZZZ","size":2})", response); + // Just verify the call doesn't crash + (void)result; +} + +// =========================================================================== +// @extract on struct arrays +// =========================================================================== + +// DISABLED: In compliant format, @extract does NOT unwrap single-element arrays. +// The intended behaviour (per reference doc) is unwrapping, but the current generator +// only applies extraction in collapsed/extended format. Re-enable if this changes. +TEST_F(TestAnnotationsJsonRpc, DISABLED_ExtractStructArray_SingleElement_Unwrapped) { + // @extract should unwrap single-element arrays on output + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::echoPoints", + R"({"input":[{"x":10,"y":20}]})", response)); + // Response should be a single object, NOT wrapped in array + EXPECT_EQ(response, R"({"x":10,"y":20})") + << "Single element should be unwrapped. Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, ExtractStructArray_SingleElement_AsArray) { + // Current behaviour: in compliant format, single element remains as array + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::echoPoints", + R"({"input":[{"x":10,"y":20}]})", response)); + EXPECT_EQ(response, R"([{"x":10,"y":20}])") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, ExtractStructArray_MultipleElements_Array) { + // Multiple elements should remain as array + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::echoPoints", + R"({"input":[{"x":1,"y":2},{"x":3,"y":4}]})", response)); + EXPECT_EQ(response, R"([{"x":1,"y":2},{"x":3,"y":4}])") << "Response: " << response; +} + +// =========================================================================== +// @optional on bool — Regression guard +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, OptionalBool_Omitted_DefaultsFalse) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::formatText", R"({"text":"hello"})", response)); + EXPECT_EQ(response, R"("hello")") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, OptionalBool_SetTrue_Applied) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::formatText", R"({"text":"hello","uppercase":true})", response)); + EXPECT_EQ(response, R"("HELLO")") << "Response: " << response; +} + +TEST_F(TestAnnotationsJsonRpc, OptionalBool_SetFalse_NoConversion) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::formatText", R"({"text":"Hello","uppercase":false})", response)); + EXPECT_EQ(response, R"("Hello")") << "Response: " << response; +} + +// =========================================================================== +// Strict type matching (Constraint C8) +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, StrictTypeMatching_StringForUint32_Rejected) { + string response; + uint32_t result = CallMethod("tags::deprecatedEcho", + R"({"value":"not_a_number"})", response); + EXPECT_NE(result, Core::ERROR_NONE) + << "String value for uint32_t param should be rejected"; +} + +TEST_F(TestAnnotationsJsonRpc, StrictTypeMatching_IntForEnumString_Rejected) { + string response; + uint32_t result = CallMethod("tags::setConnectionState", + R"({"state":1})", response); + EXPECT_NE(result, Core::ERROR_NONE) + << "Integer value for enum string param should be rejected"; +} + +// =========================================================================== +// Empty parameter list (Constraint C5) — method with no params still callable +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, EmptyParameterList_Ping_Callable) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::ping", R"({})", response)); + // Void method returns no data — response should be empty + EXPECT_TRUE(response.empty()) << "Response: " << response; +} + +// =========================================================================== +// Events via JSON-RPC — @event, @index on events, @statuslistener +// =========================================================================== + +TEST_F(TestAnnotationsJsonRpc, Event_TriggerPortEvent_Callable) { + // TriggerPortEvent is a regular method exposed via JSON-RPC + // It triggers the event internally; here we just verify the method is callable + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::triggerPortEvent", R"({"port":1,"state":"connecting"})", response)); +} + +TEST_F(TestAnnotationsJsonRpc, Event_TriggerFeaturesEvent_Callable) { + // TriggerFeaturesEvent is also a regular method + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::triggerFeaturesEvent", R"({"features":["FEAT_WIFI"]})", response)); +} + +TEST_F(TestAnnotationsJsonRpc, Event_SetFeatures_ThenTrigger_Callable) { + // Set features, then trigger the event — validates the full flow compiles and runs + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::setFeatures", R"({"features":["FEAT_BLUETOOTH","FEAT_NFC"]})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::triggerFeaturesEvent", R"({"features":["FEAT_BLUETOOTH","FEAT_NFC"]})", response)); +} + +TEST_F(TestAnnotationsJsonRpc, Event_IndexedPortEvent_DifferentPorts) { + // Trigger events on different ports — validates @index on event params works in generated code + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::triggerPortEvent", R"({"port":0,"state":"DISCONNECTED"})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::triggerPortEvent", R"({"port":1,"state":"CONNECTED"})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("tags::triggerPortEvent", R"({"port":2,"state":"auth-failed"})", response)); +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestEncodingMacJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestEncodingMacJsonRpc.cpp index 3c96d2c0..6c17e9a0 100644 --- a/tests/FunctionalTests/jsonrpc/tests/TestEncodingMacJsonRpc.cpp +++ b/tests/FunctionalTests/jsonrpc/tests/TestEncodingMacJsonRpc.cpp @@ -44,3 +44,40 @@ TEST_F(TestEncodingMacJsonRpc, EchoMacAddress) { // verify the echoed MAC matches the input EXPECT_EQ(response, "\"de:ad:be:ef:00:01\"") << "Response: " << response; } + +// =========================================================================== +// Variable-length @encode:mac — works with any buffer size, not just 6 bytes +// =========================================================================== + +TEST_F(TestEncodingMacJsonRpc, VariableMac_8Bytes_SetGet) { + // 8-byte buffer: "01:02:03:04:05:06:07:08" + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("setVariableMac", R"({"data":"01:02:03:04:05:06:07:08","size":8})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("getVariableMac", R"({"maxSize":32})", response)); + EXPECT_EQ(response, R"({"data":"01:02:03:04:05:06:07:08","written":8})") + << "Response: " << response; +} + +TEST_F(TestEncodingMacJsonRpc, VariableMac_1Byte_SetGet) { + // 1-byte buffer: just "ff" (no colons) + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("setVariableMac", R"({"data":"ff","size":1})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("getVariableMac", R"({"maxSize":32})", response)); + EXPECT_EQ(response, R"({"data":"ff","written":1})") + << "Response: " << response; +} + +TEST_F(TestEncodingMacJsonRpc, VariableMac_4Bytes_SetGet) { + // 4-byte buffer: "de:ad:be:ef" + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("setVariableMac", R"({"data":"de:ad:be:ef","size":4})", response)); + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("getVariableMac", R"({"maxSize":32})", response)); + EXPECT_EQ(response, R"({"data":"de:ad:be:ef","written":4})") + << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestExtendedFormatJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestExtendedFormatJsonRpc.cpp new file mode 100644 index 00000000..8d1096c8 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestExtendedFormatJsonRpc.cpp @@ -0,0 +1,99 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "JsonRpcTestHarness.h" +#include + +using namespace Thunder; + +class TestExtendedFormatJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +// =========================================================================== +// @extended — Methods use compliant-style named object params +// =========================================================================== + +TEST_F(TestExtendedFormatJsonRpc, Method_SingleParam_UsesNamedObject) { + // @extended methods still require named object params (like @compliant) + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("echoMethod", R"({"value":42})", response)); + EXPECT_EQ(response, "42") << "Response: " << response; +} + +TEST_F(TestExtendedFormatJsonRpc, Method_MultipleParams_UsesNamedObject) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("addMethod", R"({"a":10,"b":20})", response)); + EXPECT_EQ(response, "30") << "Response: " << response; +} + +TEST_F(TestExtendedFormatJsonRpc, Method_BareValue_NotAccepted) { + // Unlike @collapsed, @extended methods do NOT accept bare values + string response; + uint32_t result = CallMethod("echoMethod", R"(42)", response); + // Should fail because method expects named object format + EXPECT_NE(result, Core::ERROR_NONE) + << "@extended methods should not accept collapsed params"; +} + +// =========================================================================== +// @extended — Properties use collapsed format (value sent directly) +// =========================================================================== + +TEST_F(TestExtendedFormatJsonRpc, Property_Set_AcceptsDirectValue) { + // @extended properties accept the value directly (collapsed) + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("volume", R"(75)", response)); +} + +TEST_F(TestExtendedFormatJsonRpc, Property_Get_ReturnsDirectValue) { + // First set a value + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("volume", R"(88)", response)); + // In @extended format, property GET uses empty params (not "{}") + // because the collapsed format interprets "{}" as a value to parse + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("volume", "", response)); + EXPECT_EQ(response, "88") << "Response: " << response; +} + +TEST_F(TestExtendedFormatJsonRpc, Property_ReadOnly_Get_ReturnsDirectString) { + // Read-only string property — returns the string directly + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("name", "", response)); + EXPECT_EQ(response, R"("DefaultDevice")") << "Response: " << response; +} + +// =========================================================================== +// Contrast with @compliant: @extended properties are collapsed, not wrapped +// =========================================================================== + +TEST_F(TestExtendedFormatJsonRpc, Property_Get_NotWrappedInValueObject) { + // Set volume first + string response; + CallMethod("volume", R"(42)", response); + // In @extended format, property GET uses empty params + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("volume", "", response)); + EXPECT_EQ(response, "42") << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestPrefixUnderscoreJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestPrefixUnderscoreJsonRpc.cpp new file mode 100644 index 00000000..f1806f9e --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestPrefixUnderscoreJsonRpc.cpp @@ -0,0 +1,57 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "JsonRpcTestHarness.h" +#include + +using namespace Thunder; + +class TestPrefixUnderscoreJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +// Prefix with trailing underscore — methods become flat_echo, flat_add (no :: separator) + +TEST_F(TestPrefixUnderscoreJsonRpc, Echo_WithFlatPrefix) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("flat_echo", R"({"value":123})", response)); + EXPECT_EQ(response, "123") << "Response: " << response; +} + +TEST_F(TestPrefixUnderscoreJsonRpc, Add_WithFlatPrefix) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("flat_add", R"({"a":10,"b":20})", response)); + EXPECT_EQ(response, "30") << "Response: " << response; +} + +TEST_F(TestPrefixUnderscoreJsonRpc, Add_WithoutPrefix_Rejected) { + // Calling without the flat_ prefix should fail + // (uses 'add' which is unique enough — no other interface has unprefixed 'add') + string response; + EXPECT_NE(Core::ERROR_NONE, + CallMethod("add", R"({"a":10,"b":20})", response)); +} + +TEST_F(TestPrefixUnderscoreJsonRpc, Add_WithColonPrefix_Rejected) { + // Using :: separator should NOT work for underscore prefix + string response; + EXPECT_NE(Core::ERROR_NONE, + CallMethod("flat::add", R"({"a":10,"b":20})", response)); +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestWrappedInterfaceJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestWrappedInterfaceJsonRpc.cpp new file mode 100644 index 00000000..7dadaee5 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestWrappedInterfaceJsonRpc.cpp @@ -0,0 +1,50 @@ +/* + * If not stated otherwise in this file or this component's LICENSE file the + * following copyright and licenses apply: + * + * Copyright 2026 Metrological + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "JsonRpcTestHarness.h" +#include + +using namespace Thunder; + +class TestWrappedInterfaceJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +// Interface-level wrapped — ALL single-value returns wrapped using parameter name as key + +TEST_F(TestWrappedInterfaceJsonRpc, GetCounter_WrappedInObject) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("getCounter", R"({})", response)); + // Interface-level wrapped: single return value wrapped in object using parameter name as key + EXPECT_EQ(response, R"({"counter":42})") << "Response: " << response; +} + +TEST_F(TestWrappedInterfaceJsonRpc, GetName_WrappedInObject) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("getName", R"({})", response)); + EXPECT_EQ(response, R"({"name":"WrappedDevice"})") << "Response: " << response; +} + +TEST_F(TestWrappedInterfaceJsonRpc, Echo_WrappedInObject) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("echo", R"({"value":99})", response)); + EXPECT_EQ(response, R"({"result":99})") << "Response: " << response; +}