Files

345 lines
11 KiB
C++
Raw Permalink Normal View History

2026-03-31 16:29:35 -04:00
#include <gtest/gtest.h>
#include "lidl_parser.h"
// ---------------------------------------------------------------------------
// Minimal valid module
// ---------------------------------------------------------------------------
TEST(LidlParser, MinimalModule)
{
auto r = lidlParse("module test { depends [] }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
EXPECT_EQ(r.module.name, "test");
EXPECT_TRUE(r.module.methods.isEmpty());
EXPECT_TRUE(r.module.events.isEmpty());
EXPECT_TRUE(r.module.types.isEmpty());
}
// ---------------------------------------------------------------------------
// Metadata
// ---------------------------------------------------------------------------
TEST(LidlParser, AllMetadata)
{
QString src = R"(module my_mod {
version "2.0.0"
description "A test module"
category "testing"
depends [foo, bar, baz]
})";
auto r = lidlParse(src);
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
EXPECT_EQ(r.module.name, "my_mod");
EXPECT_EQ(r.module.version, "2.0.0");
EXPECT_EQ(r.module.description, "A test module");
EXPECT_EQ(r.module.category, "testing");
ASSERT_EQ(r.module.depends.size(), 3);
EXPECT_EQ(r.module.depends[0], "foo");
EXPECT_EQ(r.module.depends[1], "bar");
EXPECT_EQ(r.module.depends[2], "baz");
}
TEST(LidlParser, EmptyDepends)
{
auto r = lidlParse("module m { depends [] }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
EXPECT_TRUE(r.module.depends.isEmpty());
}
// ---------------------------------------------------------------------------
// Methods
// ---------------------------------------------------------------------------
TEST(LidlParser, SimpleMethod)
{
auto r = lidlParse("module m { method greet(name: tstr) -> tstr }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.methods.size(), 1);
auto& md = r.module.methods[0];
EXPECT_EQ(md.name, "greet");
EXPECT_EQ(md.returnType.kind, TypeExpr::Primitive);
EXPECT_EQ(md.returnType.name, "tstr");
ASSERT_EQ(md.params.size(), 1);
EXPECT_EQ(md.params[0].name, "name");
EXPECT_EQ(md.params[0].type.name, "tstr");
}
TEST(LidlParser, MethodNoParams)
{
auto r = lidlParse("module m { method getCount() -> int }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.methods.size(), 1);
EXPECT_TRUE(r.module.methods[0].params.isEmpty());
EXPECT_EQ(r.module.methods[0].returnType.name, "int");
}
TEST(LidlParser, MethodMultipleParams)
{
auto r = lidlParse("module m { method combine(a: tstr, b: tstr, n: int) -> tstr }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.methods[0].params.size(), 3);
EXPECT_EQ(r.module.methods[0].params[0].name, "a");
EXPECT_EQ(r.module.methods[0].params[1].name, "b");
EXPECT_EQ(r.module.methods[0].params[2].name, "n");
EXPECT_EQ(r.module.methods[0].params[2].type.name, "int");
}
TEST(LidlParser, MultipleMethods)
{
QString src = R"(module m {
method a() -> bool
method b(x: int) -> tstr
method c(y: float64, z: uint) -> bstr
})";
auto r = lidlParse(src);
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.methods.size(), 3);
EXPECT_EQ(r.module.methods[0].name, "a");
EXPECT_EQ(r.module.methods[1].name, "b");
EXPECT_EQ(r.module.methods[2].name, "c");
}
// ---------------------------------------------------------------------------
// Type expressions
// ---------------------------------------------------------------------------
TEST(LidlParser, ArrayType)
{
auto r = lidlParse("module m { method get() -> [tstr] }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
auto& ret = r.module.methods[0].returnType;
EXPECT_EQ(ret.kind, TypeExpr::Array);
ASSERT_EQ(ret.elements.size(), 1);
EXPECT_EQ(ret.elements[0].name, "tstr");
}
TEST(LidlParser, MapType)
{
auto r = lidlParse("module m { method get() -> {tstr: int} }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
auto& ret = r.module.methods[0].returnType;
EXPECT_EQ(ret.kind, TypeExpr::Map);
ASSERT_EQ(ret.elements.size(), 2);
EXPECT_EQ(ret.elements[0].name, "tstr");
EXPECT_EQ(ret.elements[1].name, "int");
}
TEST(LidlParser, OptionalType)
{
auto r = lidlParse("module m { method get() -> ?tstr }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
auto& ret = r.module.methods[0].returnType;
EXPECT_EQ(ret.kind, TypeExpr::Optional);
ASSERT_EQ(ret.elements.size(), 1);
EXPECT_EQ(ret.elements[0].name, "tstr");
}
TEST(LidlParser, AllPrimitiveTypes)
{
QString src = R"(module m {
method a() -> tstr
method b() -> bstr
method c() -> int
method d() -> uint
method e() -> float64
method f() -> bool
method g() -> result
method h() -> any
})";
auto r = lidlParse(src);
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.methods.size(), 8);
EXPECT_EQ(r.module.methods[0].returnType.name, "tstr");
EXPECT_EQ(r.module.methods[1].returnType.name, "bstr");
EXPECT_EQ(r.module.methods[2].returnType.name, "int");
EXPECT_EQ(r.module.methods[3].returnType.name, "uint");
EXPECT_EQ(r.module.methods[4].returnType.name, "float64");
EXPECT_EQ(r.module.methods[5].returnType.name, "bool");
EXPECT_EQ(r.module.methods[6].returnType.name, "result");
EXPECT_EQ(r.module.methods[7].returnType.name, "any");
}
// ---------------------------------------------------------------------------
// Events
// ---------------------------------------------------------------------------
TEST(LidlParser, SimpleEvent)
{
auto r = lidlParse("module m { event onData(payload: tstr) }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.events.size(), 1);
EXPECT_EQ(r.module.events[0].name, "onData");
ASSERT_EQ(r.module.events[0].params.size(), 1);
EXPECT_EQ(r.module.events[0].params[0].name, "payload");
}
TEST(LidlParser, EventNoParams)
{
auto r = lidlParse("module m { event ping() }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
EXPECT_TRUE(r.module.events[0].params.isEmpty());
}
// ---------------------------------------------------------------------------
// Keywords as names (contextual keywords)
//
// The reserved words (module/type/method/event/version/description/
// category/depends) are keywords only at the start of a declaration. In a
// name position they are ordinary identifiers. Regression: a `versionReady`
// event with a parameter literally named `version` (as emitted for a module
// whose impl declares `versionReady(const std::string& version)`) used to
// fail to re-parse with "Expected parameter name", because `version` lexes
// as a keyword.
// ---------------------------------------------------------------------------
TEST(LidlParser, KeywordAsParameterName)
{
auto r = lidlParse("module m { event versionReady(version: tstr) }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.events.size(), 1);
EXPECT_EQ(r.module.events[0].name, "versionReady");
ASSERT_EQ(r.module.events[0].params.size(), 1);
EXPECT_EQ(r.module.events[0].params[0].name, "version");
EXPECT_EQ(r.module.events[0].params[0].type.name, "tstr");
}
TEST(LidlParser, KeywordAsMethodAndParamName)
{
// `method`, `type`, `event`, `category` all used as ordinary names.
auto r = lidlParse("module m { method method(type: tstr, event: int) -> tstr }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.methods.size(), 1);
EXPECT_EQ(r.module.methods[0].name, "method");
ASSERT_EQ(r.module.methods[0].params.size(), 2);
EXPECT_EQ(r.module.methods[0].params[0].name, "type");
EXPECT_EQ(r.module.methods[0].params[1].name, "event");
}
TEST(LidlParser, KeywordAsFieldName)
{
auto r = lidlParse("module m { type T { version: tstr description: int } }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.types.size(), 1);
ASSERT_EQ(r.module.types[0].fields.size(), 2);
EXPECT_EQ(r.module.types[0].fields[0].name, "version");
EXPECT_EQ(r.module.types[0].fields[1].name, "description");
}
TEST(LidlParser, KeywordAsDependencyName)
{
auto r = lidlParse("module m { depends [version, module, foo] }");
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.depends.size(), 3);
EXPECT_EQ(r.module.depends[0], "version");
EXPECT_EQ(r.module.depends[1], "module");
EXPECT_EQ(r.module.depends[2], "foo");
}
2026-03-31 16:29:35 -04:00
// ---------------------------------------------------------------------------
// Type definitions
// ---------------------------------------------------------------------------
TEST(LidlParser, TypeDefinition)
{
QString src = R"(module m {
type Person {
name: tstr
age: int
? nickname: tstr
}
})";
auto r = lidlParse(src);
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
ASSERT_EQ(r.module.types.size(), 1);
auto& td = r.module.types[0];
EXPECT_EQ(td.name, "Person");
ASSERT_EQ(td.fields.size(), 3);
EXPECT_EQ(td.fields[0].name, "name");
EXPECT_FALSE(td.fields[0].optional);
EXPECT_EQ(td.fields[2].name, "nickname");
EXPECT_TRUE(td.fields[2].optional);
}
// ---------------------------------------------------------------------------
// Error cases
// ---------------------------------------------------------------------------
TEST(LidlParser, MissingModuleKeyword)
{
auto r = lidlParse("test { }");
ASSERT_TRUE(r.hasError());
}
TEST(LidlParser, MissingModuleName)
{
auto r = lidlParse("module { }");
ASSERT_TRUE(r.hasError());
}
TEST(LidlParser, MissingOpenBrace)
{
auto r = lidlParse("module test }");
ASSERT_TRUE(r.hasError());
}
TEST(LidlParser, MissingCloseBrace)
{
auto r = lidlParse("module test {");
ASSERT_TRUE(r.hasError());
}
TEST(LidlParser, MissingArrowInMethod)
{
auto r = lidlParse("module m { method foo() tstr }");
ASSERT_TRUE(r.hasError());
}
TEST(LidlParser, TrailingContent)
{
auto r = lidlParse("module m { } extra_stuff");
ASSERT_TRUE(r.hasError());
}
TEST(LidlParser, ErrorReportsLineColumn)
{
auto r = lidlParse("module m {\n method foo() tstr\n}");
ASSERT_TRUE(r.hasError());
EXPECT_EQ(r.errorLine, 2);
EXPECT_GT(r.errorColumn, 0);
}
// ---------------------------------------------------------------------------
// Full module
// ---------------------------------------------------------------------------
TEST(LidlParser, CompleteModule)
{
QString src = R"(module wallet {
version "1.0.0"
description "Wallet module"
category "finance"
depends [crypto, storage]
type Account {
address: tstr
balance: uint
? label: tstr
}
method createAccount(passphrase: tstr) -> tstr
method getBalance(address: tstr) -> uint
method listAccounts() -> [tstr]
method transfer(from: tstr, to: tstr, amount: uint) -> result
event onTransfer(from: tstr, to: tstr, amount: uint)
})";
auto r = lidlParse(src);
ASSERT_FALSE(r.hasError()) << r.error.toStdString();
EXPECT_EQ(r.module.name, "wallet");
EXPECT_EQ(r.module.version, "1.0.0");
EXPECT_EQ(r.module.depends.size(), 2);
EXPECT_EQ(r.module.types.size(), 1);
EXPECT_EQ(r.module.methods.size(), 4);
EXPECT_EQ(r.module.events.size(), 1);
}