diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 8d1bd90..a8aa602 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -51,6 +51,12 @@ set(SDK_SOURCES implementations/mock/mock_transport.h implementations/mock/mock_registry.h implementations/mock/logos_mock.h + logos_json_utils.cpp + logos_json_utils.h + logos_core_client.cpp + logos_core_client.h + logos_sdk_c.cpp + logos_sdk_c.h ) # Create the SDK library as STATIC instead of SHARED @@ -98,6 +104,9 @@ install(FILES logos_transport_factory.h logos_registry.h logos_registry_factory.h + logos_json_utils.h + logos_core_client.h + logos_sdk_c.h DESTINATION include ) diff --git a/cpp/logos_core_client.cpp b/cpp/logos_core_client.cpp new file mode 100644 index 0000000..ce08d3e --- /dev/null +++ b/cpp/logos_core_client.cpp @@ -0,0 +1,85 @@ +#include "logos_core_client.h" +#include "logos_api.h" +#include "logos_api_client.h" +#include "logos_object.h" +#include "logos_json_utils.h" +#include + +LogosCoreClient::LogosCoreClient(QObject* parent) + : QObject(parent) + , m_api(new LogosAPI("core", this)) +{ +} + +LogosCoreClient::~LogosCoreClient() +{ +} + +LogosAPIClient* LogosCoreClient::clientFor(const QString& pluginName) +{ + return m_api->getClient(pluginName); +} + +void LogosCoreClient::callMethodAsync(const QString& pluginName, + const QString& methodName, + const QString& paramsJson, + AsyncCallback callback) +{ + if (!callback) return; + + bool ok = false; + QString errorMessage; + QVariantList args = LogosJsonUtils::parseMethodParams(paramsJson, &ok, &errorMessage); + + if (!ok) { + callback(false, errorMessage); + return; + } + + LogosAPIClient* client = m_api->getClient(pluginName); + if (!client) { + callback(false, QString("Failed to get client for plugin: %1").arg(pluginName)); + return; + } + + client->invokeRemoteMethodAsync( + pluginName, methodName, args, + [callback, pluginName, methodName](QVariant result) { + if (result.isValid()) { + QString resultStr; + if (result.canConvert()) { + resultStr = result.toString(); + } else { + resultStr = QString("Result of type: %1").arg(result.typeName()); + } + callback(true, QString("Method call successful. Result: %1").arg(resultStr)); + } else { + callback(false, QStringLiteral("Method call returned invalid result")); + } + }); +} + +void LogosCoreClient::subscribeEvent(const QString& pluginName, + const QString& eventName, + AsyncCallback callback) +{ + if (!callback) return; + + LogosAPIClient* client = m_api->getClient(pluginName); + if (!client) { + qWarning() << "LogosCoreClient: Failed to get client for event subscription:" << pluginName; + return; + } + + LogosObject* obj = client->requestObject(pluginName); + if (!obj) { + qWarning() << "LogosCoreClient: Failed to get object for event subscription:" << pluginName; + return; + } + + client->onEvent(obj, eventName, + [callback](const QString& evName, const QVariantList& evData) { + QString json = LogosJsonUtils::formatEventJson(evName, evData); + callback(true, json); + }); +} diff --git a/cpp/logos_core_client.h b/cpp/logos_core_client.h new file mode 100644 index 0000000..b9d40cc --- /dev/null +++ b/cpp/logos_core_client.h @@ -0,0 +1,62 @@ +#ifndef LOGOS_CORE_CLIENT_H +#define LOGOS_CORE_CLIENT_H + +#include +#include +#include +#include +#include +#include + +class LogosAPI; +class LogosAPIClient; +class LogosObject; + +/** + * @brief LogosCoreClient provides a high-level async interface for calling + * plugin methods and subscribing to events from the "core" (host) perspective. + * + * Unlike creating ephemeral LogosAPI instances per call, this class maintains + * a single persistent LogosAPI("core") and reuses cached client connections. + * + * It uses LogosAPIClient::invokeRemoteMethodAsync for connection-aware calls, + * eliminating manual QTimer-based connection delays. + */ +class LogosCoreClient : public QObject +{ + Q_OBJECT + +public: + using AsyncCallback = std::function; + + explicit LogosCoreClient(QObject* parent = nullptr); + ~LogosCoreClient(); + + /** + * Call a plugin method asynchronously, with parameters provided as a JSON + * string in the [{name,value,type},...] format used by FFI consumers. + * The callback receives (success, resultMessage). + */ + void callMethodAsync(const QString& pluginName, + const QString& methodName, + const QString& paramsJson, + AsyncCallback callback); + + /** + * Subscribe to an event from a plugin. The callback fires each time + * the event is emitted, with a JSON-formatted message. + */ + void subscribeEvent(const QString& pluginName, + const QString& eventName, + AsyncCallback callback); + + /** + * Get (or lazily create) a LogosAPIClient for the named plugin. + */ + LogosAPIClient* clientFor(const QString& pluginName); + +private: + LogosAPI* m_api; +}; + +#endif // LOGOS_CORE_CLIENT_H diff --git a/cpp/logos_json_utils.cpp b/cpp/logos_json_utils.cpp new file mode 100644 index 0000000..ffe44d7 --- /dev/null +++ b/cpp/logos_json_utils.cpp @@ -0,0 +1,115 @@ +#include "logos_json_utils.h" +#include +#include +#include +#include + +namespace LogosJsonUtils { + +QVariant jsonParamToVariant(const QJsonObject& param) +{ + QString name = param.value("name").toString(); + QString value = param.value("value").toString(); + QString type = param.value("type").toString(); + + qDebug() << "LogosJsonUtils: Converting param:" << name << "value:" << value << "type:" << type; + + if (type == "string" || type == "QString") { + return QVariant(value); + } else if (type == "int" || type == "integer") { + bool ok; + int intValue = value.toInt(&ok); + return ok ? QVariant(intValue) : QVariant(); + } else if (type == "bool" || type == "boolean") { + if (value.toLower() == "true" || value == "1") { + return QVariant(true); + } else if (value.toLower() == "false" || value == "0") { + return QVariant(false); + } + return QVariant(); + } else if (type == "double" || type == "float") { + bool ok; + double doubleValue = value.toDouble(&ok); + return ok ? QVariant(doubleValue) : QVariant(); + } else { + qWarning() << "LogosJsonUtils: Unknown parameter type:" << type << "- treating as string"; + return QVariant(value); + } +} + +QVariantList parseMethodParams(const QString& json, bool* ok, QString* errorMessage) +{ + QVariantList args; + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(json.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError) { + if (ok) *ok = false; + if (errorMessage) *errorMessage = QString("JSON parse error: %1").arg(parseError.errorString()); + return args; + } + + QJsonArray paramsArray = jsonDoc.array(); + + for (const QJsonValue& paramValue : paramsArray) { + if (paramValue.isObject()) { + QJsonObject paramObj = paramValue.toObject(); + QVariant variant = jsonParamToVariant(paramObj); + if (variant.isValid()) { + args.append(variant); + } else { + if (ok) *ok = false; + if (errorMessage) *errorMessage = QString("Invalid parameter: %1").arg(paramObj.value("name").toString()); + return QVariantList(); + } + } + } + + if (ok) *ok = true; + return args; +} + +QString variantToJsonString(const QVariant& value) +{ + if (!value.isValid()) + return QStringLiteral("null"); + + switch (value.userType()) { + case QMetaType::Bool: + return value.toBool() ? QStringLiteral("true") : QStringLiteral("false"); + case QMetaType::Int: + case QMetaType::LongLong: + return QString::number(value.toLongLong()); + case QMetaType::Double: + case QMetaType::Float: + return QString::number(value.toDouble()); + case QMetaType::QString: + return value.toString(); + case QMetaType::QVariantMap: { + QJsonDocument doc(QJsonObject::fromVariantMap(value.toMap())); + return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); + } + case QMetaType::QVariantList: { + QJsonDocument doc(QJsonArray::fromVariantList(value.toList())); + return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); + } + default: + break; + } + + return value.toString(); +} + +QString formatEventJson(const QString& eventName, const QVariantList& data) +{ + QString result = QString("{\"event\":\"%1\",\"data\":[").arg(eventName); + for (int i = 0; i < data.size(); ++i) { + if (i > 0) result += ","; + result += QString("\"%1\"").arg(data[i].toString()); + } + result += "]}"; + return result; +} + +} // namespace LogosJsonUtils diff --git a/cpp/logos_json_utils.h b/cpp/logos_json_utils.h new file mode 100644 index 0000000..0e97eb7 --- /dev/null +++ b/cpp/logos_json_utils.h @@ -0,0 +1,44 @@ +#ifndef LOGOS_JSON_UTILS_H +#define LOGOS_JSON_UTILS_H + +#include +#include +#include +#include + +namespace LogosJsonUtils { + + /** + * Convert a single JSON parameter object {name, value, type} to a QVariant. + * Supports types: string, QString, int, integer, bool, boolean, double, float. + * Unknown types are treated as strings. + * Returns invalid QVariant for unparseable values (e.g. "abc" as int). + */ + QVariant jsonParamToVariant(const QJsonObject& param); + + /** + * Parse a JSON string containing an array of {name, value, type} objects + * into a QVariantList. This is the format used by FFI consumers (e.g. logos-js-sdk). + * + * On success, sets ok=true and returns the converted arguments. + * On failure (JSON parse error or invalid parameter), sets ok=false and + * fills errorMessage with a description. + */ + QVariantList parseMethodParams(const QString& json, bool* ok = nullptr, QString* errorMessage = nullptr); + + /** + * Convert a QVariant result into a JSON-formatted string suitable for + * returning through the C callback API. + */ + QString variantToJsonString(const QVariant& value); + + /** + * Format event data (event name + QVariantList payload) as a JSON string + * matching the format expected by FFI consumers: + * {"event":"","data":["","",...]} + */ + QString formatEventJson(const QString& eventName, const QVariantList& data); + +} + +#endif // LOGOS_JSON_UTILS_H diff --git a/cpp/logos_sdk_c.cpp b/cpp/logos_sdk_c.cpp new file mode 100644 index 0000000..4eed92a --- /dev/null +++ b/cpp/logos_sdk_c.cpp @@ -0,0 +1,62 @@ +#include "logos_sdk_c.h" +#include "logos_core_client.h" +#include +#include + +static LogosCoreClient* s_coreClient = nullptr; + +static LogosCoreClient* ensureClient() +{ + if (!s_coreClient) + s_coreClient = new LogosCoreClient(); + return s_coreClient; +} + +void logos_sdk_call_method_async( + const char* plugin_name, + const char* method_name, + const char* params_json, + LogosSdkCallback callback, + void* user_data) +{ + if (!callback) return; + + if (!plugin_name || !method_name) { + callback(0, "Plugin name or method name is null", user_data); + return; + } + + QString pluginStr = QString::fromUtf8(plugin_name); + QString methodStr = QString::fromUtf8(method_name); + QString paramsStr = params_json ? QString::fromUtf8(params_json) : QStringLiteral("[]"); + + ensureClient()->callMethodAsync(pluginStr, methodStr, paramsStr, + [callback, user_data](bool success, const QString& message) { + QByteArray msgBytes = message.toUtf8(); + callback(success ? 1 : 0, msgBytes.constData(), user_data); + }); +} + +void logos_sdk_register_event( + const char* plugin_name, + const char* event_name, + LogosSdkCallback callback, + void* user_data) +{ + if (!plugin_name || !event_name || !callback) return; + + QString pluginStr = QString::fromUtf8(plugin_name); + QString eventStr = QString::fromUtf8(event_name); + + ensureClient()->subscribeEvent(pluginStr, eventStr, + [callback, user_data](bool success, const QString& message) { + QByteArray msgBytes = message.toUtf8(); + callback(success ? 1 : 0, msgBytes.constData(), user_data); + }); +} + +void logos_sdk_shutdown() +{ + delete s_coreClient; + s_coreClient = nullptr; +} diff --git a/cpp/logos_sdk_c.h b/cpp/logos_sdk_c.h new file mode 100644 index 0000000..e86647a --- /dev/null +++ b/cpp/logos_sdk_c.h @@ -0,0 +1,57 @@ +#ifndef LOGOS_SDK_C_H +#define LOGOS_SDK_C_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * C callback type matching the AsyncCallback in logos_core.h: + * void(int result, const char* message, void* user_data) + */ +typedef void (*LogosSdkCallback)(int result, const char* message, void* user_data); + +/** + * Call a plugin method asynchronously. + * + * @param plugin_name Target plugin name + * @param method_name Method to call + * @param params_json JSON array of [{name,value,type},...] parameters (may be NULL for "[]") + * @param callback Receives (1=success/0=failure, message, user_data) on completion + * @param user_data Opaque pointer passed through to callback + */ +void logos_sdk_call_method_async( + const char* plugin_name, + const char* method_name, + const char* params_json, + LogosSdkCallback callback, + void* user_data +); + +/** + * Register an event listener for a specific event from a plugin. + * + * @param plugin_name Plugin that emits the event + * @param event_name Event name to listen for + * @param callback Receives (1, json_event_data, user_data) each time event fires + * @param user_data Opaque pointer passed through to callback + */ +void logos_sdk_register_event( + const char* plugin_name, + const char* event_name, + LogosSdkCallback callback, + void* user_data +); + +/** + * Shut down the SDK's internal core client, releasing connections. + * Safe to call multiple times. After this, async calls will lazily re-create + * the client on next use. + */ +void logos_sdk_shutdown(void); + +#ifdef __cplusplus +} +#endif + +#endif // LOGOS_SDK_C_H diff --git a/flake.nix b/flake.nix index e5ed093..b42c4fd 100644 --- a/flake.nix +++ b/flake.nix @@ -46,6 +46,17 @@ } ); + checks = forAllSystems ({ pkgs }: + let + common = import ./nix/default.nix { inherit pkgs; }; + src = ./.; + tests = import ./nix/tests.nix { inherit pkgs common src; }; + in + { + inherit tests; + } + ); + devShells = forAllSystems ({ pkgs }: { default = pkgs.mkShell { nativeBuildInputs = [ diff --git a/nix/include.nix b/nix/include.nix index 91dd285..c367735 100644 --- a/nix/include.nix +++ b/nix/include.nix @@ -36,7 +36,10 @@ pkgs.stdenv.mkDerivation { qt_provider_object.h qt_provider_object.cpp \ logos_transport.h logos_transport_factory.h logos_transport_factory.cpp \ logos_registry.h logos_registry_factory.h logos_registry_factory.cpp \ - plugin_registry.h; do + plugin_registry.h \ + logos_json_utils.h logos_json_utils.cpp \ + logos_core_client.h logos_core_client.cpp \ + logos_sdk_c.h logos_sdk_c.cpp; do if [ -f cpp/$file ]; then cp cpp/$file $out/include/cpp/ fi diff --git a/tests/sdk/CMakeLists.txt b/tests/sdk/CMakeLists.txt index 5058607..85ae186 100644 --- a/tests/sdk/CMakeLists.txt +++ b/tests/sdk/CMakeLists.txt @@ -38,6 +38,9 @@ add_executable(sdk_tests test_local_transport_integration.cpp test_event_system.cpp test_provider_dispatch.cpp + test_logos_json_utils.cpp + test_logos_core_client.cpp + test_logos_sdk_c.cpp fixtures/sample_provider.cpp ${GENERATED_DISPATCH} ) diff --git a/tests/sdk/test_logos_core_client.cpp b/tests/sdk/test_logos_core_client.cpp new file mode 100644 index 0000000..a5a0cc6 --- /dev/null +++ b/tests/sdk/test_logos_core_client.cpp @@ -0,0 +1,127 @@ +#include +#include +#include "logos_mock.h" +#include "logos_core_client.h" + +class LogosCoreClientTest : public ::testing::Test { +protected: + void SetUp() override { m_mock = new LogosMockSetup(); } + void TearDown() override { delete m_mock; } + LogosMockSetup* m_mock = nullptr; + + void processEvents() { + for (int i = 0; i < 10; ++i) + QCoreApplication::processEvents(); + } +}; + +TEST_F(LogosCoreClientTest, CallMethodAsync_SuccessfulCall) +{ + m_mock->when("test_module", "greet").thenReturn(QVariant("hello world")); + + LogosCoreClient client; + bool called = false; + bool success = false; + QString message; + + client.callMethodAsync("test_module", "greet", "[]", + [&](bool s, const QString& m) { called = true; success = s; message = m; }); + + processEvents(); + + EXPECT_TRUE(called); + EXPECT_TRUE(success); + EXPECT_TRUE(message.contains("hello world")); +} + +TEST_F(LogosCoreClientTest, CallMethodAsync_WithJsonParams) +{ + m_mock->when("math", "add").thenReturn(QVariant(30)); + + LogosCoreClient client; + bool called = false; + bool success = false; + + QString params = R"([{"name":"a","value":"10","type":"int"},{"name":"b","value":"20","type":"int"}])"; + client.callMethodAsync("math", "add", params, + [&](bool s, const QString&) { called = true; success = s; }); + + processEvents(); + + EXPECT_TRUE(called); + EXPECT_TRUE(success); + EXPECT_TRUE(m_mock->wasCalled("math", "add")); +} + +TEST_F(LogosCoreClientTest, CallMethodAsync_InvalidJsonReportsError) +{ + LogosCoreClient client; + bool called = false; + bool success = true; + QString message; + + client.callMethodAsync("mod", "fn", "not valid json", + [&](bool s, const QString& m) { called = true; success = s; message = m; }); + + EXPECT_TRUE(called); + EXPECT_FALSE(success); + EXPECT_TRUE(message.contains("JSON parse error")); +} + +TEST_F(LogosCoreClientTest, CallMethodAsync_InvalidParamReportsError) +{ + LogosCoreClient client; + bool called = false; + bool success = true; + QString message; + + QString params = R"([{"name":"a","value":"abc","type":"int"}])"; + client.callMethodAsync("mod", "fn", params, + [&](bool s, const QString& m) { called = true; success = s; message = m; }); + + EXPECT_TRUE(called); + EXPECT_FALSE(success); + EXPECT_TRUE(message.contains("Invalid parameter")); +} + +TEST_F(LogosCoreClientTest, CallMethodAsync_NullCallbackIsNoOp) +{ + LogosCoreClient client; + client.callMethodAsync("mod", "fn", "[]", nullptr); +} + +TEST_F(LogosCoreClientTest, CallMethodAsync_InvalidResultReportsError) +{ + // No expectation set -> mock returns invalid QVariant + m_mock->when("mod", "other").thenReturn(QVariant(1)); + + LogosCoreClient client; + bool called = false; + bool success = true; + QString message; + + client.callMethodAsync("mod", "missing_fn", "[]", + [&](bool s, const QString& m) { called = true; success = s; message = m; }); + + processEvents(); + + EXPECT_TRUE(called); + EXPECT_FALSE(success); + EXPECT_TRUE(message.contains("invalid result")); +} + +TEST_F(LogosCoreClientTest, ClientFor_ReturnsSameClientForSamePlugin) +{ + LogosCoreClient client; + auto* c1 = client.clientFor("mod"); + auto* c2 = client.clientFor("mod"); + EXPECT_EQ(c1, c2); +} + +TEST_F(LogosCoreClientTest, ClientFor_ReturnsDifferentForDifferentPlugins) +{ + LogosCoreClient client; + auto* c1 = client.clientFor("mod_a"); + auto* c2 = client.clientFor("mod_b"); + EXPECT_NE(c1, c2); +} diff --git a/tests/sdk/test_logos_json_utils.cpp b/tests/sdk/test_logos_json_utils.cpp new file mode 100644 index 0000000..cd2ef1f --- /dev/null +++ b/tests/sdk/test_logos_json_utils.cpp @@ -0,0 +1,236 @@ +#include +#include "logos_json_utils.h" +#include + +// ============================================================================= +// jsonParamToVariant tests +// ============================================================================= + +TEST(LogosJsonUtilsTest, ConvertsStringType) +{ + QJsonObject p; + p["name"] = "arg0"; p["value"] = "hello"; p["type"] = "string"; + QVariant v = LogosJsonUtils::jsonParamToVariant(p); + EXPECT_TRUE(v.isValid()); + EXPECT_EQ(v.toString(), "hello"); +} + +TEST(LogosJsonUtilsTest, ConvertsQStringType) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "qt"; p["type"] = "QString"; + EXPECT_EQ(LogosJsonUtils::jsonParamToVariant(p).toString(), "qt"); +} + +TEST(LogosJsonUtilsTest, ConvertsIntType) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "42"; p["type"] = "int"; + QVariant v = LogosJsonUtils::jsonParamToVariant(p); + EXPECT_TRUE(v.isValid()); + EXPECT_EQ(v.toInt(), 42); +} + +TEST(LogosJsonUtilsTest, ConvertsIntegerType) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "99"; p["type"] = "integer"; + EXPECT_EQ(LogosJsonUtils::jsonParamToVariant(p).toInt(), 99); +} + +TEST(LogosJsonUtilsTest, ConvertsBoolTrue) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "true"; p["type"] = "bool"; + EXPECT_TRUE(LogosJsonUtils::jsonParamToVariant(p).toBool()); + + p["value"] = "1"; + EXPECT_TRUE(LogosJsonUtils::jsonParamToVariant(p).toBool()); +} + +TEST(LogosJsonUtilsTest, ConvertsBoolFalse) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "false"; p["type"] = "bool"; + EXPECT_FALSE(LogosJsonUtils::jsonParamToVariant(p).toBool()); + + p["value"] = "0"; + EXPECT_FALSE(LogosJsonUtils::jsonParamToVariant(p).toBool()); +} + +TEST(LogosJsonUtilsTest, ConvertsBooleanType) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "true"; p["type"] = "boolean"; + EXPECT_TRUE(LogosJsonUtils::jsonParamToVariant(p).toBool()); +} + +TEST(LogosJsonUtilsTest, ConvertsDoubleType) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "3.14"; p["type"] = "double"; + EXPECT_NEAR(LogosJsonUtils::jsonParamToVariant(p).toDouble(), 3.14, 0.001); +} + +TEST(LogosJsonUtilsTest, ConvertsFloatType) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "2.718"; p["type"] = "float"; + EXPECT_NEAR(LogosJsonUtils::jsonParamToVariant(p).toDouble(), 2.718, 0.001); +} + +TEST(LogosJsonUtilsTest, InvalidIntReturnsInvalid) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "abc"; p["type"] = "int"; + EXPECT_FALSE(LogosJsonUtils::jsonParamToVariant(p).isValid()); +} + +TEST(LogosJsonUtilsTest, InvalidBoolReturnsInvalid) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "maybe"; p["type"] = "bool"; + EXPECT_FALSE(LogosJsonUtils::jsonParamToVariant(p).isValid()); +} + +TEST(LogosJsonUtilsTest, InvalidDoubleReturnsInvalid) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "xyz"; p["type"] = "double"; + EXPECT_FALSE(LogosJsonUtils::jsonParamToVariant(p).isValid()); +} + +TEST(LogosJsonUtilsTest, UnknownTypeTreatedAsString) +{ + QJsonObject p; + p["name"] = "a"; p["value"] = "data"; p["type"] = "custom_thing"; + QVariant v = LogosJsonUtils::jsonParamToVariant(p); + EXPECT_TRUE(v.isValid()); + EXPECT_EQ(v.toString(), "data"); +} + +// ============================================================================= +// parseMethodParams tests +// ============================================================================= + +TEST(LogosJsonUtilsTest, ParseMixedParams) +{ + QString json = R"([ + {"name":"arg0","value":"hello","type":"string"}, + {"name":"arg1","value":"42","type":"int"}, + {"name":"arg2","value":"true","type":"bool"}, + {"name":"arg3","value":"3.14","type":"double"} + ])"; + bool ok = false; + QString err; + QVariantList args = LogosJsonUtils::parseMethodParams(json, &ok, &err); + EXPECT_TRUE(ok); + EXPECT_TRUE(err.isEmpty()); + ASSERT_EQ(args.size(), 4); + EXPECT_EQ(args[0].toString(), "hello"); + EXPECT_EQ(args[1].toInt(), 42); + EXPECT_TRUE(args[2].toBool()); + EXPECT_NEAR(args[3].toDouble(), 3.14, 0.001); +} + +TEST(LogosJsonUtilsTest, ParseEmptyArray) +{ + bool ok = false; + QVariantList args = LogosJsonUtils::parseMethodParams("[]", &ok); + EXPECT_TRUE(ok); + EXPECT_EQ(args.size(), 0); +} + +TEST(LogosJsonUtilsTest, ParseInvalidJson) +{ + bool ok = true; + QString err; + QVariantList args = LogosJsonUtils::parseMethodParams("not json", &ok, &err); + EXPECT_FALSE(ok); + EXPECT_TRUE(err.contains("JSON parse error")); + EXPECT_EQ(args.size(), 0); +} + +TEST(LogosJsonUtilsTest, ParseInvalidParamValue) +{ + QString json = R"([{"name":"a","value":"abc","type":"int"}])"; + bool ok = true; + QString err; + QVariantList args = LogosJsonUtils::parseMethodParams(json, &ok, &err); + EXPECT_FALSE(ok); + EXPECT_TRUE(err.contains("Invalid parameter")); +} + +TEST(LogosJsonUtilsTest, ParseSingleString) +{ + QString json = R"([{"name":"arg0","value":"test","type":"string"}])"; + bool ok = false; + QVariantList args = LogosJsonUtils::parseMethodParams(json, &ok); + EXPECT_TRUE(ok); + ASSERT_EQ(args.size(), 1); + EXPECT_EQ(args[0].toString(), "test"); +} + +TEST(LogosJsonUtilsTest, ParseNullOkPointer) +{ + QVariantList args = LogosJsonUtils::parseMethodParams("[]"); + EXPECT_EQ(args.size(), 0); +} + +// ============================================================================= +// variantToJsonString tests +// ============================================================================= + +TEST(LogosJsonUtilsTest, VariantToJson_Invalid) +{ + EXPECT_EQ(LogosJsonUtils::variantToJsonString(QVariant()), "null"); +} + +TEST(LogosJsonUtilsTest, VariantToJson_String) +{ + EXPECT_EQ(LogosJsonUtils::variantToJsonString(QVariant("hello")), "hello"); +} + +TEST(LogosJsonUtilsTest, VariantToJson_Int) +{ + EXPECT_EQ(LogosJsonUtils::variantToJsonString(QVariant(42)), "42"); +} + +TEST(LogosJsonUtilsTest, VariantToJson_Bool) +{ + EXPECT_EQ(LogosJsonUtils::variantToJsonString(QVariant(true)), "true"); + EXPECT_EQ(LogosJsonUtils::variantToJsonString(QVariant(false)), "false"); +} + +TEST(LogosJsonUtilsTest, VariantToJson_Double) +{ + QString result = LogosJsonUtils::variantToJsonString(QVariant(3.14)); + EXPECT_TRUE(result.startsWith("3.14")); +} + +TEST(LogosJsonUtilsTest, VariantToJson_Map) +{ + QVariantMap map; + map["key"] = "value"; + QString result = LogosJsonUtils::variantToJsonString(QVariant(map)); + EXPECT_TRUE(result.contains("key")); + EXPECT_TRUE(result.contains("value")); +} + +// ============================================================================= +// formatEventJson tests +// ============================================================================= + +TEST(LogosJsonUtilsTest, FormatEventJson_Basic) +{ + QVariantList data; + data << QVariant("hello") << QVariant(42); + QString result = LogosJsonUtils::formatEventJson("myEvent", data); + EXPECT_EQ(result, R"({"event":"myEvent","data":["hello","42"]})"); +} + +TEST(LogosJsonUtilsTest, FormatEventJson_Empty) +{ + QString result = LogosJsonUtils::formatEventJson("evt", {}); + EXPECT_EQ(result, R"({"event":"evt","data":[]})"); +} diff --git a/tests/sdk/test_logos_sdk_c.cpp b/tests/sdk/test_logos_sdk_c.cpp new file mode 100644 index 0000000..6ee10da --- /dev/null +++ b/tests/sdk/test_logos_sdk_c.cpp @@ -0,0 +1,160 @@ +#include +#include +#include "logos_mock.h" +#include "logos_sdk_c.h" +#include + +static bool s_called = false; +static int s_result = -1; +static std::string s_message; +static void* s_user_data = nullptr; + +static void resetState() +{ + s_called = false; + s_result = -1; + s_message.clear(); + s_user_data = nullptr; +} + +static void testCCallback(int result, const char* message, void* user_data) +{ + s_called = true; + s_result = result; + s_message = message ? message : ""; + s_user_data = user_data; +} + +class LogosSdkCTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + resetState(); + } + void TearDown() override + { + logos_sdk_shutdown(); + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; + + void processEvents() { + for (int i = 0; i < 10; ++i) + QCoreApplication::processEvents(); + } +}; + +TEST_F(LogosSdkCTest, CallMethodAsync_Success) +{ + m_mock->when("mod", "fn").thenReturn(QVariant("ok")); + + logos_sdk_call_method_async("mod", "fn", "[]", testCCallback, nullptr); + processEvents(); + + EXPECT_TRUE(s_called); + EXPECT_EQ(s_result, 1); + EXPECT_TRUE(s_message.find("ok") != std::string::npos); +} + +TEST_F(LogosSdkCTest, CallMethodAsync_WithParams) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(42)); + + const char* params = R"([{"name":"a","value":"hello","type":"string"}])"; + logos_sdk_call_method_async("mod", "fn", params, testCCallback, nullptr); + processEvents(); + + EXPECT_TRUE(s_called); + EXPECT_EQ(s_result, 1); +} + +TEST_F(LogosSdkCTest, CallMethodAsync_NullPluginName) +{ + logos_sdk_call_method_async(nullptr, "fn", "[]", testCCallback, nullptr); + + EXPECT_TRUE(s_called); + EXPECT_EQ(s_result, 0); + EXPECT_TRUE(s_message.find("null") != std::string::npos); +} + +TEST_F(LogosSdkCTest, CallMethodAsync_NullMethodName) +{ + logos_sdk_call_method_async("mod", nullptr, "[]", testCCallback, nullptr); + + EXPECT_TRUE(s_called); + EXPECT_EQ(s_result, 0); +} + +TEST_F(LogosSdkCTest, CallMethodAsync_NullCallback) +{ + logos_sdk_call_method_async("mod", "fn", "[]", nullptr, nullptr); + EXPECT_FALSE(s_called); +} + +TEST_F(LogosSdkCTest, CallMethodAsync_InvalidJson) +{ + logos_sdk_call_method_async("mod", "fn", "bad json", testCCallback, nullptr); + + EXPECT_TRUE(s_called); + EXPECT_EQ(s_result, 0); + EXPECT_TRUE(s_message.find("JSON parse error") != std::string::npos); +} + +TEST_F(LogosSdkCTest, CallMethodAsync_NullParamsDefaultsToEmptyArray) +{ + m_mock->when("mod", "fn").thenReturn(QVariant("ok")); + + logos_sdk_call_method_async("mod", "fn", nullptr, testCCallback, nullptr); + processEvents(); + + EXPECT_TRUE(s_called); + EXPECT_EQ(s_result, 1); +} + +TEST_F(LogosSdkCTest, CallMethodAsync_PassesUserData) +{ + m_mock->when("mod", "fn").thenReturn(QVariant("ok")); + + int data = 999; + logos_sdk_call_method_async("mod", "fn", "[]", testCCallback, &data); + processEvents(); + + EXPECT_TRUE(s_called); + EXPECT_EQ(s_user_data, &data); +} + +TEST_F(LogosSdkCTest, RegisterEvent_NullParams) +{ + logos_sdk_register_event(nullptr, "evt", testCCallback, nullptr); + EXPECT_FALSE(s_called); + + logos_sdk_register_event("mod", nullptr, testCCallback, nullptr); + EXPECT_FALSE(s_called); + + logos_sdk_register_event("mod", "evt", nullptr, nullptr); + EXPECT_FALSE(s_called); +} + +TEST_F(LogosSdkCTest, Shutdown_SafeToCallMultipleTimes) +{ + logos_sdk_shutdown(); + logos_sdk_shutdown(); +} + +TEST_F(LogosSdkCTest, Shutdown_ThenCallRecreatesClient) +{ + m_mock->when("mod", "fn").thenReturn(QVariant("first")); + logos_sdk_call_method_async("mod", "fn", "[]", testCCallback, nullptr); + processEvents(); + EXPECT_EQ(s_result, 1); + + logos_sdk_shutdown(); + resetState(); + + m_mock->when("mod", "fn").thenReturn(QVariant("second")); + logos_sdk_call_method_async("mod", "fn", "[]", testCCallback, nullptr); + processEvents(); + EXPECT_EQ(s_result, 1); + EXPECT_TRUE(s_message.find("second") != std::string::npos); +}