#include #include #include #include "impl_header_parser.h" #include #include #include #include // Helper: find the fixtures directory. // 1. FIXTURES_DIR env var — set by CI to point to installed fixtures // 2. FIXTURES_DIR compile define — set by CMake, works during ctest in nix sandbox // 3. ../fixtures relative to binary — nix install layout ($out/bin/ + $out/fixtures/) static QString fixturesDir() { // Environment variable takes priority (set by CI or user) QByteArray envDir = qgetenv("FIXTURES_DIR"); if (!envDir.isEmpty() && QDir(envDir).exists()) return QString::fromUtf8(envDir); #ifdef FIXTURES_DIR if (QDir(FIXTURES_DIR).exists()) return QString(FIXTURES_DIR); #endif // Installed layout: $out/bin/experimental_tests + $out/fixtures/ QString binDir = QCoreApplication::applicationDirPath(); if (!binDir.isEmpty()) { QString installed = QDir::cleanPath(binDir + "/../fixtures"); if (QDir(installed).exists()) return installed; } return QDir::currentPath() + "/fixtures"; } class ImplHeaderParserTest : public ::testing::Test { protected: QString errOutput; QTextStream err{&errOutput}; }; // --------------------------------------------------------------------------- // Basic parsing // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, ParsesSampleImpl) { auto r = parseImplHeader( fixturesDir() + "/sample_impl.h", "SampleModuleImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); // Metadata from JSON EXPECT_EQ(r.module.name, "sample_module"); EXPECT_EQ(r.module.version, "1.2.3"); EXPECT_EQ(r.module.description, "A sample module for testing"); EXPECT_EQ(r.module.category, "testing"); ASSERT_EQ(r.module.depends.size(), 2); EXPECT_EQ(r.module.depends[0], "dep_a"); // Methods — should find all public methods, skip ctor/dtor/private 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( fixturesDir() + "/sample_impl.h", "SampleModuleImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); // Find specific methods and check their types auto findMethod = [&](const std::string& name) -> const MethodDecl* { for (const auto& m : r.module.methods) if (m.name == name) return &m; return nullptr; }; // std::string greet(const std::string& name) → tstr auto greet = findMethod("greet"); ASSERT_NE(greet, nullptr); EXPECT_EQ(greet->returnType.name, "tstr"); ASSERT_EQ(greet->params.size(), 1); EXPECT_EQ(greet->params[0].name, "name"); EXPECT_EQ(greet->params[0].type.name, "tstr"); // bool isValid(const std::string& input) → bool auto isValid = findMethod("isValid"); ASSERT_NE(isValid, nullptr); EXPECT_EQ(isValid->returnType.name, "bool"); // int64_t getCount() → int auto getCount = findMethod("getCount"); ASSERT_NE(getCount, nullptr); EXPECT_EQ(getCount->returnType.name, "int"); EXPECT_TRUE(getCount->params.empty()); // uint64_t getSize() → uint auto getSize = findMethod("getSize"); ASSERT_NE(getSize, nullptr); EXPECT_EQ(getSize->returnType.name, "uint"); // double getScore() → float64 auto getScore = findMethod("getScore"); ASSERT_NE(getScore, nullptr); EXPECT_EQ(getScore->returnType.name, "float64"); // void doNothing() → void auto doNothing = findMethod("doNothing"); ASSERT_NE(doNothing, nullptr); EXPECT_EQ(doNothing->returnType.name, "void"); // std::vector getNames() → [tstr] auto getNames = findMethod("getNames"); ASSERT_NE(getNames, nullptr); EXPECT_EQ(getNames->returnType.kind, TypeExpr::Array); EXPECT_EQ(getNames->returnType.elements[0].name, "tstr"); // std::vector getData() → bstr auto getData = findMethod("getData"); ASSERT_NE(getData, nullptr); EXPECT_EQ(getData->returnType.name, "bstr"); // std::vector getIds() → [int] auto getIds = findMethod("getIds"); ASSERT_NE(getIds, nullptr); EXPECT_EQ(getIds->returnType.kind, TypeExpr::Array); EXPECT_EQ(getIds->returnType.elements[0].name, "int"); // std::string combine(const std::string& a, const std::string& b, int64_t count) auto combine = findMethod("combine"); ASSERT_NE(combine, nullptr); EXPECT_EQ(combine->returnType.name, "tstr"); ASSERT_EQ(combine->params.size(), 3); EXPECT_EQ(combine->params[0].name, "a"); EXPECT_EQ(combine->params[1].name, "b"); EXPECT_EQ(combine->params[2].name, "count"); EXPECT_EQ(combine->params[2].type.name, "int"); } TEST_F(ImplHeaderParserTest, SkipsPrivateMethods) { auto r = parseImplHeader( fixturesDir() + "/sample_impl.h", "SampleModuleImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); for (const auto& m : r.module.methods) { EXPECT_NE(m.name, "internalHelper") << "Private method should not be parsed"; } } // --------------------------------------------------------------------------- // Empty class // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, EmptyClass) { auto r = parseImplHeader( fixturesDir() + "/empty_class_impl.h", "EmptyClassImpl", fixturesDir() + "/empty_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); EXPECT_TRUE(r.module.methods.empty()); // Should have a warning in err output EXPECT_TRUE(errOutput.contains("Warning")); } // --------------------------------------------------------------------------- // Complex class with access specifier changes // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, ComplexAccessSpecifiers) { auto r = parseImplHeader( fixturesDir() + "/complex_impl.h", "ComplexModuleImpl", fixturesDir() + "/empty_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); auto findMethod = [&](const std::string& name) -> const MethodDecl* { for (const auto& m : r.module.methods) if (m.name == name) return &m; return nullptr; }; // First public section EXPECT_NE(findMethod("firstMethod"), nullptr); // Second public section (after protected) EXPECT_NE(findMethod("secondMethod"), nullptr); EXPECT_NE(findMethod("thirdMethod"), nullptr); // Protected method should be skipped EXPECT_EQ(findMethod("protectedHelper"), nullptr); // Private method should be skipped EXPECT_EQ(findMethod("privateHelper"), nullptr); } // --------------------------------------------------------------------------- // Error cases // --------------------------------------------------------------------------- // A struct in an impl header becomes a contract `type` — but ONLY if the API // mentions it. A header routinely declares private helpers, and publishing // those would change the module's interface as a side effect of an internal // refactor. Verified against two real modules: openmetrics' `ModuleSource` and // the package manager's in-class `PendingAction` were both being published. TEST_F(ImplHeaderParserTest, OnlyApiReferencedStructsBecomeRecords) { auto r = parseImplHeader( fixturesDir() + "/records_impl.h", "RecordsImpl", fixturesDir() + "/records_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); std::vector names; for (const auto& t : r.module.types) names.push_back(t.name); std::sort(names.begin(), names.end()); // Blob is named directly; Wrapper too. Internal and Helper are not. ASSERT_EQ(names.size(), 2u) << "published: " << [&]{ std::string j; for (const auto& n : names) j += n + " "; return j; }(); EXPECT_EQ(names[0], "Blob"); EXPECT_EQ(names[1], "Wrapper"); // A field with a trailing comment must NOT be silently dropped: a record // published with a partial field list looks like a contract and is not one. for (const auto& t : r.module.types) { if (t.name != "Blob") continue; ASSERT_EQ(t.fields.size(), 3u); EXPECT_EQ(t.fields[2].name, "payload"); EXPECT_EQ(t.fields[2].type.name, "bstr"); } } // The closure is transitive: a record reaches the contract because something // the API names refers to it, however indirectly. TEST_F(ImplHeaderParserTest, RecordsReachableOnlyThroughAnotherRecordAreKept) { auto r = parseImplHeader( fixturesDir() + "/records_impl.h", "RecordsImpl", fixturesDir() + "/records_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); // Wrapper's own fields name Blob; both survive even though a signature // could have named only one of them. bool sawWrapper = false; for (const auto& t : r.module.types) { if (t.name != "Wrapper") continue; sawWrapper = true; ASSERT_EQ(t.fields.size(), 2u); EXPECT_EQ(t.fields[0].type.name, "Blob"); EXPECT_EQ(t.fields[1].type.elements.at(0).name, "Blob"); } EXPECT_TRUE(sawWrapper); } TEST_F(ImplHeaderParserTest, MissingHeaderFile) { auto r = parseImplHeader( "/nonexistent/path.h", "Foo", fixturesDir() + "/sample_metadata.json", err); EXPECT_TRUE(r.hasError()); EXPECT_TRUE(r.error.contains("Failed to open header")); } TEST_F(ImplHeaderParserTest, MissingMetadataFile) { auto r = parseImplHeader( fixturesDir() + "/sample_impl.h", "SampleModuleImpl", "/nonexistent/metadata.json", err); EXPECT_TRUE(r.hasError()); EXPECT_TRUE(r.error.contains("Failed to open metadata")); } TEST_F(ImplHeaderParserTest, WrongClassName) { auto r = parseImplHeader( fixturesDir() + "/sample_impl.h", "NonExistentClass", fixturesDir() + "/sample_metadata.json", err); // Not an error per se, but should find zero methods and warn ASSERT_FALSE(r.hasError()); EXPECT_TRUE(r.module.methods.empty()); EXPECT_TRUE(errOutput.contains("Warning")); } // --------------------------------------------------------------------------- // LogosMap / LogosList, Qt collections, metadata events, emitEvent detection // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, UniversalTypesAndMetadataEvents) { auto r = parseImplHeader( fixturesDir() + "/universal_impl.h", "UniversalImpl", fixturesDir() + "/universal_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); EXPECT_EQ(r.module.name, "universal_mod"); EXPECT_EQ(r.module.version, "2.0.0"); ASSERT_EQ(r.module.events.size(), 1); EXPECT_EQ(r.module.events[0].name, "onReady"); // Optional per-event description carried from metadata.json events[]. EXPECT_EQ(r.module.events[0].description, "Fired once the module is ready."); ASSERT_EQ(r.module.events[0].params.size(), 1); EXPECT_EQ(r.module.events[0].params[0].name, "info"); EXPECT_EQ(r.module.events[0].params[0].type.name, "tstr"); auto findMethod = [&](const std::string& name) -> const MethodDecl* { for (const auto& m : r.module.methods) if (m.name == name) return &m; return nullptr; }; // The fixture declares a `std::function<…> emitEvent` member. The old // legacy hook treated it specially; now such members are simply skipped // and never mistaken for a callable method. EXPECT_EQ(findMethod("emitEvent"), nullptr); auto fetchMap = findMethod("fetchMap"); ASSERT_NE(fetchMap, nullptr); EXPECT_EQ(fetchMap->returnType.kind, TypeExpr::Map); EXPECT_TRUE(fetchMap->jsonReturn); auto fetchList = findMethod("fetchList"); ASSERT_NE(fetchList, nullptr); EXPECT_EQ(fetchList->returnType.kind, TypeExpr::Array); EXPECT_EQ(fetchList->returnType.elements[0].name, "any"); EXPECT_TRUE(fetchList->jsonReturn); auto asVariantMap = findMethod("asVariantMap"); ASSERT_NE(asVariantMap, nullptr); EXPECT_EQ(asVariantMap->returnType.kind, TypeExpr::Map); EXPECT_FALSE(asVariantMap->jsonReturn); auto listNames = findMethod("listNames"); ASSERT_NE(listNames, nullptr); EXPECT_EQ(listNames->returnType.kind, TypeExpr::Array); EXPECT_EQ(listNames->returnType.elements[0].name, "tstr"); EXPECT_FALSE(listNames->jsonReturn); auto anyList = findMethod("anyList"); ASSERT_NE(anyList, nullptr); EXPECT_EQ(anyList->returnType.kind, TypeExpr::Array); EXPECT_EQ(anyList->returnType.elements[0].name, "any"); EXPECT_FALSE(anyList->jsonReturn); auto fetchResult = findMethod("fetchResult"); ASSERT_NE(fetchResult, nullptr); EXPECT_EQ(fetchResult->returnType.kind, TypeExpr::Primitive); EXPECT_EQ(fetchResult->returnType.name, "result"); EXPECT_FALSE(fetchResult->jsonReturn); EXPECT_TRUE(fetchResult->resultReturn); auto fetchResultNodiscard = findMethod("fetchResultNodiscard"); ASSERT_NE(fetchResultNodiscard, nullptr); EXPECT_EQ(fetchResultNodiscard->returnType.name, "result"); EXPECT_TRUE(fetchResultNodiscard->resultReturn); auto fetchResultStatic = findMethod("fetchResultStatic"); ASSERT_NE(fetchResultStatic, nullptr); EXPECT_EQ(fetchResultStatic->returnType.name, "result"); EXPECT_TRUE(fetchResultStatic->resultReturn); auto fetchResultNodiscardStatic = findMethod("fetchResultNodiscardStatic"); ASSERT_NE(fetchResultNodiscardStatic, nullptr); EXPECT_EQ(fetchResultNodiscardStatic->returnType.name, "result"); EXPECT_TRUE(fetchResultNodiscardStatic->resultReturn); auto fetchResultStaticNodiscard = findMethod("fetchResultStaticNodiscard"); ASSERT_NE(fetchResultStaticNodiscard, nullptr); EXPECT_EQ(fetchResultStaticNodiscard->returnType.name, "result"); EXPECT_TRUE(fetchResultStaticNodiscard->resultReturn); auto fetchResultMultiAttr = findMethod("fetchResultMultiAttr"); ASSERT_NE(fetchResultMultiAttr, nullptr); EXPECT_EQ(fetchResultMultiAttr->returnType.name, "result"); EXPECT_TRUE(fetchResultMultiAttr->resultReturn); auto fetchResultInlineStatic = findMethod("fetchResultInlineStatic"); ASSERT_NE(fetchResultInlineStatic, nullptr); EXPECT_EQ(fetchResultInlineStatic->returnType.name, "result"); EXPECT_TRUE(fetchResultInlineStatic->resultReturn); auto fetchResultConsteval = findMethod("fetchResultConsteval"); ASSERT_NE(fetchResultConsteval, nullptr); EXPECT_EQ(fetchResultConsteval->returnType.name, "result"); EXPECT_TRUE(fetchResultConsteval->resultReturn); for (const auto& m : r.module.methods) { EXPECT_NE(m.name, "void") << "Keyword should not appear as method name"; } } // --------------------------------------------------------------------------- // Event doc comments: `///` above a `logos_events:` declaration becomes the // event's description (same capture rules as methods: doc-comments only, // adjacent-only, multi-line joined with \n). // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, EventDocCommentsFromHeader) { auto r = parseImplHeader( fixturesDir() + "/documented_events_impl.h", "DocumentedEventsImpl", fixturesDir() + "/documented_events_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.events.size(), 3); // Multi-line `///` doc comment: the two lines are joined with a newline. EXPECT_EQ(r.module.events[0].name, "userLoggedIn"); EXPECT_EQ(r.module.events[0].description, "Fired once the user has authenticated.\n" "Carries the freshly issued session token."); ASSERT_EQ(r.module.events[0].params.size(), 2); EXPECT_EQ(r.module.events[0].params[0].name, "userId"); EXPECT_EQ(r.module.events[0].params[1].name, "token"); // A plain `//` comment is not a doc comment → no description captured. EXPECT_EQ(r.module.events[1].name, "heartbeat"); EXPECT_TRUE(r.module.events[1].description.empty()); // Single-line `///` doc comment. EXPECT_EQ(r.module.events[2].name, "shutdown"); EXPECT_EQ(r.module.events[2].description, "Single-line documented event."); } // --------------------------------------------------------------------------- // Issue #76: a section specifier and a declaration on the *same* physical // line (`logos_events : void foo();`, as clang-format / prettier produce) // must be parsed identically to the newline-separated form. The code after // the colon must not be discarded. // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, SameLineSectionSpecifiers) { auto r = parseImplHeader( fixturesDir() + "/same_line_events_impl.h", "SameLineEventsImpl", fixturesDir() + "/same_line_events_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); auto findEvent = [&](const std::string& name) -> const EventDecl* { for (const auto& e : r.module.events) if (e.name == name) return &e; return nullptr; }; auto findMethod = [&](const std::string& name) -> const MethodDecl* { for (const auto& m : r.module.methods) if (m.name == name) return &m; return nullptr; }; // The exact prettier form from the issue: // logos_events : void versionReady(const std::string &version); // Previously the prototype after the colon was discarded entirely. const EventDecl* versionReady = findEvent("versionReady"); ASSERT_NE(versionReady, nullptr) << "Same-line `logos_events :` prototype must still be parsed"; ASSERT_EQ(versionReady->params.size(), 1); EXPECT_EQ(versionReady->params[0].name, "version"); EXPECT_EQ(versionReady->params[0].type.name, "tstr"); // The `///` doc comment above the collapsed line must attach: in the // same-line form there is nowhere else for it to go, so documentation // must not be formatting-dependent either. EXPECT_EQ(versionReady->description, "Fired once the latest version is known."); // An event declared after the section is already open, also same-line. const EventDecl* downloadProgress = findEvent("downloadProgress"); ASSERT_NE(downloadProgress, nullptr); ASSERT_EQ(downloadProgress->params.size(), 2); EXPECT_EQ(downloadProgress->params[0].name, "id"); EXPECT_EQ(downloadProgress->params[0].type.name, "tstr"); EXPECT_EQ(downloadProgress->params[1].name, "percent"); EXPECT_EQ(downloadProgress->params[1].type.name, "int"); // The newline-separated form keeps working alongside the collapsed form. EXPECT_NE(findEvent("shutdown"), nullptr); // Exactly the three events above — no phantom or dropped entries. EXPECT_EQ(r.module.events.size(), 3); // The symmetric case: `public : ` on one line must surface the // method too (the access specifier no longer swallows the declaration). const MethodDecl* greet = findMethod("greet"); ASSERT_NE(greet, nullptr) << "Same-line `public:` declaration must still be parsed"; EXPECT_EQ(greet->returnType.name, "tstr"); ASSERT_EQ(greet->params.size(), 1); EXPECT_EQ(greet->params[0].name, "name"); EXPECT_EQ(greet->params[0].type.name, "tstr"); // The same-line events must land in events[], never leak into methods[]. EXPECT_EQ(findMethod("versionReady"), nullptr); EXPECT_EQ(findMethod("downloadProgress"), nullptr); } TEST_F(ImplHeaderParserTest, ParsesMultiLineSignature) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = dir.filePath("ml_impl.h"); { QFile f(hp); ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text)); f.write( "#pragma once\n" "#include \n" "class MlImpl {\n" "public:\n" " std::string single(const std::string& a);\n" " std::string wrapped(const std::string& first,\n" " const std::string& second);\n" "};\n"); } auto r = parseImplHeader(hp, "MlImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); QStringList names; for (const auto& m : r.module.methods) names << QString::fromStdString(m.name); EXPECT_TRUE(names.contains("single")); EXPECT_TRUE(names.contains("wrapped")) << "got: " << names.join(",").toStdString(); } // --------------------------------------------------------------------------- // Optionality, header-first // // `std::optional` used to fall through to the opaque `any` fallback with no // diagnostic, so a header-first C++ provider could not express an optional at // all: it declared one and published a contract that said something else. // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, StdOptionalBecomesOptional) { auto r = parseImplHeader( fixturesDir() + "/optional_impl.h", "OptionalImpl", fixturesDir() + "/optional_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); auto findType = [&](const char* n) -> const TypeDecl* { for (const auto& t : r.module.types) if (t.name == n) return &t; return nullptr; }; auto findMethod = [&](const char* n) -> const MethodDecl* { for (const auto& m : r.module.methods) if (m.name == n) return &m; return nullptr; }; const TypeDecl* profile = findType("Profile"); ASSERT_NE(profile, nullptr); auto fieldNamed = [&](const char* n) -> const FieldDecl* { for (const auto& f : profile->fields) if (f.name == n) return &f; return nullptr; }; // A plain member stays required. ASSERT_NE(fieldNamed("required"), nullptr); EXPECT_FALSE(fieldIsOptional(*fieldNamed("required"))); // Optional members carry the value type, not `any`. ASSERT_NE(fieldNamed("nickname"), nullptr); EXPECT_TRUE(fieldIsOptional(*fieldNamed("nickname"))); EXPECT_EQ(fieldValueType(*fieldNamed("nickname")).name, "tstr"); EXPECT_EQ(fieldValueType(*fieldNamed("age")).name, "uint"); EXPECT_EQ(fieldValueType(*fieldNamed("avatar")).name, "bstr"); // Optional composes with a declared record — and `std::optional` is // still a MENTION of Blob, so the record survives the // keep-only-referenced-records pass instead of being dropped as unused. ASSERT_NE(fieldNamed("blob"), nullptr); EXPECT_EQ(fieldValueType(*fieldNamed("blob")).kind, TypeExpr::Named); EXPECT_EQ(fieldValueType(*fieldNamed("blob")).name, "Blob"); EXPECT_NE(findType("Blob"), nullptr); // Parameters and returns, and optional nested inside a container. const MethodDecl* echo = findMethod("echoOptional"); ASSERT_NE(echo, nullptr); ASSERT_EQ(echo->params.size(), 1u); EXPECT_TRUE(paramIsOptional(echo->params[0])); EXPECT_EQ(paramValueType(echo->params[0]).name, "tstr"); EXPECT_TRUE(typeIsOptional(echo->returnType)); const MethodDecl* lst = findMethod("echoOptionalList"); ASSERT_NE(lst, nullptr); ASSERT_EQ(lst->params.size(), 1u); EXPECT_EQ(lst->params[0].type.kind, TypeExpr::Array); ASSERT_EQ(lst->params[0].type.elements.size(), 1u); EXPECT_EQ(lst->params[0].type.elements[0].kind, TypeExpr::Optional); // A required method is untouched. const MethodDecl* req = findMethod("required"); ASSERT_NE(req, nullptr); EXPECT_FALSE(paramIsOptional(req->params[0])); EXPECT_FALSE(typeIsOptional(req->returnType)); // The event parameter, likewise. ASSERT_EQ(r.module.events.size(), 1u); ASSERT_EQ(r.module.events[0].params.size(), 2u); EXPECT_FALSE(paramIsOptional(r.module.events[0].params[0])); EXPECT_TRUE(paramIsOptional(r.module.events[0].params[1])); } // std::optional> has NO LIDL type: three C++ states over a // two-state wire. It maps down to `?T` — which is what makes the author's own // declaration stop compiling against the generated codec, deliberately — and // says so here, rather than leaving a conversion error in generated code. TEST_F(ImplHeaderParserTest, NestedOptionalCollapsesAndIsReported) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = dir.filePath("nested_impl.h"); { QFile f(hp); ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text)); f.write( "#pragma once\n" "#include \n" "#include \n" "struct Rec {\n" " std::optional> collapsed;\n" "};\n" "class NestedImpl {\n" "public:\n" " Rec echo(const Rec& v);\n" "};\n"); } auto r = parseImplHeader(hp, "NestedImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); ASSERT_EQ(r.module.types[0].fields.size(), 1u); const FieldDecl& f = r.module.types[0].fields[0]; EXPECT_TRUE(fieldIsOptional(f)); // Collapsed to ONE layer — the contract may not carry a third state. EXPECT_EQ(fieldValueType(f).kind, TypeExpr::Primitive); EXPECT_EQ(fieldValueType(f).name, "tstr"); err.flush(); EXPECT_TRUE(errOutput.contains("std::optional>")) << errOutput.toStdString(); EXPECT_TRUE(errOutput.contains("no LIDL type")) << errOutput.toStdString(); } // A one-class header written to a temp dir and parsed, for the cases that are // about a single declaration rather than a whole fixture module. namespace { QString probeHeader(QTemporaryDir& dir, const QString& body) { const QString hp = dir.filePath("probe_impl.h"); QFile f(hp); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) return QString(); f.write(("#pragma once\n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" "#include \n" + body).toUtf8()); return hp; } } // namespace // `nlohmann::json` reaches `any` only through the fallback, and `any` is the // RIGHT answer for it — test_fullapi_cpp's echoAny / fireAnyEvent / anyEvent // are the conformance matrix's `any` cells. Naming it is what lets the fallback // become an error without taking them out. TEST_F(ImplHeaderParserTest, NlohmannJsonIsAnyByName) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "class ProbeImpl {\n" "public:\n" " nlohmann::json echoAny(const nlohmann::json& v);\n" " bool fireAnyEvent(const nlohmann::json& v);\n" "logos_events:\n" " void anyEvent(const nlohmann::json& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.methods.size(), 2u); EXPECT_EQ(r.module.methods[0].returnType.name, "any"); EXPECT_EQ(r.module.methods[0].params[0].type.name, "any"); EXPECT_EQ(r.module.methods[1].params[0].type.name, "any"); ASSERT_EQ(r.module.events.size(), 1u); EXPECT_EQ(r.module.events[0].params[0].type.name, "any"); } // --------------------------------------------------------------------------- // A C++ spelling with no LIDL type is a BUILD ERROR, not a silent `any`. // // cppTypeToLidl used to end with `// Fallback: treat as opaque` -> `any`, and // `any` is admitted by every backend gate. So an unrecognised spelling was // accepted in silence, published as `any`, and dispatched as a raw // `lidlImpl().f(args.at(0))` with no decode and no check. // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, NarrowNumericIsRejectedWithTheWidening) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "class ProbeImpl {\n" "public:\n" " int64_t f(uint32_t depth);\n" "};\n"); ASSERT_FALSE(hp.isEmpty()); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_TRUE(r.hasError()) << "uint32_t was admitted as `any`"; // Names the declaration, the offending type, and what to write instead. EXPECT_TRUE(r.error.contains("method 'f': parameter 'depth'")) << r.error.toStdString(); EXPECT_TRUE(r.error.contains("`uint32_t`")) << r.error.toStdString(); EXPECT_TRUE(r.error.contains("`uint64_t`")) << r.error.toStdString(); } // The offender may be nested. Report BOTH: the element with no LIDL type, and // the declaration that carries it. TEST_F(ImplHeaderParserTest, NestedOffenderNamesTheDeclarationToo) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "class ProbeImpl {\n" "public:\n" " int64_t f(const std::vector& instruction);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_TRUE(r.hasError()); EXPECT_TRUE(r.error.contains("const std::vector&")) << r.error.toStdString(); EXPECT_TRUE(r.error.contains("`uint32_t`")) << r.error.toStdString(); } // Each family gets a hint that names a replacement. A diagnostic without one // just moves the guesswork. TEST_F(ImplHeaderParserTest, EachUnsupportedFamilyNamesItsReplacement) { struct Case { const char* decl; const char* mentions; }; const Case cases[] = { {"int64_t f(float v);", "`double`"}, {"int64_t f(uint8_t v);", "std::vector"}, {"int64_t f(size_t v);", "`uint64_t`"}, {"int64_t f(const std::set& v);", "std::vector"}, {"int64_t f(const std::pair& v);", "struct"}, {"int64_t f(const std::map& v);", "`tstr`"}, }; for (const Case& c : cases) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, QString("class ProbeImpl {\npublic:\n %1\n};\n").arg(c.decl)); QString e; QTextStream es(&e); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", es); ASSERT_TRUE(r.hasError()) << c.decl; EXPECT_TRUE(r.error.contains(c.mentions)) << c.decl << "\n" << r.error.toStdString(); } } // std::unordered_map is the second C++ spelling of `{tstr: T}`. // logos_codec.h has always specialized Codec for it; the parser had not, so it // published `any`. TEST_F(ImplHeaderParserTest, UnorderedMapIsAStringKeyedMap) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "class ProbeImpl {\n" "public:\n" " int64_t f(const std::unordered_map& m);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.methods.size(), 1u); const TypeExpr& t = r.module.methods[0].params[0].type; EXPECT_EQ(t.kind, TypeExpr::Map); ASSERT_EQ(t.elements.size(), 2u); EXPECT_EQ(t.elements[0].name, "tstr"); EXPECT_EQ(t.elements[1].name, "tstr"); } // ...except in the two slots whose C++ spelling the generator WRITES OUT — a // record field's codec and an event's generated body. There it has to pick one // container name, and picking the wrong one is a compile error in code the // author never wrote. Say so at the declaration instead. TEST_F(ImplHeaderParserTest, UnorderedMapIsRejectedWhereTheSpellingIsEmitted) { for (const char* body : { "struct Rec {\n" " std::unordered_map m;\n" "};\n" "class ProbeImpl {\npublic:\n Rec echo(const Rec& v);\n};\n", "class ProbeImpl {\n" "public:\n" " bool fire();\n" "logos_events:\n" " void changed(const std::unordered_map& m);\n" "};\n"}) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, body); QString e; QTextStream es(&e); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", es); ASSERT_TRUE(r.hasError()) << body; EXPECT_TRUE(r.error.contains("std::map")) << r.error.toStdString(); } } // A helper struct the API never mentions is dropped from the contract, so an // unsupported spelling INSIDE it promises nothing and must not fail the build. // Publishing is what makes a declaration's type a promise. TEST_F(ImplHeaderParserTest, UnreferencedHelperStructDoesNotFailTheBuild) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct PendingAction {\n" " uint32_t attempts;\n" "};\n" "class ProbeImpl {\n" "public:\n" " int64_t f(int64_t n);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); EXPECT_TRUE(r.module.types.empty()); // ...and the same struct DOES fail once a method publishes it. QTemporaryDir dir2; ASSERT_TRUE(dir2.isValid()); const QString hp2 = probeHeader(dir2, "struct PendingAction {\n" " uint32_t attempts;\n" "};\n" "class ProbeImpl {\n" "public:\n" " PendingAction f(int64_t n);\n" "};\n"); QString e2; QTextStream es2(&e2); auto r2 = parseImplHeader(hp2, "ProbeImpl", fixturesDir() + "/sample_metadata.json", es2); ASSERT_TRUE(r2.hasError()); EXPECT_TRUE(r2.error.contains("type 'PendingAction': field 'attempts'")) << r2.error.toStdString(); } // The reserved LogosModuleContext hooks are framework plumbing, not contract. // They are dropped after parsing, so their spellings are not a contract defect. TEST_F(ImplHeaderParserTest, ReservedHookSpellingsAreNotContractDefects) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "class ProbeImpl {\n" "public:\n" " void onContextReady(uint32_t generation);\n" " int64_t f(int64_t n);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.methods.size(), 1u); EXPECT_EQ(r.module.methods[0].name, "f"); } // --------------------------------------------------------------------------- // Structure the record scanner could not read // // #127 made an unknown TYPE a build error. These cover the same hole one level // down: STRUCTURE. A line inside a struct body the scanner could not read as a // field used to be `continue`d and the record published without it, and a // contract missing a field is exactly as well-formed as one that has it — so // nothing downstream, in any language, could tell. // --------------------------------------------------------------------------- // A field declaration wrapped across physical lines is the field the author // wrote. It used to be ABSENT from the published record, with exit code 0. TEST_F(ImplHeaderParserTest, WrappedFieldDeclarationIsStillOneField) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct SplitRecord {\n" " std::string id;\n" " std::vector\n" " tags;\n" " uint64_t n;\n" "};\n" "class ProbeImpl {\n" "public:\n" " SplitRecord echoSplit(const SplitRecord& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); const TypeDecl& td = r.module.types[0]; ASSERT_EQ(td.fields.size(), 3u) << "a wrapped field was dropped from the record"; EXPECT_EQ(td.fields[0].name, "id"); EXPECT_EQ(td.fields[1].name, "tags"); EXPECT_EQ(td.fields[1].type.kind, TypeExpr::Array); EXPECT_EQ(td.fields[2].name, "n"); } // Where the opening brace sits cannot decide what a header means. Allman used // to make the struct not a record AT ALL, so every mention of it fell through // to `any` — and since #127 to an error telling the author to declare a struct, // which is the thing they declared. TEST_F(ImplHeaderParserTest, AllmanBraceStructIsARecord) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct AllmanRecord\n" "{\n" " std::string id;\n" " uint64_t n;\n" "};\n" "class ProbeImpl {\n" "public:\n" " AllmanRecord echoAllman(const AllmanRecord& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); EXPECT_EQ(r.module.types[0].name, "AllmanRecord"); ASSERT_EQ(r.module.types[0].fields.size(), 2u); ASSERT_EQ(r.module.methods.size(), 1u); EXPECT_EQ(r.module.methods[0].returnType.kind, TypeExpr::Named); EXPECT_EQ(r.module.methods[0].returnType.name, "AllmanRecord"); } // The K&R form has to keep parsing exactly as it did — this is the form every // record in the workspace is written in. TEST_F(ImplHeaderParserTest, KandRStructStillParses) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct NormalRecord {\n" " std::string id;\n" " uint64_t n;\n" " std::vector payload;\n" "};\n" "class ProbeImpl {\n" "public:\n" " NormalRecord echoNormal(const NormalRecord& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); ASSERT_EQ(r.module.types[0].fields.size(), 3u); EXPECT_EQ(r.module.types[0].fields[2].type.name, "bstr"); } // A nested type is not a field — and the scanner used to walk into its body, // publish the INNER type's members as the outer record's field list, and stop // at the inner `};` so none of the outer's own fields were ever seen. TEST_F(ImplHeaderParserTest, NestedTypeInARecordIsReported) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Outer {\n" " struct Inner {\n" " int64_t a;\n" " };\n" " Inner inner;\n" " std::string label;\n" "};\n" "class ProbeImpl {\n" "public:\n" " Outer echoOuter(const Outer& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_TRUE(r.hasError()) << "the outer record was published made of the " "inner type's fields"; EXPECT_TRUE(r.error.contains("type 'Outer'")) << r.error.toStdString(); EXPECT_TRUE(r.error.contains("namespace scope")) << r.error.toStdString(); } // Two declarations sharing a line matched nothing, so the struct published no // field, so no `type` was emitted at all — and the .lidl still named it in the // signature. That contract references a type it never declares. TEST_F(ImplHeaderParserTest, RecordThatPublishesNothingIsReported) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Pair {\n" " std::string first; std::string second;\n" "};\n" "class ProbeImpl {\n" "public:\n" " Pair echoPair(const Pair& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_TRUE(r.hasError()) << "emitted a contract naming an undeclared type"; EXPECT_TRUE(r.error.contains("type 'Pair'")) << r.error.toStdString(); EXPECT_TRUE(r.error.contains("ONE declaration per line")) << r.error.toStdString(); } // A body of nothing but member functions reads perfectly well and yields no // record. No single line is at fault, so it is reported on its own terms. TEST_F(ImplHeaderParserTest, RecordWithNoFieldsAtAllIsReported) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Fieldless {\n" " std::string toString() const;\n" "};\n" "class ProbeImpl {\n" "public:\n" " Fieldless echo(const Fieldless& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_TRUE(r.hasError()); EXPECT_TRUE(r.error.contains("type 'Fieldless'")) << r.error.toStdString(); EXPECT_TRUE(r.error.contains("never declares")) << r.error.toStdString(); } // A base class carries members this line-based parser cannot see, so publishing // the struct would drop exactly the fields it cannot read. TEST_F(ImplHeaderParserTest, RecordWithABaseClassIsReported) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Base {\n" " std::string id;\n" "};\n" "struct Derived : Base {\n" " uint64_t n;\n" "};\n" "class ProbeImpl {\n" "public:\n" " Derived echo(const Derived& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_TRUE(r.hasError()) << "published `Derived` without the base's fields"; EXPECT_TRUE(r.error.contains("type 'Derived'")) << r.error.toStdString(); EXPECT_TRUE(r.error.contains("base class")) << r.error.toStdString(); } // Constructs that definitively are NOT fields are skipped by RULE, so a record // that legitimately carries them still parses — the error is for what the // scanner cannot read, not for everything that is not a field. TEST_F(ImplHeaderParserTest, NonFieldMembersAreSkippedNotReported) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Rich {\n" " Rich() = default;\n" " ~Rich();\n" " std::string toString() const;\n" " static const int64_t kMax = 5;\n" " using Alias = std::string;\n" " std::string id;\n" " int64_t n;\n" "};\n" "class ProbeImpl {\n" "public:\n" " Rich echo(const Rich& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); ASSERT_EQ(r.module.types[0].fields.size(), 2u); EXPECT_EQ(r.module.types[0].fields[0].name, "id"); EXPECT_EQ(r.module.types[0].fields[1].name, "n"); } // A default value is part of the field, whichever way it is written, and may // itself contain parentheses — only parentheses in the DECLARATOR make a line a // member function. The brace form used to be dropped outright. TEST_F(ImplHeaderParserTest, DefaultedFieldsAreStillFields) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Defaulted {\n" " std::string id{\"none\"};\n" " int64_t n = 0;\n" " std::string made = makeId();\n" "};\n" "class ProbeImpl {\n" "public:\n" " Defaulted echo(const Defaulted& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); ASSERT_EQ(r.module.types[0].fields.size(), 3u); EXPECT_EQ(r.module.types[0].fields[0].name, "id"); EXPECT_EQ(r.module.types[0].fields[2].name, "made"); } // Comments come off before anything else — but a `//` inside a string literal // is not a comment. Truncating there ends the declaration before its `;`, which // used to drop the field and would now reject valid code. TEST_F(ImplHeaderParserTest, CommentStripKnowsWhatALiteralIs) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Commented {\n" " std::string url = \"http://example.invalid/x\";\n" " std::string id; // what it is\n" " int64_t n; /* and this one */\n" "};\n" "class ProbeImpl {\n" "public:\n" " Commented echo(const Commented& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); ASSERT_EQ(r.module.types[0].fields.size(), 3u); EXPECT_EQ(r.module.types[0].fields[0].name, "url"); EXPECT_EQ(r.module.types[0].fields[1].name, "id"); EXPECT_EQ(r.module.types[0].fields[2].name, "n"); } // The withdrawal that makes the whole thing safe: a helper struct the API never // names is not part of the contract, so structure the scanner could not read // inside it promises nothing. Every real module in the workspace carries one of // these (openmetrics `ModuleSource`, the package manager's `PendingAction`). TEST_F(ImplHeaderParserTest, UnreadableHelperStructDoesNotFailTheBuild) { const QString helper = "struct Helper {\n" " struct Nested {\n" " int64_t a;\n" " };\n" " std::string first; std::string second;\n" "};\n"; QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, helper + "class ProbeImpl {\n" "public:\n" " int64_t f(int64_t n);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); // ...and the same struct DOES fail the build once a method publishes it. QTemporaryDir dir2; ASSERT_TRUE(dir2.isValid()); const QString hp2 = probeHeader(dir2, helper + "class ProbeImpl {\n" "public:\n" " Helper f(const Helper& v);\n" "};\n"); QString e2; QTextStream es2(&e2); auto r2 = parseImplHeader(hp2, "ProbeImpl", fixturesDir() + "/sample_metadata.json", es2); ASSERT_TRUE(r2.hasError()); EXPECT_TRUE(r2.error.contains("type 'Helper'")) << r2.error.toStdString(); } // A member function DEFINED inline is a declaration of its own. The body's // braces, and any `;` inside it, belong to the member — not to the record — so // the fields declared after it are still the record's fields. (An earlier cut of // this change ended the scan at the body's closing brace and lost them.) TEST_F(ImplHeaderParserTest, InlineMemberBodyDoesNotEndTheRecord) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Inlined {\n" " std::string id;\n" " void reset() {\n" " id = \"\";\n" " }\n" " std::string mark() const { return id; }\n" " int64_t n;\n" "};\n" "class ProbeImpl {\n" "public:\n" " Inlined echo(const Inlined& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); ASSERT_EQ(r.module.types[0].fields.size(), 2u) << "an inline member-function body swallowed a field"; EXPECT_EQ(r.module.types[0].fields[0].name, "id"); EXPECT_EQ(r.module.types[0].fields[1].name, "n"); } // A brace INITIALISER may wrap across lines too, and its braces must not read // as a scope the scanner should skip. TEST_F(ImplHeaderParserTest, WrappedBraceInitialiserIsStillOneField) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Init {\n" " std::vector tags{\n" " \"a\", \"b\"};\n" " int64_t n;\n" "};\n" "class ProbeImpl {\n" "public:\n" " Init echo(const Init& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); ASSERT_EQ(r.module.types[0].fields.size(), 2u); EXPECT_EQ(r.module.types[0].fields[0].name, "tags"); EXPECT_EQ(r.module.types[0].fields[0].type.kind, TypeExpr::Array); EXPECT_EQ(r.module.types[0].fields[1].name, "n"); } // A brace inside a string literal is not a scope. Believing the text would make // the scanner miss the end of the struct entirely. TEST_F(ImplHeaderParserTest, BraceInAStringLiteralIsNotAScope) { QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = probeHeader(dir, "struct Literal {\n" " std::string tmpl = \"{\";\n" " int64_t n;\n" "};\n" "class ProbeImpl {\n" "public:\n" " Literal echo(const Literal& v);\n" "};\n"); auto r = parseImplHeader(hp, "ProbeImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); ASSERT_EQ(r.module.types.size(), 1u); ASSERT_EQ(r.module.types[0].fields.size(), 2u); EXPECT_EQ(r.module.types[0].fields[1].name, "n"); } // --------------------------------------------------------------------------- // Teardown hooks stay out of the contract // --------------------------------------------------------------------------- TEST_F(ImplHeaderParserTest, TeardownHooksAreNotPartOfTheContract) { // aboutToUnload() and unloadFinished() are framework plumbing, exactly like // onContextReady(). An impl that overrides one is talking to the host, not // publishing API -- leaking either would generate a consumer wrapper for a // lifecycle hook, and LogosShutdown has no LIDL type to return anyway. QTemporaryDir dir; ASSERT_TRUE(dir.isValid()); const QString hp = dir.filePath("thing_impl.h"); { QFile f(hp); ASSERT_TRUE(f.open(QIODevice::WriteOnly | QIODevice::Text)); f.write( "#pragma once\n" "#include \n" "class ThingImpl {\n" "public:\n" " std::string work();\n" " void onContextReady();\n" " LogosShutdown aboutToUnload();\n" " void unloadFinished();\n" "};\n"); } auto r = parseImplHeader(hp, "ThingImpl", fixturesDir() + "/sample_metadata.json", err); ASSERT_FALSE(r.hasError()) << r.error.toStdString(); QStringList names; for (const auto& m : r.module.methods) names << QString::fromStdString(m.name); EXPECT_TRUE(names.contains("work")) << "got: " << names.join(",").toStdString(); EXPECT_FALSE(names.contains("aboutToUnload")) << "got: " << names.join(",").toStdString(); EXPECT_FALSE(names.contains("unloadFinished")) << "got: " << names.join(",").toStdString(); EXPECT_FALSE(names.contains("onContextReady")) << "got: " << names.join(",").toStdString(); }