diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6e6b263 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cachix/install-nix-action@v27 + with: + extra_nix_config: | + experimental-features = nix-command flakes + + - uses: cachix/cachix-action@v15 + with: + name: logos-co + authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' + + - name: Build tests + run: nix build '.#tests' + + - name: Run SDK tests + run: ./result/bin/sdk_tests + + - name: Run generator tests + run: ./result/bin/generator_tests 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..5058607 --- /dev/null +++ b/tests/sdk/CMakeLists.txt @@ -0,0 +1,59 @@ +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../cpp ${CMAKE_CURRENT_BINARY_DIR}/logos_sdk) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../cpp-generator ${CMAKE_CURRENT_BINARY_DIR}/logos_generator) + +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Test) + +# --- Generate dispatch code for the sample provider fixture --- +set(SAMPLE_HEADER ${CMAKE_CURRENT_SOURCE_DIR}/fixtures/sample_provider.h) +set(GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated) +set(GENERATED_DISPATCH ${GENERATED_DIR}/logos_provider_dispatch.cpp) + +file(MAKE_DIRECTORY ${GENERATED_DIR}) + +add_custom_command( + OUTPUT ${GENERATED_DISPATCH} + COMMAND logos-cpp-generator --provider-header ${SAMPLE_HEADER} --output-dir ${GENERATED_DIR} + DEPENDS logos-cpp-generator ${SAMPLE_HEADER} + COMMENT "Generating provider dispatch for sample_provider" +) + +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 + test_logos_api.cpp + test_logos_api_client_overloads.cpp + test_transport_factory.cpp + test_registry_factory.cpp + test_plugin_registry.cpp + test_logos_instance.cpp + test_logos_provider_base.cpp + test_module_proxy.cpp + test_logos_api_provider.cpp + test_mock_transport.cpp + test_local_transport_integration.cpp + test_event_system.cpp + test_provider_dispatch.cpp + fixtures/sample_provider.cpp + ${GENERATED_DISPATCH} +) + +target_include_directories(sdk_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/fixtures + ${GENERATED_DIR} +) + +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/fixtures/sample_provider.cpp b/tests/sdk/fixtures/sample_provider.cpp new file mode 100644 index 0000000..21d84ad --- /dev/null +++ b/tests/sdk/fixtures/sample_provider.cpp @@ -0,0 +1,62 @@ +#include "sample_provider.h" +#include + +QString SampleProvider::echo(const QString& message) +{ + lastEcho = message; + return message; +} + +bool SampleProvider::installPlugin(const QString& pluginPath, bool skipIfNotNewerVersion) +{ + lastPluginPath = pluginPath; + lastSkipFlag = skipIfNotNewerVersion; + return true; +} + +QJsonArray SampleProvider::getPackages() +{ + QJsonArray arr; + arr.append(QJsonObject{{"name", "pkg_a"}}); + arr.append(QJsonObject{{"name", "pkg_b"}}); + return arr; +} + +QJsonArray SampleProvider::getPackages(const QString& category) +{ + lastCategory = category; + QJsonArray arr; + arr.append(QJsonObject{{"name", "pkg_filtered"}, {"category", category}}); + return arr; +} + +QStringList SampleProvider::getCategories() +{ + return QStringList{"network", "storage", "ui"}; +} + +QStringList SampleProvider::resolveDependencies(const QStringList& packageNames) +{ + lastDependencies = packageNames; + QStringList resolved = packageNames; + resolved.append("common_dep"); + return resolved; +} + +int SampleProvider::add(int a, int b) +{ + lastA = a; + lastB = b; + return a + b; +} + +void SampleProvider::setDirectory(const QString& directory) +{ + lastDirectory = directory; +} + +void SampleProvider::testEvent(const QString& message) +{ + lastEventMessage = message; + emitEvent("testEvent", {QVariant(message)}); +} diff --git a/tests/sdk/fixtures/sample_provider.h b/tests/sdk/fixtures/sample_provider.h new file mode 100644 index 0000000..aa9e2ac --- /dev/null +++ b/tests/sdk/fixtures/sample_provider.h @@ -0,0 +1,47 @@ +#ifndef SAMPLE_PROVIDER_H +#define SAMPLE_PROVIDER_H + +#include "logos_provider_object.h" +#include +#include +#include + +/** + * @brief Sample provider that exercises the LOGOS_PROVIDER + LOGOS_METHOD pipeline. + * + * This mimics real-world module patterns (like logos-package-manager-module) + * to verify that the code generator + SDK dispatch chain works correctly. + * The generated logos_provider_dispatch.cpp provides callMethod() and getMethods(). + */ +class SampleProvider : public LogosProviderBase { + LOGOS_PROVIDER(SampleProvider, "sample_provider", "2.0.0") + +public: + // -- Various return types -- + + LOGOS_METHOD QString echo(const QString& message); + LOGOS_METHOD bool installPlugin(const QString& pluginPath, bool skipIfNotNewerVersion); + LOGOS_METHOD QJsonArray getPackages(); + LOGOS_METHOD QJsonArray getPackages(const QString& category); + LOGOS_METHOD QStringList getCategories(); + LOGOS_METHOD QStringList resolveDependencies(const QStringList& packageNames); + LOGOS_METHOD int add(int a, int b); + + // -- void return type -- + + LOGOS_METHOD void setDirectory(const QString& directory); + LOGOS_METHOD void testEvent(const QString& message); + + // -- State for verification -- + + QString lastEcho; + QString lastPluginPath; + bool lastSkipFlag = false; + QString lastCategory; + QStringList lastDependencies; + int lastA = 0, lastB = 0; + QString lastDirectory; + QString lastEventMessage; +}; + +#endif // SAMPLE_PROVIDER_H diff --git a/tests/sdk/test_event_system.cpp b/tests/sdk/test_event_system.cpp new file mode 100644 index 0000000..063f0b1 --- /dev/null +++ b/tests/sdk/test_event_system.cpp @@ -0,0 +1,340 @@ +#include +#include +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_api_client.h" +#include "logos_api_provider.h" +#include "logos_api_consumer.h" +#include "logos_object.h" +#include "logos_provider_object.h" +#include "module_proxy.h" +#include "plugin_registry.h" +#include "local_transport.h" + +// Provider that emits events on demand +class EventTestProvider : public LogosProviderBase { +public: + QString providerName() const override { return "event_test"; } + QString providerVersion() const override { return "1.0.0"; } + + QVariant callMethod(const QString& methodName, const QVariantList& args) override + { + Q_UNUSED(methodName) Q_UNUSED(args) + return QVariant(true); + } + + QJsonArray getMethods() override { return QJsonArray(); } + + void testEmitEvent(const QString& name, const QVariantList& data) { emitEvent(name, data); } +}; + +// ── LogosAPIClient::onEventResponse(LogosObject*) ───────────────────────── + +class ClientEventResponseTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + m_api = new LogosAPI("origin"); + m_client = m_api->getClient("target"); + } + void TearDown() override + { + delete m_api; + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; + LogosAPI* m_api = nullptr; + LogosAPIClient* m_client = nullptr; +}; + +TEST_F(ClientEventResponseTest, OnEventResponseNullObjectIsNoOp) +{ + // Should not crash + m_client->onEventResponse(static_cast(nullptr), "evt", {QVariant(1)}); +} + +TEST_F(ClientEventResponseTest, OnEventResponseEmptyEventNameIsNoOp) +{ + // Get a mock object to test with + m_mock->when("target", "fn").thenReturn(QVariant(1)); + LogosObject* obj = m_client->requestObject("target"); + ASSERT_NE(obj, nullptr); + + // Should not crash, empty event name is rejected + m_client->onEventResponse(obj, "", {QVariant(1)}); + obj->release(); +} + +TEST_F(ClientEventResponseTest, OnEventResponseCallsEmitEventOnObject) +{ + // MockLogosObject::emitEvent is a no-op, so we just verify no crash + m_mock->when("target", "fn").thenReturn(QVariant(1)); + LogosObject* obj = m_client->requestObject("target"); + ASSERT_NE(obj, nullptr); + + m_client->onEventResponse(obj, "my_event", {QVariant("data")}); + obj->release(); +} + +// ── LogosAPIClient::onEventResponse(QObject*) backward compat ────────────── + +class LegacyQObjectEventTest : public QObject { + Q_OBJECT +public: + QString lastEventName; + QVariantList lastEventData; + +public slots: + void eventResponse(const QString& eventName, const QVariantList& data) + { + lastEventName = eventName; + lastEventData = data; + } +}; + +TEST_F(ClientEventResponseTest, OnEventResponseQObjectCallsSlot) +{ + LegacyQObjectEventTest receiver; + + m_client->onEventResponse(&receiver, "test_event", {QVariant(42)}); + + EXPECT_EQ(receiver.lastEventName, "test_event"); + ASSERT_EQ(receiver.lastEventData.size(), 1); + EXPECT_EQ(receiver.lastEventData[0].toInt(), 42); +} + +TEST_F(ClientEventResponseTest, OnEventResponseQObjectNullIsNoOp) +{ + m_client->onEventResponse(static_cast(nullptr), "evt", {QVariant(1)}); +} + +TEST_F(ClientEventResponseTest, OnEventResponseQObjectEmptyNameIsNoOp) +{ + LegacyQObjectEventTest receiver; + m_client->onEventResponse(&receiver, "", {QVariant(1)}); + EXPECT_TRUE(receiver.lastEventName.isEmpty()); +} + +// ── LogosAPIClient::onEvent (LogosObject callback registration) ──────────── + +TEST_F(ClientEventResponseTest, OnEventNullObjectIsNoOp) +{ + // Should not crash + m_client->onEvent(nullptr, "evt", [](const QString&, const QVariantList&) {}); +} + +// ── LogosAPIClient::informModuleToken ────────────────────────────────────── + +TEST_F(ClientEventResponseTest, InformModuleTokenDelegatesToConsumer) +{ + m_mock->when("capability_module", "fn").thenReturn(QVariant(true)); + + // informModuleToken routes through consumer -> capability_module -> informModuleToken + bool result = m_client->informModuleToken("auth_tok", "target_mod", "secret"); + // MockLogosObject::informModuleToken always returns true + EXPECT_TRUE(result); +} + +// ── LogosAPIClient::informModuleToken_module ─────────────────────────────── + +TEST_F(ClientEventResponseTest, InformModuleTokenModuleDelegatesToConsumer) +{ + m_mock->when("origin_mod", "fn").thenReturn(QVariant(true)); + + bool result = m_client->informModuleToken_module("auth_tok", "origin_mod", "target_mod", "secret"); + EXPECT_TRUE(result); +} + +// ── LogosAPIProvider::onEventResponse ────────────────────────────────────── + +class ProviderEventResponseTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + } + void TearDown() override + { + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; +}; + +TEST_F(ProviderEventResponseTest, OnEventResponseNullObjectIsNoOp) +{ + LogosAPI api("mod"); + LogosAPIProvider* prov = api.getProvider(); + // Should not crash + prov->onEventResponse(nullptr, "evt", {QVariant(1)}); +} + +TEST_F(ProviderEventResponseTest, OnEventResponseEmptyNameIsNoOp) +{ + LogosAPI api("mod"); + LogosAPIProvider* prov = api.getProvider(); + + m_mock->when("mod", "fn").thenReturn(QVariant(1)); + LogosAPIClient* client = api.getClient("mod"); + LogosObject* obj = client->requestObject("mod"); + ASSERT_NE(obj, nullptr); + + // Empty event name should be rejected + prov->onEventResponse(obj, "", {QVariant(1)}); + obj->release(); +} + +TEST_F(ProviderEventResponseTest, OnEventResponseCallsEmitEvent) +{ + LogosAPI api("mod"); + LogosAPIProvider* prov = api.getProvider(); + + m_mock->when("mod", "fn").thenReturn(QVariant(1)); + LogosAPIClient* client = api.getClient("mod"); + LogosObject* obj = client->requestObject("mod"); + ASSERT_NE(obj, nullptr); + + // MockLogosObject::emitEvent is no-op, just verify no crash + prov->onEventResponse(obj, "test_event", {QVariant("data")}); + obj->release(); +} + +// ── Full local event pipeline: Provider -> ModuleProxy -> LocalLogosObject -> Client ── + +class LocalEventPipelineTest : public ::testing::Test { +protected: + void SetUp() override + { + m_savedMode = LogosModeConfig::getMode(); + LogosModeConfig::setMode(LogosMode::Local); + TokenManager::instance().clearAllTokens(); + m_provider = new EventTestProvider(); + m_proxy = new ModuleProxy(m_provider); + } + void TearDown() override + { + PluginRegistry::unregisterPlugin("pipeline_mod"); + delete m_proxy; + delete m_provider; + LogosModeConfig::setMode(m_savedMode); + } + LogosMode m_savedMode; + EventTestProvider* m_provider = nullptr; + ModuleProxy* m_proxy = nullptr; +}; + +TEST_F(LocalEventPipelineTest, FullEventPipelineProviderToConsumerObject) +{ + LocalTransportHost host; + host.publishObject("pipeline_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("pipeline_mod", 5000); + ASSERT_NE(obj, nullptr); + + // Register event listener on the LogosObject + QString receivedEvent; + QVariantList receivedData; + obj->onEvent("pipeline_evt", [&](const QString& name, const QVariantList& data) { + receivedEvent = name; + receivedData = data; + }); + + // Provider emits -> LogosProviderBase.emitEvent -> callback -> ModuleProxy.emit eventResponse + // -> EventHelper.onEventResponse -> registered callback + m_provider->testEmitEvent("pipeline_evt", {QVariant("payload"), QVariant(99)}); + + EXPECT_EQ(receivedEvent, "pipeline_evt"); + ASSERT_EQ(receivedData.size(), 2); + EXPECT_EQ(receivedData[0].toString(), "payload"); + EXPECT_EQ(receivedData[1].toInt(), 99); + obj->release(); +} + +TEST_F(LocalEventPipelineTest, EventNotDeliveredAfterDisconnect) +{ + LocalTransportHost host; + host.publishObject("pipeline_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("pipeline_mod", 5000); + ASSERT_NE(obj, nullptr); + + int count = 0; + obj->onEvent("evt", [&](const QString&, const QVariantList&) { count++; }); + + m_provider->testEmitEvent("evt", {}); + EXPECT_EQ(count, 1); + + obj->disconnectEvents(); + m_provider->testEmitEvent("evt", {}); + EXPECT_EQ(count, 1); + + // Re-register after disconnect + obj->onEvent("evt", [&](const QString&, const QVariantList&) { count++; }); + m_provider->testEmitEvent("evt", {}); + EXPECT_EQ(count, 2); + obj->release(); +} + +TEST_F(LocalEventPipelineTest, EmitEventOnLocalLogosObjectTriggersSignal) +{ + LocalTransportHost host; + host.publishObject("pipeline_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("pipeline_mod", 5000); + ASSERT_NE(obj, nullptr); + + // Use QSignalSpy on the ModuleProxy to verify emitEvent triggers the signal + QSignalSpy spy(m_proxy, &ModuleProxy::eventResponse); + + obj->emitEvent("ext_event", {QVariant("from_outside")}); + + // emitEvent uses QueuedConnection, so process events + QCoreApplication::processEvents(); + + EXPECT_EQ(spy.count(), 1); + EXPECT_EQ(spy.at(0).at(0).toString(), "ext_event"); + QVariantList data = spy.at(0).at(1).value(); + ASSERT_EQ(data.size(), 1); + EXPECT_EQ(data[0].toString(), "from_outside"); + obj->release(); +} + +// ── Consumer onEvent integration ─────────────────────────────────────────── + +TEST_F(LocalEventPipelineTest, ConsumerOnEventRegistersCallback) +{ + LocalTransportHost host; + host.publishObject("pipeline_mod", m_proxy); + + // Seed a token so client doesn't try capability_module + TokenManager::instance().saveToken("pipeline_mod", "tok"); + + LogosAPI api("test_origin"); + LogosAPIClient* client = api.getClient("pipeline_mod"); + + // Get object handle + LogosObject* obj = client->requestObject("pipeline_mod"); + ASSERT_NE(obj, nullptr); + + // Register via client.onEvent (delegates to consumer.onEvent -> obj.onEvent) + QString receivedName; + QVariantList receivedData; + client->onEvent(obj, "client_evt", [&](const QString& name, const QVariantList& data) { + receivedName = name; + receivedData = data; + }); + + // Provider emits event + m_provider->testEmitEvent("client_evt", {QVariant("via_client")}); + + EXPECT_EQ(receivedName, "client_evt"); + ASSERT_EQ(receivedData.size(), 1); + EXPECT_EQ(receivedData[0].toString(), "via_client"); + obj->release(); +} + +#include "test_event_system.moc" diff --git a/tests/sdk/test_local_transport_integration.cpp b/tests/sdk/test_local_transport_integration.cpp new file mode 100644 index 0000000..5c60804 --- /dev/null +++ b/tests/sdk/test_local_transport_integration.cpp @@ -0,0 +1,331 @@ +#include +#include +#include +#include "logos_mode.h" +#include "logos_object.h" +#include "logos_provider_object.h" +#include "logos_api.h" +#include "token_manager.h" +#include "module_proxy.h" +#include "plugin_registry.h" +#include "local_transport.h" + +// Test provider for local transport testing +class LocalTestProvider : public LogosProviderBase { +public: + QString providerName() const override { return "local_test"; } + QString providerVersion() const override { return "1.0.0"; } + + QVariant callMethod(const QString& methodName, const QVariantList& args) override + { + lastMethod = methodName; + lastArgs = args; + if (methodName == "add" && args.size() == 2) + return QVariant(args[0].toInt() + args[1].toInt()); + if (methodName == "echo" && args.size() == 1) + return args[0]; + return returnValue; + } + + QJsonArray getMethods() override + { + QJsonArray arr; + QJsonObject m; + m["name"] = "add"; + arr.append(m); + QJsonObject m2; + m2["name"] = "echo"; + arr.append(m2); + return arr; + } + + void testEmitEvent(const QString& name, const QVariantList& data) { emitEvent(name, data); } + + QString lastMethod; + QVariantList lastArgs; + QVariant returnValue = QVariant(77); +}; + +class LocalTransportIntegrationTest : public ::testing::Test { +protected: + void SetUp() override + { + m_savedMode = LogosModeConfig::getMode(); + LogosModeConfig::setMode(LogosMode::Local); + m_provider = new LocalTestProvider(); + m_proxy = new ModuleProxy(m_provider); + } + void TearDown() override + { + PluginRegistry::unregisterPlugin("local_mod"); + delete m_proxy; + delete m_provider; + LogosModeConfig::setMode(m_savedMode); + } + LogosMode m_savedMode; + LocalTestProvider* m_provider = nullptr; + ModuleProxy* m_proxy = nullptr; +}; + +// -- LocalTransportHost -- + +TEST_F(LocalTransportIntegrationTest, PublishRegistersInPluginRegistry) +{ + LocalTransportHost host; + EXPECT_TRUE(host.publishObject("local_mod", m_proxy)); + EXPECT_TRUE(PluginRegistry::hasPlugin("local_mod")); +} + +TEST_F(LocalTransportIntegrationTest, UnpublishRemovesFromPluginRegistry) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + host.unpublishObject("local_mod"); + EXPECT_FALSE(PluginRegistry::hasPlugin("local_mod")); +} + +// -- LocalTransportConnection -- + +TEST_F(LocalTransportIntegrationTest, ConnectionAlwaysConnected) +{ + LocalTransportConnection conn; + EXPECT_TRUE(conn.isConnected()); + EXPECT_TRUE(conn.connectToHost()); + EXPECT_TRUE(conn.reconnect()); +} + +TEST_F(LocalTransportIntegrationTest, RequestObjectReturnsNullIfNotPublished) +{ + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("nonexistent", 5000); + EXPECT_EQ(obj, nullptr); +} + +TEST_F(LocalTransportIntegrationTest, RequestObjectReturnsLocalLogosObject) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + // Verify id() returns proxy address + EXPECT_EQ(obj->id(), reinterpret_cast(m_proxy)); + obj->release(); +} + +// -- LocalLogosObject::callMethod -- + +TEST_F(LocalTransportIntegrationTest, CallMethodDelegatesToProvider) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + QVariant r = obj->callMethod("auth", "add", {QVariant(3), QVariant(7)}, 5000); + EXPECT_EQ(r.toInt(), 10); + EXPECT_EQ(m_provider->lastMethod, "add"); + obj->release(); +} + +TEST_F(LocalTransportIntegrationTest, CallMethodEchoArg) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + QVariant r = obj->callMethod("auth", "echo", {QVariant("hello")}, 5000); + EXPECT_EQ(r.toString(), "hello"); + obj->release(); +} + +TEST_F(LocalTransportIntegrationTest, CallMethodDefaultReturn) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + QVariant r = obj->callMethod("auth", "unknown", {}, 5000); + EXPECT_EQ(r.toInt(), 77); + obj->release(); +} + +// -- LocalLogosObject::getMethods -- + +TEST_F(LocalTransportIntegrationTest, GetMethodsDelegatesToProvider) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + QJsonArray methods = obj->getMethods(); + EXPECT_EQ(methods.size(), 2); + obj->release(); +} + +// -- LocalLogosObject::informModuleToken -- + +TEST_F(LocalTransportIntegrationTest, InformModuleTokenDelegatesToProvider) +{ + LogosAPI api("local_origin"); + m_provider->init(&api); + + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + bool result = obj->informModuleToken("auth", "target_mod", "secret_tok", 5000); + EXPECT_TRUE(result); + EXPECT_EQ(TokenManager::instance().getToken("target_mod"), "secret_tok"); + obj->release(); +} + +// -- LocalLogosObject::onEvent (event subscription and delivery) -- + +TEST_F(LocalTransportIntegrationTest, OnEventReceivesProviderEvents) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + QString receivedEvent; + QVariantList receivedData; + obj->onEvent("my_event", [&](const QString& name, const QVariantList& data) { + receivedEvent = name; + receivedData = data; + }); + + // Provider emits event -> ModuleProxy signal -> EventHelper -> callback + m_provider->testEmitEvent("my_event", {QVariant(42)}); + + EXPECT_EQ(receivedEvent, "my_event"); + ASSERT_EQ(receivedData.size(), 1); + EXPECT_EQ(receivedData[0].toInt(), 42); + obj->release(); +} + +TEST_F(LocalTransportIntegrationTest, OnEventMultipleCallbacksSameEvent) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + int count1 = 0, count2 = 0; + obj->onEvent("evt", [&](const QString&, const QVariantList&) { count1++; }); + obj->onEvent("evt", [&](const QString&, const QVariantList&) { count2++; }); + + m_provider->testEmitEvent("evt", {}); + + EXPECT_EQ(count1, 1); + EXPECT_EQ(count2, 1); + obj->release(); +} + +TEST_F(LocalTransportIntegrationTest, OnEventDifferentEventsRouteCorrectly) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + int countA = 0, countB = 0; + obj->onEvent("eventA", [&](const QString&, const QVariantList&) { countA++; }); + obj->onEvent("eventB", [&](const QString&, const QVariantList&) { countB++; }); + + m_provider->testEmitEvent("eventA", {}); + EXPECT_EQ(countA, 1); + EXPECT_EQ(countB, 0); + + m_provider->testEmitEvent("eventB", {}); + EXPECT_EQ(countA, 1); + EXPECT_EQ(countB, 1); + obj->release(); +} + +// -- LocalLogosObject::disconnectEvents -- + +TEST_F(LocalTransportIntegrationTest, DisconnectEventsStopsDelivery) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + int count = 0; + obj->onEvent("evt", [&](const QString&, const QVariantList&) { count++; }); + + m_provider->testEmitEvent("evt", {}); + EXPECT_EQ(count, 1); + + obj->disconnectEvents(); + m_provider->testEmitEvent("evt", {}); + EXPECT_EQ(count, 1); // no increment after disconnect + obj->release(); +} + +// -- LocalLogosObject::release -- + +TEST_F(LocalTransportIntegrationTest, ReleaseDisconnectsEvents) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + int count = 0; + obj->onEvent("evt", [&](const QString&, const QVariantList&) { count++; }); + + m_provider->testEmitEvent("evt", {}); + EXPECT_EQ(count, 1); + + obj->release(); + + m_provider->testEmitEvent("evt", {}); + EXPECT_EQ(count, 1); // no delivery after release +} + +// -- getPluginMethods special case through LocalLogosObject -- + +TEST_F(LocalTransportIntegrationTest, CallGetPluginMethodsSpecialCase) +{ + LocalTransportHost host; + host.publishObject("local_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("local_mod", 5000); + ASSERT_NE(obj, nullptr); + + // callRemoteMethod with "getPluginMethods" is special-cased in ModuleProxy + QVariant r = obj->callMethod("auth", "getPluginMethods", {}, 5000); + EXPECT_TRUE(r.toJsonArray().size() > 0); + // Provider's callMethod should NOT have been called + EXPECT_TRUE(m_provider->lastMethod.isEmpty()); + obj->release(); +} diff --git a/tests/sdk/test_logos_api.cpp b/tests/sdk/test_logos_api.cpp new file mode 100644 index 0000000..24c09ae --- /dev/null +++ b/tests/sdk/test_logos_api.cpp @@ -0,0 +1,62 @@ +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_api_client.h" +#include "logos_api_provider.h" + +class LogosApiTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + } + void TearDown() override + { + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; +}; + +TEST_F(LogosApiTest, ConstructionCreatesProvider) +{ + LogosAPI api("test_module"); + EXPECT_NE(api.getProvider(), nullptr); +} + +TEST_F(LogosApiTest, GetTokenManagerReturnsSingleton) +{ + LogosAPI api("test_module"); + EXPECT_EQ(api.getTokenManager(), &TokenManager::instance()); +} + +TEST_F(LogosApiTest, GetClientReturnsNonNull) +{ + LogosAPI api("origin"); + LogosAPIClient* client = api.getClient("target_module"); + EXPECT_NE(client, nullptr); +} + +TEST_F(LogosApiTest, GetClientCachesSameModule) +{ + LogosAPI api("origin"); + LogosAPIClient* client1 = api.getClient("target"); + LogosAPIClient* client2 = api.getClient("target"); + EXPECT_EQ(client1, client2); +} + +TEST_F(LogosApiTest, GetClientDifferentModulesReturnDifferentClients) +{ + LogosAPI api("origin"); + LogosAPIClient* a = api.getClient("module_a"); + LogosAPIClient* b = api.getClient("module_b"); + EXPECT_NE(a, b); +} + +TEST_F(LogosApiTest, MultipleLogosApiInstances) +{ + LogosAPI api1("mod1"); + LogosAPI api2("mod2"); + EXPECT_NE(api1.getProvider(), api2.getProvider()); + // Both share the same token manager singleton + EXPECT_EQ(api1.getTokenManager(), api2.getTokenManager()); +} 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_api_client_overloads.cpp b/tests/sdk/test_logos_api_client_overloads.cpp new file mode 100644 index 0000000..82b7719 --- /dev/null +++ b/tests/sdk/test_logos_api_client_overloads.cpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_api_client.h" + +class LogosApiClientOverloadsTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + m_mock->when("mod", "fn").thenReturn(QVariant(42)); + m_api = new LogosAPI("origin"); + m_client = m_api->getClient("mod"); + } + void TearDown() override + { + delete m_api; + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; + LogosAPI* m_api = nullptr; + LogosAPIClient* m_client = nullptr; +}; + +TEST_F(LogosApiClientOverloadsTest, ZeroArgs) +{ + QVariant r = m_client->invokeRemoteMethod("mod", "fn"); + EXPECT_EQ(r.toInt(), 42); +} + +TEST_F(LogosApiClientOverloadsTest, OneArg) +{ + QVariant r = m_client->invokeRemoteMethod("mod", "fn", QVariant("a")); + EXPECT_EQ(r.toInt(), 42); + QVariantList last = m_mock->lastArgs("mod", "fn"); + ASSERT_EQ(last.size(), 1); + EXPECT_EQ(last[0].toString(), "a"); +} + +TEST_F(LogosApiClientOverloadsTest, TwoArgs) +{ + QVariant r = m_client->invokeRemoteMethod("mod", "fn", QVariant(1), QVariant(2)); + EXPECT_EQ(r.toInt(), 42); + QVariantList last = m_mock->lastArgs("mod", "fn"); + ASSERT_EQ(last.size(), 2); + EXPECT_EQ(last[0].toInt(), 1); + EXPECT_EQ(last[1].toInt(), 2); +} + +TEST_F(LogosApiClientOverloadsTest, ThreeArgs) +{ + m_client->invokeRemoteMethod("mod", "fn", QVariant(1), QVariant(2), QVariant(3)); + QVariantList last = m_mock->lastArgs("mod", "fn"); + EXPECT_EQ(last.size(), 3); +} + +TEST_F(LogosApiClientOverloadsTest, FourArgs) +{ + m_client->invokeRemoteMethod("mod", "fn", QVariant(1), QVariant(2), QVariant(3), QVariant(4)); + QVariantList last = m_mock->lastArgs("mod", "fn"); + EXPECT_EQ(last.size(), 4); +} + +TEST_F(LogosApiClientOverloadsTest, FiveArgs) +{ + m_client->invokeRemoteMethod("mod", "fn", QVariant(1), QVariant(2), QVariant(3), QVariant(4), QVariant(5)); + QVariantList last = m_mock->lastArgs("mod", "fn"); + EXPECT_EQ(last.size(), 5); +} + +TEST_F(LogosApiClientOverloadsTest, ArgsList) +{ + QVariantList args; + args << QVariant("x") << QVariant("y") << QVariant("z"); + m_client->invokeRemoteMethod("mod", "fn", args); + QVariantList last = m_mock->lastArgs("mod", "fn"); + EXPECT_EQ(last.size(), 3); +} + +TEST_F(LogosApiClientOverloadsTest, AsyncZeroArgs) +{ + bool called = false; + QVariant received; + m_client->invokeRemoteMethodAsync("mod", "fn", QVariantList(), + [&](QVariant v) { called = true; received = v; }); + + // Process event loop so QTimer::singleShot fires + QCoreApplication::processEvents(); + + EXPECT_TRUE(called); + EXPECT_EQ(received.toInt(), 42); +} + +TEST_F(LogosApiClientOverloadsTest, AsyncOneArg) +{ + bool called = false; + m_client->invokeRemoteMethodAsync("mod", "fn", QVariant("a"), + [&](QVariant) { called = true; }); + QCoreApplication::processEvents(); + EXPECT_TRUE(called); +} + +TEST_F(LogosApiClientOverloadsTest, AsyncTwoArgs) +{ + bool called = false; + m_client->invokeRemoteMethodAsync("mod", "fn", QVariant(1), QVariant(2), + [&](QVariant) { called = true; }); + QCoreApplication::processEvents(); + EXPECT_TRUE(called); +} + +TEST_F(LogosApiClientOverloadsTest, AsyncThreeArgs) +{ + bool called = false; + m_client->invokeRemoteMethodAsync("mod", "fn", QVariant(1), QVariant(2), QVariant(3), + [&](QVariant) { called = true; }); + QCoreApplication::processEvents(); + EXPECT_TRUE(called); +} + +TEST_F(LogosApiClientOverloadsTest, AsyncFourArgs) +{ + bool called = false; + m_client->invokeRemoteMethodAsync("mod", "fn", QVariant(1), QVariant(2), QVariant(3), QVariant(4), + [&](QVariant) { called = true; }); + QCoreApplication::processEvents(); + EXPECT_TRUE(called); +} + +TEST_F(LogosApiClientOverloadsTest, AsyncFiveArgs) +{ + bool called = false; + m_client->invokeRemoteMethodAsync("mod", "fn", QVariant(1), QVariant(2), QVariant(3), QVariant(4), QVariant(5), + [&](QVariant) { called = true; }); + QCoreApplication::processEvents(); + EXPECT_TRUE(called); +} + +TEST_F(LogosApiClientOverloadsTest, AsyncNullCallbackIgnored) +{ + // Should not crash + m_client->invokeRemoteMethodAsync("mod", "fn", QVariantList(), nullptr); + QCoreApplication::processEvents(); +} + +TEST_F(LogosApiClientOverloadsTest, GetTokenDelegatesToTokenManager) +{ + // Token was pre-seeded by mock.when() + QString token = m_client->getToken("mod"); + EXPECT_FALSE(token.isEmpty()); +} + +TEST_F(LogosApiClientOverloadsTest, GetTokenReturnsEmptyForUnknownModule) +{ + QString token = m_client->getToken("unknown_module"); + EXPECT_TRUE(token.isEmpty()); +} + +TEST_F(LogosApiClientOverloadsTest, IsConnectedInMockMode) +{ + EXPECT_TRUE(m_client->isConnected()); +} + +TEST_F(LogosApiClientOverloadsTest, ReconnectInMockMode) +{ + EXPECT_TRUE(m_client->reconnect()); +} + +TEST_F(LogosApiClientOverloadsTest, GetTokenManagerReturnsNonNull) +{ + EXPECT_NE(m_client->getTokenManager(), nullptr); +} diff --git a/tests/sdk/test_logos_api_provider.cpp b/tests/sdk/test_logos_api_provider.cpp new file mode 100644 index 0000000..8f2e8f6 --- /dev/null +++ b/tests/sdk/test_logos_api_provider.cpp @@ -0,0 +1,94 @@ +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_api_provider.h" +#include "logos_provider_object.h" + +// Minimal provider for registration testing +class RegistrationTestProvider : public LogosProviderBase { +public: + QString providerName() const override { return "reg_test"; } + QString providerVersion() const override { return "1.0.0"; } + QVariant callMethod(const QString&, const QVariantList&) override { return QVariant(); } + QJsonArray getMethods() override { return QJsonArray(); } +}; + +class LogosApiProviderTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + } + void TearDown() override + { + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; +}; + +TEST_F(LogosApiProviderTest, RegisterProviderObjectSucceeds) +{ + LogosAPI api("test_module"); + LogosAPIProvider* provider = api.getProvider(); + + auto* testProv = new RegistrationTestProvider(); + EXPECT_TRUE(provider->registerObject("test_module", testProv)); +} + +TEST_F(LogosApiProviderTest, DoubleRegistrationFails) +{ + LogosAPI api("test_module"); + LogosAPIProvider* prov = api.getProvider(); + + auto* p1 = new RegistrationTestProvider(); + auto* p2 = new RegistrationTestProvider(); + + EXPECT_TRUE(prov->registerObject("test_module", p1)); + EXPECT_FALSE(prov->registerObject("test_module", p2)); + + delete p2; // p1 is managed by ModuleProxy +} + +TEST_F(LogosApiProviderTest, NullProviderFails) +{ + LogosAPI api("test_module"); + LogosAPIProvider* prov = api.getProvider(); + + EXPECT_FALSE(prov->registerObject("test_module", static_cast(nullptr))); +} + +TEST_F(LogosApiProviderTest, EmptyNameFails) +{ + LogosAPI api("test_module"); + LogosAPIProvider* prov = api.getProvider(); + + auto* p = new RegistrationTestProvider(); + EXPECT_FALSE(prov->registerObject("", p)); + delete p; +} + +TEST_F(LogosApiProviderTest, SaveTokenWithoutRegistrationFails) +{ + LogosAPI api("test_module"); + LogosAPIProvider* prov = api.getProvider(); + EXPECT_FALSE(prov->saveToken("from_mod", "token123")); +} + +TEST_F(LogosApiProviderTest, SaveTokenAfterRegistrationSucceeds) +{ + LogosAPI api("test_module"); + LogosAPIProvider* prov = api.getProvider(); + + auto* p = new RegistrationTestProvider(); + prov->registerObject("test_module", p); + + EXPECT_TRUE(prov->saveToken("from_mod", "token123")); +} + +TEST_F(LogosApiProviderTest, RegistryUrlContainsModuleName) +{ + LogosAPI api("my_module"); + LogosAPIProvider* prov = api.getProvider(); + QString url = prov->registryUrl(); + EXPECT_TRUE(url.contains("my_module")); +} diff --git a/tests/sdk/test_logos_instance.cpp b/tests/sdk/test_logos_instance.cpp new file mode 100644 index 0000000..d7ad132 --- /dev/null +++ b/tests/sdk/test_logos_instance.cpp @@ -0,0 +1,55 @@ +#include +#include "logos_instance.h" + +class LogosInstanceTest : public ::testing::Test { +protected: + void SetUp() override + { + // Clear env so we get a fresh ID + qunsetenv("LOGOS_INSTANCE_ID"); + } + void TearDown() override + { + qunsetenv("LOGOS_INSTANCE_ID"); + } +}; + +TEST_F(LogosInstanceTest, IdReturnsNonEmpty) +{ + QString id = LogosInstance::id(); + EXPECT_FALSE(id.isEmpty()); +} + +TEST_F(LogosInstanceTest, IdIs12Chars) +{ + QString id = LogosInstance::id(); + EXPECT_EQ(id.length(), 12); +} + +TEST_F(LogosInstanceTest, IdIsStableAcrossCalls) +{ + QString id1 = LogosInstance::id(); + QString id2 = LogosInstance::id(); + EXPECT_EQ(id1, id2); +} + +TEST_F(LogosInstanceTest, IdSetsEnvVar) +{ + QString id = LogosInstance::id(); + QByteArray env = qgetenv("LOGOS_INSTANCE_ID"); + EXPECT_EQ(QString::fromUtf8(env), id); +} + +TEST_F(LogosInstanceTest, IdRespectsExistingEnvVar) +{ + qputenv("LOGOS_INSTANCE_ID", "custom_id_123"); + QString id = LogosInstance::id(); + EXPECT_EQ(id, "custom_id_123"); +} + +TEST_F(LogosInstanceTest, RegistryUrlFormat) +{ + qputenv("LOGOS_INSTANCE_ID", "abc123def456"); + QString url = LogosInstance::id("my_module"); + EXPECT_EQ(url, "local:logos_my_module_abc123def456"); +} 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_provider_base.cpp b/tests/sdk/test_logos_provider_base.cpp new file mode 100644 index 0000000..ca68664 --- /dev/null +++ b/tests/sdk/test_logos_provider_base.cpp @@ -0,0 +1,133 @@ +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_provider_object.h" + +// Minimal concrete subclass for testing LogosProviderBase +class TestProvider : public LogosProviderBase { +public: + QString providerName() const override { return "test_provider"; } + QString providerVersion() const override { return "1.0.0"; } + + bool onInitCalled = false; + LogosAPI* receivedApi = nullptr; + + QVariant callMethod(const QString& methodName, const QVariantList& args) override + { + if (methodName == "echo") { + return args.isEmpty() ? QVariant() : args[0]; + } + if (methodName == "add" && args.size() == 2) { + return QVariant(args[0].toInt() + args[1].toInt()); + } + return QVariant(); + } + + QJsonArray getMethods() override { return QJsonArray(); } + + // Expose protected emitEvent for testing + void testEmitEvent(const QString& name, const QVariantList& data) { emitEvent(name, data); } + +protected: + void onInit(LogosAPI* api) override + { + onInitCalled = true; + receivedApi = api; + } +}; + +class LogosProviderBaseTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + } + void TearDown() override + { + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; +}; + +TEST_F(LogosProviderBaseTest, InitStoresApiAndCallsOnInit) +{ + LogosAPI api("test_module"); + TestProvider provider; + + provider.init(&api); + + EXPECT_TRUE(provider.onInitCalled); + EXPECT_EQ(provider.receivedApi, &api); +} + +TEST_F(LogosProviderBaseTest, ProviderNameAndVersion) +{ + TestProvider provider; + EXPECT_EQ(provider.providerName(), "test_provider"); + EXPECT_EQ(provider.providerVersion(), "1.0.0"); +} + +TEST_F(LogosProviderBaseTest, EmitEventCallsListener) +{ + TestProvider provider; + + QString receivedEvent; + QVariantList receivedData; + + provider.setEventListener([&](const QString& name, const QVariantList& data) { + receivedEvent = name; + receivedData = data; + }); + + provider.testEmitEvent("test_event", {QVariant(42)}); + + EXPECT_EQ(receivedEvent, "test_event"); + ASSERT_EQ(receivedData.size(), 1); + EXPECT_EQ(receivedData[0].toInt(), 42); +} + +TEST_F(LogosProviderBaseTest, EmitEventWithoutListenerIsNoOp) +{ + TestProvider provider; + // Should not crash + provider.testEmitEvent("test_event", {QVariant(1)}); +} + +TEST_F(LogosProviderBaseTest, SetEventListenerReplacesCallback) +{ + TestProvider provider; + + int callCount1 = 0, callCount2 = 0; + provider.setEventListener([&](const QString&, const QVariantList&) { callCount1++; }); + provider.testEmitEvent("e", {}); + EXPECT_EQ(callCount1, 1); + + provider.setEventListener([&](const QString&, const QVariantList&) { callCount2++; }); + provider.testEmitEvent("e", {}); + EXPECT_EQ(callCount1, 1); // not called again + EXPECT_EQ(callCount2, 1); +} + +TEST_F(LogosProviderBaseTest, InformModuleTokenSavesViaTokenManager) +{ + LogosAPI api("test_module"); + TestProvider provider; + provider.init(&api); + + EXPECT_TRUE(provider.informModuleToken("other_mod", "tok123")); + EXPECT_EQ(TokenManager::instance().getToken("other_mod"), "tok123"); +} + +TEST_F(LogosProviderBaseTest, InformModuleTokenFailsIfNotInitialized) +{ + TestProvider provider; + // Not initialized — logosAPI is null + EXPECT_FALSE(provider.informModuleToken("mod", "tok")); +} + +TEST_F(LogosProviderBaseTest, CallMethodDispatch) +{ + TestProvider provider; + EXPECT_EQ(provider.callMethod("echo", {QVariant("hello")}).toString(), "hello"); + EXPECT_EQ(provider.callMethod("add", {QVariant(3), QVariant(4)}).toInt(), 7); +} 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_mock_transport.cpp b/tests/sdk/test_mock_transport.cpp new file mode 100644 index 0000000..c5c3325 --- /dev/null +++ b/tests/sdk/test_mock_transport.cpp @@ -0,0 +1,106 @@ +#include +#include "mock_transport.h" +#include "mock_store.h" + +class MockTransportTest : public ::testing::Test { +protected: + void SetUp() override + { + MockStore::instance().reset(); + } +}; + +// -- MockTransportHost -- + +TEST_F(MockTransportTest, HostPublishObjectIsNoOp) +{ + MockTransportHost host; + EXPECT_TRUE(host.publishObject("module", nullptr)); +} + +TEST_F(MockTransportTest, HostUnpublishObjectIsNoOp) +{ + MockTransportHost host; + // Should not crash + host.unpublishObject("module"); +} + +// -- MockTransportConnection -- + +TEST_F(MockTransportTest, ConnectionAlwaysConnected) +{ + MockTransportConnection conn; + EXPECT_TRUE(conn.isConnected()); +} + +TEST_F(MockTransportTest, ConnectToHostReturnsTrue) +{ + MockTransportConnection conn; + EXPECT_TRUE(conn.connectToHost()); +} + +TEST_F(MockTransportTest, ReconnectReturnsTrue) +{ + MockTransportConnection conn; + EXPECT_TRUE(conn.reconnect()); +} + +TEST_F(MockTransportTest, RequestObjectReturnsMockObject) +{ + MockTransportConnection conn; + LogosObject* obj = conn.requestObject("test_mod", 5000); + ASSERT_NE(obj, nullptr); + obj->release(); +} + +// -- MockLogosObject -- + +TEST_F(MockTransportTest, CallMethodDelegatesToMockStore) +{ + MockStore::instance().when("test_mod", "fn").thenReturn(QVariant(42)); + + MockLogosObject obj("test_mod"); + QVariant r = obj.callMethod("auth", "fn", {}, 5000); + EXPECT_EQ(r.toInt(), 42); + EXPECT_TRUE(MockStore::instance().wasCalled("test_mod", "fn")); +} + +TEST_F(MockTransportTest, CallMethodRecordsArgs) +{ + MockLogosObject obj("mod"); + obj.callMethod("auth", "fn", {QVariant("a"), QVariant("b")}, 5000); + EXPECT_TRUE(MockStore::instance().wasCalledWith("mod", "fn", {QVariant("a"), QVariant("b")})); +} + +TEST_F(MockTransportTest, InformModuleTokenReturnsTrue) +{ + MockLogosObject obj("mod"); + EXPECT_TRUE(obj.informModuleToken("auth", "target", "tok", 5000)); +} + +TEST_F(MockTransportTest, EventOperationsAreNoOps) +{ + MockLogosObject obj("mod"); + // Should not crash + obj.onEvent("event", [](const QString&, const QVariantList&) {}); + obj.disconnectEvents(); + obj.emitEvent("event", {QVariant(1)}); +} + +TEST_F(MockTransportTest, GetMethodsReturnsEmptyArray) +{ + MockLogosObject obj("mod"); + EXPECT_TRUE(obj.getMethods().isEmpty()); +} + +TEST_F(MockTransportTest, IdReturnsNonZero) +{ + MockLogosObject obj("mod"); + EXPECT_NE(obj.id(), quintptr(0)); +} + +TEST_F(MockTransportTest, ModuleName) +{ + MockLogosObject obj("my_module"); + EXPECT_EQ(obj.moduleName(), "my_module"); +} diff --git a/tests/sdk/test_module_proxy.cpp b/tests/sdk/test_module_proxy.cpp new file mode 100644 index 0000000..0f7acea --- /dev/null +++ b/tests/sdk/test_module_proxy.cpp @@ -0,0 +1,132 @@ +#include +#include +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_provider_object.h" +#include "module_proxy.h" + +// Minimal provider for proxy testing +class ProxyTestProvider : public LogosProviderBase { +public: + QString providerName() const override { return "proxy_test"; } + QString providerVersion() const override { return "1.0.0"; } + + QVariant callMethod(const QString& methodName, const QVariantList& args) override + { + lastMethodCalled = methodName; + lastArgs = args; + return returnValue; + } + + QJsonArray getMethods() override + { + QJsonArray arr; + QJsonObject m; + m["name"] = "testMethod"; + arr.append(m); + return arr; + } + + // Expose protected emitEvent for testing + void testEmitEvent(const QString& name, const QVariantList& data) { emitEvent(name, data); } + + QString lastMethodCalled; + QVariantList lastArgs; + QVariant returnValue = QVariant(99); +}; + +class ModuleProxyTest : public ::testing::Test { +protected: + void SetUp() override + { + m_mock = new LogosMockSetup(); + m_provider = new ProxyTestProvider(); + } + void TearDown() override + { + delete m_provider; + delete m_mock; + } + LogosMockSetup* m_mock = nullptr; + ProxyTestProvider* m_provider = nullptr; +}; + +TEST_F(ModuleProxyTest, CallRemoteMethodDispatchesToProvider) +{ + ModuleProxy proxy(m_provider); + QVariant r = proxy.callRemoteMethod("token", "myMethod", {QVariant(1)}); + EXPECT_EQ(r.toInt(), 99); + EXPECT_EQ(m_provider->lastMethodCalled, "myMethod"); + EXPECT_EQ(m_provider->lastArgs.size(), 1); +} + +TEST_F(ModuleProxyTest, GetPluginMethodsDispatchesToProvider) +{ + ModuleProxy proxy(m_provider); + QJsonArray methods = proxy.getPluginMethods(); + EXPECT_EQ(methods.size(), 1); +} + +TEST_F(ModuleProxyTest, GetPluginMethodsSpecialCaseInCallRemoteMethod) +{ + ModuleProxy proxy(m_provider); + QVariant r = proxy.callRemoteMethod("token", "getPluginMethods"); + // Should return the methods array as QVariant, not dispatch to provider's callMethod + EXPECT_TRUE(r.toJsonArray().size() > 0); + // Provider's callMethod should NOT have been called for "getPluginMethods" + EXPECT_TRUE(m_provider->lastMethodCalled.isEmpty()); +} + +TEST_F(ModuleProxyTest, NullProviderHandling) +{ + ModuleProxy proxy(nullptr); + QVariant r = proxy.callRemoteMethod("token", "fn"); + EXPECT_FALSE(r.isValid()); + EXPECT_EQ(proxy.getPluginMethods().size(), 0); +} + +TEST_F(ModuleProxyTest, EmptyMethodNameHandling) +{ + ModuleProxy proxy(m_provider); + QVariant r = proxy.callRemoteMethod("token", ""); + EXPECT_FALSE(r.isValid()); + EXPECT_TRUE(m_provider->lastMethodCalled.isEmpty()); +} + +TEST_F(ModuleProxyTest, SaveTokenValidation) +{ + ModuleProxy proxy(m_provider); + + EXPECT_TRUE(proxy.saveToken("mod", "tok")); + EXPECT_FALSE(proxy.saveToken("", "tok")); // empty module name + EXPECT_FALSE(proxy.saveToken("mod", "")); // empty token +} + +TEST_F(ModuleProxyTest, EventForwarding) +{ + ModuleProxy proxy(m_provider); + QSignalSpy spy(&proxy, &ModuleProxy::eventResponse); + + // The proxy sets up event listener on construction. + // When provider emits event, proxy should emit signal. + m_provider->testEmitEvent("my_event", {QVariant("data")}); + + EXPECT_EQ(spy.count(), 1); + EXPECT_EQ(spy.at(0).at(0).toString(), "my_event"); + QVariantList data = spy.at(0).at(1).value(); + ASSERT_EQ(data.size(), 1); + EXPECT_EQ(data[0].toString(), "data"); +} + +TEST_F(ModuleProxyTest, InformModuleTokenDelegatesToProvider) +{ + LogosAPI api("origin"); + m_provider->init(&api); + ModuleProxy proxy(m_provider); + + bool result = proxy.informModuleToken("auth", "target_mod", "tok123"); + EXPECT_TRUE(result); + // Provider's informModuleToken saves via TokenManager + EXPECT_EQ(TokenManager::instance().getToken("target_mod"), "tok123"); +} diff --git a/tests/sdk/test_plugin_registry.cpp b/tests/sdk/test_plugin_registry.cpp new file mode 100644 index 0000000..b3bea65 --- /dev/null +++ b/tests/sdk/test_plugin_registry.cpp @@ -0,0 +1,80 @@ +#include +#include "plugin_registry.h" + +class PluginRegistryTest : public ::testing::Test { +protected: + void TearDown() override + { + // Clean up any registered plugins + PluginRegistry::unregisterPlugin("test_plugin"); + PluginRegistry::unregisterPlugin("another_plugin"); + } +}; + +TEST_F(PluginRegistryTest, RegisterAndHasPlugin) +{ + QObject obj; + PluginRegistry::registerPlugin(&obj, "test_plugin"); + EXPECT_TRUE(PluginRegistry::hasPlugin("test_plugin")); +} + +TEST_F(PluginRegistryTest, UnregisterPlugin) +{ + QObject obj; + PluginRegistry::registerPlugin(&obj, "test_plugin"); + EXPECT_TRUE(PluginRegistry::unregisterPlugin("test_plugin")); + EXPECT_FALSE(PluginRegistry::hasPlugin("test_plugin")); +} + +TEST_F(PluginRegistryTest, GetPluginReturnsCorrectObject) +{ + QObject obj; + PluginRegistry::registerPlugin(&obj, "test_plugin"); + QObject* retrieved = PluginRegistry::getPlugin("test_plugin"); + EXPECT_EQ(retrieved, &obj); +} + +TEST_F(PluginRegistryTest, GetPluginReturnsNullForMissing) +{ + QObject* retrieved = PluginRegistry::getPlugin("nonexistent"); + EXPECT_EQ(retrieved, nullptr); +} + +TEST_F(PluginRegistryTest, HasPluginReturnsFalseForMissing) +{ + EXPECT_FALSE(PluginRegistry::hasPlugin("nonexistent")); +} + +TEST_F(PluginRegistryTest, EmptyNameGuards) +{ + EXPECT_FALSE(PluginRegistry::hasPlugin("")); + EXPECT_FALSE(PluginRegistry::unregisterPlugin("")); + EXPECT_EQ(PluginRegistry::getPlugin(""), nullptr); +} + +TEST_F(PluginRegistryTest, KeyNormalization) +{ + EXPECT_EQ(PluginRegistry::toPluginKey("My Module"), + "logos_plugin_my_module"); + EXPECT_EQ(PluginRegistry::toPluginKey("TEST"), + "logos_plugin_test"); +} + +TEST_F(PluginRegistryTest, MultiplePlugins) +{ + QObject obj1, obj2; + PluginRegistry::registerPlugin(&obj1, "test_plugin"); + PluginRegistry::registerPlugin(&obj2, "another_plugin"); + EXPECT_TRUE(PluginRegistry::hasPlugin("test_plugin")); + EXPECT_TRUE(PluginRegistry::hasPlugin("another_plugin")); + EXPECT_NE(PluginRegistry::getPlugin("test_plugin"), + PluginRegistry::getPlugin("another_plugin")); +} + +TEST_F(PluginRegistryTest, OverwritePlugin) +{ + QObject obj1, obj2; + PluginRegistry::registerPlugin(&obj1, "test_plugin"); + PluginRegistry::registerPlugin(&obj2, "test_plugin"); + EXPECT_EQ(PluginRegistry::getPlugin("test_plugin"), &obj2); +} diff --git a/tests/sdk/test_provider_dispatch.cpp b/tests/sdk/test_provider_dispatch.cpp new file mode 100644 index 0000000..34f93f2 --- /dev/null +++ b/tests/sdk/test_provider_dispatch.cpp @@ -0,0 +1,362 @@ +#include +#include +#include +#include +#include "logos_mock.h" +#include "logos_api.h" +#include "logos_api_client.h" +#include "logos_api_provider.h" +#include "logos_object.h" +#include "module_proxy.h" +#include "plugin_registry.h" +#include "local_transport.h" +#include "fixtures/sample_provider.h" + +/** + * Tests the LOGOS_PROVIDER + LOGOS_METHOD code generation pipeline end-to-end. + * + * SampleProvider uses the LOGOS_PROVIDER macro (which declares callMethod/getMethods + * but doesn't implement them). The implementation is in the generated + * logos_provider_dispatch.cpp, produced by running the actual logos-cpp-generator + * with --provider-header at build time. + * + * If these tests break, it means a change to the SDK or generator broke the + * contract that real modules (like logos-package-manager-module) depend on. + */ +class ProviderDispatchTest : public ::testing::Test { +protected: + void SetUp() override + { + m_provider = new SampleProvider(); + } + void TearDown() override + { + delete m_provider; + } + SampleProvider* m_provider = nullptr; +}; + +// -- callMethod dispatch for various signatures -- + +TEST_F(ProviderDispatchTest, EchoStringArg) +{ + QVariant r = m_provider->callMethod("echo", {QVariant("hello world")}); + EXPECT_EQ(r.toString(), "hello world"); + EXPECT_EQ(m_provider->lastEcho, "hello world"); +} + +TEST_F(ProviderDispatchTest, InstallPluginBoolArg) +{ + QVariant r = m_provider->callMethod("installPlugin", {QVariant("/path/to/plugin"), QVariant(true)}); + EXPECT_TRUE(r.toBool()); + EXPECT_EQ(m_provider->lastPluginPath, "/path/to/plugin"); + EXPECT_TRUE(m_provider->lastSkipFlag); +} + +TEST_F(ProviderDispatchTest, InstallPluginBoolFalse) +{ + m_provider->callMethod("installPlugin", {QVariant("/other"), QVariant(false)}); + EXPECT_FALSE(m_provider->lastSkipFlag); +} + +TEST_F(ProviderDispatchTest, GetPackagesNoArgs) +{ + QVariant r = m_provider->callMethod("getPackages", {}); + QJsonArray arr = r.toJsonArray(); + EXPECT_EQ(arr.size(), 2); + EXPECT_EQ(arr[0].toObject()["name"].toString(), "pkg_a"); +} + +TEST_F(ProviderDispatchTest, GetPackagesOverloadWithCategory) +{ + QVariant r = m_provider->callMethod("getPackages", {QVariant("network")}); + QJsonArray arr = r.toJsonArray(); + EXPECT_EQ(arr.size(), 1); + EXPECT_EQ(arr[0].toObject()["category"].toString(), "network"); + EXPECT_EQ(m_provider->lastCategory, "network"); +} + +TEST_F(ProviderDispatchTest, GetCategories) +{ + QVariant r = m_provider->callMethod("getCategories", {}); + QStringList cats = r.toStringList(); + EXPECT_EQ(cats.size(), 3); + EXPECT_TRUE(cats.contains("network")); + EXPECT_TRUE(cats.contains("storage")); + EXPECT_TRUE(cats.contains("ui")); +} + +TEST_F(ProviderDispatchTest, ResolveDependencies) +{ + QStringList pkgs = {"pkg_a", "pkg_b"}; + QVariant r = m_provider->callMethod("resolveDependencies", {QVariant(pkgs)}); + QStringList resolved = r.toStringList(); + EXPECT_EQ(resolved.size(), 3); + EXPECT_TRUE(resolved.contains("common_dep")); + EXPECT_EQ(m_provider->lastDependencies, pkgs); +} + +TEST_F(ProviderDispatchTest, AddIntArgs) +{ + QVariant r = m_provider->callMethod("add", {QVariant(10), QVariant(20)}); + EXPECT_EQ(r.toInt(), 30); + EXPECT_EQ(m_provider->lastA, 10); + EXPECT_EQ(m_provider->lastB, 20); +} + +TEST_F(ProviderDispatchTest, SetDirectoryVoidReturn) +{ + QVariant r = m_provider->callMethod("setDirectory", {QVariant("/tmp/plugins")}); + // void methods return QVariant(true) per generator convention + EXPECT_TRUE(r.toBool()); + EXPECT_EQ(m_provider->lastDirectory, "/tmp/plugins"); +} + +TEST_F(ProviderDispatchTest, TestEventVoidReturnEmitsEvent) +{ + QString receivedEvent; + QVariantList receivedData; + m_provider->setEventListener([&](const QString& name, const QVariantList& data) { + receivedEvent = name; + receivedData = data; + }); + + QVariant r = m_provider->callMethod("testEvent", {QVariant("fired!")}); + EXPECT_TRUE(r.toBool()); + EXPECT_EQ(m_provider->lastEventMessage, "fired!"); + EXPECT_EQ(receivedEvent, "testEvent"); + ASSERT_EQ(receivedData.size(), 1); + EXPECT_EQ(receivedData[0].toString(), "fired!"); +} + +TEST_F(ProviderDispatchTest, UnknownMethodReturnsInvalid) +{ + QVariant r = m_provider->callMethod("nonExistentMethod", {}); + EXPECT_FALSE(r.isValid()); +} + +// -- getMethods() metadata -- + +TEST_F(ProviderDispatchTest, GetMethodsReturnsAllMethods) +{ + QJsonArray methods = m_provider->getMethods(); + // Should have all LOGOS_METHOD entries (including overloaded getPackages) + EXPECT_GE(methods.size(), 9); + + // Verify some method metadata + QSet names; + for (const QJsonValue& v : methods) { + names.insert(v.toObject()["name"].toString()); + } + EXPECT_TRUE(names.contains("echo")); + EXPECT_TRUE(names.contains("installPlugin")); + EXPECT_TRUE(names.contains("getPackages")); + EXPECT_TRUE(names.contains("getCategories")); + EXPECT_TRUE(names.contains("resolveDependencies")); + EXPECT_TRUE(names.contains("add")); + EXPECT_TRUE(names.contains("setDirectory")); + EXPECT_TRUE(names.contains("testEvent")); +} + +TEST_F(ProviderDispatchTest, GetMethodsContainsParameterInfo) +{ + QJsonArray methods = m_provider->getMethods(); + // Find "installPlugin" and verify parameters + for (const QJsonValue& v : methods) { + QJsonObject m = v.toObject(); + if (m["name"].toString() == "installPlugin") { + EXPECT_TRUE(m.contains("parameters")); + QJsonArray params = m["parameters"].toArray(); + EXPECT_EQ(params.size(), 2); + EXPECT_EQ(params[0].toObject()["type"].toString(), "QString"); + EXPECT_EQ(params[1].toObject()["type"].toString(), "bool"); + return; + } + } + FAIL() << "installPlugin not found in getMethods()"; +} + +TEST_F(ProviderDispatchTest, GetMethodsReturnTypes) +{ + QJsonArray methods = m_provider->getMethods(); + for (const QJsonValue& v : methods) { + QJsonObject m = v.toObject(); + if (m["name"].toString() == "echo") { + EXPECT_EQ(m["returnType"].toString(), "QString"); + } + if (m["name"].toString() == "setDirectory") { + EXPECT_EQ(m["returnType"].toString(), "void"); + } + if (m["name"].toString() == "add") { + EXPECT_EQ(m["returnType"].toString(), "int"); + } + } +} + +// -- LOGOS_PROVIDER macro basics -- + +TEST_F(ProviderDispatchTest, ProviderNameFromMacro) +{ + EXPECT_EQ(m_provider->providerName(), "sample_provider"); +} + +TEST_F(ProviderDispatchTest, ProviderVersionFromMacro) +{ + EXPECT_EQ(m_provider->providerVersion(), "2.0.0"); +} + +// -- Full stack: SampleProvider -> ModuleProxy -> LocalLogosObject -> callMethod -- + +class ProviderDispatchLocalStackTest : public ::testing::Test { +protected: + void SetUp() override + { + m_savedMode = LogosModeConfig::getMode(); + LogosModeConfig::setMode(LogosMode::Local); + TokenManager::instance().clearAllTokens(); + m_provider = new SampleProvider(); + m_proxy = new ModuleProxy(m_provider); + } + void TearDown() override + { + PluginRegistry::unregisterPlugin("sample_mod"); + delete m_proxy; + delete m_provider; + LogosModeConfig::setMode(m_savedMode); + } + LogosMode m_savedMode; + SampleProvider* m_provider = nullptr; + ModuleProxy* m_proxy = nullptr; +}; + +TEST_F(ProviderDispatchLocalStackTest, CallThroughLocalTransport) +{ + LocalTransportHost host; + host.publishObject("sample_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("sample_mod", 5000); + ASSERT_NE(obj, nullptr); + + // Echo through full stack + QVariant r = obj->callMethod("auth", "echo", {QVariant("stack test")}, 5000); + EXPECT_EQ(r.toString(), "stack test"); + + // Add through full stack + r = obj->callMethod("auth", "add", {QVariant(100), QVariant(200)}, 5000); + EXPECT_EQ(r.toInt(), 300); + + // Overloaded getPackages(category) through full stack + r = obj->callMethod("auth", "getPackages", {QVariant("ui")}, 5000); + QJsonArray arr = r.toJsonArray(); + EXPECT_EQ(arr.size(), 1); + EXPECT_EQ(arr[0].toObject()["category"].toString(), "ui"); + + obj->release(); +} + +TEST_F(ProviderDispatchLocalStackTest, EventsThroughFullStack) +{ + LocalTransportHost host; + host.publishObject("sample_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("sample_mod", 5000); + ASSERT_NE(obj, nullptr); + + QString receivedEvent; + QVariantList receivedData; + obj->onEvent("testEvent", [&](const QString& name, const QVariantList& data) { + receivedEvent = name; + receivedData = data; + }); + + // Call testEvent which emits an event internally + obj->callMethod("auth", "testEvent", {QVariant("hello events")}, 5000); + + EXPECT_EQ(receivedEvent, "testEvent"); + ASSERT_EQ(receivedData.size(), 1); + EXPECT_EQ(receivedData[0].toString(), "hello events"); + + obj->release(); +} + +TEST_F(ProviderDispatchLocalStackTest, GetMethodsThroughFullStack) +{ + LocalTransportHost host; + host.publishObject("sample_mod", m_proxy); + + LocalTransportConnection conn; + LogosObject* obj = conn.requestObject("sample_mod", 5000); + ASSERT_NE(obj, nullptr); + + QJsonArray methods = obj->getMethods(); + EXPECT_GE(methods.size(), 9); + + obj->release(); +} + +// -- Full stack with LogosAPI: Provider registration + Client call -- + +class ProviderDispatchApiStackTest : public ::testing::Test { +protected: + void SetUp() override + { + m_savedMode = LogosModeConfig::getMode(); + LogosModeConfig::setMode(LogosMode::Local); + TokenManager::instance().clearAllTokens(); + } + void TearDown() override + { + PluginRegistry::unregisterPlugin("sample_mod"); + LogosModeConfig::setMode(m_savedMode); + } + LogosMode m_savedMode; +}; + +TEST_F(ProviderDispatchApiStackTest, RegisterProviderAndCallViaClient) +{ + // Provider side: register a SampleProvider + LogosAPI providerApi("sample_mod"); + auto* provider = new SampleProvider(); + bool registered = providerApi.getProvider()->registerObject("sample_mod", static_cast(provider)); + EXPECT_TRUE(registered); + + // Client side: call methods via LogosAPIClient + // Seed token so client skips capability_module lookup + TokenManager::instance().saveToken("sample_mod", "test-tok"); + + LogosAPI clientApi("client_mod"); + LogosAPIClient* client = clientApi.getClient("sample_mod"); + + QVariant r = client->invokeRemoteMethod("sample_mod", "echo", QVariant("api stack")); + EXPECT_EQ(r.toString(), "api stack"); + + r = client->invokeRemoteMethod("sample_mod", "add", QVariant(5), QVariant(7)); + EXPECT_EQ(r.toInt(), 12); + + r = client->invokeRemoteMethod("sample_mod", "getCategories"); + QStringList cats = r.toStringList(); + EXPECT_EQ(cats.size(), 3); +} + +TEST_F(ProviderDispatchApiStackTest, AsyncCallThroughFullApi) +{ + LogosAPI providerApi("sample_mod"); + auto* provider = new SampleProvider(); + providerApi.getProvider()->registerObject("sample_mod", static_cast(provider)); + + TokenManager::instance().saveToken("sample_mod", "test-tok"); + LogosAPI clientApi("client_mod"); + LogosAPIClient* client = clientApi.getClient("sample_mod"); + + bool called = false; + QVariant received; + client->invokeRemoteMethodAsync("sample_mod", "echo", QVariant("async test"), + [&](QVariant v) { called = true; received = v; }); + + QCoreApplication::processEvents(); + + EXPECT_TRUE(called); + EXPECT_EQ(received.toString(), "async test"); +} diff --git a/tests/sdk/test_registry_factory.cpp b/tests/sdk/test_registry_factory.cpp new file mode 100644 index 0000000..e7b821d --- /dev/null +++ b/tests/sdk/test_registry_factory.cpp @@ -0,0 +1,47 @@ +#include +#include "logos_mode.h" +#include "logos_registry.h" +#include "logos_registry_factory.h" + +class RegistryFactoryTest : public ::testing::Test { +protected: + void SetUp() override + { + m_saved = LogosModeConfig::getMode(); + } + void TearDown() override + { + LogosModeConfig::setMode(m_saved); + } + LogosMode m_saved; +}; + +TEST_F(RegistryFactoryTest, MockModeCreatesInitializedRegistry) +{ + LogosModeConfig::setMode(LogosMode::Mock); + auto reg = LogosRegistryFactory::create("local:test"); + ASSERT_NE(reg, nullptr); + EXPECT_TRUE(reg->isInitialized()); +} + +TEST_F(RegistryFactoryTest, LocalModeCreatesInitializedRegistry) +{ + LogosModeConfig::setMode(LogosMode::Local); + auto reg = LogosRegistryFactory::create("local:test"); + ASSERT_NE(reg, nullptr); + EXPECT_TRUE(reg->isInitialized()); +} + +TEST_F(RegistryFactoryTest, ModeSwitchProducesDifferentRegistries) +{ + LogosModeConfig::setMode(LogosMode::Mock); + auto mockReg = LogosRegistryFactory::create("url"); + + LogosModeConfig::setMode(LogosMode::Local); + auto localReg = LogosRegistryFactory::create("url"); + + // Both initialized, but different objects + EXPECT_TRUE(mockReg->isInitialized()); + EXPECT_TRUE(localReg->isInitialized()); + EXPECT_NE(mockReg.get(), localReg.get()); +} 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); +} diff --git a/tests/sdk/test_transport_factory.cpp b/tests/sdk/test_transport_factory.cpp new file mode 100644 index 0000000..ce035b4 --- /dev/null +++ b/tests/sdk/test_transport_factory.cpp @@ -0,0 +1,76 @@ +#include +#include "logos_mode.h" +#include "logos_transport.h" +#include "logos_transport_factory.h" +#include "mock_transport.h" + +class TransportFactoryTest : public ::testing::Test { +protected: + void SetUp() override + { + m_saved = LogosModeConfig::getMode(); + } + void TearDown() override + { + LogosModeConfig::setMode(m_saved); + } + LogosMode m_saved; +}; + +TEST_F(TransportFactoryTest, MockModeCreatesHost) +{ + LogosModeConfig::setMode(LogosMode::Mock); + auto host = LogosTransportFactory::createHost("local:test"); + ASSERT_NE(host, nullptr); + // Verify it's a MockTransportHost by calling publishObject (no-op, returns true) + EXPECT_TRUE(host->publishObject("test", nullptr)); +} + +TEST_F(TransportFactoryTest, MockModeCreatesConnection) +{ + LogosModeConfig::setMode(LogosMode::Mock); + auto conn = LogosTransportFactory::createConnection("local:test"); + ASSERT_NE(conn, nullptr); + EXPECT_TRUE(conn->isConnected()); + EXPECT_TRUE(conn->connectToHost()); +} + +TEST_F(TransportFactoryTest, MockConnectionRequestObject) +{ + LogosModeConfig::setMode(LogosMode::Mock); + auto conn = LogosTransportFactory::createConnection("local:test"); + LogosObject* obj = conn->requestObject("my_module", 5000); + ASSERT_NE(obj, nullptr); + obj->release(); +} + +TEST_F(TransportFactoryTest, LocalModeCreatesHost) +{ + LogosModeConfig::setMode(LogosMode::Local); + auto host = LogosTransportFactory::createHost("local:test"); + ASSERT_NE(host, nullptr); +} + +TEST_F(TransportFactoryTest, LocalModeCreatesConnection) +{ + LogosModeConfig::setMode(LogosMode::Local); + auto conn = LogosTransportFactory::createConnection("local:test"); + ASSERT_NE(conn, nullptr); + EXPECT_TRUE(conn->isConnected()); +} + +TEST_F(TransportFactoryTest, ModeSwitchChangesTransportType) +{ + LogosModeConfig::setMode(LogosMode::Mock); + auto mockConn = LogosTransportFactory::createConnection("url"); + // Mock is always connected + EXPECT_TRUE(mockConn->isConnected()); + + LogosModeConfig::setMode(LogosMode::Local); + auto localConn = LogosTransportFactory::createConnection("url"); + // Local is also always connected + EXPECT_TRUE(localConn->isConnected()); + + // They should be different transport implementations + EXPECT_NE(mockConn.get(), localConn.get()); +}