/*************************************************************************************************** Copyright (C) 2023 The Qt Company Ltd. SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only ***************************************************************************************************/ #include "foo.h" #include "stringbuilder.h" #include "uri.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __GNUC__ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wconversion" #endif #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __GNUC__ # pragma GCC diagnostic pop #endif #define TESTCASE_DOTNET_MAIN //#define TESTCASE_QT_MAIN #if defined(TESTCASE_DOTNET_MAIN) #define TEST_APP_STARTUP #define TEST_FUNCTION_CALLS #define TEST_APP_SHUTDOWN #define TEST_HOST_UNLOAD #elif defined(TESTCASE_QT_MAIN) #define TEST_HOST_LOAD #define TEST_FUNCTION_CALLS #define TEST_HOST_UNLOAD #endif //#define COREHOST_TRACE class tst_qtdotnet : public QObject { Q_OBJECT public: tst_qtdotnet() = default; private: int refCount = 0; bool skipCleanup = false; private slots: void initTestCase() { #ifdef COREHOST_TRACE qputenv("COREHOST_TRACE", "1"); #endif } void init() { if (!QDotNetAdapter::instance().isValid()) return; refCount = QDotNetAdapter::instance().stats().refCount; } void cleanup() { if (skipCleanup) { skipCleanup = false; QSKIP("cleanup skipped"); } if (!QDotNetAdapter::instance().isValid()) return; QVERIFY(QDotNetAdapter::instance().stats().refCount == refCount); } #ifdef TEST_APP_STARTUP void appStartup(); #endif //TEST_APP_STARTUP #ifdef TEST_HOST_LOAD void loadHost(); void runtimeProperties(); #endif //TEST_HOST_LOAD #ifdef TEST_FUNCTION_CALLS void resolveFunction(); void callFunction(); void callFunctionWithCustomMarshaling(); void callDefaultEntryPoint(); void callWithComplexArg(); #endif //TEST_FUNCTION_CALLS #ifdef TEST_HOST_LOAD void adapterInit(); #endif //TEST_HOST_LOAD #ifdef TEST_FUNCTION_CALLS void callStaticMethod(); void handleException(); void createObject(); void callInstanceMethod(); void useWrapperClassForStringBuilder(); void useWrapperClassForUri(); void emitSignalFromEvent(); void propertyBinding(); void implementInterface(); void arrayOfInts(); void arrayOfStrings(); void arrayOfObjects(); void variantNull(); void variantGet(); void variantSet(); void modelIndexNull(); void modelIndexGet(); void models(); void delegates(); #endif //TEST_FUNCTION_CALLS #ifdef TEST_APP_SHUTDOWN void appShutdown(); #endif //TEST_APP_SHUTDOWN #ifdef TEST_HOST_UNLOAD void unloadHost(); #endif //TEST_HOST_UNLOAD }; QDotNetHost dotNetHost; QThread *dotnetThread = nullptr; #ifdef TEST_APP_STARTUP void tst_qtdotnet::appStartup() { dotnetThread = QThread::create( [this]() { dotNetHost.loadApp( QDir(QCoreApplication::applicationDirPath()).filePath("FooConsoleApp.dll")); dotNetHost.runApp(); }); dotnetThread->start(); bool block = true; while (!dotNetHost.isReady()) QThread::sleep(1); QDotNetAdapter::instance().init( QDir(QCoreApplication::applicationDirPath()).filePath("Qt.DotNet.Adapter.dll"), "Qt.DotNet.Adapter", "Qt.DotNet.Adapter", &dotNetHost); } #endif //TEST_APP_STARTUP #ifdef TEST_APP_STARTUP void tst_qtdotnet::appShutdown() { if (!dotnetThread) QSKIP("App thread not running"); QtDotNet::call("FooConsoleApp.Program, FooConsoleApp", "set_KeepRunning", false); QThread::sleep(1); while (dotnetThread->isRunning()) { qInfo() << "App thread still running..."; QThread::sleep(1); } } #endif //TEST_APP_STARTUP #ifdef TEST_HOST_LOAD void tst_qtdotnet::loadHost() { QVERIFY(!dotNetHost.isLoaded()); QVERIFY(dotNetHost.load()); QVERIFY(dotNetHost.isLoaded()); } void tst_qtdotnet::runtimeProperties() { QVERIFY(dotNetHost.isLoaded()); QMap runtimeProperties = dotNetHost.runtimeProperties(); QVERIFY(!runtimeProperties.isEmpty()); for (auto prop = runtimeProperties.constBegin(); prop != runtimeProperties.constEnd(); ++prop) { qInfo() << prop.key() << "=" << QString("%1%2") .arg(prop.value().left(100)).arg(prop.value().length() > 100 ? "..." : ""); } } void tst_qtdotnet::adapterInit() { QVERIFY(!QDotNetAdapter::instance().isValid()); QDotNetAdapter::instance().init( QDir(QCoreApplication::applicationDirPath()).filePath("Qt.DotNet.Adapter.dll"), "Qt.DotNet.Adapter", "Qt.DotNet.Adapter", &dotNetHost); QVERIFY(QDotNetAdapter::instance().isValid()); } #endif //TEST_HOST_LOAD #ifdef TEST_FUNCTION_CALLS QDotNetFunction formatNumber; void tst_qtdotnet::resolveFunction() { QVERIFY(dotNetHost.isLoaded()); QVERIFY(!formatNumber.isValid()); QVERIFY(dotNetHost.resolveFunction(formatNumber, QDir(QCoreApplication::applicationDirPath()).filePath("FooLib.dll"), Foo::AssemblyQualifiedName, "FormatNumber", "FooLib.Foo+FormatNumberDelegate, FooLib")); QVERIFY(formatNumber.isValid()); } void tst_qtdotnet::callFunction() { QVERIFY(dotNetHost.isLoaded()); QVERIFY(formatNumber.isValid()); const QString formattedText = formatNumber("[{0}]", 42); QCOMPARE(formattedText, "[42]"); } struct DoubleAsInt {}; template<> struct QDotNetOutbound { using SourceType = double; using OutboundType = int; static OutboundType convert(SourceType arg) { return qRound(arg); } }; struct QUpperCaseString {}; template<> struct QDotNetNull { static QString value() { return {}; } static bool isNull(const QString& s) { return s.isNull() || s.isEmpty(); } }; template<> struct QDotNetInbound { using InboundType = QChar*; using TargetType = QString; static TargetType convert(InboundType inboundValue) { return QString(inboundValue).toUpper(); } }; void tst_qtdotnet::callFunctionWithCustomMarshaling() { QVERIFY(dotNetHost.isLoaded()); QDotNetFunction formatDouble; QVERIFY(dotNetHost.resolveFunction(formatDouble, QDir(QCoreApplication::applicationDirPath()).filePath("FooLib.dll"), Foo::AssemblyQualifiedName, "FormatNumber", "FooLib.Foo+FormatNumberDelegate, FooLib")); QVERIFY(formatDouble.isValid()); const QString formattedText = formatDouble("result = [{0}]", 41.5); QCOMPARE(formattedText, "RESULT = [42]"); } void tst_qtdotnet::callDefaultEntryPoint() { QVERIFY(dotNetHost.isLoaded()); QDotNetFunction entryPoint; QVERIFY(dotNetHost.resolveFunction(entryPoint, QDir(QCoreApplication::applicationDirPath()).filePath("FooLib.dll"), Foo::AssemblyQualifiedName, "EntryPoint")); QVERIFY(entryPoint.isValid()); QString fortyTwo("42"); const qint32 returnValue = entryPoint(fortyTwo.data(), static_cast(fortyTwo.length())); QCOMPARE(returnValue, 42); } struct Date { QString year; QString month; QString day; }; struct DateOutbound { const QChar* year; const QChar* month; const QChar* day; }; template<> struct QDotNetOutbound { using SourceType = const Date&; using OutboundType = const DateOutbound; static DateOutbound convert(SourceType arg) { return { arg.year.data(), arg.month.data(), arg.day.data() }; } }; void tst_qtdotnet::callWithComplexArg() { QVERIFY(dotNetHost.isLoaded()); QDotNetFunction formatDate; QVERIFY(dotNetHost.resolveFunction(formatDate, QDir(QCoreApplication::applicationDirPath()).filePath("FooLib.dll"), Foo::AssemblyQualifiedName, "FormatDate", "FooLib.Foo+FormatDateDelegate, FooLib")); QVERIFY(formatDate.isValid()); const Date xmas{ "2022", "12", "25" }; const QString formattedText = formatDate("Today is {0}-{1}-{2}", xmas); QCOMPARE(formattedText, "Today is 2022-12-25"); } void tst_qtdotnet::callStaticMethod() { const QDotNetType environment = QDotNetType::typeOf("System.Environment"); const auto getEnvironmentVariable = environment.staticMethod("GetEnvironmentVariable"); const QString path = getEnvironmentVariable("PATH"); QVERIFY(path.length() > 0); const QString samePath = QtDotNet::call( "System.Environment", "GetEnvironmentVariable", "PATH"); QVERIFY(path == samePath); } void tst_qtdotnet::createObject() { const auto newStringBuilder = QDotNetObject::constructor("System.Text.StringBuilder"); QDotNetObject stringBuilder = newStringBuilder(); QVERIFY(QDotNetAdapter::instance().stats().refCount == 1); } void tst_qtdotnet::callInstanceMethod() { const auto newStringBuilder = QDotNetObject::constructor("System.Text.StringBuilder"); const auto stringBuilder = newStringBuilder(); const auto append = stringBuilder.method("Append"); std::ignore = append("Hello"); std::ignore = append(" World!"); const QString helloWorld = stringBuilder.toString(); QVERIFY(helloWorld == "Hello World!"); } void tst_qtdotnet::useWrapperClassForStringBuilder() { StringBuilder sb; QVERIFY(QDotNetAdapter::instance().stats().refCount == 1); QVERIFY(sb.isValid()); sb.append("Hello").append(" "); StringBuilder sbCpy(sb); QVERIFY(QDotNetAdapter::instance().stats().refCount == 2); QVERIFY(sbCpy.isValid()); sbCpy.append("World"); sb = StringBuilder(std::move(sbCpy)); QVERIFY(QDotNetAdapter::instance().stats().refCount == 1); sb.append("!"); QCOMPARE(sb.toString(), "Hello World!"); } void tst_qtdotnet::useWrapperClassForUri() { const Uri uri(QStringLiteral( "https://user:password@www.contoso.com:80/Home/Index.htm?q1=v1&q2=v2#FragmentName")); QVERIFY(uri.segments().length() == 3); QVERIFY(uri.segments()[0]->compare("/") == 0); } void tst_qtdotnet::handleException() { StringBuilder stringBuilder(5, 5); QString helloWorld; try { stringBuilder.append("Hello"); QVERIFY(stringBuilder.toString() == "Hello"); stringBuilder.append(" World!"); helloWorld = stringBuilder.toString(); } catch (const QDotNetException &ex) { helloWorld = ex.type().cast().fullName(); } QVERIFY(helloWorld == "System.ArgumentOutOfRangeException"); } class Ping final : public QObject, public QDotNetObject, public QDotNetEventHandler { Q_OBJECT public: Q_DOTNET_OBJECT_INLINE(Ping, "System.Net.NetworkInformation.Ping, System", ); Ping() : QDotNetObject(QDotNetSafeMethod(constructor()).invoke(nullptr)) { subscribe("PingCompleted", this); } ~Ping() override = default; void sendAsync(const QString& hostNameOrAddress) { method("SendAsync", safeSendAsync).invoke(*this, hostNameOrAddress, nullptr); } void sendAsyncCancel() { method("SendAsyncCancel", safeSendAsyncCancel).invoke(*this); } signals: void pingCompleted(const QString& address, qint64 roundtripMsecs); void pingError(); private: void handleEvent(const QString& evName, QDotNetObject& evSrc, QDotNetObject& evArgs) override { if (evName != "PingCompleted") return; if (evArgs.type().fullName() != "System.Net.NetworkInformation.PingCompletedEventArgs") return; const auto getReply = evArgs.method("get_Reply"); const auto reply = getReply(); if (reply.isValid()) { const auto replyAddress = reply.method("get_Address"); const auto replyRoundtrip = reply.method("get_RoundtripTime"); emit pingCompleted(replyAddress().toString(), replyRoundtrip()); } else { emit pingError(); } } QDotNetSafeMethod safeSendAsync; QDotNetSafeMethod safeSendAsyncCancel; }; void tst_qtdotnet::emitSignalFromEvent() { Ping ping; bool waiting = true; int signalCount = 0; connect(&ping, &Ping::pingCompleted, [&waiting, &signalCount](const QString& address, qint64 roundtripMsecs) { qInfo() << "Reply from" << address << "in" << roundtripMsecs << "msecs"; signalCount++; waiting = false; }); connect(&ping, &Ping::pingError, [&waiting, &signalCount] { qInfo() << "Ping error"; signalCount++; waiting = false; }); qInfo() << "Pinging www.qt.io:"; QElapsedTimer waitTime; for (int i = 0; i < 4; ++i) { waitTime.restart(); waiting = true; ping.sendAsync("www.qt.io"); while (waiting) { QCoreApplication::processEvents(); if (waitTime.elapsed() > 3000) { ping.sendAsyncCancel(); waiting = false; qInfo() << "Ping timeout"; } } } QVERIFY(signalCount == 4); } void tst_qtdotnet::propertyBinding() { Foo foo; const QSignalSpy spy(&foo, &Foo::barChanged); for (int i = 0; i < 1000; ++i) foo.setBar(QString("hello x %1").arg(i + 1)); QVERIFY(foo.bar() == "hello x 1000"); QVERIFY(spy.count() == 1000); } struct ToUpper : IBarTransformation { Uri uri = Uri("https://qt.io/"); QString transform(const QString& bar) override { return bar.toUpper(); } Uri getUri(int n) override { return uri; } void setUri(const Uri &uri) override { this->uri = uri; } int getNumber() override { return 42; } }; void tst_qtdotnet::implementInterface() { const ToUpper transfToUpper; Foo foo(transfToUpper); foo.setBar("hello there"); QVERIFY(foo.bar() == "HELLO THERE (https://qt.io/developers)"); } void tst_qtdotnet::arrayOfInts() { QDotNetArray a(11); a[0] = 0; a[1] = 1; for (int i = 2; i < a.length(); ++i) a[i] = a[i - 1] + a[i - 2]; QVERIFY(a[10] == 55); } void tst_qtdotnet::arrayOfStrings() { QDotNetArray a(8); a[0] = "Lorem"; a[1] = "ipsum"; a[2] = "dolor"; a[3] = "sit"; a[4] = "amet,"; a[5] = "consectetur"; a[6] = "adipiscing"; a[7] = "elit."; const auto stringType = QDotNetType::typeOf("System.String"); const auto join = stringType.staticMethod>("Join"); const auto loremIpsum = join(" ", a); QVERIFY(loremIpsum == "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); } void tst_qtdotnet::arrayOfObjects() { QDotNetArray a(8); for (int i = 0; i < a.length(); ++i) a[i] = StringBuilder(); a[0]->append("Lorem"); a[1]->append(a[0]->toString()).append(" ipsum"); a[2]->append(a[1]->toString()).append(" dolor"); a[3]->append(a[2]->toString()).append(" sit"); a[4]->append(a[3]->toString()).append(" amet,"); a[5]->append(a[4]->toString()).append(" consectetur"); a[6]->append(a[5]->toString()).append(" adipiscing"); a[7]->append(a[6]->toString()).append(" elit."); QVERIFY(a[7]->toString() == "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); } void tst_qtdotnet::variantNull() { auto getVariant = QDotNetType::staticMethod("FooLib.Foo, FooLib", "GetVariant"); auto iqv = getVariant(); auto &qv = *iqv.dataAs(); QVERIFY(!qv.isValid()); } void tst_qtdotnet::variantGet() { auto getVariant = QDotNetType::staticMethod("FooLib.Foo, FooLib", "GetVariant"); auto iqv = getVariant("foobar"); auto &qv = *iqv.dataAs(); QVERIFY(qv.toString() == "foobar"); } void tst_qtdotnet::variantSet() { QVariant qv = "foobar"; IQVariant iqv(qv); auto toUpper = QDotNetType::staticMethod("FooLib.Foo, FooLib", "VariantStringToUpper"); toUpper(iqv); QVERIFY(qv.toString() == "FOOBAR"); } struct TestModel : public QStringListModel { QModelIndex getIndex(int row, int col, void *ptr) { return createIndex(row, col, ptr); } }; void tst_qtdotnet::modelIndexNull() { auto getModelIndex = QDotNetType::staticMethod("FooLib.Foo, FooLib", "GetModelIndex"); auto iqmi = getModelIndex(); auto &qmi = *iqmi.dataAs(); QVERIFY(!qmi.isValid()); } void tst_qtdotnet::modelIndexGet() { TestModel tm; auto idx = IQModelIndex(tm.getIndex(2, 3, reinterpret_cast(7))); auto idxRowColPtr = QDotNetType::staticMethod("FooLib.Foo, FooLib", "ModelIndexRowColPtr"); auto rcp = idxRowColPtr(idx); QVERIFY(rcp == 42); } struct TestListModel : public QDotNetObject { Q_DOTNET_OBJECT_INLINE(TestListModel, "FooLib.Foo+TestListModel, FooLib"); TestListModel() : QDotNetObject(constructor().invoke(nullptr)) { } QAbstractListModel *base() const { auto baseObj = method("get_Base", fnBase).invoke(*this); auto baseInterface = baseObj.cast(); return baseInterface.dataAs(); } mutable QDotNetFunction fnBase = nullptr; }; void tst_qtdotnet::models() { const auto testModel = TestListModel(); auto *baseModel = testModel.base(); auto n = baseModel->rowCount(); QVERIFY(n == 2); auto ff = baseModel->flags(baseModel->index(0)); QVERIFY(ff == (Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren)); auto it0 = baseModel->data(baseModel->index(0)); QVERIFY(it0.toString() == "FOO"); auto it1 = baseModel->data(baseModel->index(1)); QVERIFY(it1.toString() == "BAR"); skipCleanup = true; // TODO: figure out why refs are still pending here } void tst_qtdotnet::delegates() { auto plus42 = QtDotNet::call>("FooLib.Foo, FooLib", "get_Plus42"); QVERIFY(plus42(3) == 45); } #endif //TEST_FUNCTION_CALLS #ifdef TEST_HOST_UNLOAD void tst_qtdotnet::unloadHost() { QVERIFY(dotNetHost.isLoaded()); dotNetHost.unload(); QVERIFY(!dotNetHost.isLoaded()); } #endif //TEST_HOST_UNLOAD QTEST_MAIN(tst_qtdotnet) #include "tst_qtdotnet.moc"