diff --git a/.github/workflows/ProxyStubFunctionalTests.yml b/.github/workflows/ProxyStubFunctionalTests.yml index 63f6b26d..4e497346 100644 --- a/.github/workflows/ProxyStubFunctionalTests.yml +++ b/.github/workflows/ProxyStubFunctionalTests.yml @@ -52,7 +52,7 @@ jobs: run: | set -euo pipefail export DEBIAN_FRONTEND=noninteractive - PKGS="python3-venv python3-pip build-essential cmake ninja-build" + PKGS="python3-venv python3-pip build-essential cmake ninja-build libgtest-dev" if [ "${{ matrix.architecture }}" = "32" ]; then PKGS="$PKGS zlib1g-dev:i386 libssl-dev:i386 gcc-13-multilib g++-13-multilib" else @@ -86,6 +86,7 @@ jobs: -DCMAKE_VERBOSE_MAKEFILE=ON \ -DENABLE_TESTING=ON \ -DFUNCTIONAL_TESTS=ON \ + -DENABLE_TEST_RUNTIME=ON \ -DPROXYSTUB_GENERATOR=$(pwd)/ProxyStubGenerator/StubGenerator.py \ -DJSON_GENERATOR=$(pwd)/JsonGenerator/JsonGenerator.py cmake \ diff --git a/tests/FunctionalTests/CMakeLists.txt b/tests/FunctionalTests/CMakeLists.txt index 00686313..d9bffc43 100644 --- a/tests/FunctionalTests/CMakeLists.txt +++ b/tests/FunctionalTests/CMakeLists.txt @@ -25,6 +25,14 @@ option(TEST_OPTIONALS "Enable optional parameters tests" ON) option(TEST_PRIMITIVES "Enable primitive types tests" ON) option(TEST_RESTRICTIONS "Enable restrict annotation tests" ON) option(TEST_STRUCTS "Enable POD struct tests" ON) +option(TEST_ENCODING_MAC "Enable @encode:mac annotation tests" ON) +option(TEST_LENGTH_MODES "Enable @length:void/@length:return tests" ON) +option(TEST_JSON_SHAPE "Enable @wrapped/@extract shape tests" ON) +option(TEST_JSON_TEXT_KEEP "Enable @text:keep naming tests" ON) +option(TEST_JSON_TEXT_CASE "Enable @text:legacy case convention tests" ON) +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) set(CMAKE_MODULE_PATH "${CMAKE_BINARY_DIR}" CACHE BOOL "" FORCE) diff --git a/tests/FunctionalTests/README.md b/tests/FunctionalTests/README.md index 8ad46a1e..c7903953 100644 --- a/tests/FunctionalTests/README.md +++ b/tests/FunctionalTests/README.md @@ -60,7 +60,7 @@ This generates JSON-RPC dispatch code but integration with the main test executa ### JSON-RPC functional tests -A separate test executable validates JsonGenerator output using a simplified header-only architecture: +A separate test executable validates JsonGenerator output using a lightweight two-file server infrastructure: ```bash cmake -S . -B build -DBUILD_JSON_RPC_TESTS=ON @@ -71,7 +71,7 @@ cmake --build build --target JsonRpcFunctionalTests This creates `JsonRpcFunctionalTests` executable that: - Reuses the same interface definitions and implementation classes as COM-RPC tests - Tests **full round-trip JSON marshalling/unmarshalling** using Thunder's `PluginHost::JSONRPC::Invoke()` directly -- Uses **header-only infrastructure** (`JsonRpcServer.h` contains both registration system and dispatcher) +- Uses a lightweight two-file infrastructure: `JsonRpcServer.h` contains the registration system and dispatcher, `JsonRpcServer.cpp` contains the runtime bootstrap (`ThunderTestRuntime` init and `IShell` attach) - **One-line registration** per interface using lambda-based static registrars (no template specialization boilerplate) - Uses `JsonRpcTestHarness` fixtures instead of `TestHarness` - Validates that generated `J::Register()` dispatch code compiles and works correctly @@ -140,7 +140,7 @@ ProxyStubs are generated by `ProxyStubGenerator` at build time into `build/comrp ## JSON-RPC test architecture -The JSON-RPC test binary uses Thunder's `PluginHost::JSONRPC` for direct method dispatch without network transport. The infrastructure is **header-only** - all registration and dispatch logic lives in `JsonRpcServer.h`: +The JSON-RPC test binary performs direct in-process dispatcher invocation (no network transport). `JsonRpcServer` is built on `Test::JsonRPCRegister` (from `JsonRpcRegistrations.h`), which derives from `PluginHost::JSONRPCSupportsEventStatus`. The infrastructure splits across two files: `JsonRpcServer.h` contains the registration system and dispatcher, while `JsonRpcServer.cpp` contains the runtime bootstrap (`ThunderTestRuntime` initialisation and `IShell` attachment): ``` ┌────────────────────────────────────────────────────┐ @@ -158,8 +158,8 @@ The JSON-RPC test binary uses Thunder's `PluginHost::JSONRPC` for direct method │ │ │based registry) │ │ │ ↓ │ │ │ │ └─────────────────┘ │ │ JSON-RPC 2.0 │ │ │ │ │ │ params/response │ │ -│ │ MockShell │ │ │ │ -│ │ (IShell stub) │ │ │ │ +│ │ ThunderTestRuntime │ │ │ │ +│ │ + real IShell │ │ │ │ │ │ │ │ │ │ │ │ Impl (reused) │ │ │ │ │ └─────────────────────┘ └─────────────────┘ │ @@ -168,9 +168,9 @@ The JSON-RPC test binary uses Thunder's `PluginHost::JSONRPC` for direct method └────────────────────────────────────────────────────┘ ``` -**Server side** — `JsonRpcServer` inherits from `PluginHost::JSONRPC` and `PluginHost::IPlugin`. A `MockShell` provides the minimal `IShell` interface needed for initialization. Implementation classes are the same protocol-agnostic implementations used for COM-RPC tests. +**Server side** — `JsonRpcServer` derives from `Test::JsonRPCRegister` (which derives from `PluginHost::JSONRPCSupportsEventStatus`) and does not implement `PluginHost::IPlugin`. It initializes `Thunder::TestCore::ThunderTestRuntime` and attaches to a real `IShell` from the embedded Thunder `PluginHost::Server` (Controller callsign). Implementation classes are the same protocol-agnostic implementations used for COM-RPC tests. -**Registration system** (header-only in `JsonRpcServer.h`): +**Registration system** (in `JsonRpcServer.h`): - `JsonRpcRegistrationProvider`: Singleton collecting registration lambdas - `JsonRpcRegistrar`: Template class capturing register function in lambda - Static initialization: Each implementation file has one-line registrar: diff --git a/tests/FunctionalTests/common/CMakeLists.txt b/tests/FunctionalTests/common/CMakeLists.txt index 1e73f51b..6242287c 100644 --- a/tests/FunctionalTests/common/CMakeLists.txt +++ b/tests/FunctionalTests/common/CMakeLists.txt @@ -51,6 +51,13 @@ target_link_libraries(FunctionalTestCommon ${NAMESPACE}COM::${NAMESPACE}COM ) +if(ENABLE_JSON_RPC_TESTS) + target_link_libraries(FunctionalTestCommon + PUBLIC + ${NAMESPACE}Plugins::${NAMESPACE}Plugins + ) +endif() + add_library(FunctionalTestProxySources OBJECT) add_library(FunctionalTestJsonSources OBJECT) @@ -65,6 +72,7 @@ target_link_libraries(FunctionalTestJsonSources PRIVATE FunctionalTestCommon ${NAMESPACE}Core::${NAMESPACE}Core + ${NAMESPACE}Plugins::${NAMESPACE}Plugins ) macro(AddTestInterface TestName) @@ -155,6 +163,38 @@ if (TEST_ASYNC) AddTestInterface("Async" COM_RPC JSON_RPC) endif() +if (TEST_ENCODING_MAC) + AddTestInterface("EncodingMac" COM_RPC JSON_RPC) +endif() + +if (TEST_LENGTH_MODES) + AddTestInterface("LengthModes" COM_RPC) # COM-RPC only — @length:return requires non-uint32_t return type, unsupported by JsonGenerator +endif() + +if (TEST_JSON_SHAPE) + AddTestInterface("JsonShape" COM_RPC JSON_RPC) +endif() + +if (TEST_JSON_TEXT_KEEP) + AddTestInterface("JsonTextKeep" COM_RPC JSON_RPC) +endif() + +if (TEST_JSON_TEXT_CASE) + AddTestInterface("JsonTextCase" COM_RPC JSON_RPC) +endif() + +if (TEST_JSON_COMPLIANT) + AddTestInterface("JsonCompliant" COM_RPC JSON_RPC) +endif() + +if (TEST_JSON_UNCOMPLIANT_EXT) + AddTestInterface("JsonUncompliantExtended" COM_RPC JSON_RPC) +endif() + +if (TEST_JSON_UNCOMPLIANT_COL) + AddTestInterface("JsonUncompliantCollapsed" 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/TestEncodingMacImpl.cpp b/tests/FunctionalTests/common/implementations/TestEncodingMacImpl.cpp new file mode 100644 index 00000000..65add63c --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestEncodingMacImpl.cpp @@ -0,0 +1,64 @@ +/* + * 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 + +namespace Thunder { +namespace TestImplementation { + + class TestEncodingMacImpl : public FunctionalTest::ITestEncodingMac { + public: + TestEncodingMacImpl() { memset(_mac, 0, sizeof(_mac)); } + ~TestEncodingMacImpl() override = default; + + TestEncodingMacImpl(const TestEncodingMacImpl&) = delete; + TestEncodingMacImpl& operator=(const TestEncodingMacImpl&) = delete; + + Core::hresult SetMacAddress(const uint8_t mac[]) override + { + memcpy(_mac, mac, 6); + return Core::ERROR_NONE; + } + + Core::hresult GetMacAddress(uint8_t mac[]) const override + { + memcpy(mac, _mac, 6); + return Core::ERROR_NONE; + } + + Core::hresult EchoMacAddress(const uint8_t input[], uint8_t output[]) const override + { + memcpy(output, input, 6); + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestEncodingMacImpl) + INTERFACE_ENTRY(FunctionalTest::ITestEncodingMac) + END_INTERFACE_MAP + + private: + uint8_t _mac[6]; + }; + + static Factory::Registrar g_encodingMacRegistrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestJsonCompliantImpl.cpp b/tests/FunctionalTests/common/implementations/TestJsonCompliantImpl.cpp new file mode 100644 index 00000000..830e25e1 --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestJsonCompliantImpl.cpp @@ -0,0 +1,48 @@ +/* + * 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 TestJsonCompliantImpl : public FunctionalTest::ITestJsonCompliant { + public: + TestJsonCompliantImpl() = default; + ~TestJsonCompliantImpl() override = default; + + TestJsonCompliantImpl(const TestJsonCompliantImpl&) = delete; + TestJsonCompliantImpl& operator=(const TestJsonCompliantImpl&) = delete; + + Core::hresult Ping(const string& payload, string& reply) const override + { + reply = payload; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestJsonCompliantImpl) + INTERFACE_ENTRY(FunctionalTest::ITestJsonCompliant) + END_INTERFACE_MAP + }; + + static Factory::Registrar g_registrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestJsonShapeImpl.cpp b/tests/FunctionalTests/common/implementations/TestJsonShapeImpl.cpp new file mode 100644 index 00000000..f0fbe17d --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestJsonShapeImpl.cpp @@ -0,0 +1,67 @@ +/* + * 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 TestJsonShapeImpl : public FunctionalTest::ITestJsonShape { + public: + TestJsonShapeImpl() : _counter(42) {} + ~TestJsonShapeImpl() override = default; + + TestJsonShapeImpl(const TestJsonShapeImpl&) = delete; + TestJsonShapeImpl& operator=(const TestJsonShapeImpl&) = delete; + + Core::hresult GetWrappedCounter(uint32_t& counter) const override + { + counter = _counter; + return Core::ERROR_NONE; + } + + Core::hresult EchoExtractedList( + const std::vector& input, + std::vector& output) const override + { + output = input; + return Core::ERROR_NONE; + } + + Core::hresult EchoStruct( + const Dimensions& in, + Dimensions& out) const override + { + out = in; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestJsonShapeImpl) + INTERFACE_ENTRY(FunctionalTest::ITestJsonShape) + END_INTERFACE_MAP + + private: + uint32_t _counter; + }; + + static Factory::Registrar g_jsonShapeRegistrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestJsonTextCaseImpl.cpp b/tests/FunctionalTests/common/implementations/TestJsonTextCaseImpl.cpp new file mode 100644 index 00000000..b5c139c2 --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestJsonTextCaseImpl.cpp @@ -0,0 +1,48 @@ +/* + * 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 TestJsonTextCaseImpl : public FunctionalTest::ITestJsonTextCase { + public: + TestJsonTextCaseImpl() = default; + ~TestJsonTextCaseImpl() override = default; + + TestJsonTextCaseImpl(const TestJsonTextCaseImpl&) = delete; + TestJsonTextCaseImpl& operator=(const TestJsonTextCaseImpl&) = delete; + + Core::hresult EchoCaseConvention(const uint16_t sourceValue, uint16_t& resultValue) const override + { + resultValue = sourceValue; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestJsonTextCaseImpl) + INTERFACE_ENTRY(FunctionalTest::ITestJsonTextCase) + END_INTERFACE_MAP + }; + + static Factory::Registrar g_registrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestJsonTextKeepImpl.cpp b/tests/FunctionalTests/common/implementations/TestJsonTextKeepImpl.cpp new file mode 100644 index 00000000..8561c97a --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestJsonTextKeepImpl.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 TestJsonTextKeepImpl : public FunctionalTest::ITestJsonTextKeep { + public: + TestJsonTextKeepImpl() : _buildVersion(42) {} + ~TestJsonTextKeepImpl() override = default; + + TestJsonTextKeepImpl(const TestJsonTextKeepImpl&) = delete; + TestJsonTextKeepImpl& operator=(const TestJsonTextKeepImpl&) = delete; + + Core::hresult EchoMixedCaseName(const uint32_t InputValue, uint32_t& OutputValue) const override + { + OutputValue = InputValue; + return Core::ERROR_NONE; + } + + Core::hresult BuildVersion(uint32_t& BuildVersion) const override + { + BuildVersion = _buildVersion; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestJsonTextKeepImpl) + INTERFACE_ENTRY(FunctionalTest::ITestJsonTextKeep) + END_INTERFACE_MAP + + private: + uint32_t _buildVersion; + }; + + static Factory::Registrar g_registrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestJsonUncompliantCollapsedImpl.cpp b/tests/FunctionalTests/common/implementations/TestJsonUncompliantCollapsedImpl.cpp new file mode 100644 index 00000000..52a06605 --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestJsonUncompliantCollapsedImpl.cpp @@ -0,0 +1,48 @@ +/* + * 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 TestJsonUncompliantCollapsedImpl : public FunctionalTest::ITestJsonUncompliantCollapsed { + public: + TestJsonUncompliantCollapsedImpl() = default; + ~TestJsonUncompliantCollapsedImpl() override = default; + + TestJsonUncompliantCollapsedImpl(const TestJsonUncompliantCollapsedImpl&) = delete; + TestJsonUncompliantCollapsedImpl& operator=(const TestJsonUncompliantCollapsedImpl&) = delete; + + Core::hresult PingCollapsed(const string& payload, string& reply) const override + { + reply = payload; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestJsonUncompliantCollapsedImpl) + INTERFACE_ENTRY(FunctionalTest::ITestJsonUncompliantCollapsed) + END_INTERFACE_MAP + }; + + static Factory::Registrar g_registrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestJsonUncompliantExtendedImpl.cpp b/tests/FunctionalTests/common/implementations/TestJsonUncompliantExtendedImpl.cpp new file mode 100644 index 00000000..8a68bd66 --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestJsonUncompliantExtendedImpl.cpp @@ -0,0 +1,48 @@ +/* + * 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 TestJsonUncompliantExtendedImpl : public FunctionalTest::ITestJsonUncompliantExtended { + public: + TestJsonUncompliantExtendedImpl() = default; + ~TestJsonUncompliantExtendedImpl() override = default; + + TestJsonUncompliantExtendedImpl(const TestJsonUncompliantExtendedImpl&) = delete; + TestJsonUncompliantExtendedImpl& operator=(const TestJsonUncompliantExtendedImpl&) = delete; + + Core::hresult PingExtended(const string& payload, string& reply) const override + { + reply = payload; + return Core::ERROR_NONE; + } + + BEGIN_INTERFACE_MAP(TestJsonUncompliantExtendedImpl) + INTERFACE_ENTRY(FunctionalTest::ITestJsonUncompliantExtended) + END_INTERFACE_MAP + }; + + static Factory::Registrar g_registrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/implementations/TestLengthModesImpl.cpp b/tests/FunctionalTests/common/implementations/TestLengthModesImpl.cpp new file mode 100644 index 00000000..6af1e7b2 --- /dev/null +++ b/tests/FunctionalTests/common/implementations/TestLengthModesImpl.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 +#include + +namespace Thunder { +namespace TestImplementation { + + class TestLengthModesImpl : public FunctionalTest::ITestLengthModes { + public: + TestLengthModesImpl() = default; + ~TestLengthModesImpl() override = default; + + TestLengthModesImpl(const TestLengthModesImpl&) = delete; + TestLengthModesImpl& operator=(const TestLengthModesImpl&) = delete; + + Core::hresult EchoSingleByte(const uint8_t input, uint8_t output[]) const override + { + output[0] = input; + return Core::ERROR_NONE; + } + + uint16_t ReadPayload(uint8_t output[], const uint16_t maxSize) const override + { + static const uint8_t pattern[] = { 0xDE, 0xAD, 0xBE, 0xEF }; + static const uint16_t patternSize = static_cast(sizeof(pattern)); + const uint16_t count = std::min(maxSize, patternSize); + for (uint16_t i = 0; i < count; ++i) { + output[i] = pattern[i]; + } + return count; + } + + BEGIN_INTERFACE_MAP(TestLengthModesImpl) + INTERFACE_ENTRY(FunctionalTest::ITestLengthModes) + END_INTERFACE_MAP + }; + + static Factory::Registrar g_lengthModesRegistrar; + +} // namespace TestImplementation +} // namespace Thunder diff --git a/tests/FunctionalTests/common/include/ImplementationFactory.h b/tests/FunctionalTests/common/include/ImplementationFactory.h index 82078e5b..73f37984 100644 --- a/tests/FunctionalTests/common/include/ImplementationFactory.h +++ b/tests/FunctionalTests/common/include/ImplementationFactory.h @@ -19,7 +19,7 @@ #pragma once -#include +#include "Module.h" #include namespace Thunder { diff --git a/tests/FunctionalTests/common/include/JsonRpcRegistrations.h b/tests/FunctionalTests/common/include/JsonRpcRegistrations.h index ed2d0646..840b22c1 100644 --- a/tests/FunctionalTests/common/include/JsonRpcRegistrations.h +++ b/tests/FunctionalTests/common/include/JsonRpcRegistrations.h @@ -19,7 +19,7 @@ #pragma once -#include +#include "Module.h" namespace Thunder { namespace Test { diff --git a/tests/FunctionalTests/common/interfaces/ITestEncodingMac.h b/tests/FunctionalTests/common/interfaces/ITestEncodingMac.h new file mode 100644 index 00000000..f975b9a0 --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestEncodingMac.h @@ -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. + */ + +#pragma once + +#include "Ids.h" +#include "Module.h" + +namespace Thunder { +namespace FunctionalTest { + + // Exercises @encode:mac behavior for fixed-size MAC address buffers. + // @json 1.0.0 + struct EXTERNAL ITestEncodingMac : virtual public Core::IUnknown { + enum { ID = ID_TEST_ENCODING_MAC }; + + // @brief Stores a 6-byte MAC address. + // In JSON-RPC, @encode:mac should map the byte array to a string + // representation (for example, "01:23:45:67:89:ab"). + // @param mac Input 6-byte MAC address. + // @retval ERROR_NONE Address stored. + virtual Core::hresult SetMacAddress( + const uint8_t mac[] /* @in @length:6 @encode:mac */) = 0; + + // @brief Retrieves the previously stored 6-byte MAC address. + // @param mac Receives stored 6-byte MAC address. + // @retval ERROR_NONE Address returned. + virtual Core::hresult GetMacAddress( + uint8_t mac[] /* @out @length:6 @maxlength:6 @encode:mac */) const = 0; + + // @brief Echoes a MAC address to verify round-trip conversion. + // @param input Input 6-byte MAC address. + // @param output Receives echoed 6-byte MAC address. + // @retval ERROR_NONE Echo completed. + 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; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestJsonCompliant.h b/tests/FunctionalTests/common/interfaces/ITestJsonCompliant.h new file mode 100644 index 00000000..a6f94841 --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestJsonCompliant.h @@ -0,0 +1,41 @@ +/* + * 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 { + + // @json 1.0.0 + // @compliant + struct EXTERNAL ITestJsonCompliant : virtual public Core::IUnknown { + enum { ID = ID_TEST_JSON_COMPLIANT }; + + // @brief Baseline interface for compliant JSON-RPC format generation. + // @param payload Input payload. + // @param reply Receives output payload. + // @retval ERROR_NONE Operation completed. + virtual Core::hresult Ping(const string& payload /* @in */, string& reply /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestJsonShape.h b/tests/FunctionalTests/common/interfaces/ITestJsonShape.h new file mode 100644 index 00000000..b3e1e99a --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestJsonShape.h @@ -0,0 +1,67 @@ +/* + * 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 { + + // Exercises JSON payload shape tags that are not covered by existing + // interfaces. Intended primarily for JsonGenerator functional tests. + // @json 1.0.0 + struct EXTERNAL ITestJsonShape : virtual public Core::IUnknown { + enum { ID = ID_TEST_JSON_SHAPE }; + + // @brief Returns a single scalar result using an object envelope even when + // one return value would otherwise be emitted without wrapping. + // @param counter Receives the current counter value. + // @retval ERROR_NONE Counter value returned. + // @wrapped + virtual Core::hresult GetWrappedCounter(uint32_t& counter /* @out */) const = 0; + + // @brief Echoes a list and requests single-element extraction in JSON. + // When the list has exactly one element, JSON may collapse it from + // [x] to x depending on the generator behavior. + // @param input Input list to echo. + // @param output Receives echoed list. + // @retval ERROR_NONE Echo completed. + virtual Core::hresult EchoExtractedList( + const std::vector& input /* @in @extract @restrict:1..64 */, + std::vector& output /* @out @extract @restrict:1..64 */) const = 0; + + // @brief Echoes a struct using the standard (non-extracted) JSON representation. + struct Dimensions { + uint16_t width; + uint16_t height; + }; + + // @brief Echoes a struct using the standard (non-extracted) JSON representation. + // @param in Input Dimensions value. + // @param out Receives echoed Dimensions value. + // @retval ERROR_NONE Echo completed. + virtual Core::hresult EchoStruct( + const Dimensions& in /* @in */, + Dimensions& out /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestJsonTextCase.h b/tests/FunctionalTests/common/interfaces/ITestJsonTextCase.h new file mode 100644 index 00000000..495be180 --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestJsonTextCase.h @@ -0,0 +1,41 @@ +/* + * 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 { + + // @json 1.0.0 + // @text:legacy + struct EXTERNAL ITestJsonTextCase : virtual public Core::IUnknown { + enum { ID = ID_TEST_JSON_TEXT_CASE }; + + // @brief Verifies case-convention transformation in generated JSON names. + // @param sourceValue Input value for round-trip verification. + // @param resultValue Receives echoed value. + // @retval ERROR_NONE Echo completed. + virtual Core::hresult EchoCaseConvention(const uint16_t sourceValue /* @in */, uint16_t& resultValue /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestJsonTextKeep.h b/tests/FunctionalTests/common/interfaces/ITestJsonTextKeep.h new file mode 100644 index 00000000..f2d47aba --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestJsonTextKeep.h @@ -0,0 +1,48 @@ +/* + * 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 { + + // @json 1.0.0 + // @text:keep + struct EXTERNAL ITestJsonTextKeep : virtual public Core::IUnknown { + enum { ID = ID_TEST_JSON_TEXT_KEEP }; + + // @brief Verifies that the original C++ identifier casing is preserved + // in generated JSON names when the keep case convention is active. + // @param InputValue Input value used for round-trip verification. + // @param OutputValue Receives echoed value. + // @retval ERROR_NONE Echo completed. + virtual Core::hresult EchoMixedCaseName(const uint32_t InputValue /* @in */, uint32_t& OutputValue /* @out */) const = 0; + + // @property + // @brief Build version property. + // @param BuildVersion Receives current build version value. + // @retval ERROR_NONE Value returned. + virtual Core::hresult BuildVersion(uint32_t& BuildVersion /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestJsonUncompliantCollapsed.h b/tests/FunctionalTests/common/interfaces/ITestJsonUncompliantCollapsed.h new file mode 100644 index 00000000..ad139ae7 --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestJsonUncompliantCollapsed.h @@ -0,0 +1,45 @@ +/* + * 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 { + + // @json 1.0.0 + // @uncompliant:collapsed + // + // NOTE: @uncompliant:collapsed is deprecated (Thunder docs/interfaces/tags.md) and must not + // be used in new interfaces. This interface exists solely to pin generator behaviour for + // existing consumers that already use this mode. Do not copy PingCollapsed as a usage example. + struct EXTERNAL ITestJsonUncompliantCollapsed : virtual public Core::IUnknown { + enum { ID = ID_TEST_JSON_UNCOMPLIANT_COL }; + + // @brief Interface-level tag target for collapsed uncompliant mode. + // @param payload Input payload. + // @param reply Receives output payload. + // @retval ERROR_NONE Operation completed. + virtual Core::hresult PingCollapsed(const string& payload /* @in */, string& reply /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestJsonUncompliantExtended.h b/tests/FunctionalTests/common/interfaces/ITestJsonUncompliantExtended.h new file mode 100644 index 00000000..21ba47fa --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestJsonUncompliantExtended.h @@ -0,0 +1,45 @@ +/* + * 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 { + + // @json 1.0.0 + // @uncompliant:extended + // + // NOTE: @uncompliant:extended is deprecated (Thunder docs/interfaces/tags.md) and must not + // be used in new interfaces. This interface exists solely to pin generator behaviour for + // existing consumers that already use this mode. Do not copy PingExtended as a usage example. + struct EXTERNAL ITestJsonUncompliantExtended : virtual public Core::IUnknown { + enum { ID = ID_TEST_JSON_UNCOMPLIANT_EXT }; + + // @brief Interface-level tag target for extended uncompliant mode. + // @param payload Input payload. + // @param reply Receives output payload. + // @retval ERROR_NONE Operation completed. + virtual Core::hresult PingExtended(const string& payload /* @in */, string& reply /* @out */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/ITestLengthModes.h b/tests/FunctionalTests/common/interfaces/ITestLengthModes.h new file mode 100644 index 00000000..6dbed370 --- /dev/null +++ b/tests/FunctionalTests/common/interfaces/ITestLengthModes.h @@ -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. + */ + +#pragma once + +#include "Ids.h" +#include "Module.h" + +namespace Thunder { +namespace FunctionalTest { + + // Exercises less-common @length modes that are not covered in existing + // functional interfaces. + // @json 1.0.0 + struct EXTERNAL ITestLengthModes : virtual public Core::IUnknown { + enum { ID = ID_TEST_LENGTH_MODES }; + + // @brief Echoes exactly one byte, exercising the length:1 output annotation + // which signals that the output buffer holds exactly one element. + // @param input Input single-byte value. + // @param output Receives the echoed byte. + // @retval ERROR_NONE Echo completed. + virtual Core::hresult EchoSingleByte( + const uint8_t input /* @in */, + uint8_t output[] /* @out @length:1 @maxlength:1 */) const = 0; + + // @brief Returns a fixed 4-byte payload using the length:return annotation to convey the written byte count. + // Intended to cover the length:return annotation in a signature where the return type + // carries the produced byte count. + // + // NOTE: returning uint16_t instead of Core::hresult is a deliberate trade-off for + // test coverage of the length:return code path, not an oversight. The downside is + // that callers lose the ability to detect transport-level failures: there is no + // hresult channel through which such errors can be signalled. + // @param output Receives payload bytes. + // @param maxSize Maximum writable bytes in output. + // @retval Payload byte count written to output. + virtual uint16_t ReadPayload( + uint8_t output[] /* @out @length:return @maxlength:maxSize */, + const uint16_t maxSize /* @in */) const = 0; + }; + +} // namespace FunctionalTest +} // namespace Thunder diff --git a/tests/FunctionalTests/common/interfaces/Ids.h b/tests/FunctionalTests/common/interfaces/Ids.h index 5f120656..28e02add 100644 --- a/tests/FunctionalTests/common/interfaces/Ids.h +++ b/tests/FunctionalTests/common/interfaces/Ids.h @@ -39,6 +39,14 @@ namespace Thunder { ID_TEST_RESTRICTIONS = ID_INTERFACE_OFFSET + 0x00A, ID_TEST_ASYNC = ID_INTERFACE_OFFSET + 0x00B, ID_TEST_ASYNC_CALLBACK = ID_INTERFACE_OFFSET + 0x00C, + ID_TEST_JSON_SHAPE = ID_INTERFACE_OFFSET + 0x00D, + ID_TEST_JSON_TEXT_KEEP = ID_INTERFACE_OFFSET + 0x00E, + ID_TEST_JSON_TEXT_CASE = ID_INTERFACE_OFFSET + 0x00F, + ID_TEST_JSON_COMPLIANT = ID_INTERFACE_OFFSET + 0x010, + ID_TEST_JSON_UNCOMPLIANT_EXT = ID_INTERFACE_OFFSET + 0x011, + 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, }; } // namespace FunctionalTest diff --git a/tests/FunctionalTests/comrpc/CMakeLists.txt b/tests/FunctionalTests/comrpc/CMakeLists.txt index 1e8aff83..3b0d7826 100644 --- a/tests/FunctionalTests/comrpc/CMakeLists.txt +++ b/tests/FunctionalTests/comrpc/CMakeLists.txt @@ -23,8 +23,8 @@ if(TEST_ASYNC) target_sources(ComRpcFunctionalTests PRIVATE tests/TestAsync.cpp) endif() -if(TEST_BUFFER) - target_sources(ComRpcFunctionalTests PRIVATE tests/TestBuffer.cpp) +if(TEST_BUFFERS) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestBuffers.cpp) endif() if(TEST_ENUMS) @@ -43,8 +43,8 @@ if(TEST_ITERATORS) target_sources(ComRpcFunctionalTests PRIVATE tests/TestIterators.cpp) endif() -if(TEST_OPTIONAL) - target_sources(ComRpcFunctionalTests PRIVATE tests/TestOptional.cpp) +if(TEST_OPTIONALS) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestOptionals.cpp) endif() if(TEST_PRIMITIVES) @@ -59,6 +59,38 @@ if(TEST_STRUCTS) target_sources(ComRpcFunctionalTests PRIVATE tests/TestStructs.cpp) endif() +if(TEST_ENCODING_MAC) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestEncodingMac.cpp) +endif() + +if(TEST_LENGTH_MODES) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestLengthModes.cpp) +endif() + +if(TEST_JSON_SHAPE) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestJsonShape.cpp) +endif() + +if(TEST_JSON_TEXT_KEEP) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestJsonTextKeep.cpp) +endif() + +if(TEST_JSON_TEXT_CASE) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestJsonTextCase.cpp) +endif() + +if(TEST_JSON_COMPLIANT) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestJsonCompliant.cpp) +endif() + +if(TEST_JSON_UNCOMPLIANT_EXT) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestJsonUncompliantExtended.cpp) +endif() + +if(TEST_JSON_UNCOMPLIANT_COL) + target_sources(ComRpcFunctionalTests PRIVATE tests/TestJsonUncompliantCollapsed.cpp) +endif() + target_include_directories(ComRpcFunctionalTests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} # For interfaces/ prefix (generated code) diff --git a/tests/FunctionalTests/comrpc/tests/TestEncodingMac.cpp b/tests/FunctionalTests/comrpc/tests/TestEncodingMac.cpp new file mode 100644 index 00000000..014177cf --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestEncodingMac.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 "TestHarness.h" +#include + +using namespace Thunder; +using namespace Thunder::FunctionalTest; + +class TestEncodingMac : public Testing::TestHarness {}; + +TEST_F(TestEncodingMac, SetGetMacAddress) { + const uint8_t input[6] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB }; + ASSERT_EQ(_proxy->SetMacAddress(input), Core::ERROR_NONE); + + uint8_t output[6] = { 0 }; + ASSERT_EQ(_proxy->GetMacAddress(output), Core::ERROR_NONE); + + for (uint8_t i = 0; i < 6; ++i) { + EXPECT_EQ(output[i], input[i]); + } +} + +TEST_F(TestEncodingMac, EchoMacAddress) { + const uint8_t input[6] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01 }; + uint8_t output[6] = { 0 }; + + ASSERT_EQ(_proxy->EchoMacAddress(input, output), Core::ERROR_NONE); + + for (uint8_t i = 0; i < 6; ++i) { + EXPECT_EQ(output[i], input[i]); + } +} diff --git a/tests/FunctionalTests/comrpc/tests/TestJsonCompliant.cpp b/tests/FunctionalTests/comrpc/tests/TestJsonCompliant.cpp new file mode 100644 index 00000000..4ac02da8 --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestJsonCompliant.cpp @@ -0,0 +1,33 @@ +/* + * 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 TestJsonCompliant : public Testing::TestHarness {}; + +TEST_F(TestJsonCompliant, Ping) { + string reply; + ASSERT_EQ(_proxy->Ping("payload", reply), Core::ERROR_NONE); + EXPECT_EQ(reply, "payload"); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestJsonShape.cpp b/tests/FunctionalTests/comrpc/tests/TestJsonShape.cpp new file mode 100644 index 00000000..6035a3ac --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestJsonShape.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 "TestHarness.h" +#include + +using namespace Thunder; +using namespace Thunder::FunctionalTest; + +class TestJsonShape : public Testing::TestHarness {}; + +TEST_F(TestJsonShape, GetWrappedCounter) { + uint32_t counter = 0; + ASSERT_EQ(_proxy->GetWrappedCounter(counter), Core::ERROR_NONE); + EXPECT_EQ(counter, 42u); +} + +TEST_F(TestJsonShape, EchoExtractedList) { + const std::vector input { 7 }; + std::vector output; + + ASSERT_EQ(_proxy->EchoExtractedList(input, output), Core::ERROR_NONE); + ASSERT_EQ(output.size(), input.size()); + EXPECT_EQ(output[0], input[0]); +} + +TEST_F(TestJsonShape, EchoStruct) { + const ITestJsonShape::Dimensions input { 1920, 1080 }; + ITestJsonShape::Dimensions output { 0, 0 }; + + ASSERT_EQ(_proxy->EchoStruct(input, output), Core::ERROR_NONE); + EXPECT_EQ(output.width, input.width); + EXPECT_EQ(output.height, input.height); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestJsonTextCase.cpp b/tests/FunctionalTests/comrpc/tests/TestJsonTextCase.cpp new file mode 100644 index 00000000..6458afa9 --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestJsonTextCase.cpp @@ -0,0 +1,33 @@ +/* + * 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 TestJsonTextCase : public Testing::TestHarness {}; + +TEST_F(TestJsonTextCase, EchoCaseConvention) { + uint16_t result = 0; + ASSERT_EQ(_proxy->EchoCaseConvention(123, result), Core::ERROR_NONE); + EXPECT_EQ(result, 123u); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestJsonTextKeep.cpp b/tests/FunctionalTests/comrpc/tests/TestJsonTextKeep.cpp new file mode 100644 index 00000000..afcea24e --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestJsonTextKeep.cpp @@ -0,0 +1,39 @@ +/* + * 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 TestJsonTextKeep : public Testing::TestHarness {}; + +TEST_F(TestJsonTextKeep, EchoMixedCaseName) { + uint32_t output = 0; + ASSERT_EQ(_proxy->EchoMixedCaseName(77, output), Core::ERROR_NONE); + EXPECT_EQ(output, 77u); +} + +TEST_F(TestJsonTextKeep, BuildVersionProperty) { + uint32_t version = 0; + ASSERT_EQ(_proxy->BuildVersion(version), Core::ERROR_NONE); + EXPECT_EQ(version, 42u); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestJsonUncompliantCollapsed.cpp b/tests/FunctionalTests/comrpc/tests/TestJsonUncompliantCollapsed.cpp new file mode 100644 index 00000000..982749b8 --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestJsonUncompliantCollapsed.cpp @@ -0,0 +1,33 @@ +/* + * 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 TestJsonUncompliantCollapsed : public Testing::TestHarness {}; + +TEST_F(TestJsonUncompliantCollapsed, PingCollapsed) { + string reply; + ASSERT_EQ(_proxy->PingCollapsed("payload", reply), Core::ERROR_NONE); + EXPECT_EQ(reply, "payload"); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestJsonUncompliantExtended.cpp b/tests/FunctionalTests/comrpc/tests/TestJsonUncompliantExtended.cpp new file mode 100644 index 00000000..2073d0a8 --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestJsonUncompliantExtended.cpp @@ -0,0 +1,33 @@ +/* + * 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 TestJsonUncompliantExtended : public Testing::TestHarness {}; + +TEST_F(TestJsonUncompliantExtended, PingExtended) { + string reply; + ASSERT_EQ(_proxy->PingExtended("payload", reply), Core::ERROR_NONE); + EXPECT_EQ(reply, "payload"); +} diff --git a/tests/FunctionalTests/comrpc/tests/TestLengthModes.cpp b/tests/FunctionalTests/comrpc/tests/TestLengthModes.cpp new file mode 100644 index 00000000..77b96ad3 --- /dev/null +++ b/tests/FunctionalTests/comrpc/tests/TestLengthModes.cpp @@ -0,0 +1,48 @@ +/* + * 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 TestLengthModes : public Testing::TestHarness {}; + +TEST_F(TestLengthModes, EchoSingleByte) { + const uint8_t input = 0x5A; + uint8_t output[1] = { 0 }; + + ASSERT_EQ(_proxy->EchoSingleByte(input, output), Core::ERROR_NONE); + EXPECT_EQ(output[0], input); +} + +TEST_F(TestLengthModes, ReadPayloadHonorsCapacity) { + uint8_t output[16] = { 0 }; + + const uint16_t written = _proxy->ReadPayload(output, static_cast(sizeof(output))); + EXPECT_LE(written, static_cast(sizeof(output))); + // impl writes a fixed 4-byte pattern {0xDE, 0xAD, 0xBE, 0xEF}; verify bytes were actually written + EXPECT_GT(written, 0u); + EXPECT_EQ(output[0], 0xDE); + EXPECT_EQ(output[1], 0xAD); + EXPECT_EQ(output[2], 0xBE); + EXPECT_EQ(output[3], 0xEF); +} diff --git a/tests/FunctionalTests/external/thunder/CMakeLists.txt b/tests/FunctionalTests/external/thunder/CMakeLists.txt index 79bbb34f..ac15ba62 100644 --- a/tests/FunctionalTests/external/thunder/CMakeLists.txt +++ b/tests/FunctionalTests/external/thunder/CMakeLists.txt @@ -39,10 +39,12 @@ if (ENABLE_JSON_RPC_TESTS) set(CRYPTALGO ON CACHE BOOL "" FORCE) set(PLUGINS ON CACHE BOOL "" FORCE) set(WEBSOCKET ON CACHE BOOL "" FORCE) + set(ENABLE_TEST_RUNTIME ON CACHE BOOL "" FORCE) else() set(CRYPTALGO OFF CACHE BOOL "" FORCE) set(PLUGINS OFF CACHE BOOL "" FORCE) set(WEBSOCKET OFF CACHE BOOL "" FORCE) + set(ENABLE_TEST_RUNTIME OFF CACHE BOOL "" FORCE) endif() FetchContent_MakeAvailable(Thunder) diff --git a/tests/FunctionalTests/jsonrpc/CMakeLists.txt b/tests/FunctionalTests/jsonrpc/CMakeLists.txt index d29f00bb..1bfe804a 100644 --- a/tests/FunctionalTests/jsonrpc/CMakeLists.txt +++ b/tests/FunctionalTests/jsonrpc/CMakeLists.txt @@ -25,6 +25,10 @@ target_include_directories(JsonRpcFunctionalTests ${CMAKE_CURRENT_SOURCE_DIR} ) +if(TEST_ASYNC) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestAsyncJsonRpc.cpp) +endif() + if(TEST_ENUMS) target_sources(JsonRpcFunctionalTests PRIVATE tests/TestEnumsJsonRpc.cpp) endif() @@ -41,8 +45,39 @@ if(TEST_RESTRICTIONS) target_sources(JsonRpcFunctionalTests PRIVATE tests/TestRestrictionsJsonRpc.cpp) endif() +if(TEST_ENCODING_MAC) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestEncodingMacJsonRpc.cpp) +endif() + +if(TEST_JSON_SHAPE) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestJsonShapeJsonRpc.cpp) +endif() + +if(TEST_JSON_TEXT_KEEP) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestJsonTextKeepJsonRpc.cpp) +endif() + +if(TEST_JSON_TEXT_CASE) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestJsonTextCaseJsonRpc.cpp) +endif() + +if(TEST_JSON_COMPLIANT) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestJsonCompliantJsonRpc.cpp) +endif() + +if(TEST_JSON_UNCOMPLIANT_EXT) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestJsonUncompliantExtendedJsonRpc.cpp) +endif() + +if(TEST_JSON_UNCOMPLIANT_COL) + target_sources(JsonRpcFunctionalTests PRIVATE tests/TestJsonUncompliantCollapsedJsonRpc.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 PRIVATE + thunder_test_support ${NAMESPACE}Core::${NAMESPACE}Core ${NAMESPACE}COM::${NAMESPACE}COM ${NAMESPACE}WebSocket::${NAMESPACE}WebSocket diff --git a/tests/FunctionalTests/jsonrpc/JsonRpcServer.cpp b/tests/FunctionalTests/jsonrpc/JsonRpcServer.cpp index 524dbedc..4227e764 100644 --- a/tests/FunctionalTests/jsonrpc/JsonRpcServer.cpp +++ b/tests/FunctionalTests/jsonrpc/JsonRpcServer.cpp @@ -19,24 +19,43 @@ #include "JsonRpcServer.h" +#include + namespace Thunder { namespace JsonRpcServer { JsonRpcServer* g_server = nullptr; JsonRpcServer::JsonRpcServer() - : _mockShell("TestPlugin") { + const std::vector plugins; + const uint32_t result = _runtime.Initialize(plugins); + + ASSERT(result == Core::ERROR_NONE); + + _shell = _runtime.GetShell("Controller"); + ASSERT(_shell.IsValid() == true); + PluginHost::IShell::IConnectionServer::INotification* sink = nullptr; - Attach(sink, &_mockShell); - if (sink != nullptr) sink->Release(); + Attach(sink, _shell.operator->()); + if (sink != nullptr) { + sink->Release(); + } + Test::RegisterJsonRpcInterfaces(*this); } JsonRpcServer::~JsonRpcServer() { + Test::UnregisterJsonRpcInterfaces(*this); + PluginHost::IShell::IConnectionServer::INotification* sink = nullptr; Detach(sink); - if (sink != nullptr) sink->Release(); + if (sink != nullptr) { + sink->Release(); + } + + _shell.Release(); + _runtime.Deinitialize(); } } // namespace JsonRpcServer } // namespace Thunder \ No newline at end of file diff --git a/tests/FunctionalTests/jsonrpc/JsonRpcServer.h b/tests/FunctionalTests/jsonrpc/JsonRpcServer.h index f00c06c0..8070fbe5 100644 --- a/tests/FunctionalTests/jsonrpc/JsonRpcServer.h +++ b/tests/FunctionalTests/jsonrpc/JsonRpcServer.h @@ -20,8 +20,8 @@ #pragma once #include -#include "MockShell.h" #include "Module.h" +#include namespace Thunder { namespace JsonRpcServer { @@ -34,7 +34,8 @@ namespace JsonRpcServer { ~JsonRpcServer(); private: - MockShell _mockShell; + TestCore::ThunderTestRuntime _runtime; + Core::ProxyType _shell; }; extern JsonRpcServer* g_server; diff --git a/tests/FunctionalTests/jsonrpc/MockShell.h b/tests/FunctionalTests/jsonrpc/MockShell.h deleted file mode 100644 index d103bd09..00000000 --- a/tests/FunctionalTests/jsonrpc/MockShell.h +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 "Module.h" - -namespace Thunder { -namespace JsonRpcServer { - class MockShell : public PluginHost::IShell { - public: - MockShell(const string& callsign) - : _callsign(callsign) - { - } - ~MockShell() override = default; - - // This object is owned by the JsonRpcServer and is not reference counted, so AddRef/Release do nothing. - uint32_t AddRef() const override { return Core::ERROR_COMPOSIT_OBJECT; } - uint32_t Release() const override { return Core::ERROR_COMPOSIT_OBJECT; } - - // Essential IShell methods - Callsign() is used by JSONRPC - string Callsign() const override { return _callsign; } - - // Stub implementations for other required virtual methods - void EnableWebServer(const string&, const string&) override { } - void DisableWebServer() override { } - string Model() const override { return string(); } - bool Background() const override { return false; } - string Accessor() const override { return string(); } - string WebPrefix() const override { return string(); } - string Locator() const override { return string(); } - string ClassName() const override { return string(); } - string PersistentPath() const override { return string(); } - string VolatilePath() const override { return string(); } - string DataPath() const override { return string(); } - string ProxyStubPath() const override { return string(); } - string SystemPath() const override { return string(); } - string PluginPath() const override { return string(); } - string SystemRootPath() const override { return string(); } - Core::hresult SystemRootPath(const string&) override { return Core::ERROR_NONE; } - startmode StartMode() const override { return startmode::DEACTIVATED; } - Core::hresult StartMode(const startmode) override { return Core::ERROR_NONE; } - string Substitute(const string&) const override { return string(); } - bool Resumed() const override { return false; } - Core::hresult Resumed(const bool) override { return Core::ERROR_NONE; } - string HashKey() const override { return string(); } - string ConfigLine() const override { return string(); } - Core::hresult ConfigLine(const string&) override { return Core::ERROR_NONE; } - Core::hresult Metadata(string&) const override { return Core::ERROR_NONE; } - PluginHost::ISubSystem* SubSystems() override { return nullptr; } - void Notify(const string&, const string&) override { } - void Register(PluginHost::IPlugin::INotification*, const Core::OptionalType& = { }) override { } - void Unregister(PluginHost::IPlugin::INotification*, const Core::OptionalType& = { }) override { } - state State() const override { return state::DEACTIVATED; } - void* QueryInterfaceByCallsign(const uint32_t, const string&) override { return nullptr; } - Core::hresult Activate(const reason) override { return Core::ERROR_NONE; } - Core::hresult Deactivate(const reason) override { return Core::ERROR_NONE; } - Core::hresult Unavailable(const reason) override { return Core::ERROR_NONE; } - Core::hresult Hibernate(const uint32_t) override { return Core::ERROR_NONE; } - reason Reason() const override { return reason::REQUESTED; } - uint32_t Submit(const uint32_t, const Core::ProxyType&) override { return Core::ERROR_NONE; } - RPC::IStringIterator* GetLibrarySearchPaths(const string&) const override { return nullptr; } - - void Register(PluginHost::IPlugin::INotification* sink, const uint32_t interface_id) override { } - void Unregister(PluginHost::IPlugin::INotification* sink, const uint32_t interface_id) override { } - - BEGIN_INTERFACE_MAP(MockShell) - INTERFACE_ENTRY(PluginHost::IShell) - END_INTERFACE_MAP - - private: - string _callsign; - }; -} // namespace JsonRpcServer -} // namespace Thunder diff --git a/tests/FunctionalTests/jsonrpc/tests/TestAsyncJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestAsyncJsonRpc.cpp new file mode 100644 index 00000000..8f5efde6 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestAsyncJsonRpc.cpp @@ -0,0 +1,137 @@ +/* + * 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. + */ + +// Tests the following annotation tags via ITestAsync in JSON-RPC mode: +// +// @async — Calculate() maps to a non-blocking JSON-RPC call that returns a +// slot index synchronously; completion is delivered via a JSON-RPC +// event. The ICallback* parameter is replaced by event machinery; +// callers poll SlotResult or subscribe to the completion event. +// +// @default — delayMs carries @default:100; omitting it from the request body +// should succeed (the generator substitutes the default value). +// +// @index — SlotResult is an @index @property; each slot is independently +// addressable as slotResult@. + +#include +#include "JsonRpcTestHarness.h" +#include +#include +#include + +using namespace Thunder; + +class TestAsyncJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +// Helper: parse the slot number from a calculate response that contains "slot":. +// Returns 255 and marks the test failed if the field is not found. +static uint8_t ParseSlot(const string& response) +{ + auto pos = response.find("\"slot\":"); + if (pos == string::npos) { + ADD_FAILURE() << "No \"slot\" field in calculate response: " << response; + return 255; + } + return static_cast(std::stoi(response.substr(pos + 7))); +} + +// Poll slotResult@ until it returns Core::ERROR_NONE or the deadline elapses. +// Retries every 20 ms so the test is not sensitive to a fixed delay. +static Core::hresult PollSlotResult(JsonRpcTesting::JsonRpcTestHarness& harness, + uint8_t slot, string& response, + std::chrono::milliseconds timeout = std::chrono::milliseconds(2000)) +{ + const auto deadline = std::chrono::steady_clock::now() + timeout; + Core::hresult result = Core::ERROR_GENERAL; + do { + result = harness.CallMethod("slotResult@" + std::to_string(slot), "{}", response); + if (result == Core::ERROR_NONE) { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } while (std::chrono::steady_clock::now() < deadline); + return result; +} + +// @async — calculate() returns a slot index synchronously in the JSON-RPC response; +// the ICallback is replaced by the framework's event-delivery mechanism. +TEST_F(TestAsyncJsonRpc, Calculate_ReturnsSlot) +{ + string response; + ASSERT_EQ(Core::ERROR_NONE, + CallMethod("calculate", R"({"value":21,"delayMs":50})", response)); + uint8_t slot = ParseSlot(response); + ASSERT_LE(slot, 6u) << "Slot must be in range [0..6]: " << response; + + // Abort the running calculation so the slot does not leak into other tests. + CallMethod("abort", R"({"slot":)" + std::to_string(slot) + "}", response); +} + +// @default — omitting delayMs should apply the @default:100 annotation value +// and the call must still succeed. +TEST_F(TestAsyncJsonRpc, Calculate_DefaultDelay) +{ + string response; + ASSERT_EQ(Core::ERROR_NONE, + CallMethod("calculate", R"({"value":10})", response)); + uint8_t slot = ParseSlot(response); + ASSERT_LE(slot, 6u) << "Slot must be in range [0..6]: " << response; + + // Poll until the default-delay (100 ms) calculation completes so the slot is + // freed before the next test. + ASSERT_EQ(Core::ERROR_NONE, PollSlotResult(*this, slot, response)) + << "SlotResult not ready within timeout for slot " << +slot; +} + +// @async state — inProgress must report true while the calculation is still running. +TEST_F(TestAsyncJsonRpc, InProgress_WhileRunning) +{ + string response; + ASSERT_EQ(Core::ERROR_NONE, + CallMethod("calculate", R"({"value":1,"delayMs":500})", response)); + uint8_t slot = ParseSlot(response); + ASSERT_LE(slot, 6u); + + string ipResponse; + ASSERT_EQ(Core::ERROR_NONE, + CallMethod("inProgress", R"({"slot":)" + std::to_string(slot) + "}", ipResponse)); + EXPECT_NE(ipResponse.find("true"), string::npos) << "Response: " << ipResponse; + + // Abort so the slow calculation does not run for the full 500 ms. + CallMethod("abort", R"({"slot":)" + std::to_string(slot) + "}", response); +} + +// @index — SlotResult is an @index @property; after the calculation completes +// the result is read via the indexed accessor slotResult@. +TEST_F(TestAsyncJsonRpc, SlotResult_IndexProperty) +{ + string response; + ASSERT_EQ(Core::ERROR_NONE, + CallMethod("calculate", R"({"value":21,"delayMs":50})", response)); + uint8_t slot = ParseSlot(response); + ASSERT_LE(slot, 6u); + + // Poll until the 50 ms calculation finishes rather than relying on a fixed sleep. + string resultResponse; + ASSERT_EQ(Core::ERROR_NONE, PollSlotResult(*this, slot, resultResponse)) + << "SlotResult not ready within timeout for slot " << +slot; + // impl computes value * 2, so 21 * 2 = 42 + EXPECT_NE(resultResponse.find("42"), string::npos) << "Response: " << resultResponse; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestEncodingMacJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestEncodingMacJsonRpc.cpp new file mode 100644 index 00000000..3c96d2c0 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestEncodingMacJsonRpc.cpp @@ -0,0 +1,46 @@ +/* + * 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 TestEncodingMacJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +TEST_F(TestEncodingMacJsonRpc, SetGetMacAddress) { + string response; + + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("setMacAddress", R"({"mac":"01:23:45:67:89:ab"})", response)); + + response.clear(); + EXPECT_EQ(Core::ERROR_NONE, CallMethod("getMacAddress", "{}", response)); + // verify the stored MAC is returned correctly + EXPECT_EQ(response, "\"01:23:45:67:89:ab\"") << "Response: " << response; +} + +TEST_F(TestEncodingMacJsonRpc, EchoMacAddress) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("echoMacAddress", R"({"input":"de:ad:be:ef:00:01"})", response)); + // verify the echoed MAC matches the input + EXPECT_EQ(response, "\"de:ad:be:ef:00:01\"") << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestEnumsJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestEnumsJsonRpc.cpp index d5498a36..8ce09011 100644 --- a/tests/FunctionalTests/jsonrpc/tests/TestEnumsJsonRpc.cpp +++ b/tests/FunctionalTests/jsonrpc/tests/TestEnumsJsonRpc.cpp @@ -39,17 +39,17 @@ TEST_F(TestEnumsJsonRpc, DISABLED_SetGetColor) { TEST_F(TestEnumsJsonRpc, ToggleColor) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("toggleColor", R"({"color":"RED"})", response)); - EXPECT_NE(response.find("GREEN"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "\"GREEN\"") << "Response: " << response; } TEST_F(TestEnumsJsonRpc, CompareColors) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("compareColors", R"({"color1":"RED","color2":"RED"})", response)); - EXPECT_NE(response.find("true"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "true") << "Response: " << response; response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("compareColors", R"({"color1":"RED","color2":"BLUE"})", response)); - EXPECT_NE(response.find("false"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "false") << "Response: " << response; } TEST_F(TestEnumsJsonRpc, SetGetState) { @@ -58,13 +58,13 @@ TEST_F(TestEnumsJsonRpc, SetGetState) { response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("getState", "{}", response)); - EXPECT_NE(response.find("RUNNING"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "\"RUNNING\"") << "Response: " << response; } TEST_F(TestEnumsJsonRpc, NextState) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("nextState", R"({"state":"IDLE"})", response)); - EXPECT_NE(response.find("RUNNING"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "\"RUNNING\"") << "Response: " << response; } TEST_F(TestEnumsJsonRpc, SetGetPriority) { @@ -73,7 +73,7 @@ TEST_F(TestEnumsJsonRpc, SetGetPriority) { response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("getPriority", "{}", response)); - EXPECT_NE(response.find("HIGH"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "\"HIGH\"") << "Response: " << response; } TEST_F(TestEnumsJsonRpc, SetGetCapabilities) { @@ -83,7 +83,9 @@ TEST_F(TestEnumsJsonRpc, SetGetCapabilities) { response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("getCapabilities", "{}", response)); + // bitmask serialised as array; both flags must be present (order unspecified) EXPECT_NE(response.find("CAP_AUDIO"), string::npos) << "Response: " << response; + EXPECT_NE(response.find("CAP_VIDEO"), string::npos) << "Response: " << response; } TEST_F(TestEnumsJsonRpc, CurrentState_PropertyReadOnly) { @@ -103,19 +105,14 @@ TEST_F(TestEnumsJsonRpc, ComputeState) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("computeState", R"({"current":"RUNNING","desired":"STOPPED"})", response)); - // Should return derived state - expecting a valid State enum value - bool hasValidState = (response.find("IDLE") != string::npos || - response.find("RUNNING") != string::npos || - response.find("PAUSED") != string::npos || - response.find("STOPPED") != string::npos || - response.find("ERROR") != string::npos); - EXPECT_TRUE(hasValidState) << "Response should contain a valid derived State enum value: " << response; + // RUNNING != STOPPED → impl returns PAUSED + EXPECT_EQ(response, "\"PAUSED\"") << "Response: " << response; } TEST_F(TestEnumsJsonRpc, IsValidState) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("isValidState", R"({"state":"RUNNING"})", response)); - EXPECT_NE(response.find("true"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "true") << "Response: " << response; } diff --git a/tests/FunctionalTests/jsonrpc/tests/TestJsonCompliantJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestJsonCompliantJsonRpc.cpp new file mode 100644 index 00000000..740de0c0 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestJsonCompliantJsonRpc.cpp @@ -0,0 +1,33 @@ +/* + * 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 TestJsonCompliantJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +TEST_F(TestJsonCompliantJsonRpc, Ping) { + string response; + EXPECT_EQ(Core::ERROR_NONE, CallMethod("ping", R"({"payload":"abc"})", response)); + // impl echoes payload into reply; verify the value survives the compliant envelope + EXPECT_EQ(response, "\"abc\"") << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestJsonShapeJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestJsonShapeJsonRpc.cpp new file mode 100644 index 00000000..94b8292f --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestJsonShapeJsonRpc.cpp @@ -0,0 +1,49 @@ +/* + * 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 TestJsonShapeJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +TEST_F(TestJsonShapeJsonRpc, GetWrappedCounter) { + string response; + EXPECT_EQ(Core::ERROR_NONE, CallMethod("getWrappedCounter", "{}", response)); + // impl counter is initialised to 42; @wrapped encloses it in an object envelope + EXPECT_EQ(response, "{\"counter\":42}") << "Response: " << response; +} + +TEST_F(TestJsonShapeJsonRpc, EchoExtractedList) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("echoExtractedList", R"({"input":[7]})", response)); + // verify the echoed list element survives the @extract round-trip + EXPECT_EQ(response, "[7]") << "Response: " << response; +} + +TEST_F(TestJsonShapeJsonRpc, EchoStruct) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("echoStruct", R"({"in":{"width":1920,"height":1080}})", response)); + // verify both struct fields survive the standard (non-extracted) round-trip + EXPECT_EQ(response, "{\"width\":1920,\"height\":1080}") << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestJsonTextCaseJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestJsonTextCaseJsonRpc.cpp new file mode 100644 index 00000000..62f832d5 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestJsonTextCaseJsonRpc.cpp @@ -0,0 +1,34 @@ +/* + * 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 TestJsonTextCaseJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +TEST_F(TestJsonTextCaseJsonRpc, EchoCaseConvention) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("echocaseconvention", R"({"sourcevalue":123})", response)); + // @text:legacy lowercases method/param names; verify the echo-back value is correct + EXPECT_EQ(response, "123") << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestJsonTextKeepJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestJsonTextKeepJsonRpc.cpp new file mode 100644 index 00000000..707ada17 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestJsonTextKeepJsonRpc.cpp @@ -0,0 +1,42 @@ +/* + * 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 TestJsonTextKeepJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +TEST_F(TestJsonTextKeepJsonRpc, EchoMixedCaseName) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("EchoMixedCaseName", R"({"InputValue":77})", response)); + // @text:keep preserves exact C++ casing; verify the echo-back value is correct + EXPECT_EQ(response, "77") << "Response: " << response; +} + +TEST_F(TestJsonTextKeepJsonRpc, BuildVersionProperty) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("BuildVersion", "{}", response)); + // impl returns hardcoded version 42; search for the bare number (won't appear in the JSON-RPC envelope) + EXPECT_EQ(response, "42") << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestJsonUncompliantCollapsedJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestJsonUncompliantCollapsedJsonRpc.cpp new file mode 100644 index 00000000..7bd4aac7 --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestJsonUncompliantCollapsedJsonRpc.cpp @@ -0,0 +1,34 @@ +/* + * 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 TestJsonUncompliantCollapsedJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +TEST_F(TestJsonUncompliantCollapsedJsonRpc, PingCollapsed) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("pingCollapsed", R"("abc")", response)); + // impl echoes payload; collapsed mode passes and returns raw JSON string + EXPECT_EQ(response, "\"abc\"") << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestJsonUncompliantExtendedJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestJsonUncompliantExtendedJsonRpc.cpp new file mode 100644 index 00000000..f64bc69a --- /dev/null +++ b/tests/FunctionalTests/jsonrpc/tests/TestJsonUncompliantExtendedJsonRpc.cpp @@ -0,0 +1,34 @@ +/* + * 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 TestJsonUncompliantExtendedJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; + +TEST_F(TestJsonUncompliantExtendedJsonRpc, PingExtended) { + string response; + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("pingExtended", R"({"payload":"abc"})", response)); + // impl echoes payload into reply; verify the value survives the extended uncompliant envelope + EXPECT_EQ(response, "\"abc\"") << "Response: " << response; +} diff --git a/tests/FunctionalTests/jsonrpc/tests/TestPrimitivesJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestPrimitivesJsonRpc.cpp index a93cbf90..9b0e288d 100644 --- a/tests/FunctionalTests/jsonrpc/tests/TestPrimitivesJsonRpc.cpp +++ b/tests/FunctionalTests/jsonrpc/tests/TestPrimitivesJsonRpc.cpp @@ -32,7 +32,7 @@ class TestPrimitivesJsonRpc : public JsonRpcTesting::JsonRpcTestHarness {}; TEST_F(TestPrimitivesJsonRpc, EchoInt8_Positive) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoInt8", R"({"input":42})", response)); - EXPECT_NE(response.find("42"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "42") << "Response: " << response; } // DISABLED: JsonGenerator DecSInt validation bug - boundary values fail with overflow errors @@ -50,7 +50,7 @@ TEST_F(TestPrimitivesJsonRpc, DISABLED_EchoInt8_Boundaries) { TEST_F(TestPrimitivesJsonRpc, EchoInt16_Positive) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoInt16", R"({"input":1000})", response)); - EXPECT_NE(response.find("1000"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "1000") << "Response: " << response; } // DISABLED: JsonGenerator DecSInt validation bug - boundary values fail with overflow errors @@ -68,7 +68,7 @@ TEST_F(TestPrimitivesJsonRpc, DISABLED_EchoInt16_Boundaries) { TEST_F(TestPrimitivesJsonRpc, EchoInt32_Positive) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoInt32", R"({"input":100000})", response)); - EXPECT_NE(response.find("100000"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "100000") << "Response: " << response; } // DISABLED: JsonGenerator DecSInt validation bug - boundary values fail with overflow errors @@ -86,7 +86,7 @@ TEST_F(TestPrimitivesJsonRpc, DISABLED_EchoInt32_Boundaries) { TEST_F(TestPrimitivesJsonRpc, EchoInt64_Positive) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoInt64", R"({"input":9223372036854775})", response)); - EXPECT_NE(response.find("9223372036854775"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "9223372036854775") << "Response: " << response; } // DISABLED: JsonGenerator DecSInt validation bug - boundary values fail with overflow errors @@ -107,44 +107,44 @@ TEST_F(TestPrimitivesJsonRpc, EchoUInt8_Boundaries) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoUInt8", R"({"input":0})", response)); - EXPECT_NE(response.find("0"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "0") << "Response: " << response; response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoUInt8", R"({"input":255})", response)); - EXPECT_NE(response.find("255"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "255") << "Response: " << response; } TEST_F(TestPrimitivesJsonRpc, EchoUInt16_Boundaries) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoUInt16", R"({"input":0})", response)); - EXPECT_NE(response.find("0"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "0") << "Response: " << response; response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoUInt16", R"({"input":65535})", response)); - EXPECT_NE(response.find("65535"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "65535") << "Response: " << response; } TEST_F(TestPrimitivesJsonRpc, EchoUInt32_Boundaries) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoUInt32", R"({"input":0})", response)); - EXPECT_NE(response.find("0"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "0") << "Response: " << response; response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoUInt32", R"({"input":4294967295})", response)); - EXPECT_NE(response.find("4294967295"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "4294967295") << "Response: " << response; } TEST_F(TestPrimitivesJsonRpc, EchoUInt64_Boundaries) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoUInt64", R"({"input":0})", response)); - EXPECT_NE(response.find("0"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "0") << "Response: " << response; response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoUInt64", R"({"input":18446744073709551615})", response)); - EXPECT_NE(response.find("18446744073709551615"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "18446744073709551615") << "Response: " << response; } // ===== Floating point ===== @@ -167,11 +167,11 @@ TEST_F(TestPrimitivesJsonRpc, EchoBool) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoBool", R"({"input":true})", response)); - EXPECT_NE(response.find("true"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "true") << "Response: " << response; response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoBool", R"({"input":false})", response)); - EXPECT_NE(response.find("false"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "false") << "Response: " << response; } // ===== String ===== @@ -179,13 +179,13 @@ TEST_F(TestPrimitivesJsonRpc, EchoBool) { TEST_F(TestPrimitivesJsonRpc, EchoString_UTF8) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoString", R"({"input":"Hello JSON-RPC!"})", response)); - EXPECT_NE(response.find("Hello JSON-RPC!"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "\"Hello JSON-RPC!\"") << "Response: " << response; } TEST_F(TestPrimitivesJsonRpc, EchoString_Empty) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoString", R"({"input":""})", response)); - EXPECT_NE(response.find("\""), string::npos) << "Response: " << response; + EXPECT_EQ(response, "\"\"") << "Response: " << response; } // ===== Special types ===== diff --git a/tests/FunctionalTests/jsonrpc/tests/TestRestrictionsJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestRestrictionsJsonRpc.cpp index 5f1ea3c9..6be1f9d1 100644 --- a/tests/FunctionalTests/jsonrpc/tests/TestRestrictionsJsonRpc.cpp +++ b/tests/FunctionalTests/jsonrpc/tests/TestRestrictionsJsonRpc.cpp @@ -76,18 +76,18 @@ TEST_F(TestRestrictionsJsonRpc, SetName_TooLongRejected) { TEST_F(TestRestrictionsJsonRpc, ClampedAdd_ValidInputs) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("clampedAdd", R"({"a":10,"b":20})", response)); - EXPECT_NE(response.find("30"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "30") << "Response: " << response; } TEST_F(TestRestrictionsJsonRpc, IsValidPercentage) { string response; EXPECT_EQ(Core::ERROR_NONE, CallMethod("isValidPercentage", R"({"value":50})", response)); - EXPECT_NE(response.find("true"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "true") << "Response: " << response; response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("isValidPercentage", R"({"value":150})", response)); - EXPECT_NE(response.find("false"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "false") << "Response: " << response; } TEST_F(TestRestrictionsJsonRpc, IsValidRatio) { @@ -95,6 +95,6 @@ TEST_F(TestRestrictionsJsonRpc, IsValidRatio) { // Parameter name is "ratio" not "value" EXPECT_EQ(Core::ERROR_NONE, CallMethod("isValidRatio", R"({"ratio":0.5})", response)); - EXPECT_NE(response.find("true"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "true") << "Response: " << response; } diff --git a/tests/FunctionalTests/jsonrpc/tests/TestStructsJsonRpc.cpp b/tests/FunctionalTests/jsonrpc/tests/TestStructsJsonRpc.cpp index a1e06cb1..8dc163bb 100644 --- a/tests/FunctionalTests/jsonrpc/tests/TestStructsJsonRpc.cpp +++ b/tests/FunctionalTests/jsonrpc/tests/TestStructsJsonRpc.cpp @@ -35,8 +35,7 @@ TEST_F(TestStructsJsonRpc, SetGetPoint_RoundTrip) { // Get point back response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("getPoint", "{}", response)); - EXPECT_NE(response.find("100"), string::npos) << "Response: " << response; - EXPECT_NE(response.find("200"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "{\"x\":100,\"y\":200}") << "Response: " << response; } TEST_F(TestStructsJsonRpc, SetGetRectangle_RoundTrip) { @@ -48,8 +47,7 @@ TEST_F(TestStructsJsonRpc, SetGetRectangle_RoundTrip) { response.clear(); EXPECT_EQ(Core::ERROR_NONE, CallMethod("getRectangle", "{}", response)); - EXPECT_NE(response.find("10"), string::npos) << "Response: " << response; - EXPECT_NE(response.find("20"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "{\"topLeft\":{\"x\":10,\"y\":20},\"bottomRight\":{\"x\":310,\"y\":420}}") << "Response: " << response; } // DISABLED: setColor/getColor methods fail with error 30 (not found) despite being registered in JTestStructs.h. @@ -74,8 +72,7 @@ TEST_F(TestStructsJsonRpc, MovePoint) { EXPECT_EQ(Core::ERROR_NONE, CallMethod("movePoint", R"({"point":{"x":100,"y":200},"dx":50,"dy":-30})", response)); - EXPECT_NE(response.find("150"), string::npos) << "Response: " << response; - EXPECT_NE(response.find("170"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "{\"x\":150,\"y\":170}") << "Response: " << response; } TEST_F(TestStructsJsonRpc, DistanceBetweenPoints_Pythagorean) { @@ -101,8 +98,7 @@ TEST_F(TestStructsJsonRpc, EchoPoint) { // echoPoint expects "point" parameter EXPECT_EQ(Core::ERROR_NONE, CallMethod("echoPoint", R"({"point":{"x":42,"y":84}})", response)); - EXPECT_NE(response.find("42"), string::npos) << "Response: " << response; - EXPECT_NE(response.find("84"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "{\"x\":42,\"y\":84}") << "Response: " << response; } TEST_F(TestStructsJsonRpc, IsValidPoint) { @@ -110,7 +106,7 @@ TEST_F(TestStructsJsonRpc, IsValidPoint) { EXPECT_EQ(Core::ERROR_NONE, CallMethod("isValidPoint", R"({"point":{"x":100,"y":200}})", response)); - EXPECT_NE(response.find("true"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "true") << "Response: " << response; } TEST_F(TestStructsJsonRpc, CalculateRectangleArea) { @@ -118,7 +114,7 @@ TEST_F(TestStructsJsonRpc, CalculateRectangleArea) { EXPECT_EQ(Core::ERROR_NONE, CallMethod("calculateRectangleArea", R"({"rect":{"topLeft":{"x":0,"y":0},"bottomRight":{"x":10,"y":20}}})", response)); - EXPECT_NE(response.find("200"), string::npos) << "Response: " << response; // 10 * 20 = 200 + EXPECT_EQ(response, "200") << "Response: " << response; // 10 * 20 = 200 } TEST_F(TestStructsJsonRpc, GetRectangleCenter) { @@ -126,7 +122,7 @@ TEST_F(TestStructsJsonRpc, GetRectangleCenter) { EXPECT_EQ(Core::ERROR_NONE, CallMethod("getRectangleCenter", R"({"rect":{"topLeft":{"x":0,"y":0},"bottomRight":{"x":10,"y":20}}})", response)); - EXPECT_NE(response.find("5"), string::npos) << "Response: " << response; // center x=5 + EXPECT_EQ(response, "{\"x\":5,\"y\":10}") << "Response: " << response; } TEST_F(TestStructsJsonRpc, RectanglesOverlap) { @@ -135,7 +131,7 @@ TEST_F(TestStructsJsonRpc, RectanglesOverlap) { EXPECT_EQ(Core::ERROR_NONE, CallMethod("rectanglesOverlap", R"({"r1":{"topLeft":{"x":0,"y":0},"bottomRight":{"x":10,"y":10}}, "r2":{"topLeft":{"x":5,"y":5},"bottomRight":{"x":15,"y":15}}})", response)); - EXPECT_NE(response.find("true"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "true") << "Response: " << response; } TEST_F(TestStructsJsonRpc, ScaleRectangle) { @@ -143,7 +139,7 @@ TEST_F(TestStructsJsonRpc, ScaleRectangle) { EXPECT_EQ(Core::ERROR_NONE, CallMethod("scaleRectangle", R"({"rect":{"topLeft":{"x":0,"y":0},"bottomRight":{"x":10,"y":10}},"factor":2.0})", response)); - EXPECT_NE(response.find("20"), string::npos) << "Response: " << response; // scaled to 20x20 + EXPECT_EQ(response, "{\"topLeft\":{\"x\":0,\"y\":0},\"bottomRight\":{\"x\":20,\"y\":20}}") << "Response: " << response; } TEST_F(TestStructsJsonRpc, IsValidRectangle) { @@ -151,6 +147,34 @@ TEST_F(TestStructsJsonRpc, IsValidRectangle) { EXPECT_EQ(Core::ERROR_NONE, CallMethod("isValidRectangle", R"({"rect":{"topLeft":{"x":0,"y":0},"bottomRight":{"x":10,"y":10}}})", response)); - EXPECT_NE(response.find("true"), string::npos) << "Response: " << response; + EXPECT_EQ(response, "true") << "Response: " << response; +} + +// ===== @opaque — config property passes its JSON blob through without deserialisation ===== + +TEST_F(TestStructsJsonRpc, Config_Opaque_RoundTrip) { + string response; + // SET — pass the opaque blob as the value of the "config" field + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("setConfig", R"({"config":{"level":5,"label":"test"}})", response)); + + response.clear(); + // GET — verify the stored blob survives the round-trip (key order/formatting may differ) + EXPECT_EQ(Core::ERROR_NONE, CallMethod("getConfig", "{}", response)); + EXPECT_EQ(response, "{\"level\":5,\"label\":\"test\"}") << "Response: " << response; +} + +// ===== @index — slotPoint@ addresses each slot independently ===== + +TEST_F(TestStructsJsonRpc, SlotPoint_IndexedProperty) { + string response; + // SET via setSlotPoint method — slot is a named param, point is the struct + EXPECT_EQ(Core::ERROR_NONE, + CallMethod("setSlotPoint", R"({"slot":0,"point":{"x":7,"y":13}})", response)); + + response.clear(); + // GET via getSlotPoint@ indexed property + EXPECT_EQ(Core::ERROR_NONE, CallMethod("getSlotPoint@0", "{}", response)); + EXPECT_EQ(response, "{\"x\":7,\"y\":13}") << "Response: " << response; }