From d687418b00a273eeb8e2956c5a011d5c9fc80d7b Mon Sep 17 00:00:00 2001 From: Iuri Matias Date: Mon, 23 Mar 2026 20:57:17 -0400 Subject: [PATCH] add tests for cpp-generator and sdk --- README.md | 15 + cpp-generator/CMakeLists.txt | 1 + cpp-generator/generator_lib.cpp | 411 +++++++++++++++++ cpp-generator/generator_lib.h | 25 ++ cpp-generator/main.cpp | 412 +----------------- flake.nix | 3 + nix/tests.nix | 48 ++ tests/CMakeLists.txt | 15 + tests/generator/CMakeLists.txt | 23 + tests/generator/test_make_header.cpp | 141 ++++++ tests/generator/test_make_source.cpp | 158 +++++++ tests/generator/test_map_param_type.cpp | 33 ++ tests/generator/test_map_return_type.cpp | 31 ++ tests/generator/test_normalize_type.cpp | 42 ++ .../generator/test_parse_provider_header.cpp | 143 ++++++ tests/generator/test_to_pascal_case.cpp | 52 +++ .../generator/test_to_qvariant_conversion.cpp | 52 +++ tests/sdk/CMakeLists.txt | 23 + tests/sdk/test_logos_api_client_mock.cpp | 99 +++++ tests/sdk/test_logos_mock_setup.cpp | 102 +++++ tests/sdk/test_logos_mode.cpp | 55 +++ tests/sdk/test_logos_result.cpp | 174 ++++++++ tests/sdk/test_main.cpp | 9 + tests/sdk/test_mock_store.cpp | 111 +++++ tests/sdk/test_token_manager.cpp | 101 +++++ 25 files changed, 1872 insertions(+), 407 deletions(-) create mode 100644 cpp-generator/generator_lib.cpp create mode 100644 cpp-generator/generator_lib.h create mode 100644 nix/tests.nix create mode 100644 tests/CMakeLists.txt create mode 100644 tests/generator/CMakeLists.txt create mode 100644 tests/generator/test_make_header.cpp create mode 100644 tests/generator/test_make_source.cpp create mode 100644 tests/generator/test_map_param_type.cpp create mode 100644 tests/generator/test_map_return_type.cpp create mode 100644 tests/generator/test_normalize_type.cpp create mode 100644 tests/generator/test_parse_provider_header.cpp create mode 100644 tests/generator/test_to_pascal_case.cpp create mode 100644 tests/generator/test_to_qvariant_conversion.cpp create mode 100644 tests/sdk/CMakeLists.txt create mode 100644 tests/sdk/test_logos_api_client_mock.cpp create mode 100644 tests/sdk/test_logos_mock_setup.cpp create mode 100644 tests/sdk/test_logos_mode.cpp create mode 100644 tests/sdk/test_logos_result.cpp create mode 100644 tests/sdk/test_main.cpp create mode 100644 tests/sdk/test_mock_store.cpp create mode 100644 tests/sdk/test_token_manager.cpp diff --git a/README.md b/README.md index f03938e..083312a 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,21 @@ The nix build system is organized into modular files in the `/nix` directory: - `nix/bin.nix` - Generator binary compilation - `nix/lib.nix` - SDK library compilation - `nix/include.nix` - Header installation +- `nix/tests.nix` - Test suite (build + run via `nix build '.#tests'`) + +#### Run Tests + +```bash +# Build and run all tests (build fails if any test fails) +nix build '.#tests' +``` + +The test binaries are available in `result/bin/` and can be re-run with filters: + +```bash +./result/bin/sdk_tests --gtest_filter="LogosResultTest.*" +./result/bin/generator_tests --gtest_filter="*PascalCase*" +``` ### Manual Build diff --git a/cpp-generator/CMakeLists.txt b/cpp-generator/CMakeLists.txt index c80c650..149ad2a 100644 --- a/cpp-generator/CMakeLists.txt +++ b/cpp-generator/CMakeLists.txt @@ -11,6 +11,7 @@ find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core) add_executable(logos-cpp-generator main.cpp + generator_lib.cpp ) target_link_libraries(logos-cpp-generator PRIVATE Qt${QT_VERSION_MAJOR}::Core) diff --git a/cpp-generator/generator_lib.cpp b/cpp-generator/generator_lib.cpp new file mode 100644 index 0000000..fa289ba --- /dev/null +++ b/cpp-generator/generator_lib.cpp @@ -0,0 +1,411 @@ +#include "generator_lib.h" + +#include +#include +#include +#include +#include + +QString toPascalCase(const QString& name) +{ + QString out; + bool cap = true; + for (QChar c : name) { + if (!c.isLetterOrNumber()) { cap = true; continue; } + if (cap) { out.append(c.toUpper()); cap = false; } + else { out.append(c.toLower()); } + } + if (out.isEmpty()) return QString("Module"); + return out; +} + +QString normalizeType(QString t) +{ + t = t.trimmed(); + if (t.startsWith("const ")) t = t.mid(6); + t = t.trimmed(); + // Drop reference and pointer qualifiers + if (t.endsWith('&') || t.endsWith('*')) t.chop(1); + t = t.trimmed(); + return t; +} + +QString mapParamType(const QString& qtType) +{ + const QString base = normalizeType(qtType); + static const QSet known = { + "void","bool","int","double","float","QString","QStringList","QJsonArray","QVariant" + }; + if (known.contains(base)) return base; + // Fallback to QVariant for unknown types + return QString("QVariant"); +} + +QString mapReturnType(const QString& qtType) +{ + const QString base = normalizeType(qtType); + if (base.isEmpty() || base == "void") return QString("void"); + static const QSet known = { + "bool","int","double","float","QString","QStringList","QJsonArray","QVariant","LogosResult" + }; + if (known.contains(base)) return base; + return QString("QVariant"); +} + +QString toQVariantConversion(const QString& type, const QString& argExpr) +{ + if (type == "int") return argExpr + ".toInt()"; + if (type == "bool") return argExpr + ".toBool()"; + if (type == "double") return argExpr + ".toDouble()"; + if (type == "float") return argExpr + ".toFloat()"; + if (type == "QString") return argExpr + ".toString()"; + if (type == "QStringList") return argExpr + ".toStringList()"; + if (type == "QJsonArray") return "qvariant_cast(" + argExpr + ")"; + if (type == "QVariant") return argExpr; + if (type == "LogosResult") return argExpr + ".value()"; + return argExpr + ".toString()"; +} + +QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods) +{ + QString h; + QTextStream s(&h); + s << "#pragma once\n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \n"; + s << "#include \"logos_types.h\"\n"; + s << "#include \"logos_api.h\"\n"; + s << "#include \"logos_api_client.h\"\n"; + s << "#include \"logos_object.h\"\n\n"; + s << "class " << className << " {\n"; + s << "public:\n"; + s << " explicit " << className << "(LogosAPI* api);\n\n"; + s << " using RawEventCallback = std::function;\n"; + s << " using EventCallback = std::function;\n\n"; + s << " bool on(const QString& eventName, RawEventCallback callback);\n"; + s << " bool on(const QString& eventName, EventCallback callback);\n"; + s << " void setEventSource(LogosObject* source);\n"; + s << " LogosObject* eventSource() const;\n"; + s << " void trigger(const QString& eventName);\n"; + s << " void trigger(const QString& eventName, const QVariantList& data);\n"; + s << " template\n"; + s << " void trigger(const QString& eventName, Args&&... args) {\n"; + s << " trigger(eventName, packVariantList(std::forward(args)...));\n"; + s << " }\n"; + s << " void trigger(const QString& eventName, LogosObject* source, const QVariantList& data);\n"; + s << " template\n"; + s << " void trigger(const QString& eventName, LogosObject* source, Args&&... args) {\n"; + s << " trigger(eventName, source, packVariantList(std::forward(args)...));\n"; + s << " }\n\n"; + // Methods + for (const QJsonValue& v : methods) { + const QJsonObject o = v.toObject(); + const bool invokable = o.value("isInvokable").toBool(); + if (!invokable) continue; + const QString name = o.value("name").toString(); + const QString ret = mapReturnType(o.value("returnType").toString()); + s << " " << ret << " " << name << "("; + QJsonArray params = o.value("parameters").toArray(); + for (int i = 0; i < params.size(); ++i) { + QJsonObject p = params.at(i).toObject(); + QString pt = mapParamType(p.value("type").toString()); + QString pn = p.value("name").toString(); + if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray") { + s << "const " << pt << "& " << pn; + } else { + s << pt << " " << pn; + } + if (i + 1 < params.size()) s << ", "; + } + s << ");\n"; + // Async overload: same params + callback + optional Timeout + QString asyncCallbackType = (ret == "void") ? QString("std::function") : QString("std::function"; + s << " void " << name << "Async("; + for (int i = 0; i < params.size(); ++i) { + QJsonObject p = params.at(i).toObject(); + QString pt = mapParamType(p.value("type").toString()); + QString pn = p.value("name").toString(); + if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray") { + s << "const " << pt << "& " << pn; + } else { + s << pt << " " << pn; + } + if (i + 1 < params.size()) s << ", "; + } + if (params.size() > 0) s << ", "; + s << asyncCallbackType << " callback, Timeout timeout = Timeout());\n"; + } + s << "\nprivate:\n"; + s << " LogosObject* ensureReplica();\n"; + s << " template\n"; + s << " static QVariantList packVariantList(Args&&... args) {\n"; + s << " QVariantList list;\n"; + s << " list.reserve(sizeof...(Args));\n"; + s << " using Expander = int[];\n"; + s << " (void)Expander{0, (list.append(QVariant::fromValue(std::forward(args))), 0)...};\n"; + s << " return list;\n"; + s << " }\n"; + s << " LogosAPI* m_api;\n"; + s << " LogosAPIClient* m_client;\n"; + s << " QString m_moduleName;\n"; + s << " LogosObject* m_eventReplica = nullptr;\n"; + s << " LogosObject* m_eventSource = nullptr;\n"; + s << "};\n"; + return h; +} + +QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods) +{ + QString c; + QTextStream s(&c); + s << "#include \"" << headerBaseName << "\"\n\n"; + s << "#include \n\n"; + s << className << "::" << className << "(LogosAPI* api) : m_api(api), m_client(api->getClient(\"" << moduleName << "\")), m_moduleName(QStringLiteral(\"" << moduleName << "\")) {}\n\n"; + s << "LogosObject* " << className << "::ensureReplica() {\n"; + s << " if (!m_eventReplica) {\n"; + s << " LogosObject* replica = m_client->requestObject(m_moduleName);\n"; + s << " if (!replica) {\n"; + s << " qWarning() << \"" << className << ": failed to acquire remote object for events on\" << m_moduleName;\n"; + s << " return nullptr;\n"; + s << " }\n"; + s << " m_eventReplica = replica;\n"; + s << " }\n"; + s << " return m_eventReplica;\n"; + s << "}\n\n"; + s << "bool " << className << "::on(const QString& eventName, RawEventCallback callback) {\n"; + s << " if (!callback) {\n"; + s << " qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName;\n"; + s << " return false;\n"; + s << " }\n"; + s << " LogosObject* origin = ensureReplica();\n"; + s << " if (!origin) {\n"; + s << " return false;\n"; + s << " }\n"; + s << " m_client->onEvent(origin, eventName, callback);\n"; + s << " return true;\n"; + s << "}\n\n"; + s << "bool " << className << "::on(const QString& eventName, EventCallback callback) {\n"; + s << " if (!callback) {\n"; + s << " qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName;\n"; + s << " return false;\n"; + s << " }\n"; + s << " return on(eventName, [callback](const QString&, const QVariantList& data) {\n"; + s << " callback(data);\n"; + s << " });\n"; + s << "}\n\n"; + s << "void " << className << "::setEventSource(LogosObject* source) {\n"; + s << " m_eventSource = source;\n"; + s << "}\n\n"; + s << "LogosObject* " << className << "::eventSource() const {\n"; + s << " return m_eventSource;\n"; + s << "}\n\n"; + s << "void " << className << "::trigger(const QString& eventName) {\n"; + s << " trigger(eventName, QVariantList{});\n"; + s << "}\n\n"; + s << "void " << className << "::trigger(const QString& eventName, const QVariantList& data) {\n"; + s << " if (!m_eventSource) {\n"; + s << " qWarning() << \"" << className << ": no event source set for trigger\" << eventName;\n"; + s << " return;\n"; + s << " }\n"; + s << " m_client->onEventResponse(m_eventSource, eventName, data);\n"; + s << "}\n\n"; + s << "void " << className << "::trigger(const QString& eventName, LogosObject* source, const QVariantList& data) {\n"; + s << " if (!source) {\n"; + s << " qWarning() << \"" << className << ": cannot trigger\" << eventName << \"with null source\";\n"; + s << " return;\n"; + s << " }\n"; + s << " m_client->onEventResponse(source, eventName, data);\n"; + s << "}\n\n"; + for (const QJsonValue& v : methods) { + const QJsonObject o = v.toObject(); + const bool invokable = o.value("isInvokable").toBool(); + if (!invokable) continue; + const QString name = o.value("name").toString(); + const QString ret = mapReturnType(o.value("returnType").toString()); + QJsonArray params = o.value("parameters").toArray(); + // Signature + s << ret << " " << className << "::" << name << "("; + for (int i = 0; i < params.size(); ++i) { + QJsonObject p = params.at(i).toObject(); + QString pt = mapParamType(p.value("type").toString()); + QString pn = p.value("name").toString(); + if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray") { + s << "const " << pt << "& " << pn; + } else { + s << pt << " " << pn; + } + if (i + 1 < params.size()) s << ", "; + } + s << ") {\n"; + // Body: perform call + if (ret != "void") { + s << " QVariant _result = "; + } else { + s << " "; + } + if (params.size() == 0) { + s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\");\n"; + } else if (params.size() == 1) { + QJsonObject p = params.at(0).toObject(); + QString pn = p.value("name").toString(); + s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << pn << ");\n"; + } else if (params.size() == 2) { + QString p0 = params.at(0).toObject().value("name").toString(); + QString p1 = params.at(1).toObject().value("name").toString(); + s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ");\n"; + } else if (params.size() == 3) { + QString p0 = params.at(0).toObject().value("name").toString(); + QString p1 = params.at(1).toObject().value("name").toString(); + QString p2 = params.at(2).toObject().value("name").toString(); + s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ");\n"; + } else if (params.size() == 4) { + QString p0 = params.at(0).toObject().value("name").toString(); + QString p1 = params.at(1).toObject().value("name").toString(); + QString p2 = params.at(2).toObject().value("name").toString(); + QString p3 = params.at(3).toObject().value("name").toString(); + s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ", " << p3 << ");\n"; + } else if (params.size() == 5) { + QString p0 = params.at(0).toObject().value("name").toString(); + QString p1 = params.at(1).toObject().value("name").toString(); + QString p2 = params.at(2).toObject().value("name").toString(); + QString p3 = params.at(3).toObject().value("name").toString(); + QString p4 = params.at(4).toObject().value("name").toString(); + s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ", " << p3 << ", " << p4 << ");\n"; + } else { + s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", QVariantList{"; + for (int i = 0; i < params.size(); ++i) { + QString pn = params.at(i).toObject().value("name").toString(); + s << pn; + if (i + 1 < params.size()) s << ", "; + } + s << "});\n"; + } + // Return conversion + if (ret == "void") { + // nothing + } else if (ret == "bool") { + s << " return _result.toBool();\n"; + } else if (ret == "int") { + s << " return _result.toInt();\n"; + } else if (ret == "double") { + s << " return _result.toDouble();\n"; + } else if (ret == "float") { + s << " return _result.toFloat();\n"; + } else if (ret == "QString") { + s << " return _result.toString();\n"; + } else if (ret == "QStringList") { + s << " return _result.toStringList();\n"; + } else if (ret == "QJsonArray") { + s << " return qvariant_cast(_result);\n"; + }else if (ret == "LogosResult") { + s << " return _result.value();\n"; + } else { // QVariant + s << " return _result;\n"; + } + s << "}\n\n"; + // Async implementation + s << "void " << className << "::" << name << "Async("; + for (int i = 0; i < params.size(); ++i) { + QJsonObject p = params.at(i).toObject(); + QString pt = mapParamType(p.value("type").toString()); + QString pn = p.value("name").toString(); + if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray") { + s << "const " << pt << "& " << pn; + } else { + s << pt << " " << pn; + } + if (i + 1 < params.size()) s << ", "; + } + if (params.size() > 0) s << ", "; + s << "std::function callback, Timeout timeout) {\n"; + s << " if (!callback) return;\n"; + s << " m_client->invokeRemoteMethodAsync(\"" << moduleName << "\", \"" << name << "\", "; + if (params.size() == 0) { + s << "QVariantList()"; + } else if (params.size() == 1) { + s << "QVariantList() << " << params.at(0).toObject().value("name").toString(); + } else { + s << "QVariantList{"; + for (int i = 0; i < params.size(); ++i) { + s << params.at(i).toObject().value("name").toString(); + if (i + 1 < params.size()) s << ", "; + } + s << "}"; + } + s << ", [callback](QVariant v) {\n"; + if (ret == "void") { + s << " callback();\n"; + } else { + QString defaultVal; + if (ret == "bool") defaultVal = "false"; + else if (ret == "int" || ret == "double" || ret == "float") defaultVal = "0"; + else if (ret == "QString") defaultVal = "QString()"; + else if (ret == "QStringList") defaultVal = "QStringList()"; + else if (ret == "QJsonArray") defaultVal = "QJsonArray()"; + else defaultVal = ret + "{}"; + if (ret == "QVariant") { + s << " callback(v);\n"; + } else { + s << " callback(v.isValid() ? qvariant_cast<" << ret << ">(v) : " << defaultVal << ");\n"; + } + } + s << " }, timeout);\n"; + s << "}\n\n"; + } + return c; +} + +QVector parseProviderHeader(const QString& headerPath, QTextStream& err) +{ + QVector methods; + + QFile file(headerPath); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + err << "Cannot open header file: " << headerPath << "\n"; + return methods; + } + + QTextStream in(&file); + QRegularExpression re( + R"(^\s*LOGOS_METHOD\s+(.+?)\s+(\w+)\s*\(([^)]*)\)\s*;)" + ); + + while (!in.atEnd()) { + QString line = in.readLine(); + auto match = re.match(line); + if (!match.hasMatch()) continue; + + ParsedMethod m; + m.returnType = normalizeType(match.captured(1)); + m.name = match.captured(2); + + QString paramStr = match.captured(3).trimmed(); + if (!paramStr.isEmpty()) { + QStringList paramParts = paramStr.split(','); + for (const QString& part : paramParts) { + QString trimmed = part.trimmed(); + int eqIdx = trimmed.indexOf('='); + if (eqIdx > 0) trimmed = trimmed.left(eqIdx).trimmed(); + int lastSpace = trimmed.lastIndexOf(' '); + int lastAmp = trimmed.lastIndexOf('&'); + int splitAt = qMax(lastSpace, lastAmp); + if (splitAt > 0) { + QString type = normalizeType(trimmed.left(splitAt + 1)); + QString pname = trimmed.mid(splitAt + 1).trimmed(); + m.params.append({type, pname}); + } else { + m.params.append({normalizeType(trimmed), QString("arg%1").arg(m.params.size())}); + } + } + } + + methods.append(m); + } + + file.close(); + return methods; +} diff --git a/cpp-generator/generator_lib.h b/cpp-generator/generator_lib.h new file mode 100644 index 0000000..206e88a --- /dev/null +++ b/cpp-generator/generator_lib.h @@ -0,0 +1,25 @@ +#ifndef GENERATOR_LIB_H +#define GENERATOR_LIB_H + +#include +#include +#include +#include +#include + +struct ParsedMethod { + QString returnType; + QString name; + QVector> params; // (type, name) +}; + +QString toPascalCase(const QString& name); +QString normalizeType(QString t); +QString mapParamType(const QString& qtType); +QString mapReturnType(const QString& qtType); +QString toQVariantConversion(const QString& type, const QString& argExpr); +QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods); +QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods); +QVector parseProviderHeader(const QString& headerPath, QTextStream& err); + +#endif // GENERATOR_LIB_H diff --git a/cpp-generator/main.cpp b/cpp-generator/main.cpp index 7738ec3..dfe7df7 100644 --- a/cpp-generator/main.cpp +++ b/cpp-generator/main.cpp @@ -15,6 +15,7 @@ #include #include #include "logos_provider_object.h" +#include "generator_lib.h" static QJsonArray enumerateMethods(QObject* moduleInstance) { @@ -62,344 +63,11 @@ static QJsonArray enumerateMethods(QObject* moduleInstance) return methodsArray; } -static QString toPascalCase(const QString& name) -{ - QString out; - bool cap = true; - for (QChar c : name) { - if (!c.isLetterOrNumber()) { cap = true; continue; } - if (cap) { out.append(c.toUpper()); cap = false; } - else { out.append(c.toLower()); } - } - if (out.isEmpty()) return QString("Module"); - return out; -} +// toPascalCase, normalizeType, mapParamType, mapReturnType -> generator_lib.h/cpp -static QString normalizeType(QString t) -{ - t = t.trimmed(); - if (t.startsWith("const ")) t = t.mid(6); - t = t.trimmed(); - // Drop reference and pointer qualifiers - if (t.endsWith('&') || t.endsWith('*')) t.chop(1); - t = t.trimmed(); - return t; -} +// makeHeader -> generator_lib.h/cpp -static QString mapParamType(const QString& qtType) -{ - const QString base = normalizeType(qtType); - static const QSet known = { - "void","bool","int","double","float","QString","QStringList","QJsonArray","QVariant" - }; - if (known.contains(base)) return base; - // Fallback to QVariant for unknown types - return QString("QVariant"); -} - -static QString mapReturnType(const QString& qtType) -{ - const QString base = normalizeType(qtType); - if (base.isEmpty() || base == "void") return QString("void"); - static const QSet known = { - "bool","int","double","float","QString","QStringList","QJsonArray","QVariant","LogosResult" - }; - if (known.contains(base)) return base; - return QString("QVariant"); -} - -static QString makeHeader(const QString& moduleName, const QString& className, const QJsonArray& methods) -{ - QString h; - QTextStream s(&h); - s << "#pragma once\n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \n"; - s << "#include \"logos_types.h\"\n"; - s << "#include \"logos_api.h\"\n"; - s << "#include \"logos_api_client.h\"\n"; - s << "#include \"logos_object.h\"\n\n"; - s << "class " << className << " {\n"; - s << "public:\n"; - s << " explicit " << className << "(LogosAPI* api);\n\n"; - s << " using RawEventCallback = std::function;\n"; - s << " using EventCallback = std::function;\n\n"; - s << " bool on(const QString& eventName, RawEventCallback callback);\n"; - s << " bool on(const QString& eventName, EventCallback callback);\n"; - s << " void setEventSource(LogosObject* source);\n"; - s << " LogosObject* eventSource() const;\n"; - s << " void trigger(const QString& eventName);\n"; - s << " void trigger(const QString& eventName, const QVariantList& data);\n"; - s << " template\n"; - s << " void trigger(const QString& eventName, Args&&... args) {\n"; - s << " trigger(eventName, packVariantList(std::forward(args)...));\n"; - s << " }\n"; - s << " void trigger(const QString& eventName, LogosObject* source, const QVariantList& data);\n"; - s << " template\n"; - s << " void trigger(const QString& eventName, LogosObject* source, Args&&... args) {\n"; - s << " trigger(eventName, source, packVariantList(std::forward(args)...));\n"; - s << " }\n\n"; - // Methods - for (const QJsonValue& v : methods) { - const QJsonObject o = v.toObject(); - const bool invokable = o.value("isInvokable").toBool(); - if (!invokable) continue; - const QString name = o.value("name").toString(); - const QString ret = mapReturnType(o.value("returnType").toString()); - s << " " << ret << " " << name << "("; - QJsonArray params = o.value("parameters").toArray(); - for (int i = 0; i < params.size(); ++i) { - QJsonObject p = params.at(i).toObject(); - QString pt = mapParamType(p.value("type").toString()); - QString pn = p.value("name").toString(); - if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray") { - s << "const " << pt << "& " << pn; - } else { - s << pt << " " << pn; - } - if (i + 1 < params.size()) s << ", "; - } - s << ");\n"; - // Async overload: same params + callback + optional Timeout - QString asyncCallbackType = (ret == "void") ? QString("std::function") : QString("std::function"; - s << " void " << name << "Async("; - for (int i = 0; i < params.size(); ++i) { - QJsonObject p = params.at(i).toObject(); - QString pt = mapParamType(p.value("type").toString()); - QString pn = p.value("name").toString(); - if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray") { - s << "const " << pt << "& " << pn; - } else { - s << pt << " " << pn; - } - if (i + 1 < params.size()) s << ", "; - } - if (params.size() > 0) s << ", "; - s << asyncCallbackType << " callback, Timeout timeout = Timeout());\n"; - } - s << "\nprivate:\n"; - s << " LogosObject* ensureReplica();\n"; - s << " template\n"; - s << " static QVariantList packVariantList(Args&&... args) {\n"; - s << " QVariantList list;\n"; - s << " list.reserve(sizeof...(Args));\n"; - s << " using Expander = int[];\n"; - s << " (void)Expander{0, (list.append(QVariant::fromValue(std::forward(args))), 0)...};\n"; - s << " return list;\n"; - s << " }\n"; - s << " LogosAPI* m_api;\n"; - s << " LogosAPIClient* m_client;\n"; - s << " QString m_moduleName;\n"; - s << " LogosObject* m_eventReplica = nullptr;\n"; - s << " LogosObject* m_eventSource = nullptr;\n"; - s << "};\n"; - return h; -} - -static QString makeSource(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods) -{ - QString c; - QTextStream s(&c); - s << "#include \"" << headerBaseName << "\"\n\n"; - s << "#include \n\n"; - s << className << "::" << className << "(LogosAPI* api) : m_api(api), m_client(api->getClient(\"" << moduleName << "\")), m_moduleName(QStringLiteral(\"" << moduleName << "\")) {}\n\n"; - s << "LogosObject* " << className << "::ensureReplica() {\n"; - s << " if (!m_eventReplica) {\n"; - s << " LogosObject* replica = m_client->requestObject(m_moduleName);\n"; - s << " if (!replica) {\n"; - s << " qWarning() << \"" << className << ": failed to acquire remote object for events on\" << m_moduleName;\n"; - s << " return nullptr;\n"; - s << " }\n"; - s << " m_eventReplica = replica;\n"; - s << " }\n"; - s << " return m_eventReplica;\n"; - s << "}\n\n"; - s << "bool " << className << "::on(const QString& eventName, RawEventCallback callback) {\n"; - s << " if (!callback) {\n"; - s << " qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName;\n"; - s << " return false;\n"; - s << " }\n"; - s << " LogosObject* origin = ensureReplica();\n"; - s << " if (!origin) {\n"; - s << " return false;\n"; - s << " }\n"; - s << " m_client->onEvent(origin, eventName, callback);\n"; - s << " return true;\n"; - s << "}\n\n"; - s << "bool " << className << "::on(const QString& eventName, EventCallback callback) {\n"; - s << " if (!callback) {\n"; - s << " qWarning() << \"" << className << ": ignoring empty event callback for\" << eventName;\n"; - s << " return false;\n"; - s << " }\n"; - s << " return on(eventName, [callback](const QString&, const QVariantList& data) {\n"; - s << " callback(data);\n"; - s << " });\n"; - s << "}\n\n"; - s << "void " << className << "::setEventSource(LogosObject* source) {\n"; - s << " m_eventSource = source;\n"; - s << "}\n\n"; - s << "LogosObject* " << className << "::eventSource() const {\n"; - s << " return m_eventSource;\n"; - s << "}\n\n"; - s << "void " << className << "::trigger(const QString& eventName) {\n"; - s << " trigger(eventName, QVariantList{});\n"; - s << "}\n\n"; - s << "void " << className << "::trigger(const QString& eventName, const QVariantList& data) {\n"; - s << " if (!m_eventSource) {\n"; - s << " qWarning() << \"" << className << ": no event source set for trigger\" << eventName;\n"; - s << " return;\n"; - s << " }\n"; - s << " m_client->onEventResponse(m_eventSource, eventName, data);\n"; - s << "}\n\n"; - s << "void " << className << "::trigger(const QString& eventName, LogosObject* source, const QVariantList& data) {\n"; - s << " if (!source) {\n"; - s << " qWarning() << \"" << className << ": cannot trigger\" << eventName << \"with null source\";\n"; - s << " return;\n"; - s << " }\n"; - s << " m_client->onEventResponse(source, eventName, data);\n"; - s << "}\n\n"; - for (const QJsonValue& v : methods) { - const QJsonObject o = v.toObject(); - const bool invokable = o.value("isInvokable").toBool(); - if (!invokable) continue; - const QString name = o.value("name").toString(); - const QString ret = mapReturnType(o.value("returnType").toString()); - QJsonArray params = o.value("parameters").toArray(); - // Signature - s << ret << " " << className << "::" << name << "("; - for (int i = 0; i < params.size(); ++i) { - QJsonObject p = params.at(i).toObject(); - QString pt = mapParamType(p.value("type").toString()); - QString pn = p.value("name").toString(); - if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray") { - s << "const " << pt << "& " << pn; - } else { - s << pt << " " << pn; - } - if (i + 1 < params.size()) s << ", "; - } - s << ") {\n"; - // Body: perform call - if (ret != "void") { - s << " QVariant _result = "; - } else { - s << " "; - } - if (params.size() == 0) { - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\");\n"; - } else if (params.size() == 1) { - QJsonObject p = params.at(0).toObject(); - QString pn = p.value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << pn << ");\n"; - } else if (params.size() == 2) { - QString p0 = params.at(0).toObject().value("name").toString(); - QString p1 = params.at(1).toObject().value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ");\n"; - } else if (params.size() == 3) { - QString p0 = params.at(0).toObject().value("name").toString(); - QString p1 = params.at(1).toObject().value("name").toString(); - QString p2 = params.at(2).toObject().value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ");\n"; - } else if (params.size() == 4) { - QString p0 = params.at(0).toObject().value("name").toString(); - QString p1 = params.at(1).toObject().value("name").toString(); - QString p2 = params.at(2).toObject().value("name").toString(); - QString p3 = params.at(3).toObject().value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ", " << p3 << ");\n"; - } else if (params.size() == 5) { - QString p0 = params.at(0).toObject().value("name").toString(); - QString p1 = params.at(1).toObject().value("name").toString(); - QString p2 = params.at(2).toObject().value("name").toString(); - QString p3 = params.at(3).toObject().value("name").toString(); - QString p4 = params.at(4).toObject().value("name").toString(); - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", " << p0 << ", " << p1 << ", " << p2 << ", " << p3 << ", " << p4 << ");\n"; - } else { - s << "m_client->invokeRemoteMethod(\"" << moduleName << "\", \"" << name << "\", QVariantList{"; - for (int i = 0; i < params.size(); ++i) { - QString pn = params.at(i).toObject().value("name").toString(); - s << pn; - if (i + 1 < params.size()) s << ", "; - } - s << "});\n"; - } - // Return conversion - if (ret == "void") { - // nothing - } else if (ret == "bool") { - s << " return _result.toBool();\n"; - } else if (ret == "int") { - s << " return _result.toInt();\n"; - } else if (ret == "double") { - s << " return _result.toDouble();\n"; - } else if (ret == "float") { - s << " return _result.toFloat();\n"; - } else if (ret == "QString") { - s << " return _result.toString();\n"; - } else if (ret == "QStringList") { - s << " return _result.toStringList();\n"; - } else if (ret == "QJsonArray") { - s << " return qvariant_cast(_result);\n"; - }else if (ret == "LogosResult") { - s << " return _result.value();\n"; - } else { // QVariant - s << " return _result;\n"; - } - s << "}\n\n"; - // Async implementation - s << "void " << className << "::" << name << "Async("; - for (int i = 0; i < params.size(); ++i) { - QJsonObject p = params.at(i).toObject(); - QString pt = mapParamType(p.value("type").toString()); - QString pn = p.value("name").toString(); - if (pt == "QString" || pt == "QStringList" || pt == "QJsonArray") { - s << "const " << pt << "& " << pn; - } else { - s << pt << " " << pn; - } - if (i + 1 < params.size()) s << ", "; - } - if (params.size() > 0) s << ", "; - s << "std::function callback, Timeout timeout) {\n"; - s << " if (!callback) return;\n"; - s << " m_client->invokeRemoteMethodAsync(\"" << moduleName << "\", \"" << name << "\", "; - if (params.size() == 0) { - s << "QVariantList()"; - } else if (params.size() == 1) { - s << "QVariantList() << " << params.at(0).toObject().value("name").toString(); - } else { - s << "QVariantList{"; - for (int i = 0; i < params.size(); ++i) { - s << params.at(i).toObject().value("name").toString(); - if (i + 1 < params.size()) s << ", "; - } - s << "}"; - } - s << ", [callback](QVariant v) {\n"; - if (ret == "void") { - s << " callback();\n"; - } else { - QString defaultVal; - if (ret == "bool") defaultVal = "false"; - else if (ret == "int" || ret == "double" || ret == "float") defaultVal = "0"; - else if (ret == "QString") defaultVal = "QString()"; - else if (ret == "QStringList") defaultVal = "QStringList()"; - else if (ret == "QJsonArray") defaultVal = "QJsonArray()"; - else defaultVal = ret + "{}"; - if (ret == "QVariant") { - s << " callback(v);\n"; - } else { - s << " callback(v.isValid() ? qvariant_cast<" << ret << ">(v) : " << defaultVal << ");\n"; - } - } - s << " }, timeout);\n"; - s << "}\n\n"; - } - return c; -} +// makeSource -> generator_lib.h/cpp static QString makeCoreManagerHeader() { @@ -744,77 +412,7 @@ static bool writeUmbrellaSourceFromDeps(const QString& genDirPath, const QJsonAr } // ── Provider-header mode: scan LOGOS_METHOD markers and generate dispatch ──── - -struct ParsedMethod { - QString returnType; - QString name; - QVector> params; // (type, name) -}; - -static QVector parseProviderHeader(const QString& headerPath, QTextStream& err) -{ - QVector methods; - - QFile file(headerPath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - err << "Cannot open header file: " << headerPath << "\n"; - return methods; - } - - QTextStream in(&file); - QRegularExpression re( - R"(^\s*LOGOS_METHOD\s+(.+?)\s+(\w+)\s*\(([^)]*)\)\s*;)" - ); - - while (!in.atEnd()) { - QString line = in.readLine(); - auto match = re.match(line); - if (!match.hasMatch()) continue; - - ParsedMethod m; - m.returnType = normalizeType(match.captured(1)); - m.name = match.captured(2); - - QString paramStr = match.captured(3).trimmed(); - if (!paramStr.isEmpty()) { - QStringList paramParts = paramStr.split(','); - for (const QString& part : paramParts) { - QString trimmed = part.trimmed(); - int eqIdx = trimmed.indexOf('='); - if (eqIdx > 0) trimmed = trimmed.left(eqIdx).trimmed(); - int lastSpace = trimmed.lastIndexOf(' '); - int lastAmp = trimmed.lastIndexOf('&'); - int splitAt = qMax(lastSpace, lastAmp); - if (splitAt > 0) { - QString type = normalizeType(trimmed.left(splitAt + 1)); - QString pname = trimmed.mid(splitAt + 1).trimmed(); - m.params.append({type, pname}); - } else { - m.params.append({normalizeType(trimmed), QString("arg%1").arg(m.params.size())}); - } - } - } - - methods.append(m); - } - - file.close(); - return methods; -} - -static QString toQVariantConversion(const QString& type, const QString& argExpr) -{ - if (type == "int") return argExpr + ".toInt()"; - if (type == "bool") return argExpr + ".toBool()"; - if (type == "double") return argExpr + ".toDouble()"; - if (type == "float") return argExpr + ".toFloat()"; - if (type == "QString") return argExpr + ".toString()"; - if (type == "QStringList") return argExpr + ".toStringList()"; - if (type == "QJsonArray") return "qvariant_cast(" + argExpr + ")"; - if (type == "QVariant") return argExpr; - if (type == "LogosResult") return argExpr + ".value()"; - return argExpr + ".toString()"; -} +// ParsedMethod, parseProviderHeader, toQVariantConversion -> generator_lib.h/cpp static int generateProviderDispatch(const QString& headerPath, const QString& outputDir, QTextStream& out, QTextStream& err) { diff --git a/flake.nix b/flake.nix index 1e0d219..e5ed093 100644 --- a/flake.nix +++ b/flake.nix @@ -22,6 +22,7 @@ bin = import ./nix/bin.nix { inherit pkgs common src; }; lib = import ./nix/lib.nix { inherit pkgs common src; }; include = import ./nix/include.nix { inherit pkgs common src; }; + tests = import ./nix/tests.nix { inherit pkgs common src; }; # Combined SDK package sdk = pkgs.symlinkJoin { @@ -34,6 +35,7 @@ logos-cpp-bin = bin; logos-cpp-lib = lib; logos-cpp-include = include; + inherit tests; # Combined outputs (for backward compatibility) logos-cpp-sdk = sdk; @@ -54,6 +56,7 @@ buildInputs = [ pkgs.qt6.qtbase pkgs.qt6.qtremoteobjects + pkgs.gtest ]; }; }); diff --git a/nix/tests.nix b/nix/tests.nix new file mode 100644 index 0000000..90102a0 --- /dev/null +++ b/nix/tests.nix @@ -0,0 +1,48 @@ +# Builds and runs the test suite +{ pkgs, common, src }: + +pkgs.stdenv.mkDerivation { + pname = "${common.pname}-tests"; + version = common.version; + + inherit src; + + nativeBuildInputs = common.nativeBuildInputs; + buildInputs = common.buildInputs ++ [ pkgs.gtest ]; + cmakeFlags = common.cmakeFlags; + + dontUseCmakeConfigure = true; + + buildPhase = '' + runHook preBuild + + mkdir -p build-tests + cd build-tests + cmake ../tests -GNinja $cmakeFlags + ninja + cd .. + + runHook postBuild + ''; + + doCheck = true; + checkPhase = '' + runHook preCheck + cd build-tests + ctest --output-on-failure + cd .. + runHook postCheck + ''; + + installPhase = '' + runHook preInstall + + mkdir -p $out/bin + cp build-tests/sdk/sdk_tests $out/bin/ + cp build-tests/generator/generator_tests $out/bin/ + + runHook postInstall + ''; + + inherit (common) meta; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..480889d --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.14) +project(LogosCppSdkTests) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_AUTOMOC ON) + +find_package(GTest REQUIRED) +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core RemoteObjects) + +enable_testing() + +add_subdirectory(sdk) +add_subdirectory(generator) diff --git a/tests/generator/CMakeLists.txt b/tests/generator/CMakeLists.txt new file mode 100644 index 0000000..660876a --- /dev/null +++ b/tests/generator/CMakeLists.txt @@ -0,0 +1,23 @@ +add_executable(generator_tests + ${CMAKE_CURRENT_SOURCE_DIR}/../../cpp-generator/generator_lib.cpp + test_to_pascal_case.cpp + test_normalize_type.cpp + test_map_param_type.cpp + test_map_return_type.cpp + test_to_qvariant_conversion.cpp + test_make_header.cpp + test_make_source.cpp + test_parse_provider_header.cpp +) + +target_include_directories(generator_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../cpp-generator + ${CMAKE_CURRENT_SOURCE_DIR}/../../cpp +) + +target_link_libraries(generator_tests PRIVATE + GTest::gtest_main + Qt${QT_VERSION_MAJOR}::Core +) + +gtest_discover_tests(generator_tests) diff --git a/tests/generator/test_make_header.cpp b/tests/generator/test_make_header.cpp new file mode 100644 index 0000000..abedebf --- /dev/null +++ b/tests/generator/test_make_header.cpp @@ -0,0 +1,141 @@ +#include +#include +#include +#include "generator_lib.h" + +static QJsonArray makeTestMethods() +{ + QJsonArray methods; + + // A simple invokable method: int add(int a, int b) + { + QJsonObject m; + m["name"] = "add"; + m["returnType"] = "int"; + m["isInvokable"] = true; + QJsonArray params; + { + QJsonObject p; + p["type"] = "int"; + p["name"] = "a"; + params.append(p); + } + { + QJsonObject p; + p["type"] = "int"; + p["name"] = "b"; + params.append(p); + } + m["parameters"] = params; + methods.append(m); + } + + // A void method with no params + { + QJsonObject m; + m["name"] = "reset"; + m["returnType"] = "void"; + m["isInvokable"] = true; + m["parameters"] = QJsonArray(); + methods.append(m); + } + + // A non-invokable method (should be skipped) + { + QJsonObject m; + m["name"] = "internal"; + m["returnType"] = "void"; + m["isInvokable"] = false; + m["parameters"] = QJsonArray(); + methods.append(m); + } + + return methods; +} + +TEST(MakeHeaderTest, ContainsPragmaOnce) +{ + QString h = makeHeader("test_mod", "TestMod", QJsonArray()); + EXPECT_TRUE(h.contains("#pragma once")); +} + +TEST(MakeHeaderTest, ContainsClassName) +{ + QString h = makeHeader("test_mod", "TestMod", QJsonArray()); + EXPECT_TRUE(h.contains("class TestMod")); +} + +TEST(MakeHeaderTest, ContainsConstructor) +{ + QString h = makeHeader("test_mod", "TestMod", QJsonArray()); + EXPECT_TRUE(h.contains("explicit TestMod(LogosAPI* api)")); +} + +TEST(MakeHeaderTest, ContainsIncludes) +{ + QString h = makeHeader("test_mod", "TestMod", QJsonArray()); + EXPECT_TRUE(h.contains("#include \"logos_types.h\"")); + EXPECT_TRUE(h.contains("#include \"logos_api.h\"")); + EXPECT_TRUE(h.contains("#include \"logos_api_client.h\"")); + EXPECT_TRUE(h.contains("#include \"logos_object.h\"")); +} + +TEST(MakeHeaderTest, ContainsEventCallbackTypedefs) +{ + QString h = makeHeader("test_mod", "TestMod", QJsonArray()); + EXPECT_TRUE(h.contains("RawEventCallback")); + EXPECT_TRUE(h.contains("EventCallback")); +} + +TEST(MakeHeaderTest, ContainsMethodDeclarations) +{ + QJsonArray methods = makeTestMethods(); + QString h = makeHeader("test_mod", "TestMod", methods); + + EXPECT_TRUE(h.contains("int add(int a, int b)")); + EXPECT_TRUE(h.contains("void reset()")); + // Non-invokable should not appear + EXPECT_FALSE(h.contains("internal")); +} + +TEST(MakeHeaderTest, ContainsAsyncOverloads) +{ + QJsonArray methods = makeTestMethods(); + QString h = makeHeader("test_mod", "TestMod", methods); + + EXPECT_TRUE(h.contains("addAsync(")); + EXPECT_TRUE(h.contains("resetAsync(")); + // Async callback types + EXPECT_TRUE(h.contains("std::function callback")); + EXPECT_TRUE(h.contains("std::function callback")); +} + +TEST(MakeHeaderTest, ContainsPrivateMembers) +{ + QString h = makeHeader("test_mod", "TestMod", QJsonArray()); + EXPECT_TRUE(h.contains("m_api")); + EXPECT_TRUE(h.contains("m_client")); + EXPECT_TRUE(h.contains("m_moduleName")); + EXPECT_TRUE(h.contains("ensureReplica")); +} + +TEST(MakeHeaderTest, ConstRefForStringParams) +{ + QJsonArray methods; + { + QJsonObject m; + m["name"] = "greet"; + m["returnType"] = "QString"; + m["isInvokable"] = true; + QJsonArray params; + QJsonObject p; + p["type"] = "QString"; + p["name"] = "name"; + params.append(p); + m["parameters"] = params; + methods.append(m); + } + + QString h = makeHeader("mod", "Mod", methods); + EXPECT_TRUE(h.contains("const QString& name")); +} diff --git a/tests/generator/test_make_source.cpp b/tests/generator/test_make_source.cpp new file mode 100644 index 0000000..82d940b --- /dev/null +++ b/tests/generator/test_make_source.cpp @@ -0,0 +1,158 @@ +#include +#include +#include +#include "generator_lib.h" + +static QJsonObject makeMethod(const QString& name, const QString& retType, int paramCount) +{ + QJsonObject m; + m["name"] = name; + m["returnType"] = retType; + m["isInvokable"] = true; + QJsonArray params; + for (int i = 0; i < paramCount; ++i) { + QJsonObject p; + p["type"] = "int"; + p["name"] = QString("p%1").arg(i); + params.append(p); + } + m["parameters"] = params; + return m; +} + +TEST(MakeSourceTest, ContainsInclude) +{ + QString src = makeSource("mod", "Mod", "mod_api.h", QJsonArray()); + EXPECT_TRUE(src.contains("#include \"mod_api.h\"")); +} + +TEST(MakeSourceTest, ConstructorInitializesClient) +{ + QString src = makeSource("my_mod", "MyMod", "my_mod_api.h", QJsonArray()); + EXPECT_TRUE(src.contains("MyMod::MyMod(LogosAPI* api)")); + EXPECT_TRUE(src.contains("api->getClient(\"my_mod\")")); +} + +TEST(MakeSourceTest, EnsureReplicaMethod) +{ + QString src = makeSource("mod", "Mod", "mod.h", QJsonArray()); + EXPECT_TRUE(src.contains("LogosObject* Mod::ensureReplica()")); +} + +TEST(MakeSourceTest, ZeroParams) +{ + QJsonArray methods; + methods.append(makeMethod("doStuff", "int", 0)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"doStuff\")")); + EXPECT_TRUE(src.contains("return _result.toInt()")); +} + +TEST(MakeSourceTest, OneParam) +{ + QJsonArray methods; + methods.append(makeMethod("fn", "bool", 1)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"fn\", p0)")); + EXPECT_TRUE(src.contains("return _result.toBool()")); +} + +TEST(MakeSourceTest, TwoParams) +{ + QJsonArray methods; + methods.append(makeMethod("fn", "void", 2)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("m_client->invokeRemoteMethod(\"mod\", \"fn\", p0, p1)")); +} + +TEST(MakeSourceTest, ThreeParams) +{ + QJsonArray methods; + methods.append(makeMethod("fn", "QString", 3)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("p0, p1, p2")); + EXPECT_TRUE(src.contains("return _result.toString()")); +} + +TEST(MakeSourceTest, FourParams) +{ + QJsonArray methods; + methods.append(makeMethod("fn", "double", 4)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("p0, p1, p2, p3")); + EXPECT_TRUE(src.contains("return _result.toDouble()")); +} + +TEST(MakeSourceTest, FiveParams) +{ + QJsonArray methods; + methods.append(makeMethod("fn", "float", 5)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("p0, p1, p2, p3, p4")); + EXPECT_TRUE(src.contains("return _result.toFloat()")); +} + +TEST(MakeSourceTest, MoreThanFiveParamsUsesVariantList) +{ + QJsonArray methods; + methods.append(makeMethod("fn", "QVariant", 6)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("QVariantList{p0, p1, p2, p3, p4, p5}")); + EXPECT_TRUE(src.contains("return _result")); +} + +TEST(MakeSourceTest, VoidReturnNoConversion) +{ + QJsonArray methods; + methods.append(makeMethod("doIt", "void", 0)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_FALSE(src.contains("return _result")); +} + +TEST(MakeSourceTest, QStringListReturn) +{ + QJsonArray methods; + methods.append(makeMethod("getNames", "QStringList", 0)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("return _result.toStringList()")); +} + +TEST(MakeSourceTest, QJsonArrayReturn) +{ + QJsonArray methods; + methods.append(makeMethod("getData", "QJsonArray", 0)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("qvariant_cast(_result)")); +} + +TEST(MakeSourceTest, LogosResultReturn) +{ + QJsonArray methods; + methods.append(makeMethod("query", "LogosResult", 0)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("_result.value()")); +} + +TEST(MakeSourceTest, AsyncImplementation) +{ + QJsonArray methods; + methods.append(makeMethod("fn", "int", 1)); + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_TRUE(src.contains("Mod::fnAsync(")); + EXPECT_TRUE(src.contains("invokeRemoteMethodAsync")); + EXPECT_TRUE(src.contains("callback")); +} + +TEST(MakeSourceTest, NonInvokableSkipped) +{ + QJsonArray methods; + QJsonObject m; + m["name"] = "hidden"; + m["returnType"] = "void"; + m["isInvokable"] = false; + m["parameters"] = QJsonArray(); + methods.append(m); + + QString src = makeSource("mod", "Mod", "mod.h", methods); + EXPECT_FALSE(src.contains("hidden")); +} diff --git a/tests/generator/test_map_param_type.cpp b/tests/generator/test_map_param_type.cpp new file mode 100644 index 0000000..7102f80 --- /dev/null +++ b/tests/generator/test_map_param_type.cpp @@ -0,0 +1,33 @@ +#include +#include "generator_lib.h" + +TEST(MapParamTypeTest, KnownTypes) +{ + EXPECT_EQ(mapParamType("bool"), "bool"); + EXPECT_EQ(mapParamType("int"), "int"); + EXPECT_EQ(mapParamType("double"), "double"); + EXPECT_EQ(mapParamType("float"), "float"); + EXPECT_EQ(mapParamType("QString"), "QString"); + EXPECT_EQ(mapParamType("QStringList"), "QStringList"); + EXPECT_EQ(mapParamType("QJsonArray"), "QJsonArray"); + EXPECT_EQ(mapParamType("QVariant"), "QVariant"); + EXPECT_EQ(mapParamType("void"), "void"); +} + +TEST(MapParamTypeTest, ConstRef) +{ + EXPECT_EQ(mapParamType("const QString&"), "QString"); + EXPECT_EQ(mapParamType("const int&"), "int"); +} + +TEST(MapParamTypeTest, UnknownType) +{ + EXPECT_EQ(mapParamType("MyCustomType"), "QVariant"); + EXPECT_EQ(mapParamType("SomeClass*"), "QVariant"); +} + +TEST(MapParamTypeTest, LogosResultFallsBack) +{ + // LogosResult is NOT in the param known set + EXPECT_EQ(mapParamType("LogosResult"), "QVariant"); +} diff --git a/tests/generator/test_map_return_type.cpp b/tests/generator/test_map_return_type.cpp new file mode 100644 index 0000000..1764b07 --- /dev/null +++ b/tests/generator/test_map_return_type.cpp @@ -0,0 +1,31 @@ +#include +#include "generator_lib.h" + +TEST(MapReturnTypeTest, KnownTypes) +{ + EXPECT_EQ(mapReturnType("bool"), "bool"); + EXPECT_EQ(mapReturnType("int"), "int"); + EXPECT_EQ(mapReturnType("double"), "double"); + EXPECT_EQ(mapReturnType("float"), "float"); + EXPECT_EQ(mapReturnType("QString"), "QString"); + EXPECT_EQ(mapReturnType("QStringList"), "QStringList"); + EXPECT_EQ(mapReturnType("QJsonArray"), "QJsonArray"); + EXPECT_EQ(mapReturnType("QVariant"), "QVariant"); + EXPECT_EQ(mapReturnType("LogosResult"), "LogosResult"); +} + +TEST(MapReturnTypeTest, VoidHandling) +{ + EXPECT_EQ(mapReturnType("void"), "void"); + EXPECT_EQ(mapReturnType(""), "void"); +} + +TEST(MapReturnTypeTest, ConstRef) +{ + EXPECT_EQ(mapReturnType("const QString&"), "QString"); +} + +TEST(MapReturnTypeTest, UnknownType) +{ + EXPECT_EQ(mapReturnType("MyCustomType"), "QVariant"); +} diff --git a/tests/generator/test_normalize_type.cpp b/tests/generator/test_normalize_type.cpp new file mode 100644 index 0000000..73eef2f --- /dev/null +++ b/tests/generator/test_normalize_type.cpp @@ -0,0 +1,42 @@ +#include +#include "generator_lib.h" + +TEST(NormalizeTypeTest, PlainType) +{ + EXPECT_EQ(normalizeType("int"), "int"); +} + +TEST(NormalizeTypeTest, ConstRef) +{ + EXPECT_EQ(normalizeType("const QString&"), "QString"); +} + +TEST(NormalizeTypeTest, ConstValue) +{ + EXPECT_EQ(normalizeType("const int"), "int"); +} + +TEST(NormalizeTypeTest, Pointer) +{ + EXPECT_EQ(normalizeType("QObject*"), "QObject"); +} + +TEST(NormalizeTypeTest, Reference) +{ + EXPECT_EQ(normalizeType("QString&"), "QString"); +} + +TEST(NormalizeTypeTest, WithWhitespace) +{ + EXPECT_EQ(normalizeType(" const QString & "), "QString"); +} + +TEST(NormalizeTypeTest, EmptyString) +{ + EXPECT_EQ(normalizeType(""), ""); +} + +TEST(NormalizeTypeTest, NoQualifiers) +{ + EXPECT_EQ(normalizeType("QVariant"), "QVariant"); +} diff --git a/tests/generator/test_parse_provider_header.cpp b/tests/generator/test_parse_provider_header.cpp new file mode 100644 index 0000000..5ba5289 --- /dev/null +++ b/tests/generator/test_parse_provider_header.cpp @@ -0,0 +1,143 @@ +#include +#include +#include +#include "generator_lib.h" + +static QString writeTempHeader(const QString& content) +{ + QTemporaryFile* tmp = new QTemporaryFile(); + tmp->setAutoRemove(true); + tmp->open(); + QTextStream out(tmp); + out << content; + out.flush(); + QString path = tmp->fileName(); + // Keep file open so it stays on disk; caller reads it then it auto-removes + // Actually we need to close so parseProviderHeader can open it + tmp->close(); + // Keep the object alive by leaking — tests are short-lived + // Actually QTemporaryFile removes on close by default, so disable auto-remove + // and manage manually. Let's use a simpler approach: + return path; +} + +class ParseProviderHeaderTest : public ::testing::Test { +protected: + void writeFile(const QString& content) + { + m_file.setAutoRemove(true); + m_file.open(); + QTextStream out(&m_file); + out << content; + out.flush(); + // Don't close — keep the file on disk + } + + QVector parse() + { + QString path = m_file.fileName(); + QString errStr; + QTextStream err(&errStr); + return parseProviderHeader(path, err); + } + + QString parseError() + { + QString path = m_file.fileName(); + QString errStr; + QTextStream err(&errStr); + parseProviderHeader(path, err); + return errStr; + } + + QTemporaryFile m_file; +}; + +TEST_F(ParseProviderHeaderTest, SimpleMethod) +{ + writeFile(" LOGOS_METHOD void doStuff();\n"); + auto methods = parse(); + ASSERT_EQ(methods.size(), 1); + EXPECT_EQ(methods[0].returnType, "void"); + EXPECT_EQ(methods[0].name, "doStuff"); + EXPECT_TRUE(methods[0].params.isEmpty()); +} + +TEST_F(ParseProviderHeaderTest, MethodWithParams) +{ + writeFile(" LOGOS_METHOD int add(int a, int b);\n"); + auto methods = parse(); + ASSERT_EQ(methods.size(), 1); + EXPECT_EQ(methods[0].returnType, "int"); + EXPECT_EQ(methods[0].name, "add"); + ASSERT_EQ(methods[0].params.size(), 2); + EXPECT_EQ(methods[0].params[0].first, "int"); + EXPECT_EQ(methods[0].params[0].second, "a"); + EXPECT_EQ(methods[0].params[1].first, "int"); + EXPECT_EQ(methods[0].params[1].second, "b"); +} + +TEST_F(ParseProviderHeaderTest, ConstRefParam) +{ + writeFile(" LOGOS_METHOD QString greet(const QString& name);\n"); + auto methods = parse(); + ASSERT_EQ(methods.size(), 1); + ASSERT_EQ(methods[0].params.size(), 1); + EXPECT_EQ(methods[0].params[0].first, "QString"); + EXPECT_EQ(methods[0].params[0].second, "name"); +} + +TEST_F(ParseProviderHeaderTest, DefaultArgStripped) +{ + writeFile(" LOGOS_METHOD void setVal(int x = 10);\n"); + auto methods = parse(); + ASSERT_EQ(methods.size(), 1); + ASSERT_EQ(methods[0].params.size(), 1); + EXPECT_EQ(methods[0].params[0].first, "int"); + EXPECT_EQ(methods[0].params[0].second, "x"); +} + +TEST_F(ParseProviderHeaderTest, MultipleMethodsParsed) +{ + writeFile( + "class Foo : public LogosProviderBase {\n" + " LOGOS_METHOD void a();\n" + " LOGOS_METHOD int b(int x);\n" + " LOGOS_METHOD QString c(const QString& s, bool flag);\n" + "};\n" + ); + auto methods = parse(); + EXPECT_EQ(methods.size(), 3); +} + +TEST_F(ParseProviderHeaderTest, NonLogosMethods_Ignored) +{ + writeFile( + "void normalFunction();\n" + " LOGOS_METHOD void tracked();\n" + "int other(int x);\n" + ); + auto methods = parse(); + EXPECT_EQ(methods.size(), 1); + EXPECT_EQ(methods[0].name, "tracked"); +} + +TEST_F(ParseProviderHeaderTest, MissingFileReturnsEmpty) +{ + QString errStr; + QTextStream err(&errStr); + auto methods = parseProviderHeader("/nonexistent/path.h", err); + EXPECT_TRUE(methods.isEmpty()); + EXPECT_TRUE(errStr.contains("Cannot open")); +} + +TEST_F(ParseProviderHeaderTest, FallbackParamNames) +{ + writeFile(" LOGOS_METHOD void fn(int, QString);\n"); + auto methods = parse(); + ASSERT_EQ(methods.size(), 1); + // Single-token params get fallback names + ASSERT_EQ(methods[0].params.size(), 2); + EXPECT_EQ(methods[0].params[0].second, "arg0"); + EXPECT_EQ(methods[0].params[1].second, "arg1"); +} diff --git a/tests/generator/test_to_pascal_case.cpp b/tests/generator/test_to_pascal_case.cpp new file mode 100644 index 0000000..945708b --- /dev/null +++ b/tests/generator/test_to_pascal_case.cpp @@ -0,0 +1,52 @@ +#include +#include "generator_lib.h" + +TEST(ToPascalCaseTest, SnakeCase) +{ + EXPECT_EQ(toPascalCase("my_module"), "MyModule"); +} + +TEST(ToPascalCaseTest, KebabCase) +{ + EXPECT_EQ(toPascalCase("my-module"), "MyModule"); +} + +TEST(ToPascalCaseTest, DotSeparated) +{ + EXPECT_EQ(toPascalCase("my.module"), "MyModule"); +} + +TEST(ToPascalCaseTest, SingleWord) +{ + EXPECT_EQ(toPascalCase("hello"), "Hello"); +} + +TEST(ToPascalCaseTest, EmptyInput) +{ + EXPECT_EQ(toPascalCase(""), "Module"); +} + +TEST(ToPascalCaseTest, AllCaps) +{ + EXPECT_EQ(toPascalCase("MY_MODULE"), "MyModule"); +} + +TEST(ToPascalCaseTest, AlreadyPascal) +{ + EXPECT_EQ(toPascalCase("MyModule"), "Mymodule"); +} + +TEST(ToPascalCaseTest, MultipleDelimiters) +{ + EXPECT_EQ(toPascalCase("a_b-c.d"), "ABCD"); +} + +TEST(ToPascalCaseTest, LeadingDelimiters) +{ + EXPECT_EQ(toPascalCase("__test"), "Test"); +} + +TEST(ToPascalCaseTest, OnlyDelimiters) +{ + EXPECT_EQ(toPascalCase("___"), "Module"); +} diff --git a/tests/generator/test_to_qvariant_conversion.cpp b/tests/generator/test_to_qvariant_conversion.cpp new file mode 100644 index 0000000..fa25b48 --- /dev/null +++ b/tests/generator/test_to_qvariant_conversion.cpp @@ -0,0 +1,52 @@ +#include +#include "generator_lib.h" + +TEST(ToQVariantConversionTest, Int) +{ + EXPECT_EQ(toQVariantConversion("int", "v"), "v.toInt()"); +} + +TEST(ToQVariantConversionTest, Bool) +{ + EXPECT_EQ(toQVariantConversion("bool", "v"), "v.toBool()"); +} + +TEST(ToQVariantConversionTest, Double) +{ + EXPECT_EQ(toQVariantConversion("double", "v"), "v.toDouble()"); +} + +TEST(ToQVariantConversionTest, Float) +{ + EXPECT_EQ(toQVariantConversion("float", "v"), "v.toFloat()"); +} + +TEST(ToQVariantConversionTest, QString) +{ + EXPECT_EQ(toQVariantConversion("QString", "v"), "v.toString()"); +} + +TEST(ToQVariantConversionTest, QStringList) +{ + EXPECT_EQ(toQVariantConversion("QStringList", "v"), "v.toStringList()"); +} + +TEST(ToQVariantConversionTest, QJsonArray) +{ + EXPECT_EQ(toQVariantConversion("QJsonArray", "arg"), "qvariant_cast(arg)"); +} + +TEST(ToQVariantConversionTest, QVariantPassthrough) +{ + EXPECT_EQ(toQVariantConversion("QVariant", "v"), "v"); +} + +TEST(ToQVariantConversionTest, LogosResult) +{ + EXPECT_EQ(toQVariantConversion("LogosResult", "v"), "v.value()"); +} + +TEST(ToQVariantConversionTest, UnknownFallsBackToString) +{ + EXPECT_EQ(toQVariantConversion("SomeCustomType", "x"), "x.toString()"); +} diff --git a/tests/sdk/CMakeLists.txt b/tests/sdk/CMakeLists.txt new file mode 100644 index 0000000..ac818d8 --- /dev/null +++ b/tests/sdk/CMakeLists.txt @@ -0,0 +1,23 @@ +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../cpp ${CMAKE_CURRENT_BINARY_DIR}/logos_sdk) + +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test) + +add_executable(sdk_tests + test_main.cpp + test_logos_result.cpp + test_token_manager.cpp + test_mock_store.cpp + test_logos_mode.cpp + test_logos_mock_setup.cpp + test_logos_api_client_mock.cpp +) + +target_link_libraries(sdk_tests PRIVATE + logos_sdk + GTest::gtest + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::RemoteObjects + Qt${QT_VERSION_MAJOR}::Test +) + +gtest_discover_tests(sdk_tests) diff --git a/tests/sdk/test_logos_api_client_mock.cpp b/tests/sdk/test_logos_api_client_mock.cpp new file mode 100644 index 0000000..f817d59 --- /dev/null +++ b/tests/sdk/test_logos_api_client_mock.cpp @@ -0,0 +1,99 @@ +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_api_client.h" + +class LogosApiClientMockTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + } + void TearDown() override + { + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; +}; + +TEST_F(LogosApiClientMockTest, InvokeRemoteMethodReturnsMockedValue) +{ + m_mock->when("test_module", "getValue").thenReturn(QVariant(42)); + + LogosAPI api("client_module"); + LogosAPIClient* client = api.getClient("test_module"); + ASSERT_NE(client, nullptr); + + QVariant result = client->invokeRemoteMethod("test_module", "getValue"); + EXPECT_EQ(result.toInt(), 42); + EXPECT_TRUE(m_mock->wasCalled("test_module", "getValue")); +} + +TEST_F(LogosApiClientMockTest, InvokeWithSingleArg) +{ + m_mock->when("mod", "echo") + .withArgs({QVariant("hello")}) + .thenReturn(QVariant("hello back")); + + LogosAPI api("origin"); + LogosAPIClient* client = api.getClient("mod"); + ASSERT_NE(client, nullptr); + + QVariant result = client->invokeRemoteMethod("mod", "echo", QVariant("hello")); + EXPECT_EQ(result.toString(), "hello back"); +} + +TEST_F(LogosApiClientMockTest, InvokeWithMultipleArgs) +{ + m_mock->when("math", "add").thenReturn(QVariant(30)); + + LogosAPI api("origin"); + LogosAPIClient* client = api.getClient("math"); + ASSERT_NE(client, nullptr); + + QVariant result = client->invokeRemoteMethod("math", "add", QVariant(10), QVariant(20)); + EXPECT_EQ(result.toInt(), 30); + EXPECT_TRUE(m_mock->wasCalled("math", "add")); +} + +TEST_F(LogosApiClientMockTest, InvokeWithArgsList) +{ + m_mock->when("mod", "multi").thenReturn(QVariant("done")); + + LogosAPI api("origin"); + LogosAPIClient* client = api.getClient("mod"); + ASSERT_NE(client, nullptr); + + QVariantList args; + args << QVariant(1) << QVariant(2) << QVariant(3); + QVariant result = client->invokeRemoteMethod("mod", "multi", args); + EXPECT_EQ(result.toString(), "done"); +} + +TEST_F(LogosApiClientMockTest, NoExpectationReturnsInvalid) +{ + // Token still pre-seeded by when(), but no expectation for this method + m_mock->when("mod", "other").thenReturn(QVariant(1)); + + LogosAPI api("origin"); + LogosAPIClient* client = api.getClient("mod"); + ASSERT_NE(client, nullptr); + + QVariant result = client->invokeRemoteMethod("mod", "unregistered"); + EXPECT_FALSE(result.isValid()); +} + +TEST_F(LogosApiClientMockTest, CallCountTracking) +{ + m_mock->when("mod", "fn").thenReturn(QVariant(true)); + + LogosAPI api("origin"); + LogosAPIClient* client = api.getClient("mod"); + ASSERT_NE(client, nullptr); + + client->invokeRemoteMethod("mod", "fn"); + client->invokeRemoteMethod("mod", "fn"); + client->invokeRemoteMethod("mod", "fn"); + + EXPECT_EQ(m_mock->callCount("mod", "fn"), 3); +} diff --git a/tests/sdk/test_logos_mock_setup.cpp b/tests/sdk/test_logos_mock_setup.cpp new file mode 100644 index 0000000..c39bf18 --- /dev/null +++ b/tests/sdk/test_logos_mock_setup.cpp @@ -0,0 +1,102 @@ +#include +#include "logos_mock.h" + +class LogosMockSetupTest : public ::testing::Test { +protected: + void SetUp() override + { + // Ensure we start in Remote mode + LogosModeConfig::setMode(LogosMode::Remote); + MockStore::instance().reset(); + TokenManager::instance().clearAllTokens(); + } + void TearDown() override + { + LogosModeConfig::setMode(LogosMode::Remote); + } +}; + +TEST_F(LogosMockSetupTest, SwitchesToMockMode) +{ + { + LogosMockSetup mock; + EXPECT_TRUE(LogosModeConfig::isMock()); + } + // After destruction, mode is restored + EXPECT_TRUE(LogosModeConfig::isRemote()); +} + +TEST_F(LogosMockSetupTest, RestoresPreviousMode) +{ + LogosModeConfig::setMode(LogosMode::Local); + { + LogosMockSetup mock; + EXPECT_TRUE(LogosModeConfig::isMock()); + } + EXPECT_TRUE(LogosModeConfig::isLocal()); + // Restore for teardown + LogosModeConfig::setMode(LogosMode::Remote); +} + +TEST_F(LogosMockSetupTest, ResetsStoreOnConstruction) +{ + MockStore::instance().when("mod", "fn").thenReturn(QVariant(1)); + MockStore::instance().recordAndReturn("mod", "fn", {}); + + LogosMockSetup mock; + + // Previous expectations and calls cleared + EXPECT_FALSE(MockStore::instance().wasCalled("mod", "fn")); + QVariant r = MockStore::instance().recordAndReturn("mod", "fn", {}); + EXPECT_FALSE(r.isValid()); +} + +TEST_F(LogosMockSetupTest, ClearsTokensOnConstruction) +{ + TokenManager::instance().saveToken("stale", "token"); + + LogosMockSetup mock; + + EXPECT_FALSE(TokenManager::instance().hasToken("stale")); +} + +TEST_F(LogosMockSetupTest, WhenPreSeedsToken) +{ + LogosMockSetup mock; + mock.when("my_module", "doStuff").thenReturn(QVariant("ok")); + + EXPECT_TRUE(TokenManager::instance().hasToken("my_module")); +} + +TEST_F(LogosMockSetupTest, WhenSetsExpectation) +{ + LogosMockSetup mock; + mock.when("mod", "fn").thenReturn(QVariant(99)); + + QVariant r = MockStore::instance().recordAndReturn("mod", "fn", {}); + EXPECT_EQ(r.toInt(), 99); +} + +TEST_F(LogosMockSetupTest, VerificationDelegates) +{ + LogosMockSetup mock; + mock.when("mod", "fn").thenReturn(QVariant(1)); + + MockStore::instance().recordAndReturn("mod", "fn", {QVariant("arg")}); + + EXPECT_TRUE(mock.wasCalled("mod", "fn")); + EXPECT_TRUE(mock.wasCalledWith("mod", "fn", {QVariant("arg")})); + EXPECT_EQ(mock.callCount("mod", "fn"), 1); + EXPECT_EQ(mock.lastArgs("mod", "fn").size(), 1); +} + +TEST_F(LogosMockSetupTest, ResetsStoreOnDestruction) +{ + { + LogosMockSetup mock; + mock.when("mod", "fn").thenReturn(QVariant(1)); + MockStore::instance().recordAndReturn("mod", "fn", {}); + } + // After destruction, store is clean + EXPECT_FALSE(MockStore::instance().wasCalled("mod", "fn")); +} diff --git a/tests/sdk/test_logos_mode.cpp b/tests/sdk/test_logos_mode.cpp new file mode 100644 index 0000000..d9b20b2 --- /dev/null +++ b/tests/sdk/test_logos_mode.cpp @@ -0,0 +1,55 @@ +#include +#include "logos_mode.h" + +class LogosModeTest : public ::testing::Test { +protected: + void SetUp() override + { + m_saved = LogosModeConfig::getMode(); + } + void TearDown() override + { + LogosModeConfig::setMode(m_saved); + } +private: + LogosMode m_saved; +}; + +TEST_F(LogosModeTest, DefaultIsRemote) +{ + LogosModeConfig::setMode(LogosMode::Remote); + EXPECT_EQ(LogosModeConfig::getMode(), LogosMode::Remote); + EXPECT_TRUE(LogosModeConfig::isRemote()); + EXPECT_FALSE(LogosModeConfig::isLocal()); + EXPECT_FALSE(LogosModeConfig::isMock()); +} + +TEST_F(LogosModeTest, SetLocal) +{ + LogosModeConfig::setMode(LogosMode::Local); + EXPECT_EQ(LogosModeConfig::getMode(), LogosMode::Local); + EXPECT_TRUE(LogosModeConfig::isLocal()); + EXPECT_FALSE(LogosModeConfig::isRemote()); + EXPECT_FALSE(LogosModeConfig::isMock()); +} + +TEST_F(LogosModeTest, SetMock) +{ + LogosModeConfig::setMode(LogosMode::Mock); + EXPECT_EQ(LogosModeConfig::getMode(), LogosMode::Mock); + EXPECT_TRUE(LogosModeConfig::isMock()); + EXPECT_FALSE(LogosModeConfig::isRemote()); + EXPECT_FALSE(LogosModeConfig::isLocal()); +} + +TEST_F(LogosModeTest, SwitchBetweenModes) +{ + LogosModeConfig::setMode(LogosMode::Local); + EXPECT_TRUE(LogosModeConfig::isLocal()); + + LogosModeConfig::setMode(LogosMode::Mock); + EXPECT_TRUE(LogosModeConfig::isMock()); + + LogosModeConfig::setMode(LogosMode::Remote); + EXPECT_TRUE(LogosModeConfig::isRemote()); +} diff --git a/tests/sdk/test_logos_result.cpp b/tests/sdk/test_logos_result.cpp new file mode 100644 index 0000000..77f69dd --- /dev/null +++ b/tests/sdk/test_logos_result.cpp @@ -0,0 +1,174 @@ +#include +#include +#include "logos_types.h" + +TEST(LogosResultTest, GetValueSuccess) +{ + LogosResult r; + r.success = true; + r.value = QVariant(42); + + EXPECT_EQ(r.getValue(), 42); +} + +TEST(LogosResultTest, GetValueThrowsOnFailure) +{ + LogosResult r; + r.success = false; + r.error = QVariant("something went wrong"); + + EXPECT_THROW(r.getValue(), LogosResultException); +} + +TEST(LogosResultTest, GetErrorOnFailure) +{ + LogosResult r; + r.success = false; + r.error = QVariant("bad request"); + + EXPECT_EQ(r.getError(), "bad request"); +} + +TEST(LogosResultTest, GetErrorThrowsOnSuccess) +{ + LogosResult r; + r.success = true; + r.value = QVariant(1); + + EXPECT_THROW(r.getError(), LogosResultException); +} + +TEST(LogosResultTest, GetString) +{ + LogosResult r; + r.success = true; + r.value = QVariant("hello"); + + EXPECT_EQ(r.getString(), "hello"); +} + +TEST(LogosResultTest, GetBool) +{ + LogosResult r; + r.success = true; + r.value = QVariant(true); + + EXPECT_TRUE(r.getBool()); +} + +TEST(LogosResultTest, GetInt) +{ + LogosResult r; + r.success = true; + r.value = QVariant(99); + + EXPECT_EQ(r.getInt(), 99); +} + +TEST(LogosResultTest, GetValueFromMapWithKey) +{ + QVariantMap map; + map["name"] = "Alice"; + map["age"] = 30; + + LogosResult r; + r.success = true; + r.value = QVariant(map); + + EXPECT_EQ(r.getString("name"), "Alice"); + EXPECT_EQ(r.getInt("age"), 30); +} + +TEST(LogosResultTest, GetValueFromMapWithDefault) +{ + QVariantMap map; + map["name"] = "Bob"; + + LogosResult r; + r.success = true; + r.value = QVariant(map); + + EXPECT_EQ(r.getString("missing", "default"), "default"); + EXPECT_EQ(r.getInt("missing", -1), -1); +} + +TEST(LogosResultTest, GetValueFromListByIndex) +{ + QVariantMap item0; + item0["id"] = 1; + item0["label"] = "first"; + + QVariantMap item1; + item1["id"] = 2; + item1["label"] = "second"; + + QVariantList list; + list << QVariant(item0) << QVariant(item1); + + LogosResult r; + r.success = true; + r.value = QVariant(list); + + EXPECT_EQ(r.getString(0, "label"), "first"); + EXPECT_EQ(r.getInt(1, "id"), 2); +} + +TEST(LogosResultTest, GetValueFromListOutOfBoundsReturnsDefault) +{ + QVariantList list; + list << QVariant(QVariantMap{{"k", "v"}}); + + LogosResult r; + r.success = true; + r.value = QVariant(list); + + EXPECT_EQ(r.getString(99, "k", "fallback"), "fallback"); + EXPECT_EQ(r.getInt(-1, "k", -1), -1); +} + +TEST(LogosResultTest, GetList) +{ + QVariantList list; + list << 1 << 2 << 3; + + LogosResult r; + r.success = true; + r.value = QVariant(list); + + EXPECT_EQ(r.getList().size(), 3); +} + +TEST(LogosResultTest, GetMap) +{ + QVariantMap map; + map["a"] = 1; + + LogosResult r; + r.success = true; + r.value = QVariant(map); + + EXPECT_TRUE(r.getMap().contains("a")); +} + +TEST(LogosResultTest, Serialization) +{ + LogosResult original; + original.success = true; + original.value = QVariant("test_value"); + original.error = QVariant(); + + QByteArray buffer; + { + QDataStream out(&buffer, QIODevice::WriteOnly); + out << original; + } + + LogosResult restored; + { + QDataStream in(&buffer, QIODevice::ReadOnly); + in >> restored; + } + + EXPECT_EQ(restored.success, original.success); + EXPECT_EQ(restored.value.toString(), "test_value"); +} diff --git a/tests/sdk/test_main.cpp b/tests/sdk/test_main.cpp new file mode 100644 index 0000000..ac38f95 --- /dev/null +++ b/tests/sdk/test_main.cpp @@ -0,0 +1,9 @@ +#include +#include + +int main(int argc, char** argv) +{ + QCoreApplication app(argc, argv); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tests/sdk/test_mock_store.cpp b/tests/sdk/test_mock_store.cpp new file mode 100644 index 0000000..5bc3de1 --- /dev/null +++ b/tests/sdk/test_mock_store.cpp @@ -0,0 +1,111 @@ +#include +#include "mock_store.h" + +class MockStoreTest : public ::testing::Test { +protected: + void SetUp() override + { + MockStore::instance().reset(); + } +}; + +TEST_F(MockStoreTest, WhenThenReturn) +{ + MockStore::instance().when("mod", "method").thenReturn(QVariant(42)); + + QVariant result = MockStore::instance().recordAndReturn("mod", "method", {}); + EXPECT_EQ(result.toInt(), 42); +} + +TEST_F(MockStoreTest, WithArgsMatching) +{ + MockStore::instance().when("mod", "greet") + .withArgs({QVariant("hello")}) + .thenReturn(QVariant("world")); + + QVariant hit = MockStore::instance().recordAndReturn("mod", "greet", {QVariant("hello")}); + EXPECT_EQ(hit.toString(), "world"); + + QVariant miss = MockStore::instance().recordAndReturn("mod", "greet", {QVariant("bye")}); + EXPECT_FALSE(miss.isValid()); +} + +TEST_F(MockStoreTest, MatchAnyArgsByDefault) +{ + MockStore::instance().when("mod", "fn").thenReturn(QVariant(true)); + + QVariant r = MockStore::instance().recordAndReturn("mod", "fn", {QVariant(1), QVariant(2)}); + EXPECT_TRUE(r.toBool()); +} + +TEST_F(MockStoreTest, LIFOOrder) +{ + MockStore::instance().when("mod", "fn").thenReturn(QVariant("first")); + MockStore::instance().when("mod", "fn").thenReturn(QVariant("second")); + + QVariant r = MockStore::instance().recordAndReturn("mod", "fn", {}); + EXPECT_EQ(r.toString(), "second"); +} + +TEST_F(MockStoreTest, NoMatchReturnsInvalid) +{ + QVariant r = MockStore::instance().recordAndReturn("mod", "noSuchMethod", {}); + EXPECT_FALSE(r.isValid()); +} + +TEST_F(MockStoreTest, WasCalled) +{ + EXPECT_FALSE(MockStore::instance().wasCalled("mod", "fn")); + + MockStore::instance().recordAndReturn("mod", "fn", {}); + EXPECT_TRUE(MockStore::instance().wasCalled("mod", "fn")); +} + +TEST_F(MockStoreTest, WasCalledWith) +{ + MockStore::instance().recordAndReturn("mod", "fn", {QVariant(10)}); + EXPECT_TRUE(MockStore::instance().wasCalledWith("mod", "fn", {QVariant(10)})); + EXPECT_FALSE(MockStore::instance().wasCalledWith("mod", "fn", {QVariant(20)})); +} + +TEST_F(MockStoreTest, CallCount) +{ + EXPECT_EQ(MockStore::instance().callCount("mod", "fn"), 0); + MockStore::instance().recordAndReturn("mod", "fn", {}); + MockStore::instance().recordAndReturn("mod", "fn", {}); + MockStore::instance().recordAndReturn("mod", "fn", {}); + EXPECT_EQ(MockStore::instance().callCount("mod", "fn"), 3); +} + +TEST_F(MockStoreTest, LastArgs) +{ + MockStore::instance().recordAndReturn("mod", "fn", {QVariant("a")}); + MockStore::instance().recordAndReturn("mod", "fn", {QVariant("b")}); + + QVariantList last = MockStore::instance().lastArgs("mod", "fn"); + EXPECT_EQ(last.size(), 1); + EXPECT_EQ(last.at(0).toString(), "b"); +} + +TEST_F(MockStoreTest, AllCalls) +{ + MockStore::instance().recordAndReturn("m1", "f1", {}); + MockStore::instance().recordAndReturn("m2", "f2", {QVariant(1)}); + + auto calls = MockStore::instance().allCalls(); + EXPECT_EQ(calls.size(), 2); + EXPECT_EQ(calls.at(0).module, "m1"); + EXPECT_EQ(calls.at(1).module, "m2"); +} + +TEST_F(MockStoreTest, ResetClearsEverything) +{ + MockStore::instance().when("mod", "fn").thenReturn(QVariant(1)); + MockStore::instance().recordAndReturn("mod", "fn", {}); + + MockStore::instance().reset(); + + EXPECT_FALSE(MockStore::instance().wasCalled("mod", "fn")); + QVariant r = MockStore::instance().recordAndReturn("mod", "fn", {}); + EXPECT_FALSE(r.isValid()); +} diff --git a/tests/sdk/test_token_manager.cpp b/tests/sdk/test_token_manager.cpp new file mode 100644 index 0000000..9c8b29e --- /dev/null +++ b/tests/sdk/test_token_manager.cpp @@ -0,0 +1,101 @@ +#include +#include +#include "token_manager.h" + +class TokenManagerTest : public ::testing::Test { +protected: + void SetUp() override + { + TokenManager::instance().clearAllTokens(); + } +}; + +TEST_F(TokenManagerTest, SaveAndGetToken) +{ + TokenManager::instance().saveToken("mod_a", "token_a"); + EXPECT_EQ(TokenManager::instance().getToken("mod_a"), "token_a"); +} + +TEST_F(TokenManagerTest, GetMissingTokenReturnsEmpty) +{ + EXPECT_TRUE(TokenManager::instance().getToken("nonexistent").isEmpty()); +} + +TEST_F(TokenManagerTest, HasToken) +{ + EXPECT_FALSE(TokenManager::instance().hasToken("key")); + TokenManager::instance().saveToken("key", "val"); + EXPECT_TRUE(TokenManager::instance().hasToken("key")); +} + +TEST_F(TokenManagerTest, RemoveToken) +{ + TokenManager::instance().saveToken("key", "val"); + EXPECT_TRUE(TokenManager::instance().removeToken("key")); + EXPECT_FALSE(TokenManager::instance().hasToken("key")); +} + +TEST_F(TokenManagerTest, RemoveNonexistentTokenReturnsFalse) +{ + EXPECT_FALSE(TokenManager::instance().removeToken("missing")); +} + +TEST_F(TokenManagerTest, ClearAllTokens) +{ + TokenManager::instance().saveToken("a", "1"); + TokenManager::instance().saveToken("b", "2"); + TokenManager::instance().clearAllTokens(); + EXPECT_EQ(TokenManager::instance().tokenCount(), 0); +} + +TEST_F(TokenManagerTest, TokenCount) +{ + EXPECT_EQ(TokenManager::instance().tokenCount(), 0); + TokenManager::instance().saveToken("x", "1"); + EXPECT_EQ(TokenManager::instance().tokenCount(), 1); + TokenManager::instance().saveToken("y", "2"); + EXPECT_EQ(TokenManager::instance().tokenCount(), 2); +} + +TEST_F(TokenManagerTest, GetTokenKeys) +{ + TokenManager::instance().saveToken("alpha", "1"); + TokenManager::instance().saveToken("beta", "2"); + QList keys = TokenManager::instance().getTokenKeys(); + EXPECT_EQ(keys.size(), 2); + EXPECT_TRUE(keys.contains("alpha")); + EXPECT_TRUE(keys.contains("beta")); +} + +TEST_F(TokenManagerTest, OverwriteToken) +{ + TokenManager::instance().saveToken("key", "old"); + TokenManager::instance().saveToken("key", "new"); + EXPECT_EQ(TokenManager::instance().getToken("key"), "new"); + EXPECT_EQ(TokenManager::instance().tokenCount(), 1); +} + +TEST_F(TokenManagerTest, SignalTokenSaved) +{ + QSignalSpy spy(&TokenManager::instance(), &TokenManager::tokenSaved); + TokenManager::instance().saveToken("k", "v"); + EXPECT_EQ(spy.count(), 1); + EXPECT_EQ(spy.at(0).at(0).toString(), "k"); +} + +TEST_F(TokenManagerTest, SignalTokenRemoved) +{ + TokenManager::instance().saveToken("k", "v"); + QSignalSpy spy(&TokenManager::instance(), &TokenManager::tokenRemoved); + TokenManager::instance().removeToken("k"); + EXPECT_EQ(spy.count(), 1); + EXPECT_EQ(spy.at(0).at(0).toString(), "k"); +} + +TEST_F(TokenManagerTest, SignalAllTokensCleared) +{ + TokenManager::instance().saveToken("k", "v"); + QSignalSpy spy(&TokenManager::instance(), &TokenManager::allTokensCleared); + TokenManager::instance().clearAllTokens(); + EXPECT_EQ(spy.count(), 1); +}