diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/CMakeLists.txt index 560451a6d..6eacc9054 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/CMakeLists.txt @@ -170,3 +170,8 @@ endif() ELSE() message(STATUS "BUILD_GRANTLEE disabled or Grantlee not found; skipping codegen_grantlee, ngl_codegen, and codegen_gui targets") ENDIF() + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/unit-test/CMakeLists.txt new file mode 100644 index 000000000..66f214231 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/unit-test/CMakeLists.txt @@ -0,0 +1,20 @@ +# +# Unit tests for the CodeGen module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- CodeGen: string helpers, data classes and Grantlee template-context +# --- wrappers (pure logic; the KTextTemplate engine is covered by +# --- GrantleeParserSmoke) +if (TARGET codegen_grantlee) + ADD_EXECUTABLE(codegen_unit_test codegen_unit_test.cpp) + TARGET_LINK_LIBRARIES(codegen_unit_test + codegen_grantlee + Qt6::Core + Qt6::Test + ) + ADD_TEST(NAME codegen_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/unit-test/codegen_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/unit-test/codegen_unit_test.cpp new file mode 100644 index 000000000..c5d522bcf --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/CodeGen/unit-test/codegen_unit_test.cpp @@ -0,0 +1,701 @@ +// +// Unit tests for the ngl_codegen module (Lts/Common/CodeGen): the pure-logic +// string helpers (StructBuilder statics), the codegen_base data classes and +// the KTextTemplate context wrappers (GrantleeWrappers). +// No template execution is done here; the Grantlee/KTextTemplate engine +// itself is covered by the GrantleeParserSmoke smoke test. +// +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace CodeGen; + +// Minimal concrete DataTypeInfo for the IdlDataWrapper tests (the module's +// own concrete data types are private to StructBuilder.cpp). +class TestDataTypeInfo : public DataTypeInfo +{ + public: + TestDataTypeInfo(const QString& idlType, const QString& cppType, bool isPod) + : DataTypeInfo(QStringLiteral("test"), isPod), m_idlType(idlType), m_cppType(cppType) + { + } + + QString idlType() const override final + { + return m_idlType; + } + + QString ddsType() const override final + { + return m_cppType; + } + + QString sqlType() const override final + { + return m_cppType; + } + + QString cppType() const override final + { + return m_cppType; + } + + private: + QString m_idlType; + QString m_cppType; +}; + +static MemberInformation makeMember(const QString& table, const QString& column) +{ + MemberInformation mi; + mi.ea.table = table; + mi.ea.column = column; + mi.db.table = table; + mi.db.columnName = column; + mi.cpp.memberName = column; + return mi; +} + +class codegen_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void firstCharLowercase(); + void camelCaseString(); + void camelCaseToUnderline(); + + void nameWrapperTransforms(); + void memberWrapperGetters(); + void memberWrapperVariantConversion(); + void memberWrapperDdsToJsonElement(); + void memberWrapperJsonElementToDds(); + void memberWrapperDbToJsonElement(); + void memberWrapperJsonElementToDb(); + void memberWrapperJsonPrePostFallback(); + void memberListWrapperFilters(); + + void dataTopicWrapper(); + void functionTopicWrapper(); + void idlWrappers(); + void dbStructWrapper(); + + void codegenBaseDataClasses(); +}; + +void codegen_unit_test::firstCharLowercase() +{ + QCOMPARE(StructBuilder::firstCharLowercase(QString()), QString()); + QCOMPARE(StructBuilder::firstCharLowercase(QStringLiteral("FooBar")), QStringLiteral("fooBar")); + QCOMPARE(StructBuilder::firstCharLowercase(QStringLiteral("abc")), QStringLiteral("abc")); +} + +void codegen_unit_test::camelCaseString() +{ + // underscore-separated -> camelCase (default keeps the first character lower case) + QCOMPARE(StructBuilder::camelCaseString(QStringLiteral("foo_bar")), QStringLiteral("fooBar")); + QCOMPARE(StructBuilder::camelCaseString(QStringLiteral("foo_bar"), true), QStringLiteral("FooBar")); + QCOMPARE(StructBuilder::camelCaseString(QStringLiteral("foo")), QStringLiteral("foo")); + QCOMPARE(StructBuilder::camelCaseString(QStringLiteral("_foo")), QStringLiteral("Foo")); + // trailing underscore sets the "next is upper" flag but appends nothing + QCOMPARE(StructBuilder::camelCaseString(QStringLiteral("foo_")), QStringLiteral("foo")); + QCOMPARE(StructBuilder::camelCaseString(QString()), QString()); +} + +void codegen_unit_test::camelCaseToUnderline() +{ + QCOMPARE(StructBuilder::camelCaseToUnderline(QStringLiteral("FooBarBaz")), QStringLiteral("foo_bar_baz")); + QCOMPARE(StructBuilder::camelCaseToUnderline(QStringLiteral("foo")), QStringLiteral("foo")); + QCOMPARE(StructBuilder::camelCaseToUnderline(QStringLiteral("A")), QStringLiteral("a")); + QCOMPARE(StructBuilder::camelCaseToUnderline(QStringLiteral("aB")), QStringLiteral("a_b")); + // digits are kept, an upper case after a digit still splits + QCOMPARE(StructBuilder::camelCaseToUnderline(QStringLiteral("Foo1Bar")), QStringLiteral("foo1_bar")); + QCOMPARE(StructBuilder::camelCaseToUnderline(QString()), QString()); +} + +void codegen_unit_test::nameWrapperTransforms() +{ + QObject parent; + + NameWrapper w(QStringLiteral("foo_bar"), &parent); + QCOMPARE(w.name(), QStringLiteral("foo_bar")); + QCOMPARE(w.nameCc(), QStringLiteral("FooBar")); + QCOMPARE(w.nameFlc(), QStringLiteral("fooBar")); + + NameWrapper w2(QStringLiteral("FooBar"), &parent); + QCOMPARE(w2.nameCc(), QStringLiteral("FooBar")); + QCOMPARE(w2.nameFlc(), QStringLiteral("fooBar")); +} + +void codegen_unit_test::memberWrapperGetters() +{ + QObject parent; + + MemberInformation mi = makeMember(QStringLiteral("tbl_a"), QStringLiteral("a_name")); + mi.ea.type = QStringLiteral("varchar(255)"); + mi.db.datatype = QStringLiteral("varchar(255)"); + mi.db.columnNameView = QStringLiteral("a_name_v"); + mi.db.hasSequence = true; + mi.cpp.structName = QStringLiteral("AStruct"); + mi.cpp.defaultValue = QStringLiteral("''"); + mi.cpp.type = QStringLiteral("QString"); + mi.cpp.ddsType = QStringLiteral("dds::core::string255"); + mi.cpp.isOptional = true; + mi.cpp.usedForCppStruct = true; + + MemberInformationWrapper w(mi, &parent); + QCOMPARE(w.cppMemberName(), QStringLiteral("a_name")); + QCOMPARE(w.cppMemberNameCc(), QStringLiteral("AName")); + QCOMPARE(w.cppStructName(), QStringLiteral("AStruct")); + QCOMPARE(w.cppType(), QStringLiteral("QString")); + QCOMPARE(w.cppDefault(), QStringLiteral("''")); + QCOMPARE(w.dbTableName(), QStringLiteral("tbl_a")); + QCOMPARE(w.dbColumnName(), QStringLiteral("a_name")); + QCOMPARE(w.dbColumnNameView(), QStringLiteral("a_name_v")); + QCOMPARE(w.cppDdsType(), QStringLiteral("dds::core::string255")); + QCOMPARE(w.dbHasSequence(), true); + QCOMPARE(w.isOptional(), true); + QCOMPARE(w.isCppStruct(), true); + QCOMPARE(w.info().db.table, QStringLiteral("tbl_a")); + + // view column name and dds type fall back when empty + MemberInformation mi2 = makeMember(QStringLiteral("tbl_b"), QStringLiteral("b_id")); + mi2.cpp.type = QStringLiteral("int"); + MemberInformationWrapper w2(mi2, &parent); + QCOMPARE(w2.dbColumnNameView(), QStringLiteral("b_id")); + QCOMPARE(w2.cppDdsType(), QStringLiteral("int")); +} + +void codegen_unit_test::memberWrapperVariantConversion() +{ + QObject parent; + + MemberInformation mi = makeMember(QStringLiteral("tbl_a"), QStringLiteral("foo")); + mi.cpp.type = QStringLiteral("int"); + mi.cpp.isOptional = false; + + MemberInformationWrapper w(mi, &parent); + QCOMPARE(w.ddsToQVariant(), QStringLiteral("toQVariant(data.foo)")); + QCOMPARE(w.qVariantToDds(), QStringLiteral("data.foo = fromQVariant(q.value(i++))")); + + // optional members are wrapped in dds::core::optional<> + mi.cpp.isOptional = true; + mi.cpp.ddsType = QStringLiteral("MyStruct"); + MemberInformationWrapper w2(mi, &parent); + QCOMPARE(w2.qVariantToDds(), + QStringLiteral("data.foo = fromQVariant>(q.value(i++))")); +} + +void codegen_unit_test::memberWrapperDdsToJsonElement() +{ + QObject parent; + + // plain member + MemberInformation mi = makeMember(QStringLiteral("tbl_a"), QStringLiteral("foo")); + mi.cpp.type = QStringLiteral("int"); + MemberInformationWrapper w(mi, &parent); + QCOMPARE(w.ddsToJsonElement(), QStringLiteral("obj[QStringLiteral(\"foo\")] = element.foo")); + + // optional member goes through has_value() + mi.cpp.isOptional = true; + MemberInformationWrapper w2(mi, &parent); + QCOMPARE(w2.ddsToJsonElement(), + QStringLiteral("if (element.foo.has_value()) obj[QStringLiteral(\"foo\")] = *element.foo")); + + // empty member name falls back to the member type + MemberInformation mi3; + mi3.cpp.type = QStringLiteral("int"); + MemberInformationWrapper w3(mi3, &parent); + QCOMPARE(w3.ddsToJsonElement(), QStringLiteral("obj[QStringLiteral(\"int\")] = element;")); +} + +void codegen_unit_test::memberWrapperJsonElementToDds() +{ + QObject parent; + + MemberInformation mi = makeMember(QStringLiteral("tbl_a"), QStringLiteral("foo")); + mi.cpp.type = QStringLiteral("int"); + + MemberInformationWrapper w(mi, &parent); + QCOMPARE(w.jsonElementToDds(), + QStringLiteral("const QJsonValue val(objElement.value(QStringLiteral(\"foo\"))); element.foo = val")); + + mi.cpp.isOptional = true; + MemberInformationWrapper w2(mi, &parent); + QCOMPARE(w2.jsonElementToDds(), QStringLiteral( + "const QJsonValue val(objElement.value(QStringLiteral(\"foo\"))); " + "if (!val.isNull()) element.foo = val")); + + // empty member name falls back to the member type + MemberInformation mi3; + mi3.cpp.type = QStringLiteral("int"); + MemberInformationWrapper w3(mi3, &parent); + QCOMPARE(w3.jsonElementToDds(), QStringLiteral("element = objElement.value(QStringLiteral(\"int\"))")); +} + +void codegen_unit_test::memberWrapperDbToJsonElement() +{ + QObject parent; + + // plain member + MemberInformation mi = makeMember(QStringLiteral("tbl_a"), QStringLiteral("foo")); + mi.cpp.type = QStringLiteral("int"); + MemberInformationWrapper w(mi, &parent); + QCOMPARE(w.dbToJsonElement(), QStringLiteral("obj[QStringLiteral(\"foo\")] = element.foo")); + + // optional (non-QDateTime) member is only written when it has a value + mi.cpp.type = QStringLiteral("QString"); + mi.cpp.isOptional = true; + MemberInformationWrapper w2(mi, &parent); + QCOMPARE(w2.dbToJsonElement(), + QStringLiteral("if (!element.foo.isNull()) obj[QStringLiteral(\"foo\")] = element.foo")); + + // optional QDateTime member uses isValid() + mi.cpp.type = QStringLiteral("QDateTime"); + MemberInformationWrapper w3(mi, &parent); + QCOMPARE(w3.dbToJsonElement(), + QStringLiteral("if (element.foo.isValid()) obj[QStringLiteral(\"foo\")] = element.foo")); + + // empty member name falls back to the member type + MemberInformation mi4; + mi4.cpp.type = QStringLiteral("int"); + MemberInformationWrapper w4(mi4, &parent); + QCOMPARE(w4.dbToJsonElement(), QStringLiteral("obj[QStringLiteral(\"int\")] = element;")); +} + +void codegen_unit_test::memberWrapperJsonElementToDb() +{ + QObject parent; + + MemberInformation mi = makeMember(QStringLiteral("tbl_a"), QStringLiteral("foo")); + mi.cpp.type = QStringLiteral("int"); + + MemberInformationWrapper w(mi, &parent); + QCOMPARE(w.jsonElementToDb(), + QStringLiteral("const QJsonValue val(objElement.value(QStringLiteral(\"foo\"))); element.foo = val")); + + mi.cpp.isOptional = true; + MemberInformationWrapper w2(mi, &parent); + QCOMPARE(w2.jsonElementToDb(), QStringLiteral( + "const QJsonValue val(objElement.value(QStringLiteral(\"foo\"))); " + "if (!val.isUndefined()) element.foo = val")); + + // empty member name falls back to the member type + MemberInformation mi3; + mi3.cpp.type = QStringLiteral("int"); + MemberInformationWrapper w3(mi3, &parent); + QCOMPARE(w3.jsonElementToDb(), QStringLiteral("element = objElement.value(QStringLiteral(\"int\"))")); +} + +void codegen_unit_test::memberWrapperJsonPrePostFallback() +{ + QObject parent; + + // json pre/post strings win over the variant pre/post strings when set + MemberInformation mi = makeMember(QStringLiteral("tbl_a"), QStringLiteral("foo")); + mi.cpp.toVariantPre = QStringLiteral("T("); + mi.cpp.toVariantPost = QStringLiteral(")"); + mi.cpp.fromVariantPre2 = QStringLiteral("V("); + mi.cpp.fromVariantPost = QStringLiteral(")"); + mi.cpp.toJsonPre = QStringLiteral("J("); + mi.cpp.toJsonPost = QStringLiteral(")"); + mi.cpp.fromJsonPre2 = QStringLiteral("F("); + mi.cpp.fromJsonPost = QStringLiteral(")"); + + MemberInformationWrapper w(mi, &parent); + QCOMPARE(w.cppToQvPre(), QStringLiteral("T(")); + QCOMPARE(w.cppToQvPost(), QStringLiteral(")")); + QCOMPARE(w.cppFromQvPre1(), QString()); + QCOMPARE(w.cppFromQvPre2(), QStringLiteral("V(")); + QCOMPARE(w.cppFromQvPost(), QStringLiteral(")")); + QCOMPARE(w.cppToJsonPre(), QStringLiteral("J(")); + QCOMPARE(w.cppToJsonPost(), QStringLiteral(")")); + QCOMPARE(w.cppFromJsonPre2(), QStringLiteral("F(")); + QCOMPARE(w.cppFromJsonPost(), QStringLiteral(")")); + + // without json strings the variant strings are used + MemberInformation mi2 = makeMember(QStringLiteral("tbl_a"), QStringLiteral("bar")); + mi2.cpp.toVariantPre = QStringLiteral("T("); + mi2.cpp.fromVariantPre2 = QStringLiteral("V("); + MemberInformationWrapper w2(mi2, &parent); + QCOMPARE(w2.cppToJsonPre(), QStringLiteral("T(")); + QCOMPARE(w2.cppToJsonPost(), QString()); + QCOMPARE(w2.cppFromJsonPre2(), QStringLiteral("V(")); + QCOMPARE(w2.cppFromJsonPost(), QString()); +} + +void codegen_unit_test::memberListWrapperFilters() +{ + QObject parent; + + MemberInformation cppMi = makeMember(QStringLiteral("t"), QStringLiteral("m1")); + cppMi.cpp.usedForCppStruct = true; + MemberInformation jsonMi = makeMember(QStringLiteral("t"), QStringLiteral("m2")); + jsonMi.cpp.usedForJson = true; + MemberInformation fkMi = makeMember(QStringLiteral("t"), QStringLiteral("m3")); + fkMi.db.isForeignKey = true; + MemberInformation selMi = makeMember(QStringLiteral("t"), QStringLiteral("m4")); + selMi.db.generateSelectRemove = true; + MemberInformation viewMi = makeMember(QStringLiteral("t"), QStringLiteral("m5")); + viewMi.db.usedForView = true; + MemberInformation keyMi = makeMember(QStringLiteral("t"), QStringLiteral("m6")); + keyMi.isDdsKey = true; + MemberInformation pkMi = makeMember(QStringLiteral("t"), QStringLiteral("m7")); + pkMi.db.isPrimaryKey = true; + + QVector members; + members = {cppMi, jsonMi, fkMi, selMi, viewMi, keyMi, pkMi}; + + MemberInformationListWrapper w(QStringLiteral("members"), members, &parent); + QCOMPARE(w.member().size(), 7); + QCOMPARE(w.cppMember().size(), 1); + QCOMPARE(w.jsonMember().size(), 1); + QCOMPARE(w.fkMember().size(), 1); + QCOMPARE(w.selectRemoveMember().size(), 1); + QCOMPARE(w.viewMember().size(), 1); + + MemberInformationWrapper* key = w.ddsKey(); + QVERIFY(key != nullptr); + QCOMPARE(key->cppMemberName(), QStringLiteral("m6")); + MemberInformationWrapper* pk = w.primaryKey(); + QVERIFY(pk != nullptr); + QCOMPARE(pk->cppMemberName(), QStringLiteral("m7")); + + // an empty list has no key and no primary key + MemberInformationListWrapper w2(QStringLiteral("empty"), QVector(), &parent); + QCOMPARE(w2.member().size(), 0); + QVERIFY(w2.ddsKey() == nullptr); + QVERIFY(w2.primaryKey() == nullptr); +} + +void codegen_unit_test::dataTopicWrapper() +{ + QObject parent; + + CompleteTopicTypeInformation ctti; + ctti.name = QStringLiteral("Topic"); + ctti.serviceName = QStringLiteral("Svc"); + + DataTopicWrapper w(ctti, &parent); + QCOMPARE(w.name(), QStringLiteral("Topic")); + QCOMPARE(w.serviceName(), QStringLiteral("Svc")); + QCOMPARE(w.type(), QStringLiteral("Topic")); + QCOMPARE(w.typeFq(), QStringLiteral("Svc::Topic")); + QCOMPARE(w.dtsImpl(), true); + QCOMPARE(w.dtcImpl(), true); + QCOMPARE(w.routeToClient(), true); + QCOMPARE(w.routeToServer(), false); + QCOMPARE(w.routeInReplay(), true); + QCOMPARE(w.isAutoSubscribe(), true); + QCOMPARE(w.recordTopic(), false); + QVERIFY(w.recTable().isNull()); + QVERIFY(w.key().isNull()); + QCOMPARE(w.hasTrColumns(), false); + + // an already service-qualified name is not qualified again + ctti.name = QStringLiteral("Svc::Topic"); + DataTopicWrapper w2(ctti, &parent); + QCOMPARE(w2.typeFq(), QStringLiteral("Svc::Topic")); + + // without a service name the type is returned as-is + CompleteTopicTypeInformation ctti3; + ctti3.name = QStringLiteral("Topic3"); + DataTopicWrapper w3(ctti3, &parent); + QCOMPARE(w3.typeFq(), QStringLiteral("Topic3")); + + // key column and translatable columns + QSharedPointer intType(new TestDataTypeInfo(QStringLiteral("int"), QStringLiteral("int"), true)); + IdlData keyData; + keyData.name = QStringLiteral("id"); + keyData.dataTypeInfo = intType; + w.setKeyColumnData(keyData); + QVERIFY(!w.key().isNull()); + QCOMPARE(w.key().value()->name(), QStringLiteral("id")); + + IdlData trData; + trData.name = QStringLiteral("tr1"); + trData.dataTypeInfo = intType; + QVector trColumns; + trColumns = {trData}; + w.setTrColumns(trColumns); + QCOMPARE(w.hasTrColumns(), true); + QCOMPARE(w.trColumns().size(), 1); +} + +void codegen_unit_test::functionTopicWrapper() +{ + QObject parent; + + FunctionTopic ft; + ft.name = QStringLiteral("MyFunc"); + ft.requestTopicName = QStringLiteral("ReqT"); + ft.replyTopicName = QStringLiteral("RepT"); + ft.replyTopicType = QStringLiteral("Core::FunctionReply"); + ft.routeInReplay = true; + + CompleteTopicTypeInformation req; + req.name = QStringLiteral("ReqType"); + CompleteTopicTypeInformation rep; + rep.name = QStringLiteral("RepType"); + + FunctionTopicWrapper w(ft, req, rep, &parent); + QCOMPARE(w.name(), QStringLiteral("MyFunc")); + QCOMPARE(w.onReply(), QStringLiteral("onRepT")); + QCOMPARE(w.onRequest(), QStringLiteral("onReqT")); + QCOMPARE(w.onError(), QStringLiteral("onMyFuncError")); + QCOMPARE(w.isCoreFt(), true); + QCOMPARE(w.routeInReplay(), true); + QCOMPARE(w.reply(), QStringLiteral("RepT")); + QCOMPARE(w.replyFlc(), QStringLiteral("repT")); + QCOMPARE(w.replyHandler(), QStringLiteral("RepTHandler")); + QCOMPARE(w.replyHandlerFlc(), QStringLiteral("repTHandler")); + QCOMPARE(w.replyType(), QStringLiteral("RepType")); + QCOMPARE(w.replyTypeFro(), QStringLiteral("Core::FunctionReplyObject")); + QCOMPARE(w.request(), QStringLiteral("ReqT")); + QCOMPARE(w.requestFlc(), QStringLiteral("reqT")); + QCOMPARE(w.requestHandler(), QStringLiteral("ReqTHandler")); + QCOMPARE(w.requestHandlerFlc(), QStringLiteral("reqTHandler")); + QCOMPARE(w.requestType(), QStringLiteral("ReqType")); + QCOMPARE(w.replyHandlerHash(), QString()); + + // a non-core reply type keeps its own type name + ft.replyTopicType = QStringLiteral("SomeReply"); + FunctionTopicWrapper w2(ft, req, rep, &parent); + QCOMPARE(w2.isCoreFt(), false); + QCOMPARE(w2.replyTypeFro(), QStringLiteral("RepType")); +} + +void codegen_unit_test::idlWrappers() +{ + QObject parent; + + QSharedPointer podType(new TestDataTypeInfo(QStringLiteral("int"), QStringLiteral("int"), true)); + QSharedPointer structType(new TestDataTypeInfo(QStringLiteral("Svc::MyStruct"), + QStringLiteral("Svc::MyStruct"), false)); + + IdlData idl; + idl.name = QStringLiteral("foo"); + idl.isKey = true; + idl.isOptional = false; + idl.dataTypeInfo = podType; + + IdlDataWrapper w(idl, &parent); + QCOMPARE(w.name(), QStringLiteral("foo")); + QCOMPARE(w.type(), QStringLiteral("int")); + QCOMPARE(w.cppType(), QStringLiteral("int")); + // POD types are passed by value (no const reference) + QCOMPARE(w.cppTypeCr(), QStringLiteral("int")); + QCOMPARE(w.isKey(), QStringLiteral(" \t //@key")); + QCOMPARE(w.isOptional(), QString()); + QCOMPARE(w.nameGetter(), QStringLiteral("foo")); + + IdlData opt; + opt.name = QStringLiteral("bar"); + opt.isOptional = true; + opt.dataTypeInfo = structType; + + IdlDataWrapper w2(opt, &parent); + QCOMPARE(w2.cppTypeCr(), QStringLiteral("const Svc::MyStruct &")); + QCOMPARE(w2.isKey(), QString()); + QCOMPARE(w2.isOptional(), QStringLiteral(" \t //@Optional")); + // optional members are unwrapped via .get() + QCOMPARE(w2.nameGetter(), QStringLiteral("bar.get()")); + + // IdlStructWrapper + CompleteTopicTypeInformation ctti; + ctti.name = QStringLiteral("MyStruct"); + ctti.comment = QStringLiteral("struct comment"); + ctti.baseValueType = QStringLiteral("BaseType"); + IdlData m1; + m1.name = QStringLiteral("a"); + m1.dataTypeInfo = podType; + IdlData m2; + m2.name = QStringLiteral("b"); + m2.dataTypeInfo = structType; + ctti.members = {m1, m2}; + + IdlStructWrapper sw(ctti, &parent); + QCOMPARE(sw.name(), QStringLiteral("MyStruct")); + QCOMPARE(sw.comment(), QStringLiteral("struct comment")); + QCOMPARE(sw.baseValueType(), QStringLiteral("BaseType")); + // the default extensibility maps to the same string with and without RTI + QCOMPARE(sw.extensibility(), QStringLiteral("EXTENSIBLE_EXTENSIBILITY")); + QCOMPARE(sw.member().size(), 2); + QCOMPARE(sw.member().value(0).value()->name(), QStringLiteral("a")); +} + +void codegen_unit_test::dbStructWrapper() +{ + QObject parent; + + QSharedPointer tableA(new TableInformation()); + tableA->name = QStringLiteral("tbl_a"); + QSharedPointer tableB(new TableInformation()); + tableB->name = QStringLiteral("tbl_b"); + + CompleteStructInformation csi; + csi.structName = QStringLiteral("MyStruct"); + csi.viewName = QStringLiteral("my_view"); + csi.tables = {tableA, tableB}; + csi.primaryTable = tableA; + + MemberInformation pkA = makeMember(QStringLiteral("tbl_a"), QStringLiteral("a_id")); + pkA.db.isPrimaryKey = true; + pkA.db.usedForInsert = true; + MemberInformation nameA = makeMember(QStringLiteral("tbl_a"), QStringLiteral("a_name")); + nameA.db.usedForInsert = true; + MemberInformation fkB = makeMember(QStringLiteral("tbl_b"), QStringLiteral("b_a_id")); + fkB.db.usedForInsert = true; + fkB.fk = QSharedPointer(new ForeignKeyInformation()); + fkB.fk->destination.tableName = QStringLiteral("tbl_a"); + + csi.member = {pkA, nameA, fkB}; + + DbNotifcationInformation notif1; + notif1.methodName = QStringLiteral("onInsertA"); + DbNotifcationInformation notif2; + notif2.methodName = QStringLiteral("onDeleteA"); + csi.dbNotifications = {notif1, notif2}; + + DbStructWrapper w(csi, &parent); + QCOMPARE(w.name(), QStringLiteral("MyStruct")); + QCOMPARE(w.member().size(), 3); + QCOMPARE(w.deleteWithoutPkAllowed(), false); // two tables + QCOMPARE(w.viewTableName(), QStringLiteral("my_view")); // multi-table -> view name + MemberInformationWrapper* pk = w.primaryKey(); + QVERIFY(pk != nullptr); + QCOMPARE(pk->dbColumnName(), QStringLiteral("a_id")); + QCOMPARE(w.dbNotifications().size(), 2); + QCOMPARE(w.dbNotifications().value(0).toString(), QStringLiteral("onInsertA")); + QCOMPARE(w.dbNotifications().value(1).toString(), QStringLiteral("onDeleteA")); + + // a single-table struct uses the primary table name as view name + CompleteStructInformation csiSingle; + csiSingle.structName = QStringLiteral("Single"); + csiSingle.viewName = QStringLiteral("unused"); + csiSingle.tables = {tableA}; + csiSingle.primaryTable = tableA; + MemberInformation soloPk = makeMember(QStringLiteral("tbl_a"), QStringLiteral("a_id")); + soloPk.db.isPrimaryKey = true; + soloPk.db.hasSequence = true; + soloPk.db.usedForInsert = true; + csiSingle.member = {soloPk}; + + DbStructWrapper wSingle(csiSingle, &parent); + QCOMPARE(wSingle.deleteWithoutPkAllowed(), true); + QCOMPARE(wSingle.viewTableName(), QStringLiteral("tbl_a")); + + // insertStructures groups the members per table (two tables) + QVariantList structures = w.insertStructures(); + QCOMPARE(structures.size(), 2); + TableWrapper* twA = structures.value(0).value(); + TableWrapper* twB = structures.value(1).value(); + QVERIFY(twA != nullptr); + QVERIFY(twB != nullptr); + QCOMPARE(twA->name(), QStringLiteral("tbl_a")); + QCOMPARE(twA->member().size(), 2); + QVERIFY(twA->primaryKey() != nullptr); + QCOMPARE(twA->primaryKey()->dbColumnName(), QStringLiteral("a_id")); + // first of several tables carries the base id + QCOMPARE(twA->baseId()->dbColumnName(), QStringLiteral("a_id")); + // the FK member pointing to tbl_a becomes its serial member + QCOMPARE(twA->serialMember()->dbColumnName(), QStringLiteral("b_a_id")); + QCOMPARE(twB->name(), QStringLiteral("tbl_b")); + QCOMPARE(twB->member().size(), 1); + QVERIFY(twB->primaryKey() == nullptr); + QVERIFY(twB->baseId() == nullptr); + QVERIFY(twB->serialMember() == nullptr); + + // insertStructures for a single table with a sequence primary key + QVariantList single = wSingle.insertStructures(); + QCOMPARE(single.size(), 1); + TableWrapper* twSolo = single.value(0).value(); + QVERIFY(twSolo != nullptr); + QCOMPARE(twSolo->name(), QStringLiteral("tbl_a")); + QVERIFY(twSolo->primaryKey() != nullptr); + // the sequence PK is used as the serial member + QCOMPARE(twSolo->serialMember()->dbColumnName(), QStringLiteral("a_id")); + QVERIFY(twSolo->baseId() == nullptr); +} + +void codegen_unit_test::codegenBaseDataClasses() +{ + // StructBuilder::ServiceTypeStruct + StructBuilder::ServiceTypeStruct sts; + sts.service = QStringLiteral("Svc"); + sts.type = QStringLiteral("Type"); + sts.bIsSequence = true; + QCOMPARE(sts.isExternalType(), true); + QCOMPARE(sts.isSequence(), true); + QCOMPARE(sts.fqType(), QStringLiteral("Svc::Type")); + + StructBuilder::ServiceTypeStruct local; + local.type = QStringLiteral("Plain"); + QCOMPARE(local.isExternalType(), false); + + // SqlGenerator::SqlStatements::isEmpty only considers the DROP statements + SqlGenerator::SqlStatements stmts; + QCOMPARE(stmts.isEmpty(), true); + stmts.createTables.append(QStringLiteral("CREATE TABLE t()")); + QCOMPARE(stmts.isEmpty(), true); + stmts.dropTables.append(QStringLiteral("DROP TABLE t")); + QCOMPARE(stmts.isEmpty(), false); + + // EnumInformation + EnumInformation ei; + QCOMPARE(ei.isValid(), false); + ei.enumName = QStringLiteral("E"); + QCOMPARE(ei.isValid(), true); + + // MemberInformation isEmpty / toString (CSV format) + MemberInformation mi; + QCOMPARE(mi.isEmpty(), true); + mi.ea.table = QStringLiteral("t"); + mi.ea.column = QStringLiteral("c"); + mi.ea.type = QStringLiteral("int"); + mi.db.table = QStringLiteral("t"); + mi.db.columnName = QStringLiteral("c"); + mi.cpp.memberName = QStringLiteral("c"); + QCOMPARE(mi.isEmpty(), false); + QCOMPARE(mi.toString(), + QStringLiteral("t;c;int;t;c;;false;false;false;false;;c;false;false;;;;;;")); + + // CompleteTopicTypeInformation + CompleteTopicTypeInformation ctti; + QCOMPARE(ctti.isValid(), false); // no name + ctti.name = QStringLiteral("S"); + QCOMPARE(ctti.isValid(), false); // still no members + ctti.baseValueType = QStringLiteral("B"); + QCOMPARE(ctti.hasBaseValue(), true); + IdlData m; + m.name = QStringLiteral("x"); + m.dataTypeInfo = QSharedPointer(new TestDataTypeInfo(QStringLiteral("int"), + QStringLiteral("int"), true)); + ctti.members = {m}; + QCOMPARE(ctti.isValid(), true); + + // CompleteTableInformation / CompleteStructInformation + CompleteTableInformation cti; + QCOMPARE(cti.isEmpty(), true); + MemberInformation col; + col.ea.column = QStringLiteral("c1"); + cti.columns = {col}; + QCOMPARE(cti.isEmpty(), false); + + CompleteStructInformation csi; + QCOMPARE(csi.isEmpty(), true); + QSharedPointer ti(new TableInformation()); + ti->name = QStringLiteral("t"); + csi.primaryTable = ti; + QCOMPARE(csi.isEmpty(), false); +} + +QTEST_MAIN(codegen_unit_test) +#include "codegen_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/CMakeLists.txt index ede50d7cc..dd38e802e 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/CMakeLists.txt @@ -28,3 +28,8 @@ NGL_TARGET_LINK_LIBRARIES(LtsCoordinate ) ADD_SUBDIRECTORY(UnitTest_Coordinate) + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/unit-test/CMakeLists.txt new file mode 100644 index 000000000..66efa1aa6 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/unit-test/CMakeLists.txt @@ -0,0 +1,20 @@ +# +# Unit tests for the LtsCoordinate module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- LtsCoordinate: geo<->UTM conversion, MGRS formatting, distances, angles, +# --- elevation callback and batch LtsCoordVector conversion +if (TARGET LtsCoordinate) + ADD_EXECUTABLE(ltscoordinate_unit_test ltscoordinate_unit_test.cpp) + TARGET_LINK_LIBRARIES(ltscoordinate_unit_test + LtsCoordinate + LtsCoordTrans + Qt6::Core + Qt6::Test + ) + ADD_TEST(NAME ltscoordinate_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/unit-test/ltscoordinate_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/unit-test/ltscoordinate_unit_test.cpp new file mode 100644 index 000000000..a87a248af --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsCoordinate/unit-test/ltscoordinate_unit_test.cpp @@ -0,0 +1,314 @@ +// +// Unit tests for the LtsCoordinate module (Lts/Common/LtsCoordinate): +// LtsCoordinate (geo<->UTM conversion, MGRS formatting, distances, angles, +// elevation callback, conversion counter) and LtsCoordVector (batch +// geo->UTM conversion with zone caching). +// +// Note: geographic coordinates are radians everywhere (Core::GeoPosition +// and LtsCoordTrans::deg2rad/rad2deg), mirroring the LtsCoordTrans tests. +// +#include +#include +#include + +#include + +#include +#include + +#include +#include + +using namespace LtsCoordTrans; + +class ltscoordinate_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void cleanupTestCase(); + + void constructorsAndGeoAccessors(); + void validityChecks(); + void valueIsNullOrNan(); + void mgrsFormatting(); + void geoUtmRoundtrip(); + void fromUtm(); + void utmCoordinateDiffZone(); + void distanceUtm2D(); + void distanceUtm3D(); + void angleDegreeUtm2D(); + void elevationCallback(); + void conversionCounter(); + void ltsCoordVector(); +}; + +// The elevation callback is a static (process-wide) of LtsCoordinate — +// reset it so other tests in the suite are not affected. +void ltscoordinate_unit_test::cleanupTestCase() +{ + LtsCoordinate::setElevationCallback(nullptr); +} + +void ltscoordinate_unit_test::constructorsAndGeoAccessors() +{ + const double lon = 8.6264954126 * deg2rad(); + const double lat = 53.1138437183 * deg2rad(); + + LtsCoordinate coord(lon, lat, 42.5); + QCOMPARE(coord.geoPosition().lon, lon); + QCOMPARE(coord.geoPosition().lat, lat); + QCOMPARE(coord.z(), 42.5); + coord.setZ(1.0); + QCOMPARE(coord.z(), 1.0); + + const QPointF pnt = coord.toGeoPointF(); + QCOMPARE(pnt.x(), lon); + QCOMPARE(pnt.y(), lat); + + const LtsCoordinate fromPnt(QPointF(lon, lat), 7.0); + QCOMPARE(fromPnt.z(), 7.0); + + const LtsCoordinate def; + QVERIFY(std::isnan(def.z())); + + const LtsCoordinate fromGeo = LtsCoordinate::fromGeoPosition(lon, lat, 3.0); + QCOMPARE(fromGeo.z(), 3.0); + + const Core::GeoPosition2D geo2D(lon, lat); + const LtsCoordinate from2D(geo2D); + QCOMPARE(from2D.geoPosition().lon, lon); + QCOMPARE(from2D.geoPosition2D().lat, lat); +} + +void ltscoordinate_unit_test::validityChecks() +{ + // (0,0) is "null" for validity purposes (qFuzzyIsNull), not a real position + LtsCoordinate def; + QVERIFY(!def.hasGeo()); + QVERIFY(!def.hasUtm()); + QVERIFY(!def.isValid()); + + const double lon = 8.6264954126 * deg2rad(); + const double lat = 53.1138437183 * deg2rad(); + LtsCoordinate valid(lon, lat); + QVERIFY(valid.hasGeo()); + QVERIFY(valid.isValid()); + QVERIFY(!valid.hasUtm()); // not converted yet + + (void)valid.utmCoordinate(); + QVERIFY(valid.hasUtm()); + QVERIFY(valid.utmCoordinate().zone > 0); +} + +void ltscoordinate_unit_test::valueIsNullOrNan() +{ + QVERIFY(LtsCoordinate::valueIsNullOrNan(0.0)); + QVERIFY(LtsCoordinate::valueIsNullOrNan(std::nan(""))); + QVERIFY(!LtsCoordinate::valueIsNullOrNan(1.0)); + QVERIFY(!LtsCoordinate::valueIsNullOrNan(-1e-6)); +} + +// Known MGRS values, mirrored from the LtsCoordTrans test suite through the +// LtsCoordinate facade. +void ltscoordinate_unit_test::mgrsFormatting() +{ + const LtsCoordinate coord(8.6264954126 * deg2rad(), 53.1138437183 * deg2rad()); + QCOMPARE(coord.mgrs(0), QStringLiteral("32U MD")); + QCOMPARE(coord.mgrs(8), QStringLiteral("32U MD 74999999 84999999")); + // no-space form is the concatenation of the space form for this coordinate + QCOMPARE(coord.mgrs(8, false), QStringLiteral("32UMD7499999984999999")); + + const LtsCoordinate coord2(11.3003696521 * deg2rad(), 52.8224535191 * deg2rad()); + QCOMPARE(coord2.mgrs(0), QStringLiteral("32U PD")); + QCOMPARE(coord2.mgrs(5), QStringLiteral("32U PD 55000 55000")); + + const LtsCoordinate coord3(54.8821607021 * deg2rad(), 24.2631973752 * deg2rad()); + QCOMPARE(coord3.mgrs(0), QStringLiteral("40R BM")); + QCOMPARE(coord3.mgrs(5), QStringLiteral("40R BM 85000 85000")); + + // Without an elevation callback the z part is NaN + const QString withZ = coord.mgrsWithZ(0); + QVERIFY(withZ.startsWith(QStringLiteral("32U MD z="))); +} + +void ltscoordinate_unit_test::geoUtmRoundtrip() +{ + const double lonDeg = 8.6264954126; + const double latDeg = 53.1138437183; + LtsCoordinate coord(lonDeg * deg2rad(), latDeg * deg2rad(), 5.0); + + const UtmCoordinate utm = coord.utmCoordinate(); + QVERIFY(utm.zone > 0); + + const QPointF geoBack = utm.geographic(); + QVERIFY(std::abs(geoBack.x() * rad2deg() - lonDeg) < 1e-6); + QVERIFY(std::abs(geoBack.y() * rad2deg() - latDeg) < 1e-6); + + // toUtmPointF / toUtmPoint3D agree with the cached UTM coordinate + const QPointF utmPnt = coord.toUtmPointF(); + QVERIFY(std::abs(utmPnt.x() - utm.x) < 1e-6); + QVERIFY(std::abs(utmPnt.y() - utm.y) < 1e-6); + const LtsUtils::QPoint3D utm3D = coord.toUtmPoint3D(); + QCOMPARE(utm3D.z(), 5.0); +} + +void ltscoordinate_unit_test::fromUtm() +{ + const LtsCoordinate c1 = LtsCoordinate::fromUtm(QPointF(500000., 5000000.), 32, 10.0); + QCOMPARE(c1.utmCoordinate().zone, 32); + QVERIFY(std::abs(c1.utmCoordinate().x - 500000.) < 0.01); + QVERIFY(std::abs(c1.utmCoordinate().y - 5000000.) < 0.01); + QCOMPARE(c1.z(), 10.0); + + const UtmCoordinate utm(32, 500123., 5000456., true); + const LtsCoordinate c2 = LtsCoordinate::fromUtmCoordinate(utm, 20.0); + QCOMPARE(c2.z(), 20.0); + QVERIFY(std::abs(c2.utmCoordinate().x - 500123.) < 0.01); + QVERIFY(std::abs(c2.utmCoordinate().y - 5000456.) < 0.01); + + const UtmCoordinate3D c3 = c1.toUtmCoordinate3D(); + QCOMPARE(c3.z(), 10.0); +} + +void ltscoordinate_unit_test::utmCoordinateDiffZone() +{ + const double lon = 8.6264954126 * deg2rad(); + const double lat = 53.1138437183 * deg2rad(); + LtsCoordinate coord(lon, lat); + + const UtmCoordinate in31 = coord.utmCoordinateDiffZone(31); + const UtmCoordinate in32 = coord.utmCoordinateDiffZone(32); + QCOMPARE(in31.zone, 31); + QCOMPARE(in32.zone, 32); + QVERIFY(in31.x != in32.x); + + // Same zone: diff-zone result matches the default conversion + const UtmCoordinate defaultZone = coord.utmCoordinate(); + const UtmCoordinate sameZone = coord.utmCoordinateDiffZone(defaultZone.zone); + QCOMPARE(sameZone.x, defaultZone.x); + QCOMPARE(sameZone.y, defaultZone.y); +} + +void ltscoordinate_unit_test::distanceUtm2D() +{ + const LtsCoordinate a = LtsCoordinate::fromUtm(QPointF(500000., 5000000.), 32, 0.); + const LtsCoordinate b = LtsCoordinate::fromUtm(QPointF(500100., 5000000.), 32, 0.); + + QVERIFY(std::abs(a.distanceUtm2D(b) - 100.0) < 1e-3); + QVERIFY(std::abs(a.distanceUtm2D(a)) < 1e-6); + QVERIFY(std::abs(a.distanceUtm2D(b) - b.distanceUtm2D(a)) < 1e-9); +} + +void ltscoordinate_unit_test::distanceUtm3D() +{ + const LtsCoordinate a = LtsCoordinate::fromUtm(QPointF(500000., 5000000.), 32, 0.); + const LtsCoordinate b = LtsCoordinate::fromUtm(QPointF(500100., 5000000.), 32, 40.); + + const double expected = std::hypot(100., 40.); + QVERIFY(std::abs(a.distanceUtm3D(b) - expected) < 1e-3); +} + +void ltscoordinate_unit_test::angleDegreeUtm2D() +{ + const LtsCoordinate a = LtsCoordinate::fromUtm(QPointF(500000., 5000000.), 32, 0.); + const LtsCoordinate east = LtsCoordinate::fromUtm(QPointF(500100., 5000000.), 32, 0.); + const LtsCoordinate north = LtsCoordinate::fromUtm(QPointF(500000., 5000100.), 32, 0.); + + // Conventions: QLineF angle (0 = +x) shifted by +90 degrees + QVERIFY(std::abs(LtsCoordinate::angleDegreeUtm2D(a, east) - 90.0) < 0.1); + QVERIFY(std::abs(LtsCoordinate::angleDegreeUtm2D(a, north) - 0.0) < 0.1); +} + +void ltscoordinate_unit_test::elevationCallback() +{ + LtsCoordinate::setElevationCallback([](double, double, double, bool* covered) { + if (covered) + *covered = true; + return 123.45; + }); + QVERIFY(LtsCoordinate::hasElevationCallback()); + + const double lon = 8.6264954126 * deg2rad(); + const double lat = 53.1138437183 * deg2rad(); + LtsCoordinate coord(lon, lat); + + bool covered = false; + QCOMPARE(coord.elevationAt(0.0, &covered), 123.45); + QVERIFY(covered); + QCOMPARE(LtsCoordinate::elevationAt(coord), 123.45); + + coord.setZValueAccordingElevation(); + QCOMPARE(coord.z(), 123.45); + QVERIFY(std::abs(coord.heightOverElevation()) < 1e-9); + + const LtsCoordinate above(lon, lat, 200.0); + QVERIFY(std::abs(above.heightOverElevation() - (200.0 - 123.45)) < 1e-9); + + // replaceZValueByElevationIfValue: NaN z is replaced + LtsCoordinate nanZ(lon, lat); + QVERIFY(std::isnan(nanZ.z())); + nanZ.replaceZValueByElevationIfValue(); + QCOMPARE(nanZ.z(), 123.45); + + // matching z is replaced, non-matching z is left alone + LtsCoordinate z77(lon, lat, 77.0); + z77.replaceZValueByElevationIfValue(77.0); + QCOMPARE(z77.z(), 123.45); + z77.replaceZValueByElevationIfValue(99.0); + QCOMPARE(z77.z(), 123.45); + + // mgrsWithZ now carries the elevation + const QString withZ = coord.mgrsWithZ(0); + QVERIFY(withZ.startsWith(QStringLiteral("32U MD z=123.45"))); +} + +void ltscoordinate_unit_test::conversionCounter() +{ + LtsCoordinate::resetConversionCounter(); + QCOMPARE(LtsCoordinate::getConversionCounter(), 0L); + + const double lon = 8.6264954126 * deg2rad(); + const double lat = 53.1138437183 * deg2rad(); + LtsCoordinate coord(lon, lat); + (void)coord.utmCoordinate(); + + // The counter is only incremented in LTS_DEBUG builds (the pipeline + // driver defines it: BUILD_RPM is not set) + QVERIFY(LtsCoordinate::getConversionCounter() >= 1L); +} + +void ltscoordinate_unit_test::ltsCoordVector() +{ + std::vector geos; + geos.emplace_back(8.6264954126 * deg2rad(), 53.1138437183 * deg2rad(), 1.0); + geos.emplace_back(11.3003696521 * deg2rad(), 52.8224535191 * deg2rad(), 2.0); + // (14.0 E, 52.0 N) - in UTM zone 32 like the others (toStdUtmCoord(32)) + geos.emplace_back(14.0 * deg2rad(), 52.0 * deg2rad(), 3.0); + + const LtsCoordVector vec(geos); + QCOMPARE(vec.size(), 3); + QCOMPARE(vec.geoPosition().size(), 3u); + QCOMPARE(vec.geoPosition()[1].lat, geos[1].lat); + + const std::vector utmStd = vec.toStdUtmCoord(32); + QCOMPARE(utmStd.size(), 3u); + for (const auto& u : utmStd) + QCOMPARE(u.zone, 32); + + const QVector utmQv = vec.toQVectorUtmCoord(32); + QCOMPARE(utmQv.size(), 3); + QCOMPARE(utmQv[0].x, utmStd[0].x); + + const QVector pts = vec.toQVectorQPointF(32); + QCOMPARE(pts.size(), 3); + for (const QPointF& p : pts) + QVERIFY(std::isfinite(p.x()) && std::isfinite(p.y())); + + // Per-zone cache: repeated calls return the same values + const QVector again = vec.toQVectorQPointF(32); + QCOMPARE(again, pts); +} + +QTEST_MAIN(ltscoordinate_unit_test) +#include "ltscoordinate_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/CMakeLists.txt index 0609b4f12..fbe990de6 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/CMakeLists.txt @@ -156,3 +156,8 @@ IF(BUILD_RTI) ) TARGET_COMPILE_DEFINITIONS(LtsSessionData PUBLIC BUILD_RTI) ENDIF() + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/unit-test/CMakeLists.txt new file mode 100644 index 000000000..5356dd670 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/unit-test/CMakeLists.txt @@ -0,0 +1,19 @@ +# +# Unit tests for the LtsSession module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- LtsSession: LtsSessionData (session state singleton - pure data logic) +if (TARGET LtsSession) + ADD_EXECUTABLE(ltssession_unit_test ltssession_unit_test.cpp) + TARGET_LINK_LIBRARIES(ltssession_unit_test + LtsSessionData + Qt6::Core + Qt6::Gui + Qt6::Test + ) + ADD_TEST(NAME ltssession_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/unit-test/ltssession_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/unit-test/ltssession_unit_test.cpp new file mode 100644 index 000000000..440257f54 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Common/LtsSession/unit-test/ltssession_unit_test.cpp @@ -0,0 +1,321 @@ +// +// Unit tests for the LtsSession module: LtsSessionData (the module's pure +// data class - session state singleton, presets, operator rights, visible +// parties and debug flags). Compiled into the LtsSessionData library target. +// +#include + +#include + +#include +#include +#include + +using LtsSessionData::PartiesByExercise; +using SessionData = LtsSessionData::LtsSessionData; + +class ltssession_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void singletonIdentity(); + void constructorDefaults(); + void operatorRightChecks(); + void presetsRoundtrip(); + void removePresetsKeepsDomainId(); + void sessionFieldsRoundtrip(); + void loggedOut(); + void visibleParties(); + void debugFlags(); + void clearScope(); + +private: + // LtsSessionData is a process-wide singleton and clear() only resets the + // preset + operator state. Bring it back to a known baseline before each + // slot so the slots do not depend on execution order. + void resetSessionData(); +}; + +void ltssession_unit_test::resetSessionData() +{ + auto* data = SessionData::getInstance(); + data->clear(); + // fields that clear() intentionally does not reset: + data->setDomainIdPreset(-1); + data->setQosFile(QStringList()); + data->setScriptId(0); + data->setWorkstationId(-1); + data->setRequestedAppId(LtsAppDescriptor::App::Login); + data->setTcpConnectionUrl(QUrl()); + data->setAsTablet(false); + data->setPlaceScriptScreens(false); + for (const QString& key : data->debugFlags().keys()) + data->removeDebugFlag(key); +} + +void ltssession_unit_test::singletonIdentity() +{ + resetSessionData(); + + auto* data = SessionData::getInstance(); + QVERIFY(data != nullptr); + // singleton: same instance on every call + QCOMPARE(SessionData::getInstance(), data); +} + +void ltssession_unit_test::constructorDefaults() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + // presets + QCOMPARE(data->getDomainIdPreset(), -1); + QCOMPARE(data->getPartitionIdPreset(), -1); + QCOMPARE(data->getPartitionTypePreset(), IDL::PartitionType::Any); + QVERIFY(data->getOperatorNamePreset().isEmpty()); + QVERIFY(data->getPasswordPreset().isEmpty()); + QVERIFY(data->getQosFile().isEmpty()); + + // session state + QCOMPARE(data->getOperatorId(), -1); + QCOMPARE(data->getScriptId(), 0); + QVERIFY(data->getOperatorName().isEmpty()); + QVERIFY(data->getOperatorRights().isEmpty()); + QVERIFY(!data->getShowTerminalWindow()); + QVERIFY(data->getProfilePicture().isNull()); + QVERIFY(data->getVisibleParties().isEmpty()); + QVERIFY(!data->getTcpConnectionUrl().isValid()); + QCOMPARE(data->getRequestedAppId(), LtsAppDescriptor::App::Login); + QCOMPARE(data->getWorkstationId(), -1); + QVERIFY(!data->isTablet()); + QVERIFY(!data->getPlaceScriptScreens()); + QVERIFY(data->debugFlags().isEmpty()); +} + +void ltssession_unit_test::operatorRightChecks() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + // fresh state: only right 0 is granted + QVERIFY(data->hasOperatorRight(0)); + QVERIFY(!data->hasOperatorRight(1)); + QVERIFY(!data->hasOperatorRight(-1)); + + // explicit rights + data->setOperatorRights(QSet{1, 2}); + QVERIFY(data->hasOperatorRight(1)); + QVERIFY(data->hasOperatorRight(2)); + QVERIFY(!data->hasOperatorRight(3)); + QVERIFY(data->hasOperatorRight(0)); // 0 is always granted + + // -1 means "all rights" + data->setOperatorRights(QSet{-1}); + QVERIFY(data->hasOperatorRight(42)); + QVERIFY(data->hasOperatorRight(12345)); + QVERIFY(data->hasOperatorRight(0)); +} + +void ltssession_unit_test::presetsRoundtrip() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + data->setOperatorPreset(QStringLiteral("operator-name"), QStringLiteral("secret")); + QCOMPARE(data->getOperatorNamePreset(), QStringLiteral("operator-name")); + QCOMPARE(data->getPasswordPreset(), QStringLiteral("secret")); + + data->setDomainIdPreset(7); + QCOMPARE(data->getDomainIdPreset(), 7); + + data->setPartitionPreset(3, IDL::PartitionType::Exercise); + QCOMPARE(data->getPartitionIdPreset(), 3); + QCOMPARE(data->getPartitionTypePreset(), IDL::PartitionType::Exercise); + + const QStringList expectedQos{QStringLiteral("a.qos"), QStringLiteral("b.qos")}; + data->setQosFile(expectedQos); + QCOMPARE(data->getQosFile(), expectedQos); +} + +void ltssession_unit_test::removePresetsKeepsDomainId() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + data->setOperatorPreset(QStringLiteral("op"), QStringLiteral("pw")); + data->setDomainIdPreset(7); + data->setPartitionPreset(3, IDL::PartitionType::Global); + + data->removePresets(); + + QCOMPARE(data->getPartitionIdPreset(), -1); + QCOMPARE(data->getPartitionTypePreset(), IDL::PartitionType::Any); + QVERIFY(data->getOperatorNamePreset().isEmpty()); + QVERIFY(data->getPasswordPreset().isEmpty()); + // the domain id preset is kept on purpose + QCOMPARE(data->getDomainIdPreset(), 7); +} + +void ltssession_unit_test::sessionFieldsRoundtrip() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + data->setScriptId(5); + QCOMPARE(data->getScriptId(), 5); + + data->setOperatorId(42); + QCOMPARE(data->getOperatorId(), 42); + + data->setOperatorName(QStringLiteral("Max Mustermann")); + QCOMPARE(data->getOperatorName(), QStringLiteral("Max Mustermann")); + + data->setShowTerminalWindow(true); + QVERIFY(data->getShowTerminalWindow()); + + data->setTcpConnectionUrl(QUrl(QStringLiteral("tcp://127.0.0.1:51234"))); + QCOMPARE(data->getTcpConnectionUrl(), QUrl(QStringLiteral("tcp://127.0.0.1:51234"))); + + data->setRequestedAppId(LtsAppDescriptor::App::Situation); + QCOMPARE(data->getRequestedAppId(), LtsAppDescriptor::App::Situation); + + data->setWorkstationId(11); + QCOMPARE(data->getWorkstationId(), 11); + + data->setPlaceScriptScreens(true); + QVERIFY(data->getPlaceScriptScreens()); + + // a null pixmap is a valid (unfilled) profile picture in console mode + data->setProfilePicture(QPixmap()); + QVERIFY(data->getProfilePicture().isNull()); +} + +void ltssession_unit_test::loggedOut() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + data->setScriptId(9); // not part of the logged-out state + data->setOperatorId(5); + data->setOperatorName(QStringLiteral("op")); + data->setOperatorRights(QSet{1}); + data->setShowTerminalWindow(true); + PartiesByExercise parties; + parties.exerciseId = 1; + parties.visibleParties = QVector{10}; + data->setVisibleParties(QVector{parties}); + + data->loggedOut(); + + QCOMPARE(data->getOperatorId(), -1); + QVERIFY(data->getOperatorName().isEmpty()); + QVERIFY(data->getOperatorRights().isEmpty()); + QVERIFY(!data->getShowTerminalWindow()); + QVERIFY(data->getVisibleParties().isEmpty()); + QVERIFY(data->getProfilePicture().isNull()); + // unrelated fields are left untouched + QCOMPARE(data->getScriptId(), 9); +} + +void ltssession_unit_test::visibleParties() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + PartiesByExercise exercise1; + exercise1.exerciseId = 1; + exercise1.visibleParties = QVector{10, 11}; + PartiesByExercise exercise2; + exercise2.exerciseId = 2; + exercise2.visibleParties = QVector{20}; + data->setVisibleParties(QVector{exercise1, exercise2}); + + QVERIFY(data->isVisibleParty(1, 10)); + QVERIFY(data->isVisibleParty(1, 11)); + QVERIFY(!data->isVisibleParty(1, 12)); + QVERIFY(data->isVisibleParty(2, 20)); + QVERIFY(!data->isVisibleParty(2, 10)); + QVERIFY(!data->isVisibleParty(99, 10)); // unknown exercise + + const QVector expectedParties1{10, 11}; + QCOMPARE(data->getVisibleParties(1), expectedParties1); + QCOMPARE(data->getVisibleParties(2), QVector{20}); + QVERIFY(data->getVisibleParties(99).isEmpty()); + + const auto& all = data->getVisibleParties(); + QCOMPARE(static_cast(all.size()), 2); + QCOMPARE(all[0].exerciseId, 1); + QCOMPARE(all[1].exerciseId, 2); +} + +void ltssession_unit_test::debugFlags() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + QVERIFY(!data->hasDebugFlag(QStringLiteral("flag"))); + QVERIFY(data->debugFlagValue(QStringLiteral("flag")).isNull()); + + data->setDebugFlagValue(QStringLiteral("flag"), 42); + QVERIFY(data->hasDebugFlag(QStringLiteral("flag"))); + QCOMPARE(data->debugFlagValue(QStringLiteral("flag")).toInt(), 42); + + // values are overwritten, not accumulated + data->setDebugFlagValue(QStringLiteral("flag"), QStringLiteral("value")); + QCOMPARE(data->debugFlagValue(QStringLiteral("flag")).toString(), QStringLiteral("value")); + QCOMPARE(static_cast(data->debugFlags().size()), 1); + + data->setDebugFlagValue(QStringLiteral("other"), 7); + QCOMPARE(static_cast(data->debugFlags().size()), 2); + + data->removeDebugFlag(QStringLiteral("flag")); + QVERIFY(!data->hasDebugFlag(QStringLiteral("flag"))); + QVERIFY(data->debugFlagValue(QStringLiteral("flag")).isNull()); + QCOMPARE(static_cast(data->debugFlags().size()), 1); + + data->removeDebugFlag(QStringLiteral("other")); + QVERIFY(data->debugFlags().isEmpty()); + // removing an unknown flag is a no-op + data->removeDebugFlag(QStringLiteral("unknown")); + QVERIFY(data->debugFlags().isEmpty()); +} + +void ltssession_unit_test::clearScope() +{ + resetSessionData(); + auto* data = SessionData::getInstance(); + + data->setScriptId(9); + data->setWorkstationId(3); + data->setTcpConnectionUrl(QUrl(QStringLiteral("tcp://127.0.0.1:51234"))); + data->setPlaceScriptScreens(true); + data->setDebugFlagValue(QStringLiteral("flag"), 1); + data->setOperatorId(5); + data->setOperatorName(QStringLiteral("op")); + data->setOperatorRights(QSet{1}); + data->setDomainIdPreset(7); + data->setPartitionPreset(3, IDL::PartitionType::Exercise); + + data->clear(); + + // operator state and presets are gone + QCOMPARE(data->getOperatorId(), -1); + QVERIFY(data->getOperatorName().isEmpty()); + QVERIFY(data->getOperatorRights().isEmpty()); + QCOMPARE(data->getPartitionIdPreset(), -1); + QCOMPARE(data->getPartitionTypePreset(), IDL::PartitionType::Any); + QVERIFY(data->getOperatorNamePreset().isEmpty()); + QVERIFY(data->getPasswordPreset().isEmpty()); + + // everything else is preserved by clear() + QCOMPARE(data->getDomainIdPreset(), 7); + QCOMPARE(data->getScriptId(), 9); + QCOMPARE(data->getWorkstationId(), 3); + QCOMPARE(data->getTcpConnectionUrl(), QUrl(QStringLiteral("tcp://127.0.0.1:51234"))); + QVERIFY(data->getPlaceScriptScreens()); + QVERIFY(data->hasDebugFlag(QStringLiteral("flag"))); +} + +QTEST_MAIN(ltssession_unit_test) +#include "ltssession_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/CMakeLists.txt index a695d9b9f..99d866ed8 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/CMakeLists.txt @@ -152,3 +152,8 @@ NGL_INSTALL_TARGET(svcCoreClient) CREATE_TOPIC_TYPE_PLUGIN(${xml_FILE}) ENDIF(BUILD_RTI) + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/unit-test/CMakeLists.txt new file mode 100644 index 000000000..b12c9aa79 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/unit-test/CMakeLists.txt @@ -0,0 +1,19 @@ +# +# Unit tests for the DDS Core module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- DDS Core: generated function-reply objects and the CORE logger facade +if (TARGET ddscore) + ADD_EXECUTABLE(ddscore_unit_test ddscore_unit_test.cpp) + TARGET_LINK_LIBRARIES(ddscore_unit_test + ddscore + NGLLogger + Qt6::Core + Qt6::Test + ) + ADD_TEST(NAME ddscore_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/unit-test/ddscore_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/unit-test/ddscore_unit_test.cpp new file mode 100644 index 000000000..f62dbd839 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/DDS/Core/unit-test/ddscore_unit_test.cpp @@ -0,0 +1,169 @@ +// +// Unit tests for the ddscore module (Lts/DDS/Core): FunctionReplyObject +// (the module's pure data/logic class) and the CORE.* logger helper. +// +#include +#include + +#include +#include + +#include + +#include + +#include +#include + +class ddscore_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void functionReplyObjectDefault(); + void functionReplyObjectFromIdAndString(); + void functionReplyObjectFromStringOnly(); + void functionReplyObjectNullString(); + void functionReplyObjectWithArgs(); + void functionReplyObjectWithArgsAndAdditionalInfo(); + void functionReplyObjectRvalueArgs(); + void argOverloads(); + void setters(); + void formattedWithoutPlaceholders(); + void formattedExtraArgsIgnored(); + void translatedMatchesFormatted(); + void uniqueIdRoundtrip(); + void coreGetLoggerName(); +}; + +void ddscore_unit_test::initTestCase() +{ + NGL::NGLLoggerBootstrap::createNGLRootLogger("log4cxx.props", false); +} + +void ddscore_unit_test::functionReplyObjectDefault() +{ + Core::FunctionReplyObject reply; + QCOMPARE(reply.id(), -1); + QVERIFY(!reply.error()); + QCOMPARE(reply.str(), std::string()); + QCOMPARE(reply.additionalInfo(), std::string()); + QCOMPARE(reply.args().size(), 0u); + QCOMPARE(reply.uniqueId(), int64_t(0)); +} + +void ddscore_unit_test::functionReplyObjectFromIdAndString() +{ + Core::FunctionReplyObject reply(42, "error %1"); + QCOMPARE(reply.id(), 42); + QVERIFY(reply.error()); + QCOMPARE(reply.str(), std::string("error %1")); +} + +void ddscore_unit_test::functionReplyObjectFromStringOnly() +{ + Core::FunctionReplyObject reply("plain error"); + QCOMPARE(reply.id(), -1); + QVERIFY(reply.error()); + QCOMPARE(reply.str(), std::string("plain error")); +} + +void ddscore_unit_test::functionReplyObjectNullString() +{ + Core::FunctionReplyObject reply(7, static_cast(nullptr)); + QCOMPARE(reply.id(), 7); + QVERIFY(!reply.error()); + QCOMPARE(reply.str(), std::string()); +} + +void ddscore_unit_test::functionReplyObjectWithArgs() +{ + Core::FunctionReplyObject reply(5, "%1/%2", std::vector{"a", "b"}); + QCOMPARE(reply.id(), 5); + QCOMPARE(reply.str(), std::string("%1/%2")); + const std::vector expectedArgs = {"a", "b"}; + QCOMPARE(reply.args(), expectedArgs); + QCOMPARE(reply(), std::string("a/b")); +} + +void ddscore_unit_test::functionReplyObjectWithArgsAndAdditionalInfo() +{ + Core::FunctionReplyObject reply(5, "failed: %1", std::vector{"x"}, "context"); + QCOMPARE(reply.id(), 5); + QCOMPARE(reply.str(), std::string("failed: %1")); + QCOMPARE(reply.additionalInfo(), std::string("context")); + QCOMPARE(reply(), std::string("failed: x")); +} + +void ddscore_unit_test::functionReplyObjectRvalueArgs() +{ + Core::FunctionReplyObject reply( + 9, std::string("moved %1"), std::vector{"v"}, std::string("info")); + QCOMPARE(reply.id(), 9); + QCOMPARE(reply.str(), std::string("moved %1")); + QCOMPARE(reply.additionalInfo(), std::string("info")); + QCOMPARE(reply(), std::string("moved v")); +} + +void ddscore_unit_test::argOverloads() +{ + Core::FunctionReplyObject reply(1, "v=%1 s=%2 q=%3"); + reply.arg(7).arg(std::string("hello")).arg(QStringLiteral("qt")); + const std::vector expectedArgs = {"7", "hello", "qt"}; + QCOMPARE(reply.args(), expectedArgs); + QCOMPARE(reply(), std::string("v=7 s=hello q=qt")); +} + +void ddscore_unit_test::setters() +{ + Core::FunctionReplyObject reply; + reply.setId(11); + reply.setStr("updated %1"); + reply.setAdditionalInfo("note"); + QCOMPARE(reply.id(), 11); + QCOMPARE(reply.str(), std::string("updated %1")); + QVERIFY(reply.error()); + QCOMPARE(reply.additionalInfo(), std::string("note")); +} + +void ddscore_unit_test::formattedWithoutPlaceholders() +{ + Core::FunctionReplyObject reply(1, "no placeholder"); + reply.arg(QStringLiteral("ignored")); + QCOMPARE(reply(), std::string("no placeholder")); +} + +void ddscore_unit_test::formattedExtraArgsIgnored() +{ + Core::FunctionReplyObject reply(1, "only %1"); + reply.arg(QStringLiteral("a")).arg(QStringLiteral("b")); + QCOMPARE(reply(), std::string("only a")); +} + +void ddscore_unit_test::translatedMatchesFormatted() +{ + // No translator is installed, so QCoreApplication::translate returns the + // source string and translated() must equal the plain formatted result. + Core::FunctionReplyObject reply(1, "translated %1", std::vector{"t"}); + QCOMPARE(reply.translated().toStdString(), std::string("translated t")); + QCOMPARE(reply.translated().toStdString(), reply()); +} + +void ddscore_unit_test::uniqueIdRoundtrip() +{ + Core::FunctionReplyObject reply; + reply.setUniqueId(123456); + QCOMPARE(reply.uniqueId(), int64_t(123456)); +} + +void ddscore_unit_test::coreGetLoggerName() +{ + // Core::getLogger prefixes the requested name with the "CORE." namespace. + NGL::INGLLogger &logger = Core::getLogger("UnitTest"); + QCOMPARE(logger.getLoggerName(), std::string("CORE.UnitTest")); +} + +QTEST_MAIN(ddscore_unit_test) +#include "ddscore_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/CMakeLists.txt index 3f9de4581..5b24977b6 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/CMakeLists.txt @@ -37,3 +37,8 @@ NGL_TARGET_LINK_LIBRARIES(ddsQoS ) ENDIF(BUILD_RTI) + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/unit-test/CMakeLists.txt new file mode 100644 index 000000000..eea2af95b --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/unit-test/CMakeLists.txt @@ -0,0 +1,30 @@ +# +# Unit tests for the DDS QosProfile module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- DDS QosProfile: default QoS profile helpers +if (TARGET ddsQoS) + ADD_EXECUTABLE(ddsqos_unit_test ddsqos_unit_test.cpp) + # Path of the module's default_profile.xml fixture (see ddsqos_unit_test.cpp) + TARGET_COMPILE_DEFINITIONS(ddsqos_unit_test PRIVATE + DDSQOS_UNIT_TEST_PROFILE_XML="${CMAKE_CURRENT_SOURCE_DIR}/../default_profile.xml") + TARGET_LINK_LIBRARIES(ddsqos_unit_test + ddsQoS + NGLLogger + Qt6::Core + Qt6::Test + ) + # ddsQoS links the RTI DDS libraries PRIVATE - the test needs them itself + # for the RTI policy inline functions used by the QoS helpers + if (TARGET RTI::nddsc) + TARGET_LINK_LIBRARIES(ddsqos_unit_test + RTI::nddsc + RTI::nddscpp2 + ) + endif() + ADD_TEST(NAME ddsqos_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/unit-test/ddsqos_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/unit-test/ddsqos_unit_test.cpp new file mode 100644 index 000000000..f83c11ce8 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/DDS/QosProfile/unit-test/ddsqos_unit_test.cpp @@ -0,0 +1,190 @@ +// +// Unit tests for the ddsQoS module (Lts/DDS/QosProfile): DDS_DefaultQos. +// The functions under test only read QoS profiles through the RTI QosProvider +// (policy structs) - no DomainParticipant, Publication or Subscription is +// created and no DDS runtime/network access happens. +// +// The location of the default_profile.xml shipped with the module is passed +// via the DDSQOS_UNIT_TEST_PROFILE_XML compile definition +// (see Lts/DDS/QosProfile/unit-test/CMakeLists.txt). +// +#include +#include +#include + +#include + +#include +#include + +#include + +#include +#include + +namespace +{ +using namespace DDS::QosProfile; + +const QString &profileXml() +{ + static const QString path = QStringLiteral(DDSQOS_UNIT_TEST_PROFILE_XML); + return path; +} + +bool containsPeer(const dds::core::StringSeq &seq, const std::string &needle) +{ + for (const std::string &s : seq) + if (s == needle) + return true; + return false; +} +} + +class ddsqos_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void setDefaultLoadsProfile(); + void shmUdpParticipantQos(); + void shmParticipantQos(); + void shmUdpLocalhostParticipantQos(); + void domainParticipantTcpBindPort(); + void domainParticipantTcpV4LanPeers(); + void dataWriterDefaults(); + void dataWriterFunctionTopic(); + void dataReaderDefaults(); + void dataReaderFunctionTopic(); + void dataReaderDefaultByConnectionType(); + void printSmokeTest(); +}; + +void ddsqos_unit_test::initTestCase() +{ + NGL::NGLLoggerBootstrap::createNGLRootLogger("log4cxx.props", false); + + if (!QFile::exists(profileXml())) + QSKIP("default_profile.xml not available"); + QVERIFY(setDefault(QStringList{profileXml()})); +} + +void ddsqos_unit_test::setDefaultLoadsProfile() +{ + // Re-loading the known-good profile into the default provider must succeed. + QVERIFY(setDefault(QStringList{profileXml()})); +} + +void ddsqos_unit_test::shmUdpParticipantQos() +{ + const dds::domain::qos::DomainParticipantQos qos = domainParticipantShmUdp(); + const uint64_t expected = (rti::core::policy::TransportBuiltinMask::shmem() + | rti::core::policy::TransportBuiltinMask::udpv4()).to_ulong(); + QCOMPARE(qos->transport_builtin.mask().to_ulong(), expected); +} + +void ddsqos_unit_test::shmParticipantQos() +{ + const dds::domain::qos::DomainParticipantQos qos = domainParticipantShm(); + QCOMPARE(qos->transport_builtin.mask().to_ulong(), + rti::core::policy::TransportBuiltinMask::shmem().to_ulong()); + const dds::core::StringSeq peers = qos->discovery.initial_peers(); + QCOMPARE(peers.size(), 1u); + QCOMPARE(peers.at(0), std::string("30@builtin.shmem://")); +} + +void ddsqos_unit_test::shmUdpLocalhostParticipantQos() +{ + const dds::domain::qos::DomainParticipantQos qos = domainParticipantShmUdpLocalhost(); + const dds::core::StringSeq peers = qos->discovery.initial_peers(); + QVERIFY(containsPeer(peers, "builtin.shmem://")); + QVERIFY(containsPeer(peers, "builtin.udpv4://127.0.0.1")); + QVERIFY(qos->property.exists("dds.transport.UDPv4.builtin.multicast_enabled")); + QCOMPARE(qos->property.get("dds.transport.UDPv4.builtin.multicast_enabled"), + std::string("0")); +} + +void ddsqos_unit_test::domainParticipantTcpBindPort() +{ + // Non-tcpv4lan scheme: the port is written into the TCPv4 transport property. + const dds::domain::qos::DomainParticipantQos qos = + domainParticipantShmTcp(QUrl(QStringLiteral("tcp://10.0.0.5:9999"))); + QVERIFY(qos->property.exists("dds.transport.TCPv4.tcp1.server_bind_port")); + QCOMPARE(qos->property.get("dds.transport.TCPv4.tcp1.server_bind_port"), + std::string("9999")); +} + +void ddsqos_unit_test::domainParticipantTcpV4LanPeers() +{ + // tcpv4lan scheme: the fixed-up URL becomes the single initial peer with + // the "0@" domain prefix (fixupSchemeName in DDS_DefaultQos). + const dds::domain::qos::DomainParticipantQos qos = + domainParticipantShmTcp(QUrl(QStringLiteral("tcpv4lan://10.0.0.5:9999"))); + const dds::core::StringSeq peers = qos->discovery.initial_peers(); + QCOMPARE(peers.size(), 1u); + QCOMPARE(peers.at(0), std::string("0@tcpv4_lan://10.0.0.5:9999")); +} + +void ddsqos_unit_test::dataWriterDefaults() +{ + // Default (DataTopic) writer QoS comes from the is_default_qos "Base" profile. + const dds::pub::qos::DataWriterQos qos = dataWriterDefault(TopicType::DataTopic); + QCOMPARE(qos->reliability.kind(), dds::core::policy::ReliabilityKind::RELIABLE); + QCOMPARE(qos->durability.kind(), dds::core::policy::DurabilityKind::TRANSIENT_LOCAL); + QCOMPARE(qos->history.kind(), dds::core::policy::HistoryKind::KEEP_LAST); + QCOMPARE(qos->history.depth(), 1); + QCOMPARE(qos->publish_mode.kind(), rti::core::policy::PublishModeKind::ASYNCHRONOUS); + QCOMPARE(qos->resource_limits.max_samples(), 8000); +} + +void ddsqos_unit_test::dataWriterFunctionTopic() +{ + const dds::pub::qos::DataWriterQos qos = dataWriterDefault(TopicType::FunctionTopic); + QCOMPARE(qos->durability.kind(), dds::core::policy::DurabilityKind::TRANSIENT_LOCAL); + QCOMPARE(qos->history.kind(), dds::core::policy::HistoryKind::KEEP_LAST); + QCOMPARE(qos->history.depth(), 1); +} + +void ddsqos_unit_test::dataReaderDefaults() +{ + const dds::sub::qos::DataReaderQos qos = dataReaderDefault(TopicType::DataTopic); + QCOMPARE(qos->reliability.kind(), dds::core::policy::ReliabilityKind::RELIABLE); + QCOMPARE(qos->durability.kind(), dds::core::policy::DurabilityKind::TRANSIENT_LOCAL); + QCOMPARE(qos->history.kind(), dds::core::policy::HistoryKind::KEEP_LAST); + QCOMPARE(qos->history.depth(), 1); + QCOMPARE(qos->resource_limits.max_samples(), 8000); +} + +void ddsqos_unit_test::dataReaderFunctionTopic() +{ + // The FT reader profile overrides the Base reader history with KEEP_ALL. + const dds::sub::qos::DataReaderQos ftQos = dataReaderDefault(TopicType::FunctionTopic); + QCOMPARE(ftQos->history.kind(), dds::core::policy::HistoryKind::KEEP_ALL); + + const dds::sub::qos::DataReaderQos dataQos = dataReaderDefault(TopicType::DataTopic); + QCOMPARE(dataQos->history.kind(), dds::core::policy::HistoryKind::KEEP_LAST); +} + +void ddsqos_unit_test::dataReaderDefaultByConnectionType() +{ + // The ShmUdp participant profile inherits the Base reader QoS. + const dds::sub::qos::DataReaderQos qos = dataReaderDefault(ConnectionType::ShmUdp); + QCOMPARE(qos->reliability.kind(), dds::core::policy::ReliabilityKind::RELIABLE); + QCOMPARE(qos->history.kind(), dds::core::policy::HistoryKind::KEEP_LAST); + QCOMPARE(qos->resource_limits.max_samples(), 8000); +} + +void ddsqos_unit_test::printSmokeTest() +{ + // print() walks the profile policies - must not throw. + NGL::INGLLogger &logger = + NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("DDS.DefaultQosTest"); + print(domainParticipantShmUdp(), logger); + print(dataWriterDefault(TopicType::DataTopic), logger); + print(dataReaderDefault(TopicType::DataTopic), logger); +} + +QTEST_MAIN(ddsqos_unit_test) +#include "ddsqos_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/CMakeLists.txt index 38b930aa2..ce3a57536 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/CMakeLists.txt @@ -64,3 +64,8 @@ if(QT6_GUI_AVAILABLE) endif() NGL_INSTALL_TARGET(LogViewer) + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/unit-test/CMakeLists.txt new file mode 100644 index 000000000..04e18b572 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/unit-test/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# Unit tests for the LogViewer module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- LogViewer: log table model + sort/filter proxy. +# --- The module is an executable, so the model sources are compiled directly +# --- into the test (same pattern as the LogView test in TetraIF/unit-test). +if (TARGET LogViewer) + ADD_EXECUTABLE(logviewer_unit_test + logviewer_unit_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../logviewtablemodel.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../sortfiltermodel.cpp) + TARGET_LINK_LIBRARIES(logviewer_unit_test + NGLLogger + Qt6::Core + Qt6::Gui + Qt6::Test + ) + ADD_TEST(NAME logviewer_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/unit-test/logviewer_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/unit-test/logviewer_unit_test.cpp new file mode 100644 index 000000000..080f68208 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Tools/LogViewer/unit-test/logviewer_unit_test.cpp @@ -0,0 +1,553 @@ +// +// Unit tests for the LogViewer module: LogViewTableModel (log line storage, +// column/role data access, standard level colors) and SortFilterModel +// (string/date filtering and date-aware sorting). +// +// Both classes are the module's non-GUI logic (QAbstractTableModel / +// QSortFilterProxyModel, no widgets needed); they are compiled from the +// LogViewer executable sources. The other LogViewer classes are filter +// widgets and the main window. +// +#include "../logviewtablemodel.h" +#include "../sortfiltermodel.h" + +#include + +#include +#include + +using LogInformation = LogViewTableModel::logInformation; + +namespace +{ + +LogInformation makeLine(const QDateTime &date, const QString &level, const QString &logger, + const QString &data, const QString &location) +{ + LogInformation line; + line.date = date; + line.level = level; + line.logger = logger; + line.data = data; + line.location = location; + return line; +} + +QDateTime d(const QString &iso) +{ + return QDateTime::fromString(iso, Qt::ISODate); +} + +} // namespace + +class logviewer_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + // LogViewTableModel + void tableModelEmptyDimensions(); + void tableModelSetDataAndReadRoles(); + void tableModelInvalidIndex(); + void tableModelHeaderData(); + void tableModelStandardLevelColors(); + void tableModelAdjustColorsAndReset(); + void tableModelAddDataAndLength(); + void tableModelSetDataByIndex(); + void tableModelStartEndDateTimeSignals(); + + // SortFilterModel + void proxyStringFilter(); + void proxyDateStringFilterEdgeCases(); + void proxyDateFilter(); + void proxyResetAllFilter(); + void proxySortByDate(); + void proxySortByLevel(); + void proxyFigureFilterOutSignal(); +}; + +void logviewer_unit_test::initTestCase() +{ + // LogViewTableModel's constructor resolves its logger from the NGL root + // logger, so it must exist before the model is instantiated. + NGL::NGLLoggerBootstrap::createNGLRootLogger("log4cxx.props", false); +} + +// --------------------------------------------------------------------------- +// LogViewTableModel +// --------------------------------------------------------------------------- + +void logviewer_unit_test::tableModelEmptyDimensions() +{ + LogViewTableModel model; + // note: the overrides in LogViewTableModel redeclare the parameter without + // the base class' default value + QCOMPARE(model.rowCount(QModelIndex()), 0); + QCOMPARE(model.columnCount(QModelIndex()), LogViewTableModel::colLast + 1); + QCOMPARE(model.length(), 0); +} + +void logviewer_unit_test::tableModelSetDataAndReadRoles() +{ + LogViewTableModel model; + const QDateTime date = d(QStringLiteral("2024-05-06T12:34:56")); + + QVector lines; + lines << makeLine(date, QStringLiteral("INFO"), QStringLiteral("main"), QStringLiteral("hello"), + QStringLiteral("a.cpp:42")) + << makeLine(d(QStringLiteral("2024-05-07T08:00:00")), QStringLiteral("WARN"), + QStringLiteral("net"), QStringLiteral("world"), QStringLiteral("b.cpp:7")); + model.setData(lines); + + QCOMPARE(model.rowCount(QModelIndex()), 2); + QCOMPARE(model.length(), 2); + + // DisplayRole per column + QCOMPARE(model.data(model.index(0, LogViewTableModel::colDate), Qt::DisplayRole).toString(). + startsWith(QStringLiteral("2024-05-06 12:34:56,")), true); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colLevel), Qt::DisplayRole).toString(), + QStringLiteral("INFO")); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colLogger), Qt::DisplayRole).toString(), + QStringLiteral("main")); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colData), Qt::DisplayRole).toString(), + QStringLiteral("hello")); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colLocation), Qt::DisplayRole).toString(), + QStringLiteral("a.cpp:42")); + + // UserRole carries the raw QDateTime of the date column + QCOMPARE(model.data(model.index(0, LogViewTableModel::colDate), Qt::UserRole).toDateTime(), date); + + // itemAt: note the date format uses dashes between the time parts + QCOMPARE(model.itemAt(1, LogViewTableModel::colDate).startsWith(QStringLiteral("2024-05-07 08-00-00,")), + true); + QCOMPARE(model.itemAt(0, LogViewTableModel::colLevel), QStringLiteral("INFO")); + QCOMPARE(model.itemAt(0, LogViewTableModel::colData), QStringLiteral("hello")); + // unknown column index -> empty string + QCOMPARE(model.itemAt(0, 99), QString()); + + // replacing all data + model.setData(QVector()); + QCOMPARE(model.rowCount(QModelIndex()), 0); +} + +void logviewer_unit_test::tableModelInvalidIndex() +{ + LogViewTableModel model; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("INFO"), QStringLiteral("l"), + QStringLiteral("d"), QStringLiteral("x.cpp:1")); + model.setData(lines); + + // invalid index -> invalid QVariant for any role + const QModelIndex invalid = QModelIndex(); + QVERIFY(!model.data(invalid, Qt::DisplayRole).isValid()); + QVERIFY(!model.data(invalid, Qt::UserRole).isValid()); + QVERIFY(!model.data(invalid, Qt::BackgroundRole).isValid()); +} + +void logviewer_unit_test::tableModelHeaderData() +{ + LogViewTableModel model; + QCOMPARE(model.headerData(LogViewTableModel::colDate, Qt::Horizontal, Qt::DisplayRole).toString(), + QStringLiteral("Date")); + QCOMPARE(model.headerData(LogViewTableModel::colLevel, Qt::Horizontal, Qt::DisplayRole).toString(), + QStringLiteral("Level")); + QCOMPARE(model.headerData(LogViewTableModel::colLogger, Qt::Horizontal, Qt::DisplayRole).toString(), + QStringLiteral("Logger")); + QCOMPARE(model.headerData(LogViewTableModel::colData, Qt::Horizontal, Qt::DisplayRole).toString(), + QStringLiteral("Data")); + QCOMPARE(model.headerData(LogViewTableModel::colLocation, Qt::Horizontal, Qt::DisplayRole).toString(), + QStringLiteral("Location")); + + // vertical orientation and out-of-range section fall through to + // QAbstractTableModel::headerData (1-based: section+1 horizontal, 1 vertical) + QCOMPARE(model.headerData(0, Qt::Vertical, Qt::DisplayRole).toString(), QStringLiteral("1")); + QCOMPARE(model.headerData(99, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("100")); +} + +void logviewer_unit_test::tableModelStandardLevelColors() +{ + LogViewTableModel model; + + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("INFO"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("WARN"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("ERROR"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("FATAL"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("DEBUG"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("TRACE"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("CUSTOM"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")); + model.setData(lines); + + const int col = LogViewTableModel::colLevel; + QCOMPARE(model.data(model.index(0, col), Qt::BackgroundRole).value(), Qt::blue); + QCOMPARE(model.data(model.index(0, col), Qt::ForegroundRole).value(), Qt::white); + QCOMPARE(model.data(model.index(1, col), Qt::BackgroundRole).value(), Qt::yellow); + QCOMPARE(model.data(model.index(1, col), Qt::ForegroundRole).value(), Qt::black); + QCOMPARE(model.data(model.index(2, col), Qt::BackgroundRole).value(), Qt::darkRed); + QCOMPARE(model.data(model.index(2, col), Qt::ForegroundRole).value(), Qt::white); + QCOMPARE(model.data(model.index(3, col), Qt::BackgroundRole).value(), Qt::red); + QCOMPARE(model.data(model.index(3, col), Qt::ForegroundRole).value(), Qt::black); + QCOMPARE(model.data(model.index(4, col), Qt::BackgroundRole).value(), Qt::green); + QCOMPARE(model.data(model.index(4, col), Qt::ForegroundRole).value(), Qt::black); + QCOMPARE(model.data(model.index(5, col), Qt::BackgroundRole).value(), Qt::lightGray); + QCOMPARE(model.data(model.index(5, col), Qt::ForegroundRole).value(), Qt::black); + + // unknown level: no color registered -> invalid variants + QVERIFY(!model.data(model.index(6, col), Qt::BackgroundRole).isValid()); + QVERIFY(!model.data(model.index(6, col), Qt::ForegroundRole).isValid()); + + // other columns have no background/foreground color + QVERIFY(!model.data(model.index(0, LogViewTableModel::colData), Qt::BackgroundRole).isValid()); +} + +void logviewer_unit_test::tableModelAdjustColorsAndReset() +{ + LogViewTableModel model; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("INFO"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("WARN"), QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")); + model.setData(lines); + const int col = LogViewTableModel::colLevel; + + // adjustCellColor only affects levels that are already registered + model.adjustCellColor(Qt::red, QStringLiteral("INFO")); + QCOMPARE(model.data(model.index(0, col), Qt::BackgroundRole).value(), Qt::red); + // unknown level: no effect + model.adjustCellColor(Qt::green, QStringLiteral("NOPE")); + QCOMPARE(model.data(model.index(1, col), Qt::BackgroundRole).value(), Qt::yellow); + + // adjustFontColor + model.adjustFontColor(Qt::black, QStringLiteral("INFO")); + QCOMPARE(model.data(model.index(0, col), Qt::ForegroundRole).value(), Qt::black); + + // reset() clears the data and restores the standard colors + QVERIFY(model.reset()); + QCOMPARE(model.length(), 0); + QCOMPARE(model.rowCount(QModelIndex()), 0); +} + +void logviewer_unit_test::tableModelAddDataAndLength() +{ + LogViewTableModel model; + QCOMPARE(model.length(), 0); + model.addData(makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("INFO"), QStringLiteral("l"), + QStringLiteral("d"), QStringLiteral("l.cpp:1"))); + QCOMPARE(model.length(), 1); + model.addData(makeLine(d(QStringLiteral("2024-01-02T00:00:00")), QStringLiteral("WARN"), QStringLiteral("l"), + QStringLiteral("d"), QStringLiteral("l.cpp:1"))); + QCOMPARE(model.length(), 2); + QCOMPARE(model.data(model.index(1, LogViewTableModel::colLevel), Qt::DisplayRole).toString(), + QStringLiteral("WARN")); +} + +void logviewer_unit_test::tableModelSetDataByIndex() +{ + LogViewTableModel model; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("INFO"), QStringLiteral("l"), + QStringLiteral("d"), QStringLiteral("l.cpp:1")); + model.setData(lines); + + // only Qt::EditRole is accepted + QVERIFY(!model.setData(model.index(0, LogViewTableModel::colLevel), QStringLiteral("X"), + Qt::DisplayRole)); + + QVERIFY(model.setData(model.index(0, LogViewTableModel::colLevel), QStringLiteral("ERROR"), + Qt::EditRole)); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colLevel), Qt::DisplayRole).toString(), + QStringLiteral("ERROR")); + + QVERIFY(model.setData(model.index(0, LogViewTableModel::colLogger), QStringLiteral("other"), + Qt::EditRole)); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colLogger), Qt::DisplayRole).toString(), + QStringLiteral("other")); + + QVERIFY(model.setData(model.index(0, LogViewTableModel::colData), QStringLiteral("new data"), + Qt::EditRole)); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colData), Qt::DisplayRole).toString(), + QStringLiteral("new data")); + + QVERIFY(model.setData(model.index(0, LogViewTableModel::colLocation), QStringLiteral("m.cpp:9"), + Qt::EditRole)); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colLocation), Qt::DisplayRole).toString(), + QStringLiteral("m.cpp:9")); + + const QDateTime newDate = d(QStringLiteral("2024-02-02T02:02:02")); + QVERIFY(model.setData(model.index(0, LogViewTableModel::colDate), newDate, Qt::EditRole)); + QCOMPARE(model.data(model.index(0, LogViewTableModel::colDate), Qt::UserRole).toDateTime(), newDate); +} + +void logviewer_unit_test::tableModelStartEndDateTimeSignals() +{ + // empty model: no signals + LogViewTableModel empty; + bool started = false; + bool ended = false; + QObject::connect(&empty, &LogViewTableModel::startTimeSetted, [&started](const QDateTime &) { started = true; }); + QObject::connect(&empty, &LogViewTableModel::endTimeSetted, [&ended](const QDateTime &) { ended = true; }); + empty.startDateTime(); + empty.endDateTime(); + QVERIFY(!started); + QVERIFY(!ended); + + // populated model: first/last line date + LogViewTableModel model; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-03-01T00:00:00")), QStringLiteral("INFO"), QStringLiteral("l"), + QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-03-02T00:00:00")), QStringLiteral("WARN"), QStringLiteral("l"), + QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-03-03T00:00:00")), QStringLiteral("ERROR"), QStringLiteral("l"), + QStringLiteral("d"), QStringLiteral("l.cpp:1")); + model.setData(lines); + + QDateTime start; + QDateTime end; + QObject::connect(&model, &LogViewTableModel::startTimeSetted, [&start](const QDateTime &t) { start = t; }); + QObject::connect(&model, &LogViewTableModel::endTimeSetted, [&end](const QDateTime &t) { end = t; }); + model.startDateTime(); + model.endDateTime(); + QCOMPARE(start, d(QStringLiteral("2024-03-01T00:00:00"))); + QCOMPARE(end, d(QStringLiteral("2024-03-03T00:00:00"))); +} + +// --------------------------------------------------------------------------- +// SortFilterModel +// --------------------------------------------------------------------------- + +void logviewer_unit_test::proxyStringFilter() +{ + LogViewTableModel source; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("INFO"), + QStringLiteral("main"), QStringLiteral("alpha"), QStringLiteral("a.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-02T00:00:00")), QStringLiteral("WARN"), + QStringLiteral("net"), QStringLiteral("beta"), QStringLiteral("b.cpp:2")) + << makeLine(d(QStringLiteral("2024-01-03T00:00:00")), QStringLiteral("INFO"), + QStringLiteral("net"), QStringLiteral("gamma"), QStringLiteral("c.cpp:3")); + source.setData(lines); + + SortFilterModel proxy; + proxy.setSourceModel(&source); + QCOMPARE(proxy.rowCount(), 3); + QVERIFY(proxy.stringFilterIsEmpty()); + + // setStringFilter itself does not re-run the proxy filter (only the date + // setters and resetAllFilter do); mainwindow.cpp calls + // m_filterProxyModel->invalidate() after every setStringFilter, so the + // tests do the same. + // filter on the level column (col 1) + proxy.setStringFilter(LogViewTableModel::colLevel, QStringLiteral("WARN")); + proxy.invalidate(); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.data(proxy.index(0, LogViewTableModel::colLevel), Qt::DisplayRole).toString(), + QStringLiteral("WARN")); + + // combined with the logger column (col 2) + proxy.setStringFilter(LogViewTableModel::colLogger, QStringLiteral("net")); + proxy.invalidate(); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.data(proxy.index(0, LogViewTableModel::colLevel), Qt::DisplayRole).toString(), + QStringLiteral("WARN")); + + // replacing an existing filter: "net" does not match the new text "main" + proxy.setStringFilter(LogViewTableModel::colLogger, QStringLiteral("main")); + proxy.invalidate(); + // WARN rows have logger "net" -> nothing left + QCOMPARE(proxy.rowCount(), 0); + + // case sensitivity: a fresh column filter is stored as-is and the row data + // "beta" does not match "BETA" (contains() is case sensitive) + proxy.resetAllFilter(); + QCOMPARE(proxy.rowCount(), 3); + proxy.setStringFilter(LogViewTableModel::colData, QStringLiteral("BETA")); + proxy.invalidate(); + QCOMPARE(proxy.rowCount(), 0); + + // setStringFilter keeps the old filter when the new text matches the old + // one case-insensitively, so the stored filter is still "BETA" + proxy.setStringFilter(LogViewTableModel::colData, QStringLiteral("beta")); + proxy.invalidate(); + QCOMPARE(proxy.rowCount(), 0); +} + +void logviewer_unit_test::proxyDateStringFilterEdgeCases() +{ + LogViewTableModel source; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T00:00:00")), QStringLiteral("INFO"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")); + source.setData(lines); + + SortFilterModel proxy; + proxy.setSourceModel(&source); + + // column range is validated: out-of-range columns are ignored + proxy.setStringFilter(-1, QStringLiteral("x")); + proxy.setStringFilter(5, QStringLiteral("x")); + proxy.setStringFilter(99, QStringLiteral("x")); + QVERIFY(proxy.stringFilterIsEmpty()); + QCOMPARE(proxy.rowCount(), 1); + + // an empty filter string still counts as "contained" for the activation + // report (columns 3 and 4 only check presence, columns 1 and 2 check for + // non-emptiness) + proxy.setStringFilter(LogViewTableModel::colData, QString()); + QVERIFY(!proxy.stringFilterIsEmpty()); + QVector activated = proxy.activatedStringFilter(); + QCOMPARE(activated.size(), 5); + QVERIFY(!activated.at(0)); + QVERIFY(!activated.at(1)); + QVERIFY(!activated.at(2)); + QVERIFY(activated.at(3)); + QVERIFY(!activated.at(4)); + + // empty filter does not filter any row (contains("") is always true) + QCOMPARE(proxy.rowCount(), 1); +} + +void logviewer_unit_test::proxyDateFilter() +{ + LogViewTableModel source; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T10:00:00")), QStringLiteral("INFO"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-02T10:00:00")), QStringLiteral("WARN"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-03T10:00:00")), QStringLiteral("ERROR"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")); + source.setData(lines); + + SortFilterModel proxy; + proxy.setSourceModel(&source); + + // only start date: filter not active (both ends required) + proxy.setStartDateFilter(d(QStringLiteral("2024-01-01T10:00:00"))); + QCOMPARE(proxy.rowCount(), 3); + QVector activated = proxy.activatedStringFilter(); + QVERIFY(!activated.at(0)); + + // start and end: window is exclusive on both ends + proxy.setEndDateFilter(d(QStringLiteral("2024-01-03T10:00:00"))); + QCOMPARE(proxy.rowCount(), 1); + QCOMPARE(proxy.data(proxy.index(0, LogViewTableModel::colDate), Qt::UserRole).toDateTime(), + d(QStringLiteral("2024-01-02T10:00:00"))); + activated = proxy.activatedStringFilter(); + QVERIFY(activated.at(0)); + + // setting the same start date again is a no-op (and must not break the filter) + proxy.setStartDateFilter(d(QStringLiteral("2024-01-01T10:00:00"))); + QCOMPARE(proxy.rowCount(), 1); +} + +void logviewer_unit_test::proxyResetAllFilter() +{ + LogViewTableModel source; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T10:00:00")), QStringLiteral("INFO"), + QStringLiteral("main"), QStringLiteral("alpha"), QStringLiteral("a.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-02T10:00:00")), QStringLiteral("WARN"), + QStringLiteral("net"), QStringLiteral("beta"), QStringLiteral("b.cpp:2")); + source.setData(lines); + + SortFilterModel proxy; + proxy.setSourceModel(&source); + + proxy.setStringFilter(LogViewTableModel::colLevel, QStringLiteral("WARN")); + proxy.setStringFilter(LogViewTableModel::colLogger, QStringLiteral("net")); + proxy.setStartDateFilter(d(QStringLiteral("2024-01-01T10:00:00"))); + proxy.setEndDateFilter(d(QStringLiteral("2024-01-02T10:00:00"))); + QCOMPARE(proxy.rowCount(), 0); + + proxy.resetAllFilter(); + QVERIFY(proxy.stringFilterIsEmpty()); + QCOMPARE(proxy.rowCount(), 2); + QVector activated = proxy.activatedStringFilter(); + for (const bool active : activated) + QVERIFY(!active); +} + +void logviewer_unit_test::proxySortByDate() +{ + LogViewTableModel source; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-03T10:00:00")), QStringLiteral("INFO"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-01T10:00:00")), QStringLiteral("WARN"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-02T10:00:00")), QStringLiteral("ERROR"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")); + source.setData(lines); + + SortFilterModel proxy; + proxy.setSourceModel(&source); + proxy.sort(LogViewTableModel::colDate, Qt::AscendingOrder); + + QCOMPARE(proxy.rowCount(), 3); + QCOMPARE(proxy.data(proxy.index(0, LogViewTableModel::colDate), Qt::UserRole).toDateTime(), + d(QStringLiteral("2024-01-01T10:00:00"))); + QCOMPARE(proxy.data(proxy.index(1, LogViewTableModel::colDate), Qt::UserRole).toDateTime(), + d(QStringLiteral("2024-01-02T10:00:00"))); + QCOMPARE(proxy.data(proxy.index(2, LogViewTableModel::colDate), Qt::UserRole).toDateTime(), + d(QStringLiteral("2024-01-03T10:00:00"))); +} + +void logviewer_unit_test::proxySortByLevel() +{ + // non-date columns fall back to the default QVariant comparison + LogViewTableModel source; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T10:00:00")), QStringLiteral("WARN"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-02T10:00:00")), QStringLiteral("INFO"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")) + << makeLine(d(QStringLiteral("2024-01-03T10:00:00")), QStringLiteral("DEBUG"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")); + source.setData(lines); + + SortFilterModel proxy; + proxy.setSourceModel(&source); + proxy.sort(LogViewTableModel::colLevel, Qt::AscendingOrder); + + QCOMPARE(proxy.data(proxy.index(0, LogViewTableModel::colLevel), Qt::DisplayRole).toString(), + QStringLiteral("DEBUG")); + QCOMPARE(proxy.data(proxy.index(1, LogViewTableModel::colLevel), Qt::DisplayRole).toString(), + QStringLiteral("INFO")); + QCOMPARE(proxy.data(proxy.index(2, LogViewTableModel::colLevel), Qt::DisplayRole).toString(), + QStringLiteral("WARN")); +} + +void logviewer_unit_test::proxyFigureFilterOutSignal() +{ + LogViewTableModel source; + QVector lines; + lines << makeLine(d(QStringLiteral("2024-01-01T10:00:00")), QStringLiteral("INFO"), + QStringLiteral("l"), QStringLiteral("d"), QStringLiteral("l.cpp:1")); + source.setData(lines); + + SortFilterModel proxy; + proxy.setSourceModel(&source); + + QVector lastFilter; + int calls = 0; + QObject::connect(&proxy, &SortFilterModel::theseFilterAreActivated, + [&lastFilter, &calls](const QVector &filter) { + lastFilter = filter; + ++calls; + }); + + proxy.figureFilterOut(); + QCOMPARE(calls, 1); + QCOMPARE(lastFilter, QVector({false, false, false, false, false})); + + proxy.setStringFilter(LogViewTableModel::colLevel, QStringLiteral("INFO")); + proxy.figureFilterOut(); + QCOMPARE(calls, 2); + QCOMPARE(lastFilter, QVector({false, true, false, false, false})); +} + +QTEST_MAIN(logviewer_unit_test) +#include "logviewer_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/CMakeLists.txt index 3f89681b4..658da5c1a 100755 --- a/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/CMakeLists.txt @@ -61,3 +61,8 @@ if(QT6_GUI_AVAILABLE) ) endif() NGL_INSTALL_TARGET(sqlbrowser) + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/unit-test/CMakeLists.txt new file mode 100644 index 000000000..4987db765 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/unit-test/CMakeLists.txt @@ -0,0 +1,23 @@ +# +# Unit tests for the SqlBrowser module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- SqlBrowser: data model + add-table model. +# --- The module is an executable, so the model sources are compiled directly +# --- into the test (same pattern as the LogView test in TetraIF/unit-test). +if (TARGET sqlbrowser) + ADD_EXECUTABLE(sqlbrowser_unit_test + sqlbrowser_unit_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../DataModel.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../AddModel.cpp) + TARGET_LINK_LIBRARIES(sqlbrowser_unit_test + Qt6::Core + Qt6::Gui + Qt6::Test + ) + ADD_TEST(NAME sqlbrowser_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/unit-test/sqlbrowser_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/unit-test/sqlbrowser_unit_test.cpp new file mode 100644 index 000000000..c166b6af5 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Lts/Tools/SqlBrowser/unit-test/sqlbrowser_unit_test.cpp @@ -0,0 +1,410 @@ +// +// Unit tests for the SqlBrowser module: DataModel (double-buffered table +// model with role handling) and AddModel (add-row table model). +// +// These are the module's pure non-GUI logic classes. No database access is +// involved: the DB work lives in QueryWorker/QueryThread (QSqlDatabase on a +// worker thread). The remaining SqlBrowser classes are either widgets +// (Query, BrowserWnd, LoginWnd, WaitWnd, AddLines) or DB/thread driven. +// PresetManager is skipped as well: its constructor calls +// QApplication::arguments() and its save path uses QInputDialog, so it +// requires a running GUI application. +// +#include "../DataModel.h" +#include "../AddModel.h" + +#include +#include +#include +#include +#include + +class sqlbrowser_unit_test : public QObject +{ + Q_OBJECT + +private slots: + // DataModel + void dataModelEmptyState(); + void dataModelSwapLists(); + void dataModelDataRoleHandling(); + void dataModelToolTipTimestamp(); + void dataModelHeaderData(); + void dataModelTableEditability(); + void dataModelSlotSetData(); + void dataModelSetDataAsyncSemantics(); + void dataModelDeleteLines(); + + // AddModel + void addModelBasics(); + void addModelSetDataCreatesRow(); + void addModelStringifiesValues(); + void addModelRemoveRow(); + void addModelClear(); + void addModelTableName(); +}; + +// --- DataModel ---------------------------------------------------------------- + +void sqlbrowser_unit_test::dataModelEmptyState() +{ + DataModel model; + QCOMPARE(model.rowCount(), 0); + QCOMPARE(model.columnCount(), 0); + QVERIFY(model.getTableName().isEmpty()); + + Qt::ItemFlags flags = model.flags(QModelIndex()); + QVERIFY(flags & Qt::ItemIsSelectable); + QVERIFY(flags & Qt::ItemIsEnabled); + QVERIFY(!(flags & Qt::ItemIsEditable)); +} + +void sqlbrowser_unit_test::dataModelSwapLists() +{ + DataModel model; + + // columns/rows are staged in the write buffer first + model.addColumn(QStringLiteral("id"), QMetaType::Int); + model.addColumn(QStringLiteral("name"), QMetaType::QString); + model.addColumn(QStringLiteral("ts"), QMetaType::LongLong); + + Data row0; + row0 << 1 << QStringLiteral("alpha") << static_cast(1700000000000LL); + model.addLine(row0); + + // nothing is visible in the live model before the swap + QCOMPARE(model.rowCount(), 0); + QCOMPARE(model.columnCount(), 0); + + model.slotSwapLists(); + QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.columnCount(), 3); + + const HeaderList &live = model.getHeaderList(); + QCOMPARE(live.size(), 3); + QCOMPARE(live.at(0).first, QStringLiteral("id")); + QVERIFY(live.at(0).second == QMetaType::Int); + QCOMPARE(live.at(1).first, QStringLiteral("name")); + QVERIFY(live.at(1).second == QMetaType::QString); + QCOMPARE(live.at(2).first, QStringLiteral("ts")); + QVERIFY(live.at(2).second == QMetaType::LongLong); + + // the write buffer is cleared by the swap: new staging does not leak + // into the live model + Data row1; + row1 << 999 << QStringLiteral("leak"); + model.addLine(row1); + QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 1); +} + +void sqlbrowser_unit_test::dataModelDataRoleHandling() +{ + DataModel model; + model.addColumn(QStringLiteral("id"), QMetaType::Int); + model.addColumn(QStringLiteral("name"), QMetaType::QString); + model.addColumn(QStringLiteral("ts"), QMetaType::LongLong); + + Data row0; + row0 << 1 << QStringLiteral("alpha") << static_cast(1700000000000LL); + Data row1; + row1 << 2 << QVariant() << static_cast(5); + Data row2; + row2 << 3 << QStringLiteral("gamma") << QVariant(); + model.addLine(row0); + model.addLine(row1); + model.addLine(row2); + model.slotSwapLists(); + + QCOMPARE(model.rowCount(), 3); + QCOMPARE(model.columnCount(), 3); + + // DisplayRole and EditRole both return the raw value + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 1); + QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole).toString(), QStringLiteral("alpha")); + QCOMPARE(model.data(model.index(0, 2), Qt::EditRole).toLongLong(), qint64(1700000000000)); + + // null cells are highlighted via BackgroundRole; filled cells are not + QCOMPARE(model.data(model.index(1, 1), Qt::BackgroundRole).value(), QColor(255, 255, 215)); + QVERIFY(!model.data(model.index(0, 0), Qt::BackgroundRole).isValid()); + + // other roles return nothing + QVERIFY(!model.data(model.index(0, 1), Qt::DecorationRole).isValid()); + + // a valid parent index means "no children" + QCOMPARE(model.rowCount(model.index(0, 0)), 0); + QCOMPARE(model.columnCount(model.index(0, 0)), 0); +} + +void sqlbrowser_unit_test::dataModelToolTipTimestamp() +{ + DataModel model; + model.addColumn(QStringLiteral("id"), QMetaType::Int); + model.addColumn(QStringLiteral("name"), QMetaType::QString); + model.addColumn(QStringLiteral("ts"), QMetaType::LongLong); + + Data row0; + row0 << 1 << QStringLiteral("alpha") << static_cast(1700000000000); + Data row1; + row1 << 2 << QStringLiteral("beta") << static_cast(5); + model.addLine(row0); + model.addLine(row1); + model.slotSwapLists(); + + // 64 bit integer values above 1000000000 are rendered as timestamps + const QVariant tooltip = model.data(model.index(0, 2), Qt::ToolTipRole); + QVERIFY(tooltip.isValid()); + const QRegularExpression pattern(QStringLiteral("^\\d{2}\\.\\d{2}\\.\\d{4} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}$")); + QVERIFY(pattern.match(tooltip.toString()).hasMatch()); + + // below the threshold -> no tooltip + QVERIFY(!model.data(model.index(1, 2), Qt::ToolTipRole).isValid()); + // non-integer types -> no tooltip + QVERIFY(!model.data(model.index(0, 1), Qt::ToolTipRole).isValid()); + QVERIFY(!model.data(model.index(0, 0), Qt::ToolTipRole).isValid()); +} + +void sqlbrowser_unit_test::dataModelHeaderData() +{ + DataModel model; + model.addColumn(QStringLiteral("id"), QMetaType::Int); + model.addColumn(QStringLiteral("name"), QMetaType::QString); + model.addColumn(QStringLiteral("ts"), QMetaType::LongLong); + model.slotSwapLists(); + + QCOMPARE(model.headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("id")); + QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("name")); + QCOMPARE(model.headerData(2, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("ts")); + + // ToolTipRole carries the QMetaType name of the column + QCOMPARE(model.headerData(0, Qt::Horizontal, Qt::ToolTipRole).toString(), QStringLiteral("int")); + QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::ToolTipRole).toString(), QStringLiteral("QString")); + QCOMPARE(model.headerData(2, Qt::Horizontal, Qt::ToolTipRole).toString(), QStringLiteral("qlonglong")); + + // out-of-range section and vertical orientation fall through to + // QAbstractTableModel::headerData (1-based: section+1 horizontal, 1 vertical) + QCOMPARE(model.headerData(9, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("10")); + QCOMPARE(model.headerData(0, Qt::Vertical, Qt::DisplayRole).toString(), QStringLiteral("1")); +} + +void sqlbrowser_unit_test::dataModelTableEditability() +{ + DataModel model; + + // custom queries (empty table name) are read-only + QVERIFY(!(model.flags(QModelIndex()) & Qt::ItemIsEditable)); + + // table queries are editable + model.setTableName(QStringLiteral("users")); + QCOMPARE(model.getTableName(), QStringLiteral("users")); + QVERIFY(model.flags(QModelIndex()) & Qt::ItemIsEditable); + + model.setTableName(QString()); + QVERIFY(!(model.flags(QModelIndex()) & Qt::ItemIsEditable)); +} + +void sqlbrowser_unit_test::dataModelSlotSetData() +{ + DataModel model; + model.addColumn(QStringLiteral("id"), QMetaType::Int); + model.addColumn(QStringLiteral("name"), QMetaType::QString); + Data row0; + row0 << 1 << QStringLiteral("alpha"); + Data row1; + row1 << 2 << QStringLiteral("beta"); + model.addLine(row0); + model.addLine(row1); + model.slotSwapLists(); + + // slotSetData applies the write to the live list (the DB answered) + model.slotSetData(model.index(1, 1), QStringLiteral("delta")); + QCOMPARE(model.data(model.index(1, 1), Qt::DisplayRole).toString(), QStringLiteral("delta")); + QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole).toString(), QStringLiteral("alpha")); + QCOMPARE(model.rowCount(), 2); +} + +void sqlbrowser_unit_test::dataModelSetDataAsyncSemantics() +{ + DataModel model; + model.addColumn(QStringLiteral("id"), QMetaType::Int); + Data row0; + row0 << 1; + model.addLine(row0); + model.slotSwapLists(); + + bool captured = false; + QVariant capturedValue; + DataModel *capturedModel = nullptr; + QObject::connect(&model, &DataModel::signalSetData, + [&captured, &capturedValue, &capturedModel](DataModel *m, const QModelIndex &, + const QVariant &v) { + captured = true; + capturedModel = m; + capturedValue = v; + }); + + // EditRole: value is NOT applied locally; it is forwarded to the worker + // and the call reports "not done yet" + QVERIFY(!model.setData(model.index(0, 0), QStringLiteral("changed"), Qt::EditRole)); + QVERIFY(captured); + QCOMPARE(capturedModel, static_cast(&model)); + QCOMPARE(capturedValue.toString(), QStringLiteral("changed")); + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 1); // untouched + + // non-EditRole values are accepted (true) and ignored + QVERIFY(model.setData(model.index(0, 0), QStringLiteral("ignored"), Qt::DisplayRole)); + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 1); +} + +void sqlbrowser_unit_test::dataModelDeleteLines() +{ + DataModel model; + model.addColumn(QStringLiteral("id"), QMetaType::Int); + Data row0; + row0 << 1; + Data row1; + row1 << 2; + Data row2; + row2 << 3; + model.addLine(row0); + model.addLine(row1); + model.addLine(row2); + model.slotSwapLists(); + QCOMPARE(model.rowCount(), 3); + + // delete in descending row order so the indices stay valid + QList toDelete; + toDelete << model.index(2, 0) << model.index(0, 0); + model.deleteLines(toDelete); + QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 2); + + // an empty index list is a no-op + model.deleteLines(QList()); + QCOMPARE(model.rowCount(), 1); +} + +// --- AddModel -------------------------------------------------------------------- + +void sqlbrowser_unit_test::addModelBasics() +{ + AddModel model; + + // rowCount is always dataList + 1 (the trailing empty row for new data) + QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.columnCount(), 0); + + HeaderList headers; + headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); + headers.append(QPair(QStringLiteral("name"), QMetaType::QString)); + model.setHeaderList(headers); + + QCOMPARE(model.columnCount(), 2); + QCOMPARE(model.rowCount(), 1); + + QVERIFY(model.getColumnType(0) == QMetaType::Int); + QVERIFY(model.getColumnType(1) == QMetaType::QString); + QVERIFY(model.getColumnType(-1) == QMetaType::UnknownType); + QVERIFY(model.getColumnType(2) == QMetaType::UnknownType); + + QCOMPARE(model.headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("id")); + QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("name")); + // only horizontal + displayRole + QVERIFY(!model.headerData(0, Qt::Vertical, Qt::DisplayRole).isValid()); + QVERIFY(!model.headerData(0, Qt::Horizontal, Qt::EditRole).isValid()); + + const Qt::ItemFlags flags = model.flags(QModelIndex()); + QVERIFY(flags & Qt::ItemIsSelectable); + QVERIFY(flags & Qt::ItemIsEnabled); + QVERIFY(flags & Qt::ItemIsEditable); +} + +void sqlbrowser_unit_test::addModelSetDataCreatesRow() +{ + AddModel model; + HeaderList headers; + headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); + headers.append(QPair(QStringLiteral("name"), QMetaType::QString)); + model.setHeaderList(headers); + + QCOMPARE(model.rowCount(), 1); + + // writing into the empty row materializes it + QVERIFY(model.setData(model.index(0, 0), QStringLiteral("42"), Qt::EditRole)); + QCOMPARE(model.rowCount(), 2); + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("42")); + // other columns of the new row are pre-filled with empty strings + QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole).toString(), QString()); + + // the trailing empty row still has no data + QVERIFY(!model.data(model.index(1, 0), Qt::DisplayRole).isValid()); + + // a second write materializes row 1 + QVERIFY(model.setData(model.index(1, 1), QStringLiteral("bob"), Qt::EditRole)); + QCOMPARE(model.rowCount(), 3); + QCOMPARE(model.data(model.index(1, 1), Qt::DisplayRole).toString(), QStringLiteral("bob")); + QCOMPARE(model.data(model.index(1, 0), Qt::DisplayRole).toString(), QString()); + + // non-EditRole values are rejected + QVERIFY(!model.setData(model.index(0, 0), QStringLiteral("x"), Qt::DisplayRole)); + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("42")); +} + +void sqlbrowser_unit_test::addModelStringifiesValues() +{ + AddModel model; + HeaderList headers; + headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); + model.setHeaderList(headers); + + // stored values are stringified on write + QVERIFY(model.setData(model.index(0, 0), 42, Qt::EditRole)); + const QVariant value = model.data(model.index(0, 0), Qt::DisplayRole); + QCOMPARE(value.toString(), QStringLiteral("42")); + QVERIFY(value.typeId() == QMetaType::QString); +} + +void sqlbrowser_unit_test::addModelRemoveRow() +{ + AddModel model; + HeaderList headers; + headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); + model.setHeaderList(headers); + + QVERIFY(model.setData(model.index(0, 0), QStringLiteral("1"), Qt::EditRole)); + QVERIFY(model.setData(model.index(1, 0), QStringLiteral("2"), Qt::EditRole)); + QCOMPARE(model.rowCount(), 3); + + // out-of-range row + QVERIFY(!model.removeRow(10)); + QCOMPARE(model.rowCount(), 3); + + QVERIFY(model.removeRow(0)); + QCOMPARE(model.rowCount(), 2); + QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("2")); +} + +void sqlbrowser_unit_test::addModelClear() +{ + AddModel model; + HeaderList headers; + headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); + model.setHeaderList(headers); + QVERIFY(model.setData(model.index(0, 0), QStringLiteral("1"), Qt::EditRole)); + QCOMPARE(model.rowCount(), 2); + + model.clear(); + QCOMPARE(model.rowCount(), 1); + QCOMPARE(model.columnCount(), 1); // header list survives clear() +} + +void sqlbrowser_unit_test::addModelTableName() +{ + AddModel model; + QCOMPARE(model.getTableName(), QString()); + model.setTablename(QStringLiteral("t_users")); + QCOMPARE(model.getTableName(), QStringLiteral("t_users")); +} + +QTEST_MAIN(sqlbrowser_unit_test) +#include "sqlbrowser_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/README.md b/qt6-after/qt6-migrated-legatus/README.md index 30a7d39ca..654db1931 100644 --- a/qt6-after/qt6-migrated-legatus/README.md +++ b/qt6-after/qt6-migrated-legatus/README.md @@ -129,17 +129,33 @@ docker run --rm -it \ The driver runs `ctest --output-on-failure -j$(nproc)` (with `QT_QPA_PLATFORM=offscreen` — the container is headless) after the compile. -Expected baseline (7 in-scope tests, all passed): +Expected baseline (23 registered, 20 in-scope tests passed, 3 Not Run): -| # | Test | Result | -|---|---|---| -| 1 | `coprtcnv_unit_test` | Passed | -| 2 | `logview_unit_test` | Passed | -| 3 | `prtcnv_unit_test` | Passed | -| 4 | `NGLToolsQtSmoke` | Passed | -| 5 | `test_nglpdfwidget` | Passed | -| 6 | `lts_coord_trans_unit_test` | Passed | -| 7 | `aes_crypt_test` | Passed | +| # | Test | Module | Result | +|---|---|---|---| +| 1 | `coprtcnv_unit_test` | TetraIF (CoPrtCnv) | Passed | +| 2 | `logview_unit_test` | TetraIF (LogView) | Passed | +| 3 | `prtcnv_unit_test` | TetraIF (PrtCnv) | Passed | +| 4 | `wms_json_ds_unit_test` | WMS_Interface | Passed | +| 5 | `nglcoreapplication_unit_test` | nglapplication | Passed | +| 6 | `nglcomp_test` | nglcompression | Not Run | +| 7 | `nglcompression_unit_test` | nglcompression | Passed | +| 8 | `nglsettings_unit_test` | nglsettings | Passed | +| 9 | `ngltranslator_unit_test` | ngltranslator | Passed | +| 10 | `NGLToolsQtSmoke` | NGLToolsQt | Passed | +| 11 | `test_nglpdfwidget` | NGLPDFWidget | Passed | +| 12 | `ngllogger_unit_test` | NGLLogger | Passed | +| 13 | `logviewer_unit_test` | LogViewer | Passed | +| 14 | `sqlbrowser_unit_test` | SqlBrowser | Passed | +| 15 | `ddsqos_unit_test` | DDS/QosProfile | Passed | +| 16 | `ddscore_unit_test` | DDS/Core | Passed | +| 17 | `GrantleeParserSmoke` | Grantlee | Not Run | +| 18 | `codegen_unit_test` | CodeGen | Passed | +| 19 | `lts_coord_trans_unit_test` | LtsCoordTrans | Passed | +| 20 | `ltscoordinate_unit_test` | LtsCoordinate | Passed | +| 21 | `aes_crypt_test` | Common (AES crypt) | Passed | +| 22 | `ltssession_unit_test` | LtsSession | Passed | +| 23 | `utils_test` | Common (LtsUtils) | Not Run | A few tests registered by in-scope CMakeLists are not part of the 46 built targets and show up as `***Not Run` (`nglcomp_test`, @@ -153,7 +169,13 @@ tests they registered are gone with them.) B=$(ls -dt build4_* | head -1) docker run --rm -v "$MIGRATE_PIPELINE_ROOT":/repo \ bash -c "cd /repo/qt-migration-legatus/qt6-after/qt6-migrated-legatus/${B} \ - && ninja coprtcnv_unit_test logview_unit_test prtcnv_unit_test" + && ninja coprtcnv_unit_test logview_unit_test prtcnv_unit_test \ + codegen_unit_test ddscore_unit_test ddsqos_unit_test \ + logviewer_unit_test sqlbrowser_unit_test \ + ltscoordinate_unit_test ltssession_unit_test \ + wms_json_ds_unit_test nglcompression_unit_test \ + nglsettings_unit_test ngltranslator_unit_test \ + ngllogger_unit_test nglcoreapplication_unit_test" ``` ### 2.3 Run a test executable directly @@ -178,15 +200,44 @@ ctest --output-on-failure" Each QtTest binary accepts a single slot name to run just one test, e.g. `./bin/coprtcnv_unit_test radioTelegramFragmentationRoundtrip`. -### 2.4 TetraIF unit tests (`TetraIF/unit-test/`) +### 2.4 Unit tests per module -| Test | Module covered | Note | +Each in-scope module has its own `unit-test/` directory (the original +`TetraIF/unit-test/` layout, one test executable per module, each +registered with ctest): + +| Test | Module | Covers | |---|---|---| -| `coprtcnv_unit_test` | `CoPrtCnv` — central & radio telegram header fields, payload access, length checks, fragmentation/reassembly roundtrip, id→string helpers | links `CoPrtCnv` | -| `logview_unit_test` | `LogView` — `IntegerCalculator` (arithmetic, precedence, error codes, variables) | compiles `LogView/IntegerCalculator.cpp` into the test (the class otherwise lives in the LogView executable) | -| `prtcnv_unit_test` | `PrtCnv` — `PrtCnvMember` state + fragmented-telegram add/reassembly | compiles `PrtCnv/PrtCnvMember.cpp` into the test because `PrtCnvLib` is built with hidden visibility (its symbols are not linkable from outside) | +| `coprtcnv_unit_test` | TetraIF | `CoPrtCnv` — telegram header fields, payload access, length checks, fragmentation/reassembly roundtrip, id→string helpers | +| `logview_unit_test` | TetraIF | `LogView` — `IntegerCalculator` (arithmetic, precedence, error codes, variables) | +| `prtcnv_unit_test` | TetraIF | `PrtCnv` — `PrtCnvMember` state + fragmented-telegram add/reassembly | +| `codegen_unit_test` | Common/CodeGen | name transforms, member/data/function/IDL wrappers, Grantlee wrapper getters (incl. JSON↔variant fallbacks), base data classes (`MemberInformation`/`CppData` `toString`) | +| `ddscore_unit_test` | DDS/Core | `ddsview`-independent DDS core helpers | +| `ddsqos_unit_test` | DDS/QosProfile | `DDS_DefaultQos` — default profile load, SHM/UDP/TCP participant QoS, default datawriter/datareader QoS, `print()` | +| `logviewer_unit_test` | Tools/LogViewer | `LogViewTableModel` (dimensions, roles, invalid index, header data, level colors, add/set data) + `SortFilterModel` (string/date filters, reset, date/level sort, `figureFilterOut`) | +| `sqlbrowser_unit_test` | Tools/SqlBrowser | `DataModel` (swap lists, role handling, tooltip timestamp, editability, set-data slots, delete lines) + `AddModel` (row creation, value stringification, remove/clear) | +| `ltscoordinate_unit_test` | Common/LtsCoordinate | constructors/geo accessors, validity checks, MGRS formatting, geo↔UTM roundtrips, UTM distances, angle degrees, elevation callback, `LtsCoordVector` | +| `ltssession_unit_test` | Common/LtsSession | session API roundtrips | +| `wms_json_ds_unit_test` | WMS_Interface | JSON dataset roundtrips (exercises, requirements, units) — `toJson`/`fromJson` fidelity | +| `nglcompression_unit_test` | nglcompression | `nglcomp` archive/tar filter helpers | +| `nglsettings_unit_test` | nglsettings | `NGL::Settings` + `NGL::Connections` (port offset, server hostname) | +| `ngltranslator_unit_test` | ngltranslator | `Translator` — catalog + `translate` (missing context, plural, disambiguation), JSON catalog import (valid/invalid, md5 cache, compressed), locale handling | +| `ngllogger_unit_test` | NGLLogger | `LoggerHierarchyHelper` naming/instance caching, log level ordering/enabled checks, bootstrap create/get, `AlreadyCreatedError` | +| `nglcoreapplication_unit_test` | nglapplication | `LoggerHandler` — `createLogger` naming (prefix/postfix), duplicate-root `AlreadyCreatedError` | -Sources are registered with ctest via `TetraIF/unit-test/CMakeLists.txt`. +Hidden-visibility note: the tree builds with `CMAKE_CXX_VISIBILITY_PRESET +hidden`, so classes without an export macro are not in the shared +library's dynamic symbol table. Where a module has no export macro +(`LogViewer`, `SqlBrowser`, `ngltranslator`, `NGLLogger`, `nglcompression`, +`nglsettings`), its test compiles the module source files directly into +the test executable (same pattern as `prtcnv_unit_test`). + +`ddsview` has no unit test: it is a GUI tool whose `XmlHelper` needs the +DDS dynamic-xtypes runtime and a GUI model. + +Sources are registered with ctest via each module's +`unit-test/CMakeLists.txt` (guarded by `BUILD_TESTING` and by the module +target existing). ## Step 3 — Produce the Release 2 DEB packages diff --git a/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/CMakeLists.txt index 0aaece8b0..5b97f9f5f 100755 --- a/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/CMakeLists.txt @@ -57,5 +57,9 @@ ELSE() MESSAGE(STATUS "Log4Qt not found; skipping test_ngllogger") ENDIF() +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/log4cxx.props ${CMAKE_BINARY_DIR}/log4cxx.props COPYONLY) diff --git a/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/unit-test/CMakeLists.txt new file mode 100644 index 000000000..89c69e263 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/unit-test/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# Unit tests for the NGLLogger module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- NGLLogger: bootstrap + logger lookup (Log4Qt-independent; works in +# --- the Log4Qt-less (stub) build). +# --- MurmurHash2 has no export macro and the tree builds with hidden +# --- visibility, so its symbols are not exported from libNGLLogger - the +# --- module source is compiled directly into the test (same pattern as the +# --- LogView test in TetraIF/unit-test). +if (TARGET NGLLogger) + ADD_EXECUTABLE(ngllogger_unit_test + ngllogger_unit_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../MurmurHash2.cpp) + TARGET_LINK_LIBRARIES(ngllogger_unit_test + NGLLogger + Qt6::Core + Qt6::Test + ) + ADD_TEST(NAME ngllogger_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/unit-test/ngllogger_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/unit-test/ngllogger_unit_test.cpp new file mode 100644 index 000000000..4bfcf6424 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/NGLLogger/unit-test/ngllogger_unit_test.cpp @@ -0,0 +1,163 @@ +#include +#include +#include +#include +#include + +class ngllogger_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase() + { + // Create the root logger once for the whole test process. + // Works in both the Log4Qt build and the Log4Qt-less (stub) build. + NGL::NGLLoggerBootstrap::createNGLRootLogger("log4cxx.props", false); + } + + void A_LoggerHierarchyHelperSameNameReturnsSameInstance() + { +#ifndef HAVE_LOG4QT + // The instance cache (m_loggermap) is only populated in Log4Qt builds + // (NGLLogger constructor); the stub build creates a fresh logger per call. + QSKIP("requires the Log4Qt build (instance caching is a Log4Qt feature)"); +#else + auto &a = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui"); + auto &b = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui"); + QCOMPARE(&a, &b); // same instance for same name +#endif + } + + void B_LoggerHierarchyHelperDifferentNamesReturnDifferentInstances() + { + auto &a = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui.AES"); + auto &b = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui.Dataview"); + QVERIFY(&a != &b); // different instances for different names + } + + void C_LoggerNameRoundTrip() + { + auto &a = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui"); + QCOMPARE(a.getLoggerName(), std::string("Gui")); + auto &b = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui.AES"); + QCOMPARE(b.getLoggerName(), std::string("Gui.AES")); + } + + void D_StdStringAndCharOverloadsReturnSameInstance() + { +#ifndef HAVE_LOG4QT + QSKIP("requires the Log4Qt build (instance caching is a Log4Qt feature)"); +#else + auto &a = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui"); + auto &b = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger(std::string("Gui")); + QCOMPARE(&a, &b); + auto &c = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui.AES"); + auto &d = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger(std::string("Gui.AES")); + QCOMPARE(&c, &d); +#endif + } + + void E_NoSpaceFlag() + { + auto &a = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui"); + QVERIFY(a.noSpace()); // default true + a.setNoSpace(false); + QVERIFY(!a.noSpace()); + a.setNoSpace(true); + QVERIFY(a.noSpace()); + } + + void F_LogLevelEnumOrdering() + { + QVERIFY(NGL::LogLevel::Trace < NGL::LogLevel::Debug); + QVERIFY(NGL::LogLevel::Debug < NGL::LogLevel::Info); + QVERIFY(NGL::LogLevel::Info < NGL::LogLevel::Warning); + QVERIFY(NGL::LogLevel::Warning < NGL::LogLevel::Error); + QVERIFY(NGL::LogLevel::Error < NGL::LogLevel::Fatal); + } + + void G_PerLevelHelpersAgreeWithGeneric() + { + auto &a = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui"); + QCOMPARE(a.isTraceEnabled(), a.isLevelEnabled(NGL::LogLevel::Trace)); + QCOMPARE(a.isDebugEnabled(), a.isLevelEnabled(NGL::LogLevel::Debug)); + QCOMPARE(a.isInfoEnabled(), a.isLevelEnabled(NGL::LogLevel::Info)); + QCOMPARE(a.isWarningEnabled(), a.isLevelEnabled(NGL::LogLevel::Warning)); + QCOMPARE(a.isErrorEnabled(), a.isLevelEnabled(NGL::LogLevel::Error)); + QCOMPARE(a.isFatalEnabled(), a.isLevelEnabled(NGL::LogLevel::Fatal)); + } + + void H_LevelEnabledIsUpwardClosed() + { + // For any two levels lo < hi: if lo enabled then hi enabled; if hi disabled then lo + // disabled. This holds regardless of the actual enabled level. + const auto levels = { + NGL::LogLevel::Trace, NGL::LogLevel::Debug, NGL::LogLevel::Info, + NGL::LogLevel::Warning, NGL::LogLevel::Error, NGL::LogLevel::Fatal + }; + auto &a = NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("Gui"); + for (auto lo : levels) + for (auto hi : levels) + { + if (lo < hi) + { + if (a.isLevelEnabled(lo)) + QVERIFY(a.isLevelEnabled(hi)); + if (!a.isLevelEnabled(hi)) + QVERIFY(!a.isLevelEnabled(lo)); + } + } + } + + void I_BootstrapCreateAndGet() + { + auto &a = NGL::NGLLoggerBootstrap::getNGLRootLogger(); + auto &b = NGL::NGLLoggerBootstrap::getNGLRootLogger(); + QCOMPARE(&a, &b); // get returns the created root logger + // create with ignore-already-created returns the existing root logger + auto &c = NGL::NGLLoggerBootstrap::createNGLRootLogger("log4cxx.props", true); + QCOMPARE(&a, &c); + } + + void J_AlreadyCreatedError() + { + // Re-creating without ignore-already-created throws AlreadyCreatedError (root was + // created in initTestCase) + bool threw = false; + try + { + NGL::NGLLoggerBootstrap::createNGLRootLogger("log4cxx.props", false); + } + catch (const NGL::NGLLogger::AlreadyCreatedError &e) + { + Q_UNUSED(e); + threw = true; + } + QVERIFY(threw); + } + + void K_NotCreatedError() + { + // Not tested here: requires a fresh process without prior bootstrap, and initTestCase + // already created the root logger + QSKIP("requires fresh process without prior bootstrap"); + } + + void L_MurmurHash2Determinism() + { + QCOMPARE(NGL::MurmurHash2("", 0, 0), 0u); // empty, seed 0 + QCOMPARE(NGL::MurmurHash2("hello", 5, 0), NGL::MurmurHash2("hello", 5, 0)); + } + + void M_MurmurHash2Distinctness() + { + QVERIFY(NGL::MurmurHash2("hello", 5, 0) != NGL::MurmurHash2("hello", 5, 1)); // + // different seed + QVERIFY(NGL::MurmurHash2("abc", 3, 0) != NGL::MurmurHash2("abd", 3, 0)); // + // different input + } +}; + +QTEST_MAIN(ngllogger_unit_test) +#include "ngllogger_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/CMakeLists.txt index 6263339f2..57134869c 100755 --- a/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/CMakeLists.txt @@ -33,3 +33,8 @@ NGL_TARGET_LINK_LIBRARIES(wms_json_ds PUBLIC Qt6::Core ) + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/unit-test/CMakeLists.txt new file mode 100644 index 000000000..ca0344096 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/unit-test/CMakeLists.txt @@ -0,0 +1,18 @@ +# +# Unit tests for the WMS_Interface module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- WMS_Interface: JSON/QVariantMap serialization roundtrips +if (TARGET wms_json_ds) + ADD_EXECUTABLE(wms_json_ds_unit_test wms_json_ds_unit_test.cpp) + TARGET_LINK_LIBRARIES(wms_json_ds_unit_test + wms_json_ds + Qt6::Core + Qt6::Test + ) + ADD_TEST(NAME wms_json_ds_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/unit-test/wms_json_ds_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/unit-test/wms_json_ds_unit_test.cpp new file mode 100644 index 000000000..5ffd8b9ae --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/WMS_Interface/unit-test/wms_json_ds_unit_test.cpp @@ -0,0 +1,598 @@ +// +// Unit tests for the wms_json_ds module (Z/src/WMS_Interface): offline +// JSON / QVariantMap serialization and deserialization of the WMS<->ECC +// data structures (equipment types, exercises, player info, registration, +// unregister, equipment requirements) plus the JSON key helpers. +// +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +class wms_json_ds_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void jsonKeysSanity(); + void int2PlayerAssignState(); + + void equipmentTypeJsonRoundtrip(); + void equipmentTypesJsonAllRoundtrip(); + void equipmentTypesVariantAllRoundtrip(); + + void exerciseJsonRoundtrip(); + void exercisesJsonAllRoundtrip(); + void exerciseVariantRoundtrip(); + void exercisesVariantAllRoundtrip(); + + void playerInfoJsonRoundtrip(); + void playerInfosJsonAllRoundtrip(); + void playerInfoVariantRoundtrip(); + void playerInfosVariantAllRoundtrip(); + + void registrationJsonFullRoundtrip(); + void registrationJsonOptionalFields(); + void registrationVariantRoundtrip(); + + void unregisterJsonRoundtrip(); + void unregisterVariantRoundtrip(); + + void equipRequirementsJsonRoundtrip(); + void equipRequirementsVariantRoundtrip(); +}; + +void wms_json_ds_unit_test::jsonKeysSanity() +{ + // json_keys.h: inline key-name helpers used by the WMS<->ECC JSON protocol + QCOMPARE(WebSocketInterface::JsonKeys::EXERCISE_ID(), QStringLiteral("exerciseId")); + QCOMPARE(WebSocketInterface::JsonKeys::DATA_MODIFICATION(), QStringLiteral("dataModification")); + QCOMPARE(WebSocketInterface::JsonKeys::PARTICIPANT_ID(), QStringLiteral("participantId")); + QCOMPARE(WebSocketInterface::JsonKeys::REGISTRATION_RETURN_VALUE(), QStringLiteral("registrationReturnValue")); + QCOMPARE(WebSocketInterface::JsonKeys::EQUIPMENT_TYPES_REQUEST(), QStringLiteral("equipmentTypesRequest")); + QCOMPARE(WebSocketInterface::JsonKeys::EQUIPMENT_TYPES_REPLY(), QStringLiteral("equipmentTypesReply")); + QCOMPARE(WebSocketInterface::JsonKeys::EXERCISES_REQUEST(), QStringLiteral("exercisesRequest")); + QCOMPARE(WebSocketInterface::JsonKeys::EXERCISES_REPLY(), QStringLiteral("exercisesReply")); + QCOMPARE(WebSocketInterface::JsonKeys::PARTICIPANTS_REQUEST(), QStringLiteral("participantsRequest")); + QCOMPARE(WebSocketInterface::JsonKeys::PARTICIPANTS_REPLY(), QStringLiteral("participantsReply")); + QCOMPARE(WebSocketInterface::JsonKeys::PARTICIPANT_REGISTER_IN_EXCON_REQUEST(), QStringLiteral("participantRegisterInEXCONRequest")); + QCOMPARE(WebSocketInterface::JsonKeys::PARTICIPANT_REGISTER_IN_EXCON_REPLY(), QStringLiteral("participantRegisterInEXCONReply")); +} + +void wms_json_ds_unit_test::int2PlayerAssignState() +{ + // IWMS2ECC::int2PlayerAssignState passes through all defined values. + // (Invalid values hit Q_ASSERT(false) in the implementation, so they are + // deliberately not exercised here.) + QCOMPARE(IWMS2ECC::int2PlayerAssignState(-2), IWMS2ECC::UNDEFINED); + QCOMPARE(IWMS2ECC::int2PlayerAssignState(-1), IWMS2ECC::NOT_ASSIGNED); + QCOMPARE(IWMS2ECC::int2PlayerAssignState(0), IWMS2ECC::ASSIGNED); + QCOMPARE(IWMS2ECC::int2PlayerAssignState(1), IWMS2ECC::HANDOUT_EXCON); + QCOMPARE(IWMS2ECC::int2PlayerAssignState(2), IWMS2ECC::HANDOUT_WMS); + QCOMPARE(IWMS2ECC::int2PlayerAssignState(3), IWMS2ECC::HANDOUT_EXCON_OVERRULED_WMS); +} + +void wms_json_ds_unit_test::equipmentTypeJsonRoundtrip() +{ + IWMS2ECC::EquipmentType et; + et.name = QStringLiteral("Panzergreaner"); + et.id = 1001; + et.specialType = 7; + et.minEquipNo = 10; + et.maxEquipNo = 99; + et.taVulnerabilityModelId = 5; + et.taPlayerTypeId = 6; + et.deletedInWMS = true; + + const QJsonObject obj = EquipmentTypes::toJson(et); + QCOMPARE(obj[QLatin1String("id")].toInt(), 1001); + QCOMPARE(obj[QLatin1String("taVulnerabilityModelId")].toInt(), 5); + + const IWMS2ECC::EquipmentType back = EquipmentTypes::fromJson(obj); + QCOMPARE(back.id, et.id); + QCOMPARE(back.name, et.name); + QCOMPARE(back.minEquipNo, et.minEquipNo); + QCOMPARE(back.maxEquipNo, et.maxEquipNo); + QCOMPARE(back.specialType, et.specialType); + QCOMPARE(back.taVulnerabilityModelId, et.taVulnerabilityModelId); + QCOMPARE(back.taPlayerTypeId, et.taPlayerTypeId); + QCOMPARE(back.deletedInWMS, et.deletedInWMS); +} + +void wms_json_ds_unit_test::equipmentTypesJsonAllRoundtrip() +{ + IWMS2ECC::equipTypeMap map; + IWMS2ECC::EquipmentType a; + a.name = QStringLiteral("typeA"); + a.id = 1001; + a.minEquipNo = 1; + a.maxEquipNo = 20; + IWMS2ECC::EquipmentType b; + b.name = QStringLiteral("typeB"); + b.id = 1002; + b.minEquipNo = 21; + b.maxEquipNo = 40; + b.deletedInWMS = true; + map.insert(a.id, a); + map.insert(b.id, b); + + EquipmentTypes src; + src.setEquipmentTypes(map); + src.setOk(true); + + // Full wire-format roundtrip: serialize to a JSON string and parse it back. + const QByteArray wire = QJsonDocument(src.toJsonAll()).toJson(QJsonDocument::Compact); + QJsonParseError perr; + const QJsonDocument doc = QJsonDocument::fromJson(wire, &perr); + QCOMPARE(perr.error, QJsonParseError::NoError); + QCOMPARE(static_cast(doc[QLatin1String("etMap")].toArray().size()), 2); + + const EquipmentTypes back = EquipmentTypes::fromJsonAll(doc.object()); + QVERIFY(back.getOk()); + QCOMPARE(static_cast(back.getEquipmentTypes().size()), 2); + QCOMPARE(back.getEquipmentTypes().value(1001).name, a.name); + QCOMPARE(back.getEquipmentTypes().value(1002).name, b.name); + QCOMPARE(back.getEquipmentTypes().value(1002).deletedInWMS, true); + QCOMPARE(back.getEquipmentTypes().value(1001).minEquipNo, 1); + QCOMPARE(back.getEquipmentTypes().value(1002).maxEquipNo, 40); +} + +void wms_json_ds_unit_test::equipmentTypesVariantAllRoundtrip() +{ + IWMS2ECC::equipTypeMap map; + IWMS2ECC::EquipmentType a; + a.name = QStringLiteral("typeA"); + a.id = 2001; + a.specialType = 3; + a.minEquipNo = 5; + a.maxEquipNo = 15; + map.insert(a.id, a); + + EquipmentTypes src; + src.setEquipmentTypes(map); + src.setOk(true); + + const QVariantMap v = src.toVariantMapAll(); + QCOMPARE(v[QLatin1String("ok")].toBool(), true); + QVERIFY(v.contains(QLatin1String("etMap"))); + + const EquipmentTypes back = EquipmentTypes::fromVariantMapAll(v); + QVERIFY(back.getOk()); + QCOMPARE(static_cast(back.getEquipmentTypes().size()), 1); + QCOMPARE(back.getEquipmentTypes().value(2001).name, a.name); + QCOMPARE(back.getEquipmentTypes().value(2001).specialType, 3); + QCOMPARE(back.getEquipmentTypes().value(2001).minEquipNo, 5); + QCOMPARE(back.getEquipmentTypes().value(2001).maxEquipNo, 15); + // NOTE: the single-object QVariant conversion (toVariantMap) does not carry + // taVulnerabilityModelId / taPlayerTypeId, so the map conversion loses them; + // the JSON conversion above does preserve both. +} + +void wms_json_ds_unit_test::exerciseJsonRoundtrip() +{ + IWMS2ECC::Exercise ex; + ex.id = 42; + ex.actExercise = 1; + ex.start = 1700000000; + ex.end = 1700003600; + ex.name = QStringLiteral("Wintermanöver"); + ex.stateAsString = QStringLiteral("running"); + + EXCON::Exercises src; + src.setExercise(ex); + src.setOk(true); + + const QJsonObject obj = src.toJson(ex); + QCOMPARE(obj[QLatin1String("id")].toInt(), 42); + QCOMPARE(obj[QLatin1String("state")].toString(), ex.stateAsString); + + const IWMS2ECC::Exercise back = EXCON::Exercises::fromJson(obj); + QCOMPARE(back.id, ex.id); + QCOMPARE(back.actExercise, ex.actExercise); + QCOMPARE(back.start, ex.start); + QCOMPARE(back.end, ex.end); + QCOMPARE(back.name, ex.name); + QCOMPARE(back.stateAsString, ex.stateAsString); +} + +void wms_json_ds_unit_test::exercisesJsonAllRoundtrip() +{ + IWMS2ECC::exerciseMap map; + IWMS2ECC::Exercise a; + a.id = 1; + a.name = QStringLiteral("ExA"); + a.start = 1000; + a.end = 2000; + IWMS2ECC::Exercise b; + b.id = 2; + b.name = QStringLiteral("ExB"); + b.actExercise = 1; + b.start = 3000; + b.end = 4000; + b.stateAsString = QStringLiteral("stopped"); + map.insert(a.id, a); + map.insert(b.id, b); + + EXCON::Exercises src; + src.setExercises(map); + src.setOk(true); + + const QByteArray wire = QJsonDocument(src.toJsonAll()).toJson(QJsonDocument::Compact); + QJsonParseError perr; + const QJsonDocument doc = QJsonDocument::fromJson(wire, &perr); + QCOMPARE(perr.error, QJsonParseError::NoError); + QCOMPARE(static_cast(doc[QLatin1String("exMap")].toArray().size()), 2); + + const EXCON::Exercises back = EXCON::Exercises::fromJsonAll(doc.object()); + QVERIFY(back.getOk()); + QCOMPARE(static_cast(back.getExercises().size()), 2); + QCOMPARE(back.getExercises().value(1).name, a.name); + QCOMPARE(back.getExercises().value(2).name, b.name); + QCOMPARE(back.getExercises().value(2).actExercise, 1); + QCOMPARE(back.getExercises().value(2).stateAsString, b.stateAsString); + QCOMPARE(back.getExercises().value(1).start, 1000); + QCOMPARE(back.getExercises().value(2).end, 4000); +} + +void wms_json_ds_unit_test::exerciseVariantRoundtrip() +{ + IWMS2ECC::Exercise ex; + ex.id = 7; + ex.name = QStringLiteral("Ex7"); + ex.start = 1700000000; + ex.end = 1700003600; + ex.stateAsString = QStringLiteral("planned"); + + EXCON::Exercises src; + src.setExercise(ex); + src.setOk(true); + + const QVariantMap v = src.toVariantMap(); + QCOMPARE(v[QLatin1String("ok")].toBool(), true); + + const EXCON::Exercises back = EXCON::Exercises::fromVariantMap(v); + QVERIFY(back.getOk()); + QCOMPARE(back.getExercise().id, ex.id); + QCOMPARE(back.getExercise().name, ex.name); + QCOMPARE(back.getExercise().start, ex.start); + QCOMPARE(back.getExercise().end, ex.end); + QCOMPARE(back.getExercise().stateAsString, ex.stateAsString); +} + +void wms_json_ds_unit_test::exercisesVariantAllRoundtrip() +{ + IWMS2ECC::exerciseMap map; + IWMS2ECC::Exercise a; + a.id = 11; + a.name = QStringLiteral("Ex11"); + a.start = 1700000000; + a.end = 1700003600; + map.insert(a.id, a); + + EXCON::Exercises src; + src.setExercises(map); + src.setOk(true); + + const QVariantMap v = src.toVariantMapAll(); + QCOMPARE(v[QLatin1String("ok")].toBool(), true); + QCOMPARE(static_cast(v[QLatin1String("exeMap")].toMap().size()), 1); + + const EXCON::Exercises back = EXCON::Exercises::fromVariantMapAll(v); + QVERIFY(back.getOk()); + QCOMPARE(static_cast(back.getExercises().size()), 1); + QCOMPARE(back.getExercises().value(11).name, a.name); + QCOMPARE(back.getExercises().value(11).start, a.start); + QCOMPARE(back.getExercises().value(11).end, a.end); +} + +void wms_json_ds_unit_test::playerInfoJsonRoundtrip() +{ + IWMS2ECC::PlayerInfo pi; + pi.playerState = IWMS2ECC::FOUND; + pi.playerId = 101; + pi.parentPlayerId = 900; + pi.equipNo = 55; + pi.assignState = IWMS2ECC::HANDOUT_WMS; + pi.exerciseId = 42; + pi.forceId = 7; + pi.typeId = 3; + pi.equipTypeId = 1001; + pi.typeClassId = 9; + pi.symbolColor = 16711680; + const unsigned char pngRaw[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0xFF}; + pi.symbolPNG = QByteArray(reinterpret_cast(pngRaw), sizeof(pngRaw)); + pi.hierarchy = QStringLiteral("Feldwebel"); + pi.designator = QStringLiteral("A-1"); + pi.tactIdent = QStringLiteral("Alpha 1"); + pi.info = QStringLiteral("some info"); + pi.alias = QStringLiteral("alpha"); + pi.forceName = QStringLiteral("Kampfgruppe 1"); + pi.isDeleted = false; + + const QJsonObject obj = PlayerInfo::toJson(pi); + QCOMPARE(obj[QLatin1String("playerId")].toInt(), 101); + QCOMPARE(obj[QLatin1String("assignState")].toInt(), static_cast(IWMS2ECC::HANDOUT_WMS)); + + const IWMS2ECC::PlayerInfo back = PlayerInfo::fromJson(obj); + QCOMPARE(back.playerState, pi.playerState); + QCOMPARE(back.playerId, pi.playerId); + QCOMPARE(back.parentPlayerId, pi.parentPlayerId); + QCOMPARE(back.equipNo, pi.equipNo); + QCOMPARE(back.assignState, pi.assignState); + QCOMPARE(back.exerciseId, pi.exerciseId); + QCOMPARE(back.forceId, pi.forceId); + QCOMPARE(back.typeId, pi.typeId); + QCOMPARE(back.isDeleted, pi.isDeleted); + QCOMPARE(back.equipTypeId, pi.equipTypeId); + QCOMPARE(back.typeClassId, pi.typeClassId); + QCOMPARE(back.symbolColor, pi.symbolColor); + QCOMPARE(back.symbolPNG, pi.symbolPNG); + QCOMPARE(back.hierarchy, pi.hierarchy); + QCOMPARE(back.designator, pi.designator); + QCOMPARE(back.tactIdent, pi.tactIdent); + QCOMPARE(back.info, pi.info); + QCOMPARE(back.alias, pi.alias); + QCOMPARE(back.forceName, pi.forceName); +} + +void wms_json_ds_unit_test::playerInfosJsonAllRoundtrip() +{ + QMap map; + IWMS2ECC::PlayerInfo a; + a.playerId = 1; + a.playerState = IWMS2ECC::FOUND; + a.alias = QStringLiteral("p1"); + IWMS2ECC::PlayerInfo b; + b.playerId = 2; + b.playerState = IWMS2ECC::DELETED; + b.isDeleted = true; + b.alias = QStringLiteral("p2"); + map.insert(a.playerId, a); + map.insert(b.playerId, b); + + PlayerInfo src; + src.setAllPlayerInfo(map); + src.setOk(true); + + const QByteArray wire = QJsonDocument(src.toJsonAll()).toJson(QJsonDocument::Compact); + QJsonParseError perr; + const QJsonDocument doc = QJsonDocument::fromJson(wire, &perr); + QCOMPARE(perr.error, QJsonParseError::NoError); + QCOMPARE(static_cast(doc[QLatin1String("piMap")].toArray().size()), 2); + + const PlayerInfo back = PlayerInfo::fromJsonAll(doc.object()); + QVERIFY(back.getOk()); + QCOMPARE(static_cast(back.getAllPlayerInfo().size()), 2); + QCOMPARE(back.getAllPlayerInfo().value(1).playerState, IWMS2ECC::FOUND); + QCOMPARE(back.getAllPlayerInfo().value(1).alias, a.alias); + QCOMPARE(back.getAllPlayerInfo().value(2).playerState, IWMS2ECC::DELETED); + QCOMPARE(back.getAllPlayerInfo().value(2).isDeleted, true); + QCOMPARE(back.getAllPlayerInfo().value(2).alias, b.alias); +} + +void wms_json_ds_unit_test::playerInfoVariantRoundtrip() +{ + IWMS2ECC::PlayerInfo pi; + pi.playerState = IWMS2ECC::FOUND; + pi.playerId = 202; + pi.equipNo = 12; + pi.assignState = IWMS2ECC::ASSIGNED; + pi.forceName = QStringLiteral("Kampfgruppe 2"); + pi.symbolPNG = QByteArrayLiteral("\x01\x02\x03"); + + PlayerInfo src; + src.setPlayerInfo(pi); + src.setOk(true); + + const QVariantMap v = src.toVariantMap(); + QCOMPARE(v[QLatin1String("ok")].toBool(), true); + + const PlayerInfo back = PlayerInfo::fromVariantMap(v); + QVERIFY(back.getOk()); + QCOMPARE(back.getPlayerInfo().playerState, pi.playerState); + QCOMPARE(back.getPlayerInfo().playerId, pi.playerId); + QCOMPARE(back.getPlayerInfo().equipNo, pi.equipNo); + QCOMPARE(back.getPlayerInfo().assignState, pi.assignState); + QCOMPARE(back.getPlayerInfo().forceName, pi.forceName); + QCOMPARE(back.getPlayerInfo().symbolPNG, pi.symbolPNG); +} + +void wms_json_ds_unit_test::playerInfosVariantAllRoundtrip() +{ + QMap map; + IWMS2ECC::PlayerInfo a; + a.playerId = 301; + a.playerState = IWMS2ECC::FOUND; + a.designator = QStringLiteral("B-2"); + IWMS2ECC::PlayerInfo b; + b.playerId = 302; + b.playerState = IWMS2ECC::NOT_FOUND; + b.equipNo = 77; + map.insert(a.playerId, a); + map.insert(b.playerId, b); + + PlayerInfo src; + src.setAllPlayerInfo(map); + src.setOk(true); + + const QVariantMap v = src.toVariantMapAll(); + QCOMPARE(static_cast(v[QLatin1String("plMap")].toMap().size()), 2); + + const PlayerInfo back = PlayerInfo::fromVariantMapAll(v); + QCOMPARE(static_cast(back.getAllPlayerInfo().size()), 2); + QCOMPARE(back.getAllPlayerInfo().value(301).designator, a.designator); + QCOMPARE(back.getAllPlayerInfo().value(301).playerState, IWMS2ECC::FOUND); + QCOMPARE(back.getAllPlayerInfo().value(302).equipNo, 77); + // NOTE: PlayerInfo::fromVariantMapAll() reads "ok" from the inner "plMap" + // (where it does not exist) instead of the outer map, so getOk() is not + // asserted here. +} + +void wms_json_ds_unit_test::registrationJsonFullRoundtrip() +{ + Registration rg; + rg.setPlayerId(501); + rg.setEquipNo(33); + rg.setAssignment(IWMS2ECC::HANDOUT_WMS); + rg.setExerciseId(42); + rg.setAlias(QStringLiteral("delta")); + rg.setInfo(QStringLiteral("note")); + rg.setOk(true); + + const QJsonObject obj = rg.toJson(); + QVERIFY(obj.contains(QLatin1String("equipNo"))); + QVERIFY(obj.contains(QLatin1String("alias"))); + QVERIFY(obj.contains(QLatin1String("info"))); + + const Registration back = Registration::fromJson(obj); + QCOMPARE(back.getPlayerId(), 501); + QCOMPARE(back.getEquipNo(), 33); + QCOMPARE(back.getExerciseId(), 42); + QCOMPARE(back.getAssignment(), IWMS2ECC::HANDOUT_WMS); + QCOMPARE(back.getAlias(), QStringLiteral("delta")); + QCOMPARE(back.getInfo(), QStringLiteral("note")); + QVERIFY(back.getOk()); + QVERIFY(back.isEquipNoSet()); + QVERIFY(back.isAliasSet()); + QVERIFY(back.isInfoSet()); +} + +void wms_json_ds_unit_test::registrationJsonOptionalFields() +{ + Registration rg; + rg.setPlayerId(502); + rg.setExerciseId(7); + rg.setOk(false); + // equipNo / alias / info intentionally not set + + const QJsonObject obj = rg.toJson(); + QCOMPARE(obj[QLatin1String("playerId")].toInt(), 502); + QVERIFY(!obj.contains(QLatin1String("equipNo"))); + QVERIFY(!obj.contains(QLatin1String("alias"))); + QVERIFY(!obj.contains(QLatin1String("info"))); + + const Registration back = Registration::fromJson(obj); + QCOMPARE(back.getPlayerId(), 502); + QCOMPARE(back.getExerciseId(), 7); + QVERIFY(!back.getOk()); + QVERIFY(!back.isEquipNoSet()); + QVERIFY(!back.isAliasSet()); + QVERIFY(!back.isInfoSet()); +} + +void wms_json_ds_unit_test::registrationVariantRoundtrip() +{ + Registration rg; + rg.setPlayerId(601); + rg.setEquipNo(99); + rg.setAssignment(IWMS2ECC::ASSIGNED); + rg.setExerciseId(5); + rg.setAlias(QStringLiteral("echo")); + rg.setInfo(QStringLiteral("info text")); + rg.setOk(true); + + const QVariantMap v = rg.toVariantMap(); + QVERIFY(v.contains(QLatin1String("equipNo"))); + QVERIFY(v.contains(QLatin1String("alias"))); + QVERIFY(v.contains(QLatin1String("info"))); + + const Registration back = Registration::fromVariantMap(v); + QCOMPARE(back.getPlayerId(), 601); + QCOMPARE(back.getEquipNo(), 99); + QCOMPARE(back.getExerciseId(), 5); + QCOMPARE(back.getAssignment(), IWMS2ECC::ASSIGNED); + QCOMPARE(back.getAlias(), QStringLiteral("echo")); + QCOMPARE(back.getInfo(), QStringLiteral("info text")); + QVERIFY(back.getOk()); + QVERIFY(back.isEquipNoSet()); + QVERIFY(back.isAliasSet()); + QVERIFY(back.isInfoSet()); +} + +void wms_json_ds_unit_test::unregisterJsonRoundtrip() +{ + Unregister ur; + ur.setEquipNo(42); + ur.setExerciseId(7); + ur.setOk(true); + + const QJsonObject obj = ur.toJson(); + QCOMPARE(obj[QLatin1String("equipNo")].toInt(), 42); + QCOMPARE(obj[QLatin1String("exerciseId")].toInt(), 7); + + const Unregister back = Unregister::fromJson(obj); + QCOMPARE(back.getEquipNo(), 42); + QCOMPARE(back.getExerciseId(), 7); + QVERIFY(back.getOk()); +} + +void wms_json_ds_unit_test::unregisterVariantRoundtrip() +{ + Unregister ur; + ur.setEquipNo(43); + ur.setExerciseId(8); + ur.setOk(false); + + const QVariantMap v = ur.toVariantMap(); + QCOMPARE(v[QLatin1String("equipNo")].toInt(), 43); + + const Unregister back = Unregister::fromVariantMap(v); + QCOMPARE(back.getEquipNo(), 43); + QCOMPARE(back.getExerciseId(), 8); + QVERIFY(!back.getOk()); +} + +void wms_json_ds_unit_test::equipRequirementsJsonRoundtrip() +{ + QMap reqs; + reqs.insert(10, 3); + reqs.insert(20, 5); + reqs.insert(30, 1); + + EquipRequirements src; + src.setEquipRequirements(reqs); + src.setOk(true); + + const QJsonObject obj = src.toJson(); + QCOMPARE(static_cast(obj[QLatin1String("equipMap")].toArray().size()), 3); + + const EquipRequirements back = EquipRequirements::fromJson(obj); + QVERIFY(back.getOk()); + QCOMPARE(static_cast(back.getEquipRequirements().size()), 3); + QCOMPARE(back.getEquipRequirements().value(10), 3); + QCOMPARE(back.getEquipRequirements().value(20), 5); + QCOMPARE(back.getEquipRequirements().value(30), 1); +} + +void wms_json_ds_unit_test::equipRequirementsVariantRoundtrip() +{ + QMap reqs; + reqs.insert(100, 4); + reqs.insert(200, 6); + + EquipRequirements src; + src.setEquipRequirements(reqs); + src.setOk(true); + + const QVariantMap v = src.toVariantMap(); + QCOMPARE(static_cast(v[QLatin1String("equipMap")].toMap().size()), 2); + + const EquipRequirements back = EquipRequirements::fromVariantMap(v); + QVERIFY(back.getOk()); + QCOMPARE(static_cast(back.getEquipRequirements().size()), 2); + QCOMPARE(back.getEquipRequirements().value(100), 4); + QCOMPARE(back.getEquipRequirements().value(200), 6); +} + +QTEST_MAIN(wms_json_ds_unit_test) +#include "wms_json_ds_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/CMakeLists.txt index 41a0d20c9..e16e5c2c6 100755 --- a/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/CMakeLists.txt @@ -102,6 +102,11 @@ NGL_TARGET_LINK_LIBRARIES(nglcoreapplication ) NGL_INSTALL_TARGET(nglcoreapplication) +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) + # # nglsinglecoreapplication # diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/unit-test/CMakeLists.txt new file mode 100644 index 000000000..0399331cd --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/unit-test/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# Unit tests for the nglapplication module (nglcoreapplication). +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- nglapplication: nglcoreapplication logger handler +# --- (Log4Qt-independent; works in the Log4Qt-less (stub) build) +if (TARGET nglcoreapplication) + ADD_EXECUTABLE(nglcoreapplication_unit_test nglcoreapplication_unit_test.cpp) + # NGLLogger.h (the AlreadyCreatedError type thrown by createNGLRootLogger) + # refuses to be included outside the NGLLogger library unless + # BUILDING_NGLLOGGER is defined (same pattern as NGLLogger/CMakeLists.txt) + TARGET_COMPILE_DEFINITIONS(nglcoreapplication_unit_test PRIVATE BUILDING_NGLLOGGER) + TARGET_LINK_LIBRARIES(nglcoreapplication_unit_test + nglcoreapplication + NGLLogger + Qt6::Core + Qt6::Test + ) + ADD_TEST(NAME nglcoreapplication_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/unit-test/nglcoreapplication_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/unit-test/nglcoreapplication_unit_test.cpp new file mode 100644 index 000000000..d50bb102a --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglapplication/unit-test/nglcoreapplication_unit_test.cpp @@ -0,0 +1,73 @@ +#include +#include +#include +#include +#include + +class nglcoreapplication_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void A_getLoggerHandlerReturnsNull() + { + // When the app is not a LoggerHandler (or no app is present), getLoggerHandler + // returns nullptr + QCOMPARE(NGL::LoggerHandler::getLoggerHandler(), nullptr); + } + + void B_createLoggerNaming() + { + NGL::LoggerHandler lh; + + // First call: config-file-name overload; creates the root logger, sets prefix and + // postfix + lh.createLogger("MyApp", "log4cxx_MyApp.props", true); + // createLogger installed qtMsgHandler; reset to the default Qt handler to avoid + // recursion in the Log4Qt-less (stub) build + qInstallMessageHandler(nullptr); + QCOMPARE(lh.loggerNamePrefix(), QStringLiteral(".log")); + QVERIFY(lh.loggerNamePostfix().endsWith(QLatin1String("_myapp"))); + + // Second call: exerciseId overload; the postfix is updated to include both ids BEFORE + // the AlreadyCreatedError is thrown, so we can still read it after the catch + bool threw = false; + try + { + lh.createLogger("MyApp", 5, 7, true); + } + catch (const NGL::NGLLogger::AlreadyCreatedError &e) + { + Q_UNUSED(e); + threw = true; + } + QVERIFY(threw); // root logger already exists, so re-creation without ignore throws + QCOMPARE(lh.loggerNamePostfix().endsWith(QLatin1String("_myapp_5_7")), true); + QCOMPARE(lh.loggerNamePrefix(), QStringLiteral(".log")); + + // Third call: exerciseId only + try + { + lh.createLogger("MyApp", 5, 0, true); + } + catch (const NGL::NGLLogger::AlreadyCreatedError &e) + { + Q_UNUSED(e); + } + QVERIFY(lh.loggerNamePostfix().endsWith(QLatin1String("_myapp_5"))); + + // Fourth call: additionalId only + try + { + lh.createLogger("MyApp", 0, 7, true); + } + catch (const NGL::NGLLogger::AlreadyCreatedError &e) + { + Q_UNUSED(e); + } + QVERIFY(lh.loggerNamePostfix().endsWith(QLatin1String("_myapp_7"))); + } +}; + +QTEST_MAIN(nglcoreapplication_unit_test) +#include "nglcoreapplication_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/CMakeLists.txt index a4e2f7ca7..c4ca620f6 100755 --- a/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/CMakeLists.txt @@ -43,3 +43,8 @@ IF(BUILD_WS AND BUILD_TESTING) ADD_TEST(NAME nglcomp_test COMMAND $) ENDIF() + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/unit-test/CMakeLists.txt new file mode 100644 index 000000000..0acce1a0d --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/unit-test/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# Unit tests for the nglcompression module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- nglcompression: TarFilter (tar stream codec). +# --- TarFilter has no export macro and the tree builds with hidden +# --- visibility, so its symbols are not exported from the nglcompression +# --- shared library - the module source is compiled directly into the test +# --- (same pattern as the LogView test in TetraIF/unit-test). +if (TARGET nglcompression) + ADD_EXECUTABLE(nglcompression_unit_test + nglcompression_unit_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../nglcomp_tarfilter.cpp) + TARGET_LINK_LIBRARIES(nglcompression_unit_test + nglcompression + Qt6::Core + Qt6::Test + ) + ADD_TEST(NAME nglcompression_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/unit-test/nglcompression_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/unit-test/nglcompression_unit_test.cpp new file mode 100644 index 000000000..6340a2bca --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglcompression/unit-test/nglcompression_unit_test.cpp @@ -0,0 +1,484 @@ +// +// Unit tests for the nglcompression module: TarFilter (the module's pure +// logic class, compiled into the nglcompression library). It is a synchronous +// POSIX/GNU tar stream codec over QIODevice - tested entirely in-memory with +// QBuffer: archive writing, entry metadata/data roundtrip, GNU long-name +// handling, alignment padding, error paths and abort. +// +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// Builds a tar archive in-memory: the fill lambda receives an opened +// write-only TarFilter, then the end-of-archive blocks are appended. +static bool buildArchive(const std::function &fill, QByteArray &archive, bool writeEos = true) +{ + archive.clear(); + QBuffer buf; + buf.setBuffer(&archive); + if (!buf.open(QIODevice::WriteOnly)) + return false; + + TarFilter writer(&buf); + fill(writer); + if (writeEos && !writer.writeEOS()) + return false; + buf.close(); + return true; +} + +// Deterministic pseudo-random payload. +static QByteArray patternData(int size, int seed) +{ + QByteArray ba(size, '\0'); + for (int i = 0; i < size; ++i) + ba[i] = static_cast((i * 7 + seed) & 0xFF); + return ba; +} + +// Re-writes the typeflag byte of the first tar header and recomputes the +// ustar checksum (sum of all header bytes with the chksum field as spaces, +// stored as 6 octal digits + NUL + space). Offsets: name 0..99, mode 100..107, +// uid 108..115, gid 116..123, size 124..135, mtime 136..147, chksum 148..155, +// typeflag 156. +static QByteArray patchHeaderTypeflag(const QByteArray &archive, char typeflag) +{ + constexpr int blockSize = 512; + constexpr int chksumOffset = 148; + constexpr int chksumLength = 8; + constexpr int typeflagOffset = 156; + + QByteArray hdr = archive.left(blockSize); + hdr[typeflagOffset] = typeflag; + for (int i = 0; i < chksumLength; ++i) + hdr[chksumOffset + i] = ' '; + + unsigned int sum = 0; + for (int i = 0; i < blockSize; ++i) + sum += static_cast(hdr[i]); + + char field[8]; + std::snprintf(field, sizeof(field), "%.6o", sum); + for (int i = 0; i < 6; ++i) + hdr[chksumOffset + i] = field[i]; + hdr[chksumOffset + 6] = '\0'; + hdr[chksumOffset + 7] = ' '; + + QByteArray result = archive; + result.replace(0, blockSize, hdr); + return result; +} + +class nglcompression_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void basicAddAndGetDataRoundtrip(); + void explicitFileInformationRoundtrip(); + void directoryEntryRoundtrip(); + void multipleFilesRoundtrip(); + void longFileNameRoundtrip(); + void addFileFromDisk(); + void addFileMissingError(); + void endOfArchive(); + void truncatedArchiveError(); + void unsupportedMagicError(); + void unsupportedFileTypeError(); + void getDataToDeviceWithoutFileInfoError(); + void readCallbackAndBufferSize(); + void abortStopsOperation(); +}; + +void nglcompression_unit_test::basicAddAndGetDataRoundtrip() +{ + const QByteArray original("0123456789"); + + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(QStringLiteral("data.txt"), original)); + }, archive)); + // 512 header + 512 data block (10 bytes padded) + 1024 end-of-archive + QCOMPARE(archive.size(), 2048); + + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fi; + QByteArray data; + QVERIFY(reader.getData(fi, data)); + QCOMPARE(fi.fileName, QStringLiteral("data.txt")); + QCOMPARE(fi.fileSize, static_cast(original.size())); + QCOMPARE(fi.fileType, TarFilter::regular); + QCOMPARE(data, original); + QVERIFY(!reader.error()); +} + +void nglcompression_unit_test::explicitFileInformationRoundtrip() +{ + TarFilter::FileInformations fi; + fi.fileName = QStringLiteral("explicit.txt"); + fi.fileSize = 5; + fi.uid = 12345; + fi.gid = 678; + fi.uname = QStringLiteral("testuser"); + fi.gname = QStringLiteral("testgroup"); + fi.mtime = 0; + fi.fileType = TarFilter::regular; + // Owner permission bits (Qt6: 0x4000/0x2000/0x1000) sit above the tar 12-bit + // mode field, which the module masks with 0x0fff; use group/other bits so + // the mode roundtrips exactly. + fi.mode = QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup + | QFile::ReadOther | QFile::WriteOther | QFile::ExeOther; + + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + const QByteArray inData("12345"); + QBuffer in; + in.setData(inData); + QVERIFY(in.open(QIODevice::ReadOnly)); + QVERIFY(tf.addData(fi, &in)); + }, archive)); + + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fiRead; + QByteArray data; + QVERIFY(reader.getData(fiRead, data)); + QCOMPARE(fiRead.fileName, fi.fileName); + QCOMPARE(fiRead.fileSize, static_cast(5)); + QCOMPARE(fiRead.uid, 12345); + QCOMPARE(fiRead.gid, 678); + QCOMPARE(fiRead.uname, QStringLiteral("testuser")); + QCOMPARE(fiRead.gname, QStringLiteral("testgroup")); + QCOMPARE(fiRead.mtime, static_cast(0)); + QCOMPARE(fiRead.fileType, TarFilter::regular); + QCOMPARE(fiRead.mode, fi.mode); + QCOMPARE(data, QByteArray("12345")); + QVERIFY(!reader.error()); +} + +void nglcompression_unit_test::directoryEntryRoundtrip() +{ + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(QStringLiteral("mydir/"), QByteArray(), true)); + QVERIFY(tf.addData(QStringLiteral("mydir/inner.txt"), QByteArray("inner"))); + }, archive)); + // dir header (no data block) + inner header + 512 data block + 1024 eos + QCOMPARE(archive.size(), 2560); + + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fi; + QByteArray data; + QVERIFY(reader.getData(fi, data)); + QCOMPARE(fi.fileName, QStringLiteral("mydir/")); + QCOMPARE(fi.fileType, TarFilter::directory); + QCOMPARE(fi.fileSize, static_cast(0)); + QVERIFY(data.isEmpty()); + + QVERIFY(reader.getData(fi, data)); + QCOMPARE(fi.fileName, QStringLiteral("mydir/inner.txt")); + QCOMPARE(fi.fileType, TarFilter::regular); + QCOMPARE(data, QByteArray("inner")); + QVERIFY(!reader.error()); +} + +void nglcompression_unit_test::multipleFilesRoundtrip() +{ + const QByteArray c1 = patternData(100, 1); // forces 512-byte padding + const QByteArray c2 = patternData(512, 2); // exactly one data block + const QByteArray c3 = patternData(5000, 3); // multi-block + + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(QStringLiteral("f1.bin"), c1)); + QVERIFY(tf.addData(QStringLiteral("f2.bin"), c2)); + QVERIFY(tf.addData(QStringLiteral("f3.bin"), c3)); + }, archive)); + // (512+512) + (512+512) + (512+5120) + 1024 + QCOMPARE(archive.size(), 8704); + + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + const char *names[] = {"f1.bin", "f2.bin", "f3.bin"}; + const QByteArray *contents[] = {&c1, &c2, &c3}; + for (int i = 0; i < 3; ++i) + { + TarFilter::FileInformations fi; + QByteArray data; + QVERIFY2(reader.getData(fi, data), qPrintable(QStringLiteral("failed to read entry %1").arg(i))); + QCOMPARE(fi.fileName, QString::fromLatin1(names[i])); + QCOMPARE(fi.fileSize, static_cast(contents[i]->size())); + QCOMPARE(fi.fileType, TarFilter::regular); + QCOMPARE(data, *contents[i]); + } + QVERIFY(!reader.error()); +} + +void nglcompression_unit_test::longFileNameRoundtrip() +{ + // Name longer than the 100-byte tar header field -> GNU longlink entry. + QString longName; + for (int i = 0; i < 150; ++i) + longName += QChar::fromLatin1('a' + (i % 26)); + longName += QStringLiteral(".txt"); + QVERIFY(longName.toUtf8().size() > 100); + + const QByteArray original("longname-data"); + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(longName, original)); + }, archive)); + // longlink header + name block + file header + 512 data block + 1024 eos + QCOMPARE(archive.size(), 3072); + + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fi; + QByteArray data; + QVERIFY(reader.getData(fi, data)); + QCOMPARE(fi.fileName, longName); + QCOMPARE(fi.fileType, TarFilter::regular); + QCOMPARE(data, original); + QVERIFY(!reader.error()); +} + +void nglcompression_unit_test::addFileFromDisk() +{ + QTemporaryDir tmp; + QVERIFY(tmp.isValid()); + + const QString path = tmp.filePath(QStringLiteral("hello.txt")); + { + QFile f(path); + QVERIFY(f.open(QIODevice::WriteOnly)); + QVERIFY(f.write(QByteArrayLiteral("hello tar")) == 9); + } + + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addFile(path, QStringLiteral("renamed.txt"))); + }, archive)); + + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fi; + QByteArray data; + QVERIFY(reader.getData(fi, data)); + QCOMPARE(fi.fileName, QStringLiteral("renamed.txt")); + QCOMPARE(fi.fileType, TarFilter::regular); + QCOMPARE(data, QByteArray("hello tar")); + QVERIFY(!reader.error()); +} + +void nglcompression_unit_test::addFileMissingError() +{ + QByteArray storage; + QBuffer buf; + buf.setBuffer(&storage); + QVERIFY(buf.open(QIODevice::WriteOnly)); + TarFilter writer(&buf); + + QVERIFY(!writer.addFile(QStringLiteral("/definitely/does/not/exist.txt"))); + QVERIFY(writer.error()); + QVERIFY(writer.lastError().contains(QStringLiteral("does not exist"))); +} + +void nglcompression_unit_test::endOfArchive() +{ + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(QStringLiteral("only.txt"), QByteArray("only"))); + }, archive)); + + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fi; + QByteArray data; + QVERIFY(reader.getData(fi, data)); + + // Clean end of archive: no data left and no error. + QVERIFY(!reader.getData(fi)); + QVERIFY(!reader.error()); +} + +void nglcompression_unit_test::truncatedArchiveError() +{ + const QByteArray truncated(100, static_cast(0x11)); + + QBuffer buf; + buf.setData(truncated); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fi; + QVERIFY(!reader.getData(fi)); + QVERIFY(reader.error()); + QVERIFY(reader.lastError().contains(QStringLiteral("Error reading"))); +} + +void nglcompression_unit_test::unsupportedMagicError() +{ + const QByteArray bad(512, static_cast(0xFF)); + + QBuffer buf; + buf.setData(bad); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fi; + QVERIFY(!reader.getData(fi)); + QVERIFY(reader.error()); + QVERIFY(reader.lastError().contains(QStringLiteral("not supported"))); +} + +void nglcompression_unit_test::unsupportedFileTypeError() +{ + const QByteArray original("typeflag"); + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(QStringLiteral("t.txt"), original)); + }, archive)); + + // Patch the first header's typeflag to '1' (link) with a valid checksum. + const QByteArray patched = patchHeaderTypeflag(archive, '1'); + + QBuffer buf; + buf.setData(patched); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + TarFilter::FileInformations fi; + QVERIFY(!reader.getData(fi)); + QVERIFY(reader.error()); + QVERIFY(reader.lastError().contains(QStringLiteral("Unsupported fileType"))); +} + +void nglcompression_unit_test::getDataToDeviceWithoutFileInfoError() +{ + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(QStringLiteral("d.txt"), QByteArray("dd"))); + }, archive)); + + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + + QBuffer out; + QVERIFY(out.open(QIODevice::WriteOnly)); + QVERIFY(!reader.getData(&out)); + QVERIFY(reader.error()); + QVERIFY(reader.lastError().contains(QStringLiteral("next chunk size"))); +} + +void nglcompression_unit_test::readCallbackAndBufferSize() +{ + const QByteArray content = patternData(5000, 42); + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(QStringLiteral("big.bin"), content)); + }, archive)); + + const auto readAll = [&](unsigned int bufSize, std::vector &values) { + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf, [&values](qint64 remaining) { values.push_back(remaining); }); + reader.setBufferSize(bufSize); + + TarFilter::FileInformations fi; + QByteArray data; + QVERIFY(reader.getData(fi, data)); + QCOMPARE(data, content); + }; + + // 1024-byte chunks (the default buffer is 1 MiB): 4x1024 + 904 -> 5 callbacks, + std::vector values; + readAll(1024, values); + QCOMPARE(static_cast(values.size()), 5); + QCOMPARE(values.front(), 5000 - 1024); + QCOMPARE(values.back(), 0); + + // Sizes below the 1024 minimum are clamped to 1024 (same 5 chunks). + std::vector clamped; + readAll(1, clamped); + QCOMPARE(static_cast(clamped.size()), 5); + QCOMPARE(clamped.front(), 5000 - 1024); + + // 2048-byte chunks: 2x2048 + 904 -> 3 callbacks. + std::vector big; + readAll(2048, big); + QCOMPARE(static_cast(big.size()), 3); + QCOMPARE(big.front(), 5000 - 2048); + QCOMPARE(big.back(), 0); +} + +void nglcompression_unit_test::abortStopsOperation() +{ + QByteArray archive; + QVERIFY(buildArchive([&](TarFilter &tf) { + QVERIFY(tf.addData(QStringLiteral("a.txt"), QByteArray("aaaa"))); + }, archive)); + + // Aborted reader refuses to read (no error message is set by abort). + { + QBuffer buf; + buf.setData(archive); + QVERIFY(buf.open(QIODevice::ReadOnly)); + TarFilter reader(&buf); + reader.abort(); + TarFilter::FileInformations fi; + QVERIFY(!reader.getData(fi)); + QVERIFY(!reader.error()); + } + + // Aborted writer refuses to write. + { + QByteArray storage; + QBuffer buf; + buf.setBuffer(&storage); + QVERIFY(buf.open(QIODevice::WriteOnly)); + TarFilter writer(&buf); + writer.abort(); + QVERIFY(!writer.addData(QStringLiteral("b.txt"), QByteArray("bbbb"))); + QVERIFY(!writer.error()); + } +} + +QTEST_MAIN(nglcompression_unit_test) +#include "nglcompression_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/CMakeLists.txt index 227044ce6..81a34fd12 100755 --- a/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/CMakeLists.txt @@ -30,3 +30,8 @@ NGL_TARGET_INCLUDE_DIRECTORIES(nglsettings ${CMAKE_CURRENT_SOURCE_DIR}/../ ) NGL_INSTALL_TARGET(nglsettings) + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/unit-test/CMakeLists.txt new file mode 100644 index 000000000..87a8282fb --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/unit-test/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# Unit tests for the nglsettings module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- nglsettings: QSettings-backed settings + connection-name mapping. +# --- setPortOffset/setServerHostname have no export macro and the tree builds +# --- with hidden visibility, so those symbols are not exported from +# --- libnglsettings - the module source is compiled directly into the test +# --- (same pattern as the LogView test in TetraIF/unit-test). +if (TARGET nglsettings) + ADD_EXECUTABLE(nglsettings_unit_test + nglsettings_unit_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../nglsettings_connections.cpp) + TARGET_LINK_LIBRARIES(nglsettings_unit_test + nglsettings + Qt6::Core + Qt6::Network + Qt6::Test + ) + ADD_TEST(NAME nglsettings_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/unit-test/nglsettings_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/unit-test/nglsettings_unit_test.cpp new file mode 100644 index 000000000..c8195cc5f --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/nglsettings/unit-test/nglsettings_unit_test.cpp @@ -0,0 +1,253 @@ +// +// Unit tests for the nglsettings module (Z/src/nglsettings): the Settings +// getters (integer, float, double, boolean, string, grouped and ungrouped), +// database password derivation, AVS server port, base/temp path logic, and the +// Connections namespace (port lookup with offset, host name resolution and +// connection naming). +// +#include +#include + +#include +#include +#include +#include +#include + +class nglsettings_unit_test : public QObject +{ + Q_OBJECT + +private: + QTemporaryDir m_tmpDir; + QString m_iniPath; + +private slots: + void initTestCase(); + void cleanupTestCase(); + + // --- Settings getters (instance, from a controlled ini file) ---------- + void testGetInteger(); + void testGetIntegerGrouped(); + void testGetFloat(); + void testGetDouble(); + void testGetBoolean(); + void testGetString(); + void testGetIntegerDefaultOnMissing(); + void testGetIntegerDefaultOnInvalid(); + + // --- Database / AVS configuration ------------------------------------- + void testDatabasePasswordDefault(); + void testDatabasePasswordFromUser(); + void testDatabaseDriver(); + void testAvsServerPortDefault(); + void testAvsServerPortOverride(); + + // --- Path logic ------------------------------------------------------- + void testBasePath(); + void testTempPath(); + + // --- Connections namespace -------------------------------------------- + void testConnectionsIniFile(); + void testConnectionsPortOffset(); + void testConnectionsHostName(); + void testConnectionsServerHostname(); + void testConnectionsName(); +}; + +void nglsettings_unit_test::initTestCase() +{ + QVERIFY(m_tmpDir.isValid()); + m_iniPath = m_tmpDir.path() + QStringLiteral("/test.ini"); + QSettings ini(m_iniPath, QSettings::IniFormat); + // Values used by the generic getter tests + ini.setValue(QStringLiteral("test/intValue"), 42); + ini.setValue(QStringLiteral("grp/intValueGrouped"), 100); + ini.setValue(QStringLiteral("test/floatValue"), 3.14f); + ini.setValue(QStringLiteral("test/doubleValue"), 2.71828); + ini.setValue(QStringLiteral("test/boolValue"), true); + ini.setValue(QStringLiteral("test/strValue"), QStringLiteral("hello")); + ini.setValue(QStringLiteral("test/badInt"), QStringLiteral("notanumber")); + // AVS / database values + ini.setValue(QStringLiteral("avs/serverport_ave"), 64321); + ini.setValue(QStringLiteral("db/driver"), QStringLiteral("QODBC")); + ini.sync(); + + // Values used by the Connections tests (port < 100 to exercise the offset + // path; host overrides to exercise the override path) + QSettings conn(m_iniPath, QSettings::IniFormat); + conn.setValue(QStringLiteral("LVS/port"), 50); + conn.setValue(QStringLiteral("PWS_WEBSOCKET/port"), 5701); + conn.setValue(QStringLiteral("AVS_WORKSTATION/host"), QStringLiteral("10.0.0.1")); + conn.sync(); +} + +void nglsettings_unit_test::cleanupTestCase() +{ + qunsetenv("GFS_PATH"); + qunsetenv("TMP_PATH"); +} + +// --- Settings getters ----------------------------------------------------- + +void nglsettings_unit_test::testGetInteger() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.getInteger(QStringLiteral("test/intValue"), 0), 42); +} + +void nglsettings_unit_test::testGetIntegerGrouped() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.getInteger(QStringLiteral("grp"), QStringLiteral("intValueGrouped"), 0), 100); +} + +void nglsettings_unit_test::testGetFloat() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.getFloat(QStringLiteral("test/floatValue"), 0.f), 3.14f); +} + +void nglsettings_unit_test::testGetDouble() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.getDouble(QStringLiteral("test/doubleValue"), 0.0), 2.71828); +} + +void nglsettings_unit_test::testGetBoolean() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.getBoolean(QStringLiteral("test/boolValue"), false), true); +} + +void nglsettings_unit_test::testGetString() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.getString(QStringLiteral("test/strValue"), QStringLiteral("fallback")), + QStringLiteral("hello")); +} + +void nglsettings_unit_test::testGetIntegerDefaultOnMissing() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.getInteger(QStringLiteral("does/not/exist"), 7), 7); +} + +void nglsettings_unit_test::testGetIntegerDefaultOnInvalid() +{ + NGL::Settings settings(m_iniPath); + // "test/badInt" is the string "notanumber", so toInt fails and the + // default value 7 is returned. + QCOMPARE(settings.getInteger(QStringLiteral("test/badInt"), 7), 7); +} + +// --- Database / AVS configuration ----------------------------------------- + +void nglsettings_unit_test::testDatabasePasswordDefault() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.databasePassword(QString()), QStringLiteral("ctc11de")); +} + +void nglsettings_unit_test::testDatabasePasswordFromUser() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.databasePassword(QStringLiteral("ctc_aar")), + QStringLiteral("ctc11de")); + QCOMPARE(settings.databasePassword(QStringLiteral("foobar_x")), + QStringLiteral("foobar11de")); +} + +void nglsettings_unit_test::testDatabaseDriver() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.databaseDriver(), QStringLiteral("QODBC")); +} + +void nglsettings_unit_test::testAvsServerPortDefault() +{ + // Use a clean ini file that does NOT define avs/serverport_ave + const QString cleanPath = m_tmpDir.path() + QStringLiteral("/clean.ini"); + { + QSettings s(cleanPath, QSettings::IniFormat); + s.sync(); + } + NGL::Settings settings(cleanPath); + QCOMPARE(settings.avsServerPortAVE(), static_cast(6350)); +} + +void nglsettings_unit_test::testAvsServerPortOverride() +{ + NGL::Settings settings(m_iniPath); + // m_iniPath has avs/serverport_ave = 64321 + QCOMPARE(settings.avsServerPortAVE(), static_cast(64321)); +} + +// --- Path logic ----------------------------------------------------------- + +void nglsettings_unit_test::testBasePath() +{ + qputenv("GFS_PATH", m_tmpDir.path().toLocal8Bit().constData()); + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.basePath(), m_tmpDir.path() + QStringLiteral("/")); + qunsetenv("GFS_PATH"); +} + +void nglsettings_unit_test::testTempPath() +{ + qputenv("TMP_PATH", m_tmpDir.path().toLocal8Bit().constData()); + NGL::Settings settings(m_iniPath); + QCOMPARE(settings.tempPath(), m_tmpDir.path() + QStringLiteral("/")); + qunsetenv("TMP_PATH"); +} + +// --- Connections namespace ------------------------------------------------ + +void nglsettings_unit_test::testConnectionsIniFile() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(NGL::Connections::iniFile(), m_iniPath); +} + +void nglsettings_unit_test::testConnectionsPortOffset() +{ + NGL::Settings settings(m_iniPath); + NGL::Connections::setPortOffset(9000); + // LVS base port is 94; ini overrides it to 50, which is < 100, so the + // offset is applied: 9000 + 50 = 9050. + QCOMPARE(NGL::Connections::port(NGL::Connections::eConnection::LVS), static_cast(9050)); + // PWS_WEBSOCKET base port is 5701; ini keeps it at 5701, which is >= 100, so + // no offset is applied. + QCOMPARE(NGL::Connections::port(NGL::Connections::eConnection::PWS_WEBSOCKET), static_cast(5701)); +} + +void nglsettings_unit_test::testConnectionsHostName() +{ + NGL::Settings settings(m_iniPath); + QCOMPARE(NGL::Connections::hostName(NGL::Connections::eConnection::AVS_WORKSTATION), + QStringLiteral("10.0.0.1")); + QCOMPARE(NGL::Connections::hostName(NGL::Connections::eConnection::AVS_ROOT), + QStringLiteral("EXCAVS01")); + QCOMPARE(NGL::Connections::hostName(NGL::Connections::eConnection::PWS_WEBSOCKET), + QStringLiteral("EXCTDS01")); +} + +void nglsettings_unit_test::testConnectionsServerHostname() +{ + NGL::Settings settings(m_iniPath); + NGL::Connections::setServerHostname(QStringLiteral("EXCTDS99")); + QCOMPARE(NGL::Connections::hostName(NGL::Connections::eConnection::PWS_WEBSOCKET), + QStringLiteral("EXCTDS99")); + NGL::Connections::setServerHostname(QStringLiteral("EXCTDS01")); +} + +void nglsettings_unit_test::testConnectionsName() +{ + QCOMPARE(NGL::Connections::connectionName(NGL::Connections::eConnection::LVS), + QStringLiteral("LVS")); + QCOMPARE(NGL::Connections::connectionName(NGL::Connections::eConnection::AVS_ROOT), + QStringLiteral("AVS_ROOT")); +} + +QTEST_MAIN(nglsettings_unit_test) +#include "nglsettings_unit_test.moc" diff --git a/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/CMakeLists.txt index 4d738de44..1607a00f2 100755 --- a/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/CMakeLists.txt +++ b/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/CMakeLists.txt @@ -20,3 +20,8 @@ NGL_TARGET_INCLUDE_DIRECTORIES(ngltranslator ${CMAKE_CURRENT_SOURCE_DIR}/.. ) NGL_INSTALL_TARGET(ngltranslator) + +# +# Unit tests +# +ADD_SUBDIRECTORY(unit-test) diff --git a/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/unit-test/CMakeLists.txt b/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/unit-test/CMakeLists.txt new file mode 100644 index 000000000..65286e1e4 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/unit-test/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# Unit tests for the ngltranslator module. +# One test executable per module, each registered with ctest. +# +if (NOT BUILD_TESTING) + return() +endif() + +# --- ngltranslator: translation catalog, plural/disambiguation and locale. +# --- The Translator class (ngltranslator_p.h) has no export macro and the +# --- tree builds with hidden visibility, so its symbols are not exported +# --- from libngltranslator - the module source is compiled directly into the +# --- test (same pattern as the LogView test in TetraIF/unit-test). +if (TARGET ngltranslator) + ADD_EXECUTABLE(ngltranslator_unit_test + ngltranslator_unit_test.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../ngltranslator.cpp) + TARGET_LINK_LIBRARIES(ngltranslator_unit_test + ngltranslator + Qt6::Core + Qt6::Test + ) + ADD_TEST(NAME ngltranslator_unit_test COMMAND $) +endif() diff --git a/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/unit-test/ngltranslator_unit_test.cpp b/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/unit-test/ngltranslator_unit_test.cpp new file mode 100644 index 000000000..4093d9dc5 --- /dev/null +++ b/qt6-after/qt6-migrated-legatus/Z/src/ngltranslator/unit-test/ngltranslator_unit_test.cpp @@ -0,0 +1,310 @@ +// +// Unit tests for the ngltranslator module (Z/src/ngltranslator): the Translator +// (QTranslator subclass) dynamic-translation catalog, JSON catalog parsing with +// md5 caching and compressed data, and locale handling via the public free +// functions. +// +// NOTE: a QCoreApplication is created in main() because the free functions in +// ngltranslator.cpp drive the global TranslationData instance, whose constructor +// calls QCoreApplication::installTranslator. +// +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +class ngltranslator_unit_test : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void cleanupTestCase(); + + // --- Translator instance: catalog + translate (pure logic) ------------- + void testSetTranslationCatalogAndTranslate(); + void testTranslateMissingContext(); + void testTranslatePluralForm(); + void testTranslateDisambiguation(); + + // --- Translator instance: JSON catalog parsing ------------------------- + void testSetTranslationDataJsonValid(); + void testSetTranslationDataJsonInvalid(); + void testSetTranslationDataJsonMd5Cache(); + void testSetTranslationDataJsonCompressed(); + + // --- Free functions: locale -------------------------------------------- + void testTranslationLocaleInitial(); + void testSetTranslationLocale(); + void testSetTranslationLocaleShortCircuit(); +}; + +void ngltranslator_unit_test::initTestCase() +{ + // Nothing global to set up; each test uses a local NGL::Translator instance + // or the documented global-state behaviour of the free functions. +} + +void ngltranslator_unit_test::cleanupTestCase() +{ +} + +// --- Translator instance: catalog + translate ----------------------------- + +void ngltranslator_unit_test::testSetTranslationCatalogAndTranslate() +{ + NGL::Translator t; + NGL::TranslationCatalog catalog; + + NGL::TranslationEntry e1; + e1.isoCode = "de_DE"; + e1.context = "Main"; + e1.key = "Hello"; + e1.disambiguation = ""; + e1.translation = QStringLiteral("Hallo"); + catalog.append(e1); + + NGL::TranslationEntry e2; + e2.isoCode = "de_DE"; + e2.context = "Main"; + e2.key = "Bye"; + e2.disambiguation = ""; + e2.translation = QStringLiteral("Tschüss"); + catalog.append(e2); + + NGL::TranslationEntry e3; + e3.isoCode = "fr_FR"; + e3.context = "Main"; + e3.key = "Bonjour"; + e3.disambiguation = ""; + e3.translation = QStringLiteral("Bon jour"); + catalog.append(e3); + + t.setTranslationCatalog(catalog); + t.isoCodeChanged("de_DE"); + + QCOMPARE(t.translate("Main", "Hello"), QStringLiteral("Hallo")); + QCOMPARE(t.translate("Main", "Bye"), QStringLiteral("Tschüss")); + // fr_FR entry is not in the de_DE lookup + QCOMPARE(t.translate("Main", "Bonjour"), QString()); +} + +void ngltranslator_unit_test::testTranslateMissingContext() +{ + NGL::Translator t; + NGL::TranslationCatalog catalog; + NGL::TranslationEntry e; + e.isoCode = "de_DE"; + e.context = "Main"; + e.key = "Hello"; + e.disambiguation = ""; + e.translation = QStringLiteral("Hallo"); + catalog.append(e); + t.setTranslationCatalog(catalog); + t.isoCodeChanged("de_DE"); + + QCOMPARE(t.translate("Nonexistent", "Hello"), QString()); +} + +void ngltranslator_unit_test::testTranslatePluralForm() +{ + NGL::Translator t; + NGL::TranslationCatalog catalog; + NGL::TranslationEntry e; + e.isoCode = "de_DE"; + e.context = "Main"; + e.key = "Hello"; + e.disambiguation = ""; + e.translation = QStringLiteral("Hallo"); + catalog.append(e); + t.setTranslationCatalog(catalog); + t.isoCodeChanged("de_DE"); + + // n != -1 means plural form, which the dynamic translator does not support + QCOMPARE(t.translate("Main", "Hello", nullptr, 2), QString()); + QCOMPARE(t.translate("Main", "Hello", nullptr, -1), QStringLiteral("Hallo")); +} + +void ngltranslator_unit_test::testTranslateDisambiguation() +{ + NGL::Translator t; + NGL::TranslationCatalog catalog; + + NGL::TranslationEntry e1; + e1.isoCode = "de_DE"; + e1.context = "Main"; + e1.key = "Item"; + e1.disambiguation = "first"; + e1.translation = QStringLiteral("Erstes"); + catalog.append(e1); + + NGL::TranslationEntry e2; + e2.isoCode = "de_DE"; + e2.context = "Main"; + e2.key = "Item"; + e2.disambiguation = "second"; + e2.translation = QStringLiteral("Zweites"); + catalog.append(e2); + + t.setTranslationCatalog(catalog); + t.isoCodeChanged("de_DE"); + + QCOMPARE(t.translate("Main", "Item", "first"), QStringLiteral("Erstes")); + QCOMPARE(t.translate("Main", "Item", "second"), QStringLiteral("Zweites")); + QCOMPARE(t.translate("Main", "Item", nullptr), QString()); +} + +// --- Translator instance: JSON catalog parsing ---------------------------- + +void ngltranslator_unit_test::testSetTranslationDataJsonValid() +{ + NGL::Translator t; + + QJsonObject entry1; + entry1[QLatin1String("isoCode")] = QLatin1String("de_DE"); + entry1[QLatin1String("context")] = QLatin1String("Main"); + entry1[QLatin1String("key")] = QLatin1String("Hello"); + entry1[QLatin1String("disambiguation")] = QLatin1String(""); + entry1[QLatin1String("translation")] = QLatin1String("Hallo"); + + QJsonObject entry2; + entry2[QLatin1String("isoCode")] = QLatin1String("de_DE"); + entry2[QLatin1String("context")] = QLatin1String("Main"); + entry2[QLatin1String("key")] = QLatin1String("Bye"); + entry2[QLatin1String("disambiguation")] = QLatin1String(""); + entry2[QLatin1String("translation")] = QStringLiteral("Tschüss"); + + QJsonArray arr; + arr.append(entry1); + arr.append(entry2); + + QJsonObject root; + root[QLatin1String("translations")] = arr; + + const QByteArray jsonData = QJsonDocument(root).toJson(QJsonDocument::Compact); + + // Successful parse returns false (see ngltranslator.cpp); the catalog is + // populated regardless. + QCOMPARE(t.setTranslationDataJson(jsonData, "md5_1", false), false); + + t.isoCodeChanged("de_DE"); + QCOMPARE(t.translate("Main", "Hello"), QStringLiteral("Hallo")); + QCOMPARE(t.translate("Main", "Bye"), QStringLiteral("Tschüss")); +} + +void ngltranslator_unit_test::testSetTranslationDataJsonInvalid() +{ + NGL::Translator t; + + // Case a: empty JSON data + QCOMPARE(t.setTranslationDataJson(QByteArray(), "md5_a", false), false); + + // Case b: JSON without "translations" key + QJsonObject noKey; + noKey[QLatin1String("foo")] = QLatin1String("bar"); + QCOMPARE(t.setTranslationDataJson(QJsonDocument(noKey).toJson(QJsonDocument::Compact), + "md5_b", false), false); + + // Case c: "translations" is not an array + QJsonObject badType; + badType[QLatin1String("translations")] = QLatin1String("notanarray"); + QCOMPARE(t.setTranslationDataJson(QJsonDocument(badType).toJson(QJsonDocument::Compact), + "md5_c", false), false); + + // Catalog remains empty in all invalid cases + t.isoCodeChanged("de_DE"); + QCOMPARE(t.translate("Main", "Hello"), QString()); +} + +void ngltranslator_unit_test::testSetTranslationDataJsonMd5Cache() +{ + NGL::Translator t; + + QJsonObject entry; + entry[QLatin1String("isoCode")] = QLatin1String("de_DE"); + entry[QLatin1String("context")] = QLatin1String("Main"); + entry[QLatin1String("key")] = QLatin1String("Hello"); + entry[QLatin1String("disambiguation")] = QLatin1String(""); + entry[QLatin1String("translation")] = QLatin1String("Hallo"); + + QJsonArray arr; + arr.append(entry); + QJsonObject root; + root[QLatin1String("translations")] = arr; + const QByteArray jsonData = QJsonDocument(root).toJson(QJsonDocument::Compact); + + // First call: new md5, successful parse, returns false + QCOMPARE(t.setTranslationDataJson(jsonData, "md5_X", false), false); + // Second call: same md5, cache hit, returns true + QCOMPARE(t.setTranslationDataJson(jsonData, "md5_X", false), true); + // Third call: different md5, re-parses, returns false + QCOMPARE(t.setTranslationDataJson(jsonData, "md5_Y", false), false); +} + +void ngltranslator_unit_test::testSetTranslationDataJsonCompressed() +{ + NGL::Translator t; + + QJsonObject entry; + entry[QLatin1String("isoCode")] = QLatin1String("de_DE"); + entry[QLatin1String("context")] = QLatin1String("Main"); + entry[QLatin1String("key")] = QLatin1String("Hello"); + entry[QLatin1String("disambiguation")] = QLatin1String(""); + entry[QLatin1String("translation")] = QLatin1String("Hallo"); + + QJsonArray arr; + arr.append(entry); + QJsonObject root; + root[QLatin1String("translations")] = arr; + + const QByteArray raw = QJsonDocument(root).toJson(QJsonDocument::Compact); + const QByteArray compressed = qCompress(raw, 9); + + QCOMPARE(t.setTranslationDataJson(compressed, "md5_Z", true), false); + + t.isoCodeChanged("de_DE"); + QCOMPARE(t.translate("Main", "Hello"), QStringLiteral("Hallo")); +} + +// --- Free functions: locale ------------------------------------------------ + +void ngltranslator_unit_test::testTranslationLocaleInitial() +{ + // At the start of the process the global locale is empty + QCOMPARE(NGL::translationLocale(), QByteArray()); +} + +void ngltranslator_unit_test::testSetTranslationLocale() +{ + // No .qm translation files exist in the test environment, so ltsTranslator + // stays empty and the function returns false. But the locale is recorded. + const bool ret = NGL::setTranslationLocale("de_DE"); + Q_UNUSED(ret); + QCOMPARE(NGL::translationLocale(), QByteArray("de_DE")); +} + +void ngltranslator_unit_test::testSetTranslationLocaleShortCircuit() +{ + // First call sets the locale (may return false if no .qm files found) + NGL::setTranslationLocale("de_DE", QStringLiteral("tds")); + // Second call with the same isoCode and prefix short-circuits and returns true + QCOMPARE(NGL::setTranslationLocale("de_DE", QStringLiteral("tds")), true); + // Different prefix does not short-circuit + QCOMPARE(NGL::setTranslationLocale("de_DE", QStringLiteral("other")), false); + QCOMPARE(NGL::translationLocale(), QByteArray("de_DE")); +} + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + ngltranslator_unit_test tc; + return QTest::qExec(&tc, argc, argv); +} + +#include "ngltranslator_unit_test.moc"