diff --git a/cpp-generator/experimental/impl_header_parser.cpp b/cpp-generator/experimental/impl_header_parser.cpp index d129016..5c43639 100644 --- a/cpp-generator/experimental/impl_header_parser.cpp +++ b/cpp-generator/experimental/impl_header_parser.cpp @@ -1,5 +1,7 @@ #include "impl_header_parser.h" +#include "metadata_dependencies.h" + #include #include #include @@ -392,9 +394,9 @@ ImplParseResult parseImplHeader(const QString& headerPath, result.module.version = obj.value("version").toString().toStdString(); result.module.description = obj.value("description").toString().toStdString(); result.module.category = obj.value("category").toString().toStdString(); - QJsonArray deps = obj.value("dependencies").toArray(); - for (const QJsonValue& v : deps) - result.module.depends.push_back(v.toString().toStdString()); + const QJsonArray deps = obj.value("dependencies").toArray(); + for (const QString& depName : dependencyNames(deps)) + result.module.depends.push_back(depName.toStdString()); // Read events declared in metadata.json QJsonArray events = obj.value("events").toArray(); diff --git a/cpp-generator/legacy/generator_lib.cpp b/cpp-generator/legacy/generator_lib.cpp index 12df051..f71a8c0 100644 --- a/cpp-generator/legacy/generator_lib.cpp +++ b/cpp-generator/legacy/generator_lib.cpp @@ -1,5 +1,7 @@ #include "generator_lib.h" +#include "metadata_dependencies.h" + #include #include #include @@ -1262,3 +1264,128 @@ QString makeSourceLp(const QString& moduleName, const QString& className, const } return c; } + +// ── Umbrella (logos_sdk.h / logos_sdk.cpp) over a module's dependencies ────── + +QString makeUmbrellaHeaderFromDeps(const QJsonArray& deps, const QStringList& interfaceNames, ApiStyle apiStyle, const QString& originName) +{ + const QStringList depNames = dependencyNames(deps); + + QString content; + QTextStream s(&content); + + // Lp (Qt-free) umbrella: no LogosAPI. Each dep wrapper self-creates its + // lp_client on behalf of `originName` (this module), so the struct is + // default-constructible and the glue just does `new LogosModules()`. + if (apiStyle == ApiStyle::Lp) { + s << "#pragma once\n"; + s << "#include \n"; + if (!interfaceNames.isEmpty()) { + s << "#include \n"; + s << "#include \n"; + } + for (const QString& depName : depNames) + s << "#include \"" << depName << "_api.h\"\n"; + for (const QString& ifaceName : interfaceNames) + s << "#include \"" << ifaceName << "_api.h\"\n"; + s << "\n"; + s << "struct LogosModules {\n"; + s << " LogosModules()"; + bool first = true; + for (const QString& depName : depNames) { + s << (first ? " : " : ",\n "); + first = false; + s << depName << "(\"" << originName << "\")"; + } + s << " {}\n"; + for (const QString& depName : depNames) + s << " " << toPascalCase(depName) << " " << depName << ";\n"; + // Interface dependencies: bound at runtime. The bound wrapper is a + // THIN handle over per-provider State the umbrella OWNS for the + // module's lifetime — so a transient `modules().bind_x(p)` temporary + // can register an async callback / event subscription that outlives + // it (the LpClient + RAII subscriptions persist in the map). Keyed by + // provider so repeated binds to the same provider share one client. + for (const QString& ifaceName : interfaceNames) { + const QString className = toPascalCase(ifaceName); + s << " " << className << " bind_" << ifaceName << "(const std::string& moduleName) {\n"; + s << " auto& _st = m_" << ifaceName << "_bound[moduleName];\n"; + s << " if (!_st) _st = std::make_unique<" << className << "::State>(moduleName, \"" << originName << "\");\n"; + s << " return " << className << "(_st.get());\n"; + s << " }\n"; + } + for (const QString& ifaceName : interfaceNames) { + const QString className = toPascalCase(ifaceName); + s << " std::map> m_" + << ifaceName << "_bound;\n"; + } + s << "};\n"; + return content; + } + + // The shape doesn't depend on apiStyle — each dep emits a single + // `_api.h` whose class signature shape was already decided + // at codegen time. The umbrella just `#include`s and aggregates + // each wrapper into the flat `LogosModules` struct. + // + // Only the modules explicitly listed in `metadata.json# + // dependencies` are exposed. Apps that need to manage the core + // (basecamp, logoscore) use liblogos' C API directly rather than + // the typed `LogosModules` aggregate. + // + // Interface dependencies (`metadata.json#interface_dependencies`) are + // NOT fixed members — they bind to a runtime-chosen module — so each + // gets a `bind_(moduleName)` factory instead, returning a bound + // wrapper by value. + s << "#pragma once\n"; + // is only needed for the std::string bind_ overloads; + // omit it when there are no interfaces so the umbrella stays identical + // to its historical form for dependency-only modules. + if (!interfaceNames.isEmpty()) s << "#include \n"; + s << "#include \"logos_api.h\"\n"; + s << "#include \"logos_api_client.h\"\n\n"; + for (const QString& depName : depNames) + s << "#include \"" << depName << "_api.h\"\n"; + for (const QString& ifaceName : interfaceNames) + s << "#include \"" << ifaceName << "_api.h\"\n"; + s << "\n"; + + s << "struct LogosModules {\n"; + s << " explicit LogosModules(LogosAPI* api) : api(api)"; + for (const QString& depName : depNames) + s << ", \n " << depName << "(api)"; + s << " {}\n"; + s << " LogosAPI* api;\n"; + for (const QString& depName : depNames) + s << " " << toPascalCase(depName) << " " << depName << ";\n"; + // Bind factories — one per interface dependency. Two overloads so + // both Qt-typed (QString) and std-typed (std::string) call sites can + // pass the runtime module name without converting at the call site. + for (const QString& ifaceName : interfaceNames) { + const QString className = toPascalCase(ifaceName); + s << " " << className << " bind_" << ifaceName << "(const QString& moduleName) {\n"; + s << " return " << className << "(api, moduleName);\n"; + s << " }\n"; + s << " " << className << " bind_" << ifaceName << "(const std::string& moduleName) {\n"; + s << " return " << className << "(api, QString::fromStdString(moduleName));\n"; + s << " }\n"; + } + s << "};\n"; + return content; +} + +QString makeUmbrellaSourceFromDeps(const QJsonArray& deps, const QStringList& interfaceNames) +{ + // Each dep emits one wrapper `.cpp` (Qt or std — decided at codegen time, + // file name is the same either way), `#include`'d here. Interface wrappers + // (`_api.cpp`) are #include'd the same way. + QString content; + QTextStream s(&content); + s << "#include \"logos_sdk.h\"\n\n"; + for (const QString& depName : dependencyNames(deps)) + s << "#include \"" << depName << "_api.cpp\"\n"; + for (const QString& ifaceName : interfaceNames) + s << "#include \"" << ifaceName << "_api.cpp\"\n"; + s << "\n"; + return content; +} diff --git a/cpp-generator/legacy/generator_lib.h b/cpp-generator/legacy/generator_lib.h index 69e26e8..6e744ad 100644 --- a/cpp-generator/legacy/generator_lib.h +++ b/cpp-generator/legacy/generator_lib.h @@ -102,4 +102,18 @@ QString makeHeaderLp(const QString& moduleName, const QString& className, const QString makeSourceLp(const QString& moduleName, const QString& className, const QString& headerBaseName, const QJsonArray& methods, const QJsonArray& events = {}, BindMode bindMode = BindMode::Static, const QJsonArray& records = {}); QVector parseProviderHeader(const QString& headerPath, QTextStream& err); +// The umbrella (`logos_sdk.h` / `logos_sdk.cpp`) over a module's declared +// `metadata.json#dependencies` + interface dependencies: one `#include` and one +// `LogosModules` member per dep, so a module reaches its deps as +// `modules().`. `deps` is the raw metadata array — elements are read +// through dependencyNames() (metadata_dependencies.h), never element by +// element, so includes and members can never disagree about what it declares. +// +// ApiStyle::Lp emits the Qt-free umbrella: no LogosAPI member, each wrapper +// self-creates its lp_client on behalf of `originName` (the module being +// generated for), so the struct is default-constructible. Qt emits the +// LogosAPI-threading form, where `originName` is unused. +QString makeUmbrellaHeaderFromDeps(const QJsonArray& deps, const QStringList& interfaceNames, ApiStyle apiStyle = ApiStyle::Qt, const QString& originName = QString()); +QString makeUmbrellaSourceFromDeps(const QJsonArray& deps, const QStringList& interfaceNames); + #endif // GENERATOR_LIB_H diff --git a/cpp-generator/legacy/main.cpp b/cpp-generator/legacy/main.cpp index 5879864..f82b143 100644 --- a/cpp-generator/legacy/main.cpp +++ b/cpp-generator/legacy/main.cpp @@ -16,6 +16,7 @@ #include #include "logos_provider_interface.h" #include "generator_lib.h" +#include "metadata_dependencies.h" #include "../experimental/lidl_compat.h" #include "../experimental/impl_header_parser.h" #include "../experimental/lidl_emit_common.h" // lidlTypeToQt — the one Qt type mapper @@ -440,134 +441,11 @@ static bool writeUmbrellaHeader(const QString& genDirPath, QTextStream& err) static bool writeUmbrellaHeaderFromDeps(const QString& genDirPath, const QJsonArray& deps, const QStringList& interfaceNames, QTextStream& err, ApiStyle apiStyle = ApiStyle::Qt, const QString& originName = QString()) { - // Lp (Qt-free) umbrella: no LogosAPI. Each dep wrapper self-creates its - // lp_client on behalf of `originName` (this module), so the struct is - // default-constructible and the glue just does `new LogosModules()`. - if (apiStyle == ApiStyle::Lp) { - QDir genDir(genDirPath); - QString content; - QTextStream s(&content); - s << "#pragma once\n"; - s << "#include \n"; - if (!interfaceNames.isEmpty()) { - s << "#include \n"; - s << "#include \n"; - } - for (const QJsonValue& v : deps) { - if (!v.isString()) continue; - s << "#include \"" << v.toString() << "_api.h\"\n"; - } - for (const QString& ifaceName : interfaceNames) - s << "#include \"" << ifaceName << "_api.h\"\n"; - s << "\n"; - s << "struct LogosModules {\n"; - s << " LogosModules()"; - bool first = true; - for (const QJsonValue& v : deps) { - if (!v.isString()) continue; - s << (first ? " : " : ",\n "); - first = false; - s << v.toString() << "(\"" << originName << "\")"; - } - s << " {}\n"; - for (const QJsonValue& v : deps) { - if (!v.isString()) continue; - const QString depName = v.toString(); - s << " " << toPascalCase(depName) << " " << depName << ";\n"; - } - // Interface dependencies: bound at runtime. The bound wrapper is a - // THIN handle over per-provider State the umbrella OWNS for the - // module's lifetime — so a transient `modules().bind_x(p)` temporary - // can register an async callback / event subscription that outlives - // it (the LpClient + RAII subscriptions persist in the map). Keyed by - // provider so repeated binds to the same provider share one client. - for (const QString& ifaceName : interfaceNames) { - const QString className = toPascalCase(ifaceName); - s << " " << className << " bind_" << ifaceName << "(const std::string& moduleName) {\n"; - s << " auto& _st = m_" << ifaceName << "_bound[moduleName];\n"; - s << " if (!_st) _st = std::make_unique<" << className << "::State>(moduleName, \"" << originName << "\");\n"; - s << " return " << className << "(_st.get());\n"; - s << " }\n"; - } - for (const QString& ifaceName : interfaceNames) { - const QString className = toPascalCase(ifaceName); - s << " std::map> m_" - << ifaceName << "_bound;\n"; - } - s << "};\n"; - QFile outFile(genDir.filePath("logos_sdk.h")); - if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { - err << "Failed to write umbrella header: " << outFile.fileName() << "\n"; - return false; - } - outFile.write(content.toUtf8()); - outFile.close(); - return true; - } - - // Generate logos_sdk.h from metadata.json's dependencies list. The - // shape doesn't depend on apiStyle — each dep emits a single - // `_api.h` whose class signature shape was already decided - // at codegen time. The umbrella just `#include`s and aggregates - // each wrapper into the flat `LogosModules` struct. - // - // Only the modules explicitly listed in `metadata.json# - // dependencies` are exposed. Apps that need to manage the core - // (basecamp, logoscore) use liblogos' C API directly rather than - // the typed `LogosModules` aggregate. - // - // Interface dependencies (`metadata.json#interface_dependencies`) are - // NOT fixed members — they bind to a runtime-chosen module — so each - // gets a `bind_(moduleName)` factory instead, returning a bound - // wrapper by value. + // Emission lives in generator_lib (makeUmbrellaHeaderFromDeps) next to the + // per-module wrapper emitters, so the aggregate can be asserted on without + // a filesystem; this writes what it returns. QDir genDir(genDirPath); - QString content; - QTextStream s(&content); - s << "#pragma once\n"; - // is only needed for the std::string bind_ overloads; - // omit it when there are no interfaces so the umbrella stays identical - // to its historical form for dependency-only modules. - if (!interfaceNames.isEmpty()) s << "#include \n"; - s << "#include \"logos_api.h\"\n"; - s << "#include \"logos_api_client.h\"\n\n"; - for (const QJsonValue& v : deps) { - if (!v.isString()) continue; - QString depName = v.toString(); - s << "#include \"" << depName << "_api.h\"\n"; - } - for (const QString& ifaceName : interfaceNames) { - s << "#include \"" << ifaceName << "_api.h\"\n"; - } - s << "\n"; - - s << "struct LogosModules {\n"; - s << " explicit LogosModules(LogosAPI* api) : api(api)"; - for (const QJsonValue& v : deps) { - if (!v.isString()) continue; - QString depName = v.toString(); - s << ", \n " << depName << "(api)"; - } - s << " {}\n"; - s << " LogosAPI* api;\n"; - for (const QJsonValue& v : deps) { - if (!v.isString()) continue; - QString depName = v.toString(); - QString className = toPascalCase(depName); - s << " " << className << " " << depName << ";\n"; - } - // Bind factories — one per interface dependency. Two overloads so - // both Qt-typed (QString) and std-typed (std::string) call sites can - // pass the runtime module name without converting at the call site. - for (const QString& ifaceName : interfaceNames) { - const QString className = toPascalCase(ifaceName); - s << " " << className << " bind_" << ifaceName << "(const QString& moduleName) {\n"; - s << " return " << className << "(api, moduleName);\n"; - s << " }\n"; - s << " " << className << " bind_" << ifaceName << "(const std::string& moduleName) {\n"; - s << " return " << className << "(api, QString::fromStdString(moduleName));\n"; - s << " }\n"; - } - s << "};\n"; + const QString content = makeUmbrellaHeaderFromDeps(deps, interfaceNames, apiStyle, originName); QFile outFile(genDir.filePath("logos_sdk.h")); if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { @@ -612,23 +490,10 @@ static bool writeUmbrellaSource(const QString& genDirPath, QTextStream& err) static bool writeUmbrellaSourceFromDeps(const QString& genDirPath, const QJsonArray& deps, const QStringList& interfaceNames, QTextStream& err) { - // Generate logos_sdk.cpp from metadata.json's dependencies list. - // Each dep emits one wrapper `.cpp` (Qt or std — decided at codegen - // time, file name is the same either way), `#include`'d here. - // Interface wrappers (`_api.cpp`) are #include'd the same way. + // Emission lives in generator_lib (makeUmbrellaSourceFromDeps), alongside + // the header's; this writes what it returns. QDir genDir(genDirPath); - QString content; - QTextStream s(&content); - s << "#include \"logos_sdk.h\"\n\n"; - for (const QJsonValue& v : deps) { - if (!v.isString()) continue; - QString depName = v.toString(); - s << "#include \"" << depName << "_api.cpp\"\n"; - } - for (const QString& ifaceName : interfaceNames) { - s << "#include \"" << ifaceName << "_api.cpp\"\n"; - } - s << "\n"; + const QString content = makeUmbrellaSourceFromDeps(deps, interfaceNames); QFile outFile(genDir.filePath("logos_sdk.cpp")); if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { @@ -1185,9 +1050,7 @@ int legacy_main(int argc, char* argv[]) #endif int overallStatus = 0; - for (const QJsonValue& v : deps) { - if (!v.isString()) continue; - const QString depName = v.toString(); + for (const QString& depName : dependencyNames(deps)) { const QString pluginFileName = depName + "_plugin" + suffix; const QString pluginPath = moduleDir.filePath(pluginFileName); if (!QFileInfo::exists(pluginPath)) { @@ -1213,10 +1076,8 @@ int legacy_main(int argc, char* argv[]) } return overallStatus; } else { - for (const QJsonValue& v : deps) { - if (v.isString()) { - out << v.toString() << "\n"; - } + for (const QString& depName : dependencyNames(deps)) { + out << depName << "\n"; } out.flush(); return 0; diff --git a/cpp-generator/metadata_dependencies.h b/cpp-generator/metadata_dependencies.h new file mode 100644 index 0000000..2b79984 --- /dev/null +++ b/cpp-generator/metadata_dependencies.h @@ -0,0 +1,45 @@ +#ifndef METADATA_DEPENDENCIES_H +#define METADATA_DEPENDENCIES_H + +#include +#include +#include +#include +#include + +/// The module named by one `metadata.json` `dependencies[]` element. +/// +/// An element is either a bare name or an object holding that name alongside +/// the constraints an installer resolves it by (version range, signer DID); +/// generation needs the name only. Empty for an element that names nothing. +inline QString dependencyName(const QJsonValue& entry) +{ + if (entry.isString()) { + return entry.toString(); + } + if (entry.isObject()) { + return entry.toObject().value("name").toString(); + } + return QString(); +} + +/// Every module named by a `metadata.json` `dependencies[]` array, in order. +/// +/// Read the array through this rather than iterating it: an emitter that walks +/// `deps` itself decides on its own what an element names, and one that decides +/// differently from its neighbours emits an aggregate whose members and includes +/// disagree — which does not fail until the generated code is compiled. +/// Elements that name nothing are dropped. +inline QStringList dependencyNames(const QJsonArray& entries) +{ + QStringList names; + for (const QJsonValue& entry : entries) { + const QString name = dependencyName(entry); + if (!name.isEmpty()) { + names.append(name); + } + } + return names; +} + +#endif // METADATA_DEPENDENCIES_H diff --git a/nix/bin.nix b/nix/bin.nix index 526700a..8397422 100644 --- a/nix/bin.nix +++ b/nix/bin.nix @@ -46,6 +46,7 @@ pkgs.stdenv.mkDerivation { cp cpp-generator/experimental/lidl_compat.h \ cpp-generator/experimental/impl_header_parser.h cpp-generator/experimental/impl_header_parser.cpp \ cpp-generator/experimental/lidl_emit_common.h cpp-generator/experimental/lidl_emit_common.cpp \ + cpp-generator/metadata_dependencies.h \ $out/share/lidl-frontend/ runHook postInstall diff --git a/tests/experimental/fixtures/object_deps_metadata.json b/tests/experimental/fixtures/object_deps_metadata.json new file mode 100644 index 0000000..b151b18 --- /dev/null +++ b/tests/experimental/fixtures/object_deps_metadata.json @@ -0,0 +1,15 @@ +{ + "name": "sample_module", + "version": "1.2.3", + "description": "A sample module whose dependencies carry resolution constraints", + "author": "Test", + "type": "core", + "category": "testing", + "main": "sample_module_plugin", + "dependencies": [ + "dep_a", + { "name": "dep_b", "version": "=1.2.3" }, + { "name": "dep_c", "version": "^2.0", "signer": "did:jwk:abc" }, + {} + ] +} diff --git a/tests/experimental/test_impl_header_parser.cpp b/tests/experimental/test_impl_header_parser.cpp index bdfca02..d405afc 100644 --- a/tests/experimental/test_impl_header_parser.cpp +++ b/tests/experimental/test_impl_header_parser.cpp @@ -64,6 +64,25 @@ TEST_F(ImplHeaderParserTest, ParsesSampleImpl) EXPECT_GE(r.module.methods.size(), 10); } +// A dependency entry may carry the constraints an installer resolves it by, +// and generation still needs the name. Read as a plain string, an object entry +// came back empty and the module it names vanished from the generated +// LogosModules aggregate, so every call through it failed to compile. +TEST_F(ImplHeaderParserTest, ReadsDependenciesDeclaredInObjectForm) +{ + auto r = parseImplHeader( + fixturesDir() + "/sample_impl.h", + "SampleModuleImpl", + fixturesDir() + "/object_deps_metadata.json", + err); + ASSERT_FALSE(r.hasError()) << r.error.toStdString(); + + ASSERT_EQ(r.module.depends.size(), 3); + EXPECT_EQ(r.module.depends[0], "dep_a"); + EXPECT_EQ(r.module.depends[1], "dep_b"); + EXPECT_EQ(r.module.depends[2], "dep_c"); +} + TEST_F(ImplHeaderParserTest, MethodTypes) { auto r = parseImplHeader( diff --git a/tests/generator/CMakeLists.txt b/tests/generator/CMakeLists.txt index 0075af2..b7a8a65 100644 --- a/tests/generator/CMakeLists.txt +++ b/tests/generator/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(generator_tests test_to_qvariant_conversion.cpp test_make_header.cpp test_make_source.cpp + test_make_umbrella.cpp test_parse_provider_header.cpp test_records.cpp ) diff --git a/tests/generator/test_make_umbrella.cpp b/tests/generator/test_make_umbrella.cpp new file mode 100644 index 0000000..258a4ac --- /dev/null +++ b/tests/generator/test_make_umbrella.cpp @@ -0,0 +1,129 @@ +#include +#include +#include +#include +#include "generator_lib.h" + +// The umbrella aggregates a module's declared `metadata.json#dependencies` into +// `LogosModules`. A dependency entry is either a bare name or an object holding +// that name alongside the constraints an installer resolves it by, and the two +// forms have to generate identical code — the constraints are the installer's +// business, not the generator's. +// +// What makes this worth asserting on rather than trusting: the aggregate is +// emitted by several passes over the same array (includes, constructor +// initialisers, members), so a form only one pass understands yields a member +// whose type was never included — an aggregate that no longer compiles, and one +// that nothing catches until a module builds against it. + +namespace { + +QJsonArray depsMixedForms() +{ + QJsonObject withVersion; + withVersion["name"] = "dep_b"; + withVersion["version"] = "=1.2.3"; + + QJsonObject withSigner; + withSigner["name"] = "dep_c"; + withSigner["version"] = "^2.0"; + withSigner["signer"] = "did:jwk:abc"; + + QJsonArray deps; + deps.append("dep_a"); + deps.append(withVersion); + deps.append(withSigner); + return deps; +} + +} // namespace + +// Lp is the umbrella every `interface: universal` core module and every cdylib +// module generates (logos-plugin-qt picks --api-style lp for both). +TEST(MakeUmbrellaTest, LpAggregatesDependenciesDeclaredInEitherForm) +{ + const QString h = makeUmbrellaHeaderFromDeps(depsMixedForms(), {}, ApiStyle::Lp, "sample_module"); + + EXPECT_TRUE(h.contains("#include \"dep_a_api.h\"")) << h.toStdString(); + EXPECT_TRUE(h.contains("#include \"dep_b_api.h\"")) << h.toStdString(); + EXPECT_TRUE(h.contains("#include \"dep_c_api.h\"")) << h.toStdString(); + + EXPECT_TRUE(h.contains("DepA dep_a;")) << h.toStdString(); + EXPECT_TRUE(h.contains("DepB dep_b;")) << h.toStdString(); + EXPECT_TRUE(h.contains("DepC dep_c;")) << h.toStdString(); + + // Lp wrappers self-create their lp_client on behalf of this module. + EXPECT_TRUE(h.contains("dep_b(\"sample_module\")")) << h.toStdString(); + EXPECT_TRUE(h.contains("dep_c(\"sample_module\")")) << h.toStdString(); +} + +TEST(MakeUmbrellaTest, QtAggregatesDependenciesDeclaredInEitherForm) +{ + const QString h = makeUmbrellaHeaderFromDeps(depsMixedForms(), {}, ApiStyle::Qt); + + EXPECT_TRUE(h.contains("#include \"dep_a_api.h\"")) << h.toStdString(); + EXPECT_TRUE(h.contains("#include \"dep_b_api.h\"")) << h.toStdString(); + EXPECT_TRUE(h.contains("#include \"dep_c_api.h\"")) << h.toStdString(); + + EXPECT_TRUE(h.contains("DepA dep_a;")) << h.toStdString(); + EXPECT_TRUE(h.contains("DepB dep_b;")) << h.toStdString(); + EXPECT_TRUE(h.contains("DepC dep_c;")) << h.toStdString(); + + EXPECT_TRUE(h.contains("dep_b(api)")) << h.toStdString(); + EXPECT_TRUE(h.contains("dep_c(api)")) << h.toStdString(); +} + +// Every dep a member declaration mentions must have been included, in both +// flavors — the pairing is the invariant, independent of which form declared it. +TEST(MakeUmbrellaTest, EveryMemberTypeIsIncluded) +{ + for (ApiStyle style : {ApiStyle::Lp, ApiStyle::Qt}) { + const QString h = makeUmbrellaHeaderFromDeps(depsMixedForms(), {}, style, "sample_module"); + for (const QString& dep : {QStringLiteral("dep_a"), QStringLiteral("dep_b"), QStringLiteral("dep_c")}) { + const bool included = h.contains("#include \"" + dep + "_api.h\""); + const bool member = h.contains(toPascalCase(dep) + " " + dep + ";"); + EXPECT_EQ(included, member) + << "'" << dep.toStdString() << "' is a member without an include (or vice versa):\n" + << h.toStdString(); + } + } +} + +TEST(MakeUmbrellaTest, SourceIncludesEveryDependencyWrapper) +{ + const QString c = makeUmbrellaSourceFromDeps(depsMixedForms(), {"some_iface"}); + + EXPECT_TRUE(c.contains("#include \"dep_a_api.cpp\"")) << c.toStdString(); + EXPECT_TRUE(c.contains("#include \"dep_b_api.cpp\"")) << c.toStdString(); + EXPECT_TRUE(c.contains("#include \"dep_c_api.cpp\"")) << c.toStdString(); + EXPECT_TRUE(c.contains("#include \"some_iface_api.cpp\"")) << c.toStdString(); +} + +// An entry that names nothing is dropped rather than emitted as an empty +// member, and drops out of the aggregate entirely. +TEST(MakeUmbrellaTest, EntryNamingNothingIsDropped) +{ + QJsonArray deps; + deps.append("dep_a"); + deps.append(QJsonObject{}); + deps.append(QJsonObject{{"version", "=1.0.0"}}); + + for (ApiStyle style : {ApiStyle::Lp, ApiStyle::Qt}) { + const QString h = makeUmbrellaHeaderFromDeps(deps, {}, style, "sample_module"); + EXPECT_TRUE(h.contains("DepA dep_a;")) << h.toStdString(); + EXPECT_FALSE(h.contains("#include \"_api.h\"")) << h.toStdString(); + EXPECT_FALSE(h.contains("Module ;")) << h.toStdString(); + } +} + +// A module with no dependencies still gets a compilable, empty aggregate. +TEST(MakeUmbrellaTest, NoDependenciesStillEmitsTheAggregate) +{ + const QString lp = makeUmbrellaHeaderFromDeps({}, {}, ApiStyle::Lp, "sample_module"); + EXPECT_TRUE(lp.contains("struct LogosModules {")) << lp.toStdString(); + EXPECT_TRUE(lp.contains("LogosModules() {}")) << lp.toStdString(); + + const QString qt = makeUmbrellaHeaderFromDeps({}, {}, ApiStyle::Qt); + EXPECT_TRUE(qt.contains("struct LogosModules {")) << qt.toStdString(); + EXPECT_TRUE(qt.contains("LogosAPI* api;")) << qt.toStdString(); +}