Add per-module unit-test dirs for the Release 2 scope
One unit-test/ directory per module/target, mirroring the existing TetraIF/unit-test/ layout (one QtTest executable per module, guarded by BUILD_TESTING and the module target, each registered with ctest): Lts/Common/CodeGen codegen_unit_test Lts/Common/LtsCoordinate ltscoordinate_unit_test Lts/Common/LtsSession ltssession_unit_test Lts/DDS/Core ddscore_unit_test Lts/DDS/QosProfile ddsqos_unit_test Lts/Tools/LogViewer logviewer_unit_test Lts/Tools/SqlBrowser sqlbrowser_unit_test Z/src/NGLLogger ngllogger_unit_test Z/src/WMS_Interface wms_json_ds_unit_test Z/src/nglapplication nglcoreapplication_unit_test Z/src/nglcompression nglcompression_unit_test Z/src/nglsettings nglsettings_unit_test Z/src/ngltranslator ngltranslator_unit_test Where a module is built with hidden visibility and has no export macro (LogViewer, SqlBrowser, ngltranslator, NGLLogger, nglcompression, nglsettings), its sources are compiled directly into the test executable, the same pattern as prtcnv_unit_test. ctest baseline in the container (QT_QPA_PLATFORM=offscreen): 20/23 passed (13 new + 7 pre-existing in-scope tests), 3 Not Run as before (nglcomp_test, GrantleeParserSmoke, utils_test). README: ctest baseline table extended to the 23 registered tests and section 2.4 now documents the per-module test layout. ddsview has no unit test (GUI tool, XmlHelper needs the DDS dynamic-xtypes runtime).
This commit is contained in:
1 parent
a62b729577
commit
09b6df8bc3
40 files changed
+4966
-17
No files matched your search
@@ -170,3 +170,8 @@ endif()
|
|||||||
ELSE()
|
ELSE()
|
||||||
message(STATUS "BUILD_GRANTLEE disabled or Grantlee not found; skipping codegen_grantlee, ngl_codegen, and codegen_gui targets")
|
message(STATUS "BUILD_GRANTLEE disabled or Grantlee not found; skipping codegen_grantlee, ngl_codegen, and codegen_gui targets")
|
||||||
ENDIF()
|
ENDIF()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:codegen_unit_test>)
|
||||||
|
endif()
|
||||||
@@ -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 <CodeGen/GrantleeWrappers.h>
|
||||||
|
#include <CodeGen/SqlGenerator.h>
|
||||||
|
#include <CodeGen/StructBuilder.h>
|
||||||
|
|
||||||
|
#include <QtCore/QObject>
|
||||||
|
#include <QtCore/QSharedPointer>
|
||||||
|
#include <QtCore/QString>
|
||||||
|
#include <QtCore/QVariant>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
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<int>(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<dds::core::optional<MyStruct>>(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<MemberInformation> 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<MemberInformation>(), &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<DataTypeInfo> 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<IdlDataWrapper*>()->name(), QStringLiteral("id"));
|
||||||
|
|
||||||
|
IdlData trData;
|
||||||
|
trData.name = QStringLiteral("tr1");
|
||||||
|
trData.dataTypeInfo = intType;
|
||||||
|
QVector<IdlData> 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<DataTypeInfo> podType(new TestDataTypeInfo(QStringLiteral("int"), QStringLiteral("int"), true));
|
||||||
|
QSharedPointer<DataTypeInfo> 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<IdlDataWrapper*>()->name(), QStringLiteral("a"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void codegen_unit_test::dbStructWrapper()
|
||||||
|
{
|
||||||
|
QObject parent;
|
||||||
|
|
||||||
|
QSharedPointer<TableInformation> tableA(new TableInformation());
|
||||||
|
tableA->name = QStringLiteral("tbl_a");
|
||||||
|
QSharedPointer<TableInformation> 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<ForeignKeyInformation>(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*>();
|
||||||
|
TableWrapper* twB = structures.value(1).value<TableWrapper*>();
|
||||||
|
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<TableWrapper*>();
|
||||||
|
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<DataTypeInfo>(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<TableInformation> ti(new TableInformation());
|
||||||
|
ti->name = QStringLiteral("t");
|
||||||
|
csi.primaryTable = ti;
|
||||||
|
QCOMPARE(csi.isEmpty(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(codegen_unit_test)
|
||||||
|
#include "codegen_unit_test.moc"
|
||||||
@@ -28,3 +28,8 @@ NGL_TARGET_LINK_LIBRARIES(LtsCoordinate
|
|||||||
)
|
)
|
||||||
|
|
||||||
ADD_SUBDIRECTORY(UnitTest_Coordinate)
|
ADD_SUBDIRECTORY(UnitTest_Coordinate)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:ltscoordinate_unit_test>)
|
||||||
|
endif()
|
||||||
+314
@@ -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 <LtsCoordinate/lts_coordinate.h>
|
||||||
|
#include <LtsCoordinate/lts_coord_vector.h>
|
||||||
|
#include <LtsCoordTrans/lts_coord_trans.h>
|
||||||
|
|
||||||
|
#include <Core/core.hpp>
|
||||||
|
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
#include <QtCore/QPointF>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<Core::GeoPosition> 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<UtmCoordinate> utmStd = vec.toStdUtmCoord(32);
|
||||||
|
QCOMPARE(utmStd.size(), 3u);
|
||||||
|
for (const auto& u : utmStd)
|
||||||
|
QCOMPARE(u.zone, 32);
|
||||||
|
|
||||||
|
const QVector<UtmCoordinate> utmQv = vec.toQVectorUtmCoord(32);
|
||||||
|
QCOMPARE(utmQv.size(), 3);
|
||||||
|
QCOMPARE(utmQv[0].x, utmStd[0].x);
|
||||||
|
|
||||||
|
const QVector<QPointF> 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<QPointF> again = vec.toQVectorQPointF(32);
|
||||||
|
QCOMPARE(again, pts);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(ltscoordinate_unit_test)
|
||||||
|
#include "ltscoordinate_unit_test.moc"
|
||||||
@@ -156,3 +156,8 @@ IF(BUILD_RTI)
|
|||||||
)
|
)
|
||||||
TARGET_COMPILE_DEFINITIONS(LtsSessionData PUBLIC BUILD_RTI)
|
TARGET_COMPILE_DEFINITIONS(LtsSessionData PUBLIC BUILD_RTI)
|
||||||
ENDIF()
|
ENDIF()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:ltssession_unit_test>)
|
||||||
|
endif()
|
||||||
+321
@@ -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 <LtsSession/lts_session_data.h>
|
||||||
|
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
#include <QtGui/QPixmap>
|
||||||
|
#include <QtCore/QSet>
|
||||||
|
#include <QtCore/QUrl>
|
||||||
|
|
||||||
|
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<int>{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<int>{-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<int>{1});
|
||||||
|
data->setShowTerminalWindow(true);
|
||||||
|
PartiesByExercise parties;
|
||||||
|
parties.exerciseId = 1;
|
||||||
|
parties.visibleParties = QVector<int>{10};
|
||||||
|
data->setVisibleParties(QVector<PartiesByExercise>{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<int>{10, 11};
|
||||||
|
PartiesByExercise exercise2;
|
||||||
|
exercise2.exerciseId = 2;
|
||||||
|
exercise2.visibleParties = QVector<int>{20};
|
||||||
|
data->setVisibleParties(QVector<PartiesByExercise>{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<int> expectedParties1{10, 11};
|
||||||
|
QCOMPARE(data->getVisibleParties(1), expectedParties1);
|
||||||
|
QCOMPARE(data->getVisibleParties(2), QVector<int>{20});
|
||||||
|
QVERIFY(data->getVisibleParties(99).isEmpty());
|
||||||
|
|
||||||
|
const auto& all = data->getVisibleParties();
|
||||||
|
QCOMPARE(static_cast<int>(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<int>(data->debugFlags().size()), 1);
|
||||||
|
|
||||||
|
data->setDebugFlagValue(QStringLiteral("other"), 7);
|
||||||
|
QCOMPARE(static_cast<int>(data->debugFlags().size()), 2);
|
||||||
|
|
||||||
|
data->removeDebugFlag(QStringLiteral("flag"));
|
||||||
|
QVERIFY(!data->hasDebugFlag(QStringLiteral("flag")));
|
||||||
|
QVERIFY(data->debugFlagValue(QStringLiteral("flag")).isNull());
|
||||||
|
QCOMPARE(static_cast<int>(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<int>{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"
|
||||||
@@ -152,3 +152,8 @@ NGL_INSTALL_TARGET(svcCoreClient)
|
|||||||
CREATE_TOPIC_TYPE_PLUGIN(${xml_FILE})
|
CREATE_TOPIC_TYPE_PLUGIN(${xml_FILE})
|
||||||
|
|
||||||
ENDIF(BUILD_RTI)
|
ENDIF(BUILD_RTI)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:ddscore_unit_test>)
|
||||||
|
endif()
|
||||||
@@ -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 <QtCore/QCoreApplication>
|
||||||
|
#include <QtCore/QString>
|
||||||
|
|
||||||
|
#include <Core/function_reply_object.h>
|
||||||
|
#include <Core/logger.h>
|
||||||
|
|
||||||
|
#include <NGLLogger/NGLLoggerBootstrap.h>
|
||||||
|
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<const char *>(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<std::string>{"a", "b"});
|
||||||
|
QCOMPARE(reply.id(), 5);
|
||||||
|
QCOMPARE(reply.str(), std::string("%1/%2"));
|
||||||
|
const std::vector<std::string> 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<std::string>{"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<std::string>{"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<std::string> 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<std::string>{"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"
|
||||||
@@ -37,3 +37,8 @@ NGL_TARGET_LINK_LIBRARIES(ddsQoS
|
|||||||
)
|
)
|
||||||
|
|
||||||
ENDIF(BUILD_RTI)
|
ENDIF(BUILD_RTI)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:ddsqos_unit_test>)
|
||||||
|
endif()
|
||||||
@@ -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 <QtCore/QFile>
|
||||||
|
#include <QtCore/QString>
|
||||||
|
#include <QtCore/QUrl>
|
||||||
|
|
||||||
|
#include <QosProfile/DDS_DefaultQos.h>
|
||||||
|
|
||||||
|
#include <NGLLogger/INGLLogger.h>
|
||||||
|
#include <NGLLogger/NGLLoggerBootstrap.h>
|
||||||
|
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -64,3 +64,8 @@ if(QT6_GUI_AVAILABLE)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
NGL_INSTALL_TARGET(LogViewer)
|
NGL_INSTALL_TARGET(LogViewer)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:logviewer_unit_test>)
|
||||||
|
endif()
|
||||||
@@ -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 <NGLLogger/NGLLoggerBootstrap.h>
|
||||||
|
|
||||||
|
#include <QtCore/QDateTime>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
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<LogInformation> 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<LogInformation>());
|
||||||
|
QCOMPARE(model.rowCount(QModelIndex()), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logviewer_unit_test::tableModelInvalidIndex()
|
||||||
|
{
|
||||||
|
LogViewTableModel model;
|
||||||
|
QVector<LogInformation> 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<LogInformation> 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<QColor>(), Qt::blue);
|
||||||
|
QCOMPARE(model.data(model.index(0, col), Qt::ForegroundRole).value<QColor>(), Qt::white);
|
||||||
|
QCOMPARE(model.data(model.index(1, col), Qt::BackgroundRole).value<QColor>(), Qt::yellow);
|
||||||
|
QCOMPARE(model.data(model.index(1, col), Qt::ForegroundRole).value<QColor>(), Qt::black);
|
||||||
|
QCOMPARE(model.data(model.index(2, col), Qt::BackgroundRole).value<QColor>(), Qt::darkRed);
|
||||||
|
QCOMPARE(model.data(model.index(2, col), Qt::ForegroundRole).value<QColor>(), Qt::white);
|
||||||
|
QCOMPARE(model.data(model.index(3, col), Qt::BackgroundRole).value<QColor>(), Qt::red);
|
||||||
|
QCOMPARE(model.data(model.index(3, col), Qt::ForegroundRole).value<QColor>(), Qt::black);
|
||||||
|
QCOMPARE(model.data(model.index(4, col), Qt::BackgroundRole).value<QColor>(), Qt::green);
|
||||||
|
QCOMPARE(model.data(model.index(4, col), Qt::ForegroundRole).value<QColor>(), Qt::black);
|
||||||
|
QCOMPARE(model.data(model.index(5, col), Qt::BackgroundRole).value<QColor>(), Qt::lightGray);
|
||||||
|
QCOMPARE(model.data(model.index(5, col), Qt::ForegroundRole).value<QColor>(), 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<LogInformation> 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<QColor>(), Qt::red);
|
||||||
|
// unknown level: no effect
|
||||||
|
model.adjustCellColor(Qt::green, QStringLiteral("NOPE"));
|
||||||
|
QCOMPARE(model.data(model.index(1, col), Qt::BackgroundRole).value<QColor>(), Qt::yellow);
|
||||||
|
|
||||||
|
// adjustFontColor
|
||||||
|
model.adjustFontColor(Qt::black, QStringLiteral("INFO"));
|
||||||
|
QCOMPARE(model.data(model.index(0, col), Qt::ForegroundRole).value<QColor>(), 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<LogInformation> 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<LogInformation> 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<LogInformation> 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<LogInformation> 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<bool> 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<LogInformation> 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<bool> 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<LogInformation> 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<bool> activated = proxy.activatedStringFilter();
|
||||||
|
for (const bool active : activated)
|
||||||
|
QVERIFY(!active);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logviewer_unit_test::proxySortByDate()
|
||||||
|
{
|
||||||
|
LogViewTableModel source;
|
||||||
|
QVector<LogInformation> 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<LogInformation> 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<LogInformation> 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<bool> lastFilter;
|
||||||
|
int calls = 0;
|
||||||
|
QObject::connect(&proxy, &SortFilterModel::theseFilterAreActivated,
|
||||||
|
[&lastFilter, &calls](const QVector<bool> &filter) {
|
||||||
|
lastFilter = filter;
|
||||||
|
++calls;
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy.figureFilterOut();
|
||||||
|
QCOMPARE(calls, 1);
|
||||||
|
QCOMPARE(lastFilter, QVector<bool>({false, false, false, false, false}));
|
||||||
|
|
||||||
|
proxy.setStringFilter(LogViewTableModel::colLevel, QStringLiteral("INFO"));
|
||||||
|
proxy.figureFilterOut();
|
||||||
|
QCOMPARE(calls, 2);
|
||||||
|
QCOMPARE(lastFilter, QVector<bool>({false, true, false, false, false}));
|
||||||
|
}
|
||||||
|
|
||||||
|
QTEST_MAIN(logviewer_unit_test)
|
||||||
|
#include "logviewer_unit_test.moc"
|
||||||
@@ -61,3 +61,8 @@ if(QT6_GUI_AVAILABLE)
|
|||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
NGL_INSTALL_TARGET(sqlbrowser)
|
NGL_INSTALL_TARGET(sqlbrowser)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:sqlbrowser_unit_test>)
|
||||||
|
endif()
|
||||||
+410
@@ -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 <QtCore/QMetaType>
|
||||||
|
#include <QtCore/QPair>
|
||||||
|
#include <QtCore/QRegularExpression>
|
||||||
|
#include <QtGui/QColor>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
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<qint64>(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<qint64>(1700000000000LL);
|
||||||
|
Data row1;
|
||||||
|
row1 << 2 << QVariant() << static_cast<qint64>(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>(), 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<qint64>(1700000000000);
|
||||||
|
Data row1;
|
||||||
|
row1 << 2 << QStringLiteral("beta") << static_cast<qint64>(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<DataModel *>(&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<QModelIndex> 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<QModelIndex>());
|
||||||
|
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<QString, QMetaType::Type>(QStringLiteral("id"), QMetaType::Int));
|
||||||
|
headers.append(QPair<QString, QMetaType::Type>(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<QString, QMetaType::Type>(QStringLiteral("id"), QMetaType::Int));
|
||||||
|
headers.append(QPair<QString, QMetaType::Type>(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<QString, QMetaType::Type>(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<QString, QMetaType::Type>(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<QString, QMetaType::Type>(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"
|
||||||
@@ -129,17 +129,33 @@ docker run --rm -it \
|
|||||||
|
|
||||||
The driver runs `ctest --output-on-failure -j$(nproc)` (with
|
The driver runs `ctest --output-on-failure -j$(nproc)` (with
|
||||||
`QT_QPA_PLATFORM=offscreen` — the container is headless) after the compile.
|
`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 |
|
| # | Test | Module | Result |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | `coprtcnv_unit_test` | Passed |
|
| 1 | `coprtcnv_unit_test` | TetraIF (CoPrtCnv) | Passed |
|
||||||
| 2 | `logview_unit_test` | Passed |
|
| 2 | `logview_unit_test` | TetraIF (LogView) | Passed |
|
||||||
| 3 | `prtcnv_unit_test` | Passed |
|
| 3 | `prtcnv_unit_test` | TetraIF (PrtCnv) | Passed |
|
||||||
| 4 | `NGLToolsQtSmoke` | Passed |
|
| 4 | `wms_json_ds_unit_test` | WMS_Interface | Passed |
|
||||||
| 5 | `test_nglpdfwidget` | Passed |
|
| 5 | `nglcoreapplication_unit_test` | nglapplication | Passed |
|
||||||
| 6 | `lts_coord_trans_unit_test` | Passed |
|
| 6 | `nglcomp_test` | nglcompression | Not Run |
|
||||||
| 7 | `aes_crypt_test` | Passed |
|
| 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
|
A few tests registered by in-scope CMakeLists are not part of the 46
|
||||||
built targets and show up as `***Not Run` (`nglcomp_test`,
|
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)
|
B=$(ls -dt build4_* | head -1)
|
||||||
docker run --rm -v "$MIGRATE_PIPELINE_ROOT":/repo <image> \
|
docker run --rm -v "$MIGRATE_PIPELINE_ROOT":/repo <image> \
|
||||||
bash -c "cd /repo/qt-migration-legatus/qt6-after/qt6-migrated-legatus/${B} \
|
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
|
### 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,
|
Each QtTest binary accepts a single slot name to run just one test,
|
||||||
e.g. `./bin/coprtcnv_unit_test radioTelegramFragmentationRoundtrip`.
|
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` |
|
| `coprtcnv_unit_test` | TetraIF | `CoPrtCnv` — telegram header fields, payload access, length checks, fragmentation/reassembly roundtrip, id→string helpers |
|
||||||
| `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) |
|
| `logview_unit_test` | TetraIF | `LogView` — `IntegerCalculator` (arithmetic, precedence, error codes, variables) |
|
||||||
| `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) |
|
| `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
|
## Step 3 — Produce the Release 2 DEB packages
|
||||||
|
|
||||||
|
|||||||
@@ -57,5 +57,9 @@ ELSE()
|
|||||||
MESSAGE(STATUS "Log4Qt not found; skipping test_ngllogger")
|
MESSAGE(STATUS "Log4Qt not found; skipping test_ngllogger")
|
||||||
ENDIF()
|
ENDIF()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
|
|
||||||
CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/log4cxx.props ${CMAKE_BINARY_DIR}/log4cxx.props COPYONLY)
|
CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/log4cxx.props ${CMAKE_BINARY_DIR}/log4cxx.props COPYONLY)
|
||||||
@@ -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 $<TARGET_FILE:ngllogger_unit_test>)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#include <NGLLogger/NGLLoggerBootstrap.h>
|
||||||
|
#include <NGLLogger/NGLLogger.h>
|
||||||
|
#include <NGLLogger/MurmurHash2.h>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
#include <QtTest/QTestEventLoop>
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -33,3 +33,8 @@ NGL_TARGET_LINK_LIBRARIES(wms_json_ds
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:wms_json_ds_unit_test>)
|
||||||
|
endif()
|
||||||
+598
@@ -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 <WMS_Interface/IWMS2ECC.h>
|
||||||
|
#include <WMS_Interface/json_keys.h>
|
||||||
|
#include <WMS_Interface/wms_ecc_equipRequirements.h>
|
||||||
|
#include <WMS_Interface/wms_ecc_equipmenttypes.h>
|
||||||
|
#include <WMS_Interface/wms_ecc_exercises.h>
|
||||||
|
#include <WMS_Interface/wms_ecc_playerInfo.h>
|
||||||
|
#include <WMS_Interface/wms_ecc_registration.h>
|
||||||
|
#include <WMS_Interface/wms_ecc_unregister.h>
|
||||||
|
|
||||||
|
#include <QtCore/QByteArray>
|
||||||
|
#include <QtCore/QJsonArray>
|
||||||
|
#include <QtCore/QJsonDocument>
|
||||||
|
#include <QtCore/QJsonObject>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
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<int>(doc[QLatin1String("etMap")].toArray().size()), 2);
|
||||||
|
|
||||||
|
const EquipmentTypes back = EquipmentTypes::fromJsonAll(doc.object());
|
||||||
|
QVERIFY(back.getOk());
|
||||||
|
QCOMPARE(static_cast<int>(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<int>(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<int>(doc[QLatin1String("exMap")].toArray().size()), 2);
|
||||||
|
|
||||||
|
const EXCON::Exercises back = EXCON::Exercises::fromJsonAll(doc.object());
|
||||||
|
QVERIFY(back.getOk());
|
||||||
|
QCOMPARE(static_cast<int>(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<int>(v[QLatin1String("exeMap")].toMap().size()), 1);
|
||||||
|
|
||||||
|
const EXCON::Exercises back = EXCON::Exercises::fromVariantMapAll(v);
|
||||||
|
QVERIFY(back.getOk());
|
||||||
|
QCOMPARE(static_cast<int>(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<const char *>(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<int>(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<int, IWMS2ECC::PlayerInfo> 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<int>(doc[QLatin1String("piMap")].toArray().size()), 2);
|
||||||
|
|
||||||
|
const PlayerInfo back = PlayerInfo::fromJsonAll(doc.object());
|
||||||
|
QVERIFY(back.getOk());
|
||||||
|
QCOMPARE(static_cast<int>(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<int, IWMS2ECC::PlayerInfo> 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<int>(v[QLatin1String("plMap")].toMap().size()), 2);
|
||||||
|
|
||||||
|
const PlayerInfo back = PlayerInfo::fromVariantMapAll(v);
|
||||||
|
QCOMPARE(static_cast<int>(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<int, int> 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<int>(obj[QLatin1String("equipMap")].toArray().size()), 3);
|
||||||
|
|
||||||
|
const EquipRequirements back = EquipRequirements::fromJson(obj);
|
||||||
|
QVERIFY(back.getOk());
|
||||||
|
QCOMPARE(static_cast<int>(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<int, int> 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<int>(v[QLatin1String("equipMap")].toMap().size()), 2);
|
||||||
|
|
||||||
|
const EquipRequirements back = EquipRequirements::fromVariantMap(v);
|
||||||
|
QVERIFY(back.getOk());
|
||||||
|
QCOMPARE(static_cast<int>(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"
|
||||||
@@ -102,6 +102,11 @@ NGL_TARGET_LINK_LIBRARIES(nglcoreapplication
|
|||||||
)
|
)
|
||||||
NGL_INSTALL_TARGET(nglcoreapplication)
|
NGL_INSTALL_TARGET(nglcoreapplication)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
|
|
||||||
#
|
#
|
||||||
# nglsinglecoreapplication
|
# nglsinglecoreapplication
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -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 $<TARGET_FILE:nglcoreapplication_unit_test>)
|
||||||
|
endif()
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#include <nglapplication/nglloggerhandler.h>
|
||||||
|
#include <NGLLogger/NGLLoggerBootstrap.h>
|
||||||
|
#include <NGLLogger/NGLLogger.h>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
#include <QtTest/QTestEventLoop>
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -43,3 +43,8 @@ IF(BUILD_WS AND BUILD_TESTING)
|
|||||||
|
|
||||||
ADD_TEST(NAME nglcomp_test COMMAND $<TARGET_FILE:nglcomp_test>)
|
ADD_TEST(NAME nglcomp_test COMMAND $<TARGET_FILE:nglcomp_test>)
|
||||||
ENDIF()
|
ENDIF()
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:nglcompression_unit_test>)
|
||||||
|
endif()
|
||||||
+484
@@ -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 <time.h>
|
||||||
|
|
||||||
|
#include <nglcompression/nglcomp_tarfilter.h>
|
||||||
|
|
||||||
|
#include <QtCore/QBuffer>
|
||||||
|
#include <QtCore/QByteArray>
|
||||||
|
#include <QtCore/QFile>
|
||||||
|
#include <QtCore/QTemporaryDir>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// 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<void(TarFilter &)> &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<char>((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<unsigned char>(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<qint64>(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<qint64>(5));
|
||||||
|
QCOMPARE(fiRead.uid, 12345);
|
||||||
|
QCOMPARE(fiRead.gid, 678);
|
||||||
|
QCOMPARE(fiRead.uname, QStringLiteral("testuser"));
|
||||||
|
QCOMPARE(fiRead.gname, QStringLiteral("testgroup"));
|
||||||
|
QCOMPARE(fiRead.mtime, static_cast<time_t>(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<qint64>(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<qint64>(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<char>(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<char>(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<qint64> &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<qint64> values;
|
||||||
|
readAll(1024, values);
|
||||||
|
QCOMPARE(static_cast<int>(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<qint64> clamped;
|
||||||
|
readAll(1, clamped);
|
||||||
|
QCOMPARE(static_cast<int>(clamped.size()), 5);
|
||||||
|
QCOMPARE(clamped.front(), 5000 - 1024);
|
||||||
|
|
||||||
|
// 2048-byte chunks: 2x2048 + 904 -> 3 callbacks.
|
||||||
|
std::vector<qint64> big;
|
||||||
|
readAll(2048, big);
|
||||||
|
QCOMPARE(static_cast<int>(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"
|
||||||
@@ -30,3 +30,8 @@ NGL_TARGET_INCLUDE_DIRECTORIES(nglsettings
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/../
|
${CMAKE_CURRENT_SOURCE_DIR}/../
|
||||||
)
|
)
|
||||||
NGL_INSTALL_TARGET(nglsettings)
|
NGL_INSTALL_TARGET(nglsettings)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:nglsettings_unit_test>)
|
||||||
|
endif()
|
||||||
@@ -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 <nglsettings/nglsettings_settings.h>
|
||||||
|
#include <nglsettings/nglsettings_connections.h>
|
||||||
|
|
||||||
|
#include <QtCore/QByteArray>
|
||||||
|
#include <QtCore/QDir>
|
||||||
|
#include <QtCore/QSettings>
|
||||||
|
#include <QtCore/QTemporaryDir>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
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<quint16>(6350));
|
||||||
|
}
|
||||||
|
|
||||||
|
void nglsettings_unit_test::testAvsServerPortOverride()
|
||||||
|
{
|
||||||
|
NGL::Settings settings(m_iniPath);
|
||||||
|
// m_iniPath has avs/serverport_ave = 64321
|
||||||
|
QCOMPARE(settings.avsServerPortAVE(), static_cast<quint16>(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<unsigned short>(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<unsigned short>(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"
|
||||||
@@ -20,3 +20,8 @@ NGL_TARGET_INCLUDE_DIRECTORIES(ngltranslator
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/..
|
${CMAKE_CURRENT_SOURCE_DIR}/..
|
||||||
)
|
)
|
||||||
NGL_INSTALL_TARGET(ngltranslator)
|
NGL_INSTALL_TARGET(ngltranslator)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Unit tests
|
||||||
|
#
|
||||||
|
ADD_SUBDIRECTORY(unit-test)
|
||||||
@@ -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 $<TARGET_FILE:ngltranslator_unit_test>)
|
||||||
|
endif()
|
||||||
+310
@@ -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 <ngltranslator/ngltranslator.h>
|
||||||
|
#include <ngltranslator/ngltranslator_p.h>
|
||||||
|
|
||||||
|
#include <QtCore/QByteArray>
|
||||||
|
#include <QtCore/QCoreApplication>
|
||||||
|
#include <QtCore/QJsonArray>
|
||||||
|
#include <QtCore/QJsonDocument>
|
||||||
|
#include <QtCore/QJsonObject>
|
||||||
|
#include <QtCore/QVector>
|
||||||
|
#include <QtTest/QTest>
|
||||||
|
|
||||||
|
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"
|
||||||
Reference in new issue
Block a user