2026-03-31 16:29:35 -04:00
|
|
|
#include <gtest/gtest.h>
|
2026-06-18 13:30:32 +01:00
|
|
|
#include <QTemporaryDir>
|
2026-07-28 17:24:49 -03:00
|
|
|
#include <algorithm>
|
2026-03-31 16:29:35 -04:00
|
|
|
#include "impl_header_parser.h"
|
|
|
|
|
#include <QCoreApplication>
|
|
|
|
|
#include <QDir>
|
|
|
|
|
#include <QFile>
|
|
|
|
|
#include <QTextStream>
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-31 12:18:18 +02:00
|
|
|
// 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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 16:29:35 -04:00
|
|
|
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
|
2026-06-16 21:00:42 -03:00
|
|
|
auto findMethod = [&](const std::string& name) -> const MethodDecl* {
|
2026-03-31 16:29:35 -04:00
|
|
|
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");
|
2026-06-16 21:00:42 -03:00
|
|
|
EXPECT_TRUE(getCount->params.empty());
|
2026-03-31 16:29:35 -04:00
|
|
|
|
|
|
|
|
// 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<std::string> 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<uint8_t> getData() → bstr
|
|
|
|
|
auto getData = findMethod("getData");
|
|
|
|
|
ASSERT_NE(getData, nullptr);
|
|
|
|
|
EXPECT_EQ(getData->returnType.name, "bstr");
|
|
|
|
|
|
|
|
|
|
// std::vector<int64_t> 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();
|
2026-06-16 21:00:42 -03:00
|
|
|
EXPECT_TRUE(r.module.methods.empty());
|
2026-03-31 16:29:35 -04:00
|
|
|
// 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();
|
|
|
|
|
|
2026-06-16 21:00:42 -03:00
|
|
|
auto findMethod = [&](const std::string& name) -> const MethodDecl* {
|
2026-03-31 16:29:35 -04:00
|
|
|
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
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
2026-07-28 17:24:49 -03:00
|
|
|
// 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<std::string> 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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 16:29:35 -04:00
|
|
|
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());
|
2026-06-16 21:00:42 -03:00
|
|
|
EXPECT_TRUE(r.module.methods.empty());
|
2026-03-31 16:29:35 -04:00
|
|
|
EXPECT_TRUE(errOutput.contains("Warning"));
|
|
|
|
|
}
|
2026-04-13 13:29:26 -04:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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");
|
2026-06-04 17:06:12 -03:00
|
|
|
// Optional per-event description carried from metadata.json events[].
|
|
|
|
|
EXPECT_EQ(r.module.events[0].description, "Fired once the module is ready.");
|
2026-04-13 13:29:26 -04:00
|
|
|
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");
|
|
|
|
|
|
2026-06-16 21:00:42 -03:00
|
|
|
auto findMethod = [&](const std::string& name) -> const MethodDecl* {
|
2026-04-13 13:29:26 -04:00
|
|
|
for (const auto& m : r.module.methods)
|
|
|
|
|
if (m.name == name) return &m;
|
|
|
|
|
return nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-02 15:54:54 -03:00
|
|
|
// 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);
|
|
|
|
|
|
2026-04-13 13:29:26 -04:00
|
|
|
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);
|
|
|
|
|
|
2026-04-21 09:38:37 -04:00
|
|
|
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);
|
|
|
|
|
|
2026-06-18 13:30:32 +01:00
|
|
|
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);
|
|
|
|
|
|
2026-04-13 13:29:26 -04:00
|
|
|
for (const auto& m : r.module.methods) {
|
|
|
|
|
EXPECT_NE(m.name, "void") << "Keyword should not appear as method name";
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-06-04 17:06:12 -03:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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");
|
2026-06-16 21:00:42 -03:00
|
|
|
EXPECT_TRUE(r.module.events[1].description.empty());
|
2026-06-04 17:06:12 -03:00
|
|
|
|
|
|
|
|
// Single-line `///` doc comment.
|
|
|
|
|
EXPECT_EQ(r.module.events[2].name, "shutdown");
|
|
|
|
|
EXPECT_EQ(r.module.events[2].description, "Single-line documented event.");
|
|
|
|
|
}
|
2026-06-09 14:46:15 -03:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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();
|
|
|
|
|
|
2026-06-16 21:00:42 -03:00
|
|
|
auto findEvent = [&](const std::string& name) -> const EventDecl* {
|
2026-06-09 14:46:15 -03:00
|
|
|
for (const auto& e : r.module.events)
|
|
|
|
|
if (e.name == name) return &e;
|
|
|
|
|
return nullptr;
|
|
|
|
|
};
|
2026-06-16 21:00:42 -03:00
|
|
|
auto findMethod = [&](const std::string& name) -> const MethodDecl* {
|
2026-06-09 14:46:15 -03:00
|
|
|
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 : <decl>` 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);
|
|
|
|
|
}
|
2026-06-18 13:30:32 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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 <string>\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();
|
|
|
|
|
}
|
2026-07-31 07:19:47 -03:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Optionality, header-first
|
|
|
|
|
//
|
|
|
|
|
// `std::optional<T>` 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<Blob>` 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<std::optional<T>> 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 <optional>\n"
|
|
|
|
|
"#include <string>\n"
|
|
|
|
|
"struct Rec {\n"
|
|
|
|
|
" std::optional<std::optional<std::string>> 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<std::optional<std::string>>"))
|
|
|
|
|
<< errOutput.toStdString();
|
|
|
|
|
EXPECT_TRUE(errOutput.contains("no LIDL type")) << errOutput.toStdString();
|
|
|
|
|
}
|
2026-07-31 10:16:30 -03:00
|
|
|
|
|
|
|
|
// 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 <logos_json.h>\n"
|
|
|
|
|
"#include <cstdint>\n"
|
|
|
|
|
"#include <map>\n"
|
|
|
|
|
"#include <optional>\n"
|
|
|
|
|
"#include <set>\n"
|
|
|
|
|
"#include <string>\n"
|
|
|
|
|
"#include <unordered_map>\n"
|
|
|
|
|
"#include <utility>\n"
|
|
|
|
|
"#include <vector>\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");
|
|
|
|
|
}
|
2026-07-31 10:16:30 -03:00
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// 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<uint32_t>& 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<uint32_t>&")) << 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<uint8_t>"},
|
|
|
|
|
{"int64_t f(size_t v);", "`uint64_t`"},
|
|
|
|
|
{"int64_t f(const std::set<std::string>& v);", "std::vector<T>"},
|
|
|
|
|
{"int64_t f(const std::pair<std::string, std::string>& v);", "struct"},
|
|
|
|
|
{"int64_t f(const std::map<int64_t, std::string>& 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<std::string, T> 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<std::string, std::string>& 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<std::string, std::string> 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<std::string, std::string>& 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<std::string, T>")) << 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");
|
|
|
|
|
}
|