diff options
Diffstat (limited to 'tests/auto/qmlls/cli/tst_qmlls_cli.cpp')
-rw-r--r-- | tests/auto/qmlls/cli/tst_qmlls_cli.cpp | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/tests/auto/qmlls/cli/tst_qmlls_cli.cpp b/tests/auto/qmlls/cli/tst_qmlls_cli.cpp new file mode 100644 index 0000000000..344ed48e64 --- /dev/null +++ b/tests/auto/qmlls/cli/tst_qmlls_cli.cpp @@ -0,0 +1,309 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include "tst_qmlls_cli.h" + +using namespace Qt::StringLiterals; + +void tst_qmlls_cli::initTestCase() +{ + QQmlDataTest::initTestCase(); + + m_qmllsPath = QLibraryInfo::path(QLibraryInfo::BinariesPath) + QLatin1String("/qmlls"); +#ifdef Q_OS_WIN + m_qmllsPath += QLatin1String(".exe"); +#endif + // allow overriding of the executable, to be able to use a qmlEcho script (as described in + // qmllanguageservertool.cpp) + m_qmllsPath = qEnvironmentVariable("QMLLS", m_qmllsPath); + m_server.setProgram(m_qmllsPath); +} + +void tst_qmlls_cli::cleanup() +{ + m_server.closeWriteChannel(); + m_server.waitForFinished(); + QTRY_COMPARE(m_server.state(), QProcess::NotRunning); + QCOMPARE(m_server.exitStatus(), QProcess::NormalExit); +} + +// Helper structs to avoid confusions between expected and unexpected messages and between expected +// and unexpected diagnostics. +struct ExpectedMessages : public QStringList +{ + using QStringList::QStringList; +}; +struct UnexpectedMessages : public QStringList +{ + using QStringList::QStringList; +}; +struct ExpectedDiagnostics : public QStringList +{ + using QStringList::QStringList; +}; +struct UnexpectedDiagnostics : public QStringList +{ + using QStringList::QStringList; +}; + +// Extra environment variables to be added to qmlls's environment. +struct Environment : public QList<QPair<QString, QString>> +{ + using QList<QPair<QString, QString>>::QList; +}; + +void tst_qmlls_cli::warnings_data() +{ + QTest::addColumn<QStringList>("args"); + QTest::addColumn<Environment>("environment"); + QTest::addColumn<QString>("filePath"); + // messages are printed to stderr and not shown in editor: + QTest::addColumn<ExpectedMessages>("expectedMessages"); + QTest::addColumn<UnexpectedMessages>("unexpectedMessages"); + // diagnostics are passed via LSP to be shown in editor: + QTest::addColumn<ExpectedDiagnostics>("expectedDiagnostics"); + QTest::addColumn<UnexpectedDiagnostics>("unexpectedDiagnostics"); + + const Environment defaultEnv; + const QString dir1 = testFile(u"ImportPath1"_s); + const QString dir2 = testFile(u"ImportPath2"_s); + const QString notDir = testFile(u"ImportPath1/SomeModule/qmldir"_s); + const QString wrongDir = testFile(u"ImportPathInexistent"_s); + + const QString fileImportingDir1 = testFile(u"sourceFolder/ImportFromImportPath1.qml"_s); + const QString fileImportingBothDirs = testFile(u"sourceFolder/ImportFromBothPaths.qml"_s); + + const QString importWarningDir1 = u"Warnings occurred while importing module \"SomeModule\""_s; + const QString importWarningDir2 = u"Warnings occurred while importing module \"AnotherModule\""_s; + + const UnexpectedMessages noUnexpectedMessages; + const QString warnAboutQmllsIniFiles{ + u"Using the build directories found in the .qmlls.ini file. Your build folder might not be found if no .qmlls.ini files are present in the root source folder."_s + }; + + QTest::addRow("2-build-dirs") + << QStringList{ u"--build-dir"_s, dir1, u"-b"_s, dir2 } << defaultEnv + << fileImportingDir1 + << ExpectedMessages{ u"Using build directories passed by -b: \"%1\", \"%2\"."_s.arg( + dir1, dir2) } + << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{} + << UnexpectedDiagnostics{ importWarningDir1 }; + + QTest::addRow("build-dir-not-dir") + << QStringList{ u"--build-dir"_s, notDir, u"-b"_s, dir2 } << defaultEnv + << fileImportingBothDirs + << ExpectedMessages{ u"Argument \"%1\" passed to -b is not a directory."_s.arg(notDir) } + << UnexpectedMessages{ warnAboutQmllsIniFiles } + << ExpectedDiagnostics{ importWarningDir1 } + << UnexpectedDiagnostics{ importWarningDir2 }; + + QTest::addRow("build-dir-not-existing") + << QStringList{ u"--build-dir"_s, wrongDir, u"-b"_s, dir2 } << defaultEnv + << fileImportingBothDirs + << ExpectedMessages{ u"Argument \"%1\" passed to -b does not exist."_s.arg(wrongDir) } + << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{} + << UnexpectedDiagnostics{}; + + QTest::addRow("build-dir-from-environment") + << QStringList{} + << Environment{ { u"QMLLS_BUILD_DIRS"_s, + u"%1%2%3"_s.arg(dir1, QDir::listSeparator(), dir2) } } + << fileImportingBothDirs + << ExpectedMessages{ u"Using build directories passed from environment variable \"QMLLS_BUILD_DIRS\": \"%1\", \"%2\"."_s + .arg(dir1, dir2) } + << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{} + << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 }; + + QTest::addRow("build-dir-from-environment-not-existing") + << QStringList{} + << Environment{ { u"QMLLS_BUILD_DIRS"_s, + QStringList{ dir1, wrongDir, notDir }.join(QDir::listSeparator()) } } + << fileImportingDir1 + << ExpectedMessages{ u"Argument \"%1\" from environment variable \"QMLLS_BUILD_DIRS\" does not exist."_s + .arg(wrongDir), + u"Argument \"%1\" from environment variable \"QMLLS_BUILD_DIRS\" is not a directory."_s + .arg(notDir) } + << UnexpectedMessages{ warnAboutQmllsIniFiles } << ExpectedDiagnostics{} + << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 }; + + QTest::addRow("ignore-environment-with-option") + << QStringList{ u"--build-dir"_s, dir1 } + << Environment{ { u"QMLLS_BUILD_DIRS"_s, dir2 } } << fileImportingBothDirs + << ExpectedMessages{ u"Using build directories passed by -b: \"%1\"."_s.arg(dir1) } + << UnexpectedMessages{ dir2, warnAboutQmllsIniFiles } + << ExpectedDiagnostics{ importWarningDir2 } + << UnexpectedDiagnostics{ importWarningDir1 }; + + QTest::addRow("loadFromConfigFile") + << QStringList{} << Environment{} << fileImportingDir1 + << ExpectedMessages{ warnAboutQmllsIniFiles } << UnexpectedMessages{} + << ExpectedDiagnostics{ importWarningDir1 } << UnexpectedDiagnostics{}; + + QTest::addRow("2-import-paths") + << QStringList{ u"-I"_s, dir1, u"-I"_s, dir2 } << Environment{} << fileImportingBothDirs + << ExpectedMessages{ u"Using import directories passed by -I: \"%1\", \"%2\"."_s.arg( + dir1, dir2) } + << UnexpectedMessages{} << ExpectedDiagnostics{} + << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 }; + + QTest::addRow("import-paths-ignore-env") + << QStringList{ u"-I"_s, dir1, } << Environment{ { u"QML_IMPORT_PATH"_s, dir2 } } + << fileImportingBothDirs + << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1) } + << UnexpectedMessages{ u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s.arg(dir2)} + << ExpectedDiagnostics{importWarningDir2} << UnexpectedDiagnostics{ importWarningDir1 }; + + QTest::addRow("2-import-paths-mixed") + << QStringList{ u"-I"_s, dir1, u"-E"_s } + << Environment{ { u"QML_IMPORT_PATH"_s, dir2 } } << fileImportingBothDirs + << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1), + u"Using import directories passed from environment variable \"QML_IMPORT_PATH\": \"%1\"."_s + .arg(dir2) } + << UnexpectedMessages{} << ExpectedDiagnostics{} + << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 }; + + QTest::addRow("2-import-paths-deprecated") + << QStringList{ u"-I"_s, dir1, u"-E"_s } + << Environment{ { u"QML2_IMPORT_PATH"_s, dir2 } } << fileImportingBothDirs + << ExpectedMessages{ u"Using import directories passed by -I: \"%1\"."_s.arg(dir1), + u"Using import directories passed from the deprecated environment variable \"QML2_IMPORT_PATH\": \"%1\"."_s + .arg(dir2) } + << UnexpectedMessages{} << ExpectedDiagnostics{} + << UnexpectedDiagnostics{ importWarningDir1, importWarningDir2 }; +} + +auto tst_qmlls_cli::startServerRAII() +{ + startServerImpl(); + return qScopeGuard([this]() { this->stopServerImpl(); }); +} + +void tst_qmlls_cli::startServerImpl() +{ + m_protocol = std::make_unique<QLanguageServerProtocol>( + [this](const QByteArray &data) { m_server.write(data); }); + + connect(&m_server, &QProcess::readyReadStandardOutput, this, [this]() { + QByteArray data = m_server.readAllStandardOutput(); + m_protocol->receiveData(data); + }); + + m_server.start(); + + QLspSpecification::InitializeParams clientInfo; + clientInfo.rootUri = QUrl::fromLocalFile(dataDirectory() + "/default").toString().toUtf8(); + + QLspSpecification::TextDocumentClientCapabilities tDoc; + tDoc.typeDefinition = QLspSpecification::TypeDefinitionClientCapabilities{ false, false }; + + QLspSpecification::PublishDiagnosticsClientCapabilities pDiag; + tDoc.publishDiagnostics = pDiag; + pDiag.versionSupport = true; + clientInfo.capabilities.textDocument = tDoc; + bool didInit = false; + m_protocol->requestInitialize( + clientInfo, [this, &didInit](const QLspSpecification::InitializeResult &serverInfo) { + Q_UNUSED(serverInfo); + m_protocol->notifyInitialized(QLspSpecification::InitializedParams()); + didInit = true; + }); + QTRY_COMPARE_WITH_TIMEOUT(didInit, true, 10000); +} + +void tst_qmlls_cli::stopServerImpl() +{ + m_server.closeWriteChannel(); + m_server.waitForFinished(); + QTRY_COMPARE(m_server.state(), QProcess::NotRunning); + QCOMPARE(m_server.exitStatus(), QProcess::NormalExit); +} + +void tst_qmlls_cli::warnings() +{ + QFETCH(QStringList, args); + QFETCH(Environment, environment); + QFETCH(ExpectedMessages, expectedMessages); + QFETCH(UnexpectedMessages, unexpectedMessages); + QFETCH(QString, filePath); + QFETCH(ExpectedDiagnostics, expectedDiagnostics); + QFETCH(UnexpectedDiagnostics, unexpectedDiagnostics); + + QProcessEnvironment processEnvironment = QProcessEnvironment::systemEnvironment(); + for (const auto &entry : environment) + processEnvironment.insert(entry.first, entry.second); + m_server.setProcessEnvironment(processEnvironment); + m_server.setArguments(args); + + QList<int> countExpectedMessages(expectedMessages.size(), 0); + QList<int> countUnexpectedMessages(unexpectedMessages.size(), 0); + QList<int> countExpectedDiagnostics(expectedDiagnostics.size(), 0); + QList<int> countUnexpectedDiagnostics(unexpectedDiagnostics.size(), 0); + + auto guard = qScopeGuard([this]() { + // note: the lambda used in the "connect"-call references local variables, so disconnect the + // lambda via QScopedGuard to avoid its captured references to dangle + disconnect(&m_server, &QProcess::readyReadStandardOutput, nullptr, nullptr); + }); + connect(&m_server, &QProcess::readyReadStandardError, this, + [this, &expectedMessages, &countExpectedMessages, &unexpectedMessages, + &countUnexpectedMessages]() { + const auto data = QString::fromUtf8(m_server.readAllStandardError()); + if (data.isEmpty()) + return; + + for (int i = 0; i < expectedMessages.size(); ++i) { + if (data.contains(expectedMessages[i])) + ++countExpectedMessages[i]; + } + for (int i = 0; i < unexpectedMessages.size(); ++i) { + if (data.contains(unexpectedMessages[i])) + ++countUnexpectedMessages[i]; + } + }); + + auto guard2 = startServerRAII(); + + // each expected message should appear exactly one time + QTRY_COMPARE_WITH_TIMEOUT(countExpectedMessages, QList<int>(expectedMessages.size(), 1), 500); + // each unexpected message should appear exactly zero times + QCOMPARE(countUnexpectedMessages, QList<int>(unexpectedMessages.size(), 0)); + + bool diagnosticOk = false; + m_protocol->registerPublishDiagnosticsNotificationHandler( + [&diagnosticOk, &expectedDiagnostics, &countExpectedDiagnostics, &unexpectedDiagnostics, + &countUnexpectedDiagnostics](const QByteArray &, + const QLspSpecification::PublishDiagnosticsParams &p) { + for (const auto &d : p.diagnostics) { + const QString message = QString::fromUtf8(d.message); + for (int i = 0; i < expectedDiagnostics.size(); ++i) { + if (message.contains(expectedDiagnostics[i])) + ++countExpectedDiagnostics[i]; + } + for (int i = 0; i < unexpectedDiagnostics.size(); ++i) { + if (message.contains(unexpectedDiagnostics[i])) + ++countUnexpectedDiagnostics[i]; + } + } + diagnosticOk = true; + }); + + QFile file(filePath); + QVERIFY(file.open(QIODevice::ReadOnly)); + + QLspSpecification::DidOpenTextDocumentParams oParams; + QLspSpecification::TextDocumentItem textDocument; + QByteArray uri = QUrl::fromLocalFile(filePath).toEncoded(); + textDocument.uri = uri; + textDocument.text = file.readAll(); + oParams.textDocument = textDocument; + m_protocol->notifyDidOpenTextDocument(oParams); + + QTRY_VERIFY_WITH_TIMEOUT(diagnosticOk, 3000); + // each expected diagnostic should appear exactly one time + QCOMPARE(countExpectedDiagnostics, QList<int>(expectedDiagnostics.size(), 1)); + // each unexpected diagnostic should appear exactly zero times + QCOMPARE(countUnexpectedDiagnostics, QList<int>(unexpectedDiagnostics.size(), 0)); + +} + +QTEST_MAIN(tst_qmlls_cli) |