diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/lsp.qbs | 10 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs | 2 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/modules/Prefix/m2/m2.qbs | 2 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/modules/Prefix/m3/m3.qbs | 2 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/lsp/modules/m/m.qbs | 2 | ||||
-rw-r--r-- | tests/auto/blackbox/tst_blackbox.cpp | 104 | ||||
-rw-r--r-- | tests/auto/blackbox/tst_blackbox.h | 2 | ||||
-rw-r--r-- | tests/auto/language/tst_language.cpp | 8 | ||||
-rw-r--r-- | tests/lspclient/CMakeLists.txt | 8 | ||||
-rw-r--r-- | tests/lspclient/lspclient.cpp | 315 | ||||
-rw-r--r-- | tests/lspclient/lspclient.qbs | 10 | ||||
-rw-r--r-- | tests/tests.qbs | 1 |
13 files changed, 457 insertions, 10 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3a868fbac..55dcad789 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(auto) add_subdirectory(benchmarker) add_subdirectory(fuzzy-test) +add_subdirectory(lspclient) diff --git a/tests/auto/blackbox/testdata/lsp/lsp.qbs b/tests/auto/blackbox/testdata/lsp/lsp.qbs new file mode 100644 index 000000000..2e30ad930 --- /dev/null +++ b/tests/auto/blackbox/testdata/lsp/lsp.qbs @@ -0,0 +1,10 @@ +Project { + Product { + name: "dep" + Depends { name: "m" } + Depends { name: "Prefix"; submodules: ["m1", "m2", "m3"] } + } + Product { + Depends { name: "dep" } + } +} diff --git a/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs new file mode 100644 index 000000000..84957060c --- /dev/null +++ b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m1/m1.qbs @@ -0,0 +1,2 @@ +Module { +} diff --git a/tests/auto/blackbox/testdata/lsp/modules/Prefix/m2/m2.qbs b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m2/m2.qbs new file mode 100644 index 000000000..84957060c --- /dev/null +++ b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m2/m2.qbs @@ -0,0 +1,2 @@ +Module { +} diff --git a/tests/auto/blackbox/testdata/lsp/modules/Prefix/m3/m3.qbs b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m3/m3.qbs new file mode 100644 index 000000000..84957060c --- /dev/null +++ b/tests/auto/blackbox/testdata/lsp/modules/Prefix/m3/m3.qbs @@ -0,0 +1,2 @@ +Module { +} diff --git a/tests/auto/blackbox/testdata/lsp/modules/m/m.qbs b/tests/auto/blackbox/testdata/lsp/modules/m/m.qbs new file mode 100644 index 000000000..84957060c --- /dev/null +++ b/tests/auto/blackbox/testdata/lsp/modules/m/m.qbs @@ -0,0 +1,2 @@ +Module { +} diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp index 1b4341f12..575f1a3cf 100644 --- a/tests/auto/blackbox/tst_blackbox.cpp +++ b/tests/auto/blackbox/tst_blackbox.cpp @@ -6321,6 +6321,102 @@ static QJsonObject getNextSessionPacket(QProcess &session, QByteArray &data) return QJsonDocument::fromJson(QByteArray::fromBase64(msg)).object(); } +static void sendSessionPacket(QProcess &sessionProc, const QJsonObject &message) +{ + const QByteArray data = QJsonDocument(message).toJson().toBase64(); + sessionProc.write("qbsmsg:"); + sessionProc.write(QByteArray::number(data.length())); + sessionProc.write("\n"); + sessionProc.write(data); +} + +void TestBlackbox::qbsLanguageServer_data() +{ + QTest::addColumn<QString>("request"); + QTest::addColumn<QString>("location"); + QTest::addColumn<QString>("expectedReply"); + + QTest::addRow("follow to module") << "--goto-def" + << (testDataDir + "/lsp/lsp.qbs:4:9") + << (testDataDir + "/lsp/modules/m/m.qbs:1:1"); + QTest::addRow("follow to submodules") + << "--goto-def" + << (testDataDir + "/lsp/lsp.qbs:5:35") + << ((testDataDir + "/lsp/modules/Prefix/m1/m1.qbs:1:1\n") + + (testDataDir + "/lsp/modules/Prefix/m2/m2.qbs:1:1\n") + + (testDataDir + "/lsp/modules/Prefix/m3/m3.qbs:1:1")); + QTest::addRow("follow to product") << "--goto-def" + << (testDataDir + "/lsp/lsp.qbs:8:19") + << (testDataDir + "/lsp/lsp.qbs:2:5"); +} + +void TestBlackbox::qbsLanguageServer() +{ + QFETCH(QString, request); + QFETCH(QString, location); + QFETCH(QString, expectedReply); + + QDir::setCurrent(testDataDir + "/lsp"); + QProcess sessionProc; + sessionProc.start(qbsExecutableFilePath, QStringList("session")); + + QVERIFY(sessionProc.waitForStarted()); + + QByteArray incomingData; + + // Wait for and verify hello packet. + QJsonObject receivedMessage = getNextSessionPacket(sessionProc, incomingData); + const QString socketPath = receivedMessage.value("lsp-socket").toString(); + QVERIFY(!socketPath.isEmpty()); + + // Resolve project. + QJsonObject resolveMessage; + resolveMessage.insert("type", "resolve-project"); + resolveMessage.insert("top-level-profile", profileName()); + resolveMessage.insert("project-file-path", QDir::currentPath() + "/lsp.qbs"); + resolveMessage.insert("build-root", QDir::currentPath()); + resolveMessage.insert("settings-directory", settings()->baseDirectory()); + sendSessionPacket(sessionProc, resolveMessage); + bool receivedReply = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QVERIFY(!receivedMessage.isEmpty()); + const QString msgType = receivedMessage.value("type").toString(); + if (msgType == "project-resolved") { + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + } + } + QVERIFY(receivedReply); + + // Employ client app to send request. + QProcess lspClient; + const QFileInfo qbsFileInfo(qbsExecutableFilePath); + const QString clientFilePath = HostOsInfo::appendExecutableSuffix( + qbsFileInfo.absolutePath() + "/qbs_lspclient"); + lspClient.start(clientFilePath, {"--socket", socketPath, request, location}); + QVERIFY2(lspClient.waitForStarted(), qPrintable(lspClient.errorString())); + QVERIFY2(lspClient.waitForFinished(), qPrintable(lspClient.errorString())); + QString errMsg; + if (lspClient.exitStatus() != QProcess::NormalExit) + errMsg = lspClient.errorString(); + if (errMsg.isEmpty()) + errMsg = QString::fromLocal8Bit(lspClient.readAllStandardError()); + QVERIFY2(lspClient.exitCode() == 0, qPrintable(errMsg)); + QVERIFY2(errMsg.isEmpty(), qPrintable(errMsg)); + QString output = QString::fromUtf8(lspClient.readAllStandardOutput().trimmed()); + output.replace("\r\n", "\n"); + QCOMPARE(output, expectedReply); + + QJsonObject quitRequest; + quitRequest.insert("type", "quit"); + sendSessionPacket(sessionProc, quitRequest); + QVERIFY(sessionProc.waitForFinished(3000)); +} + void TestBlackbox::qbsSession() { QDir::setCurrent(testDataDir + "/qbs-session"); @@ -6337,11 +6433,7 @@ void TestBlackbox::qbsSession() QVERIFY(sessionProc.waitForStarted()); const auto sendPacket = [&sessionProc](const QJsonObject &message) { - const QByteArray data = QJsonDocument(message).toJson().toBase64(); - sessionProc.write("qbsmsg:"); - sessionProc.write(QByteArray::number(data.length())); - sessionProc.write("\n"); - sessionProc.write(data); + sendSessionPacket(sessionProc, message); }; static const auto envToJson = [](const QProcessEnvironment &env) { @@ -6365,7 +6457,7 @@ void TestBlackbox::qbsSession() // Wait for and verify hello packet. QJsonObject receivedMessage = getNextSessionPacket(sessionProc, incomingData); QCOMPARE(receivedMessage.value("type"), "hello"); - QCOMPARE(receivedMessage.value("api-level").toInt(), 4); + QCOMPARE(receivedMessage.value("api-level").toInt(), 5); QCOMPARE(receivedMessage.value("api-compat-level").toInt(), 2); // Resolve & verify structure diff --git a/tests/auto/blackbox/tst_blackbox.h b/tests/auto/blackbox/tst_blackbox.h index c662395e3..4447709f4 100644 --- a/tests/auto/blackbox/tst_blackbox.h +++ b/tests/auto/blackbox/tst_blackbox.h @@ -265,6 +265,8 @@ private slots: void qbsConfigImport_data(); void qbsConfigExport(); void qbsConfigExport_data(); + void qbsLanguageServer_data(); + void qbsLanguageServer(); void qbsSession(); void qbsVersion(); void qtBug51237(); diff --git a/tests/auto/language/tst_language.cpp b/tests/auto/language/tst_language.cpp index 6a29876dc..343b059c9 100644 --- a/tests/auto/language/tst_language.cpp +++ b/tests/auto/language/tst_language.cpp @@ -739,7 +739,7 @@ void TestLanguage::enumerateProjectProperties() auto products = productsFromProject(project); QCOMPARE(products.size(), 1); auto product = products.values().front(); - auto files = product->groups.front()->allFiles(); + auto files = product->groups.front()->files; QCOMPARE(product->groups.size(), size_t(1)); QCOMPARE(files.size(), size_t(1)); auto fileName = FileInfo::fileName(files.front()->absoluteFilePath); @@ -3049,7 +3049,7 @@ void TestLanguage::relaxedErrorMode() QVERIFY(missingFile->enabled); QCOMPARE(missingFile->groups.size(), size_t(1)); QVERIFY(missingFile->groups.front()->enabled); - QCOMPARE(missingFile->groups.front()->allFiles().size(), size_t(2)); + QCOMPARE(missingFile->groups.front()->files.size(), size_t(2)); const ResolvedProductConstPtr fine = productMap.value("fine"); QVERIFY(fine->enabled); QCOMPARE(fine->allFiles().size(), size_t(1)); @@ -3445,10 +3445,10 @@ void TestLanguage::wildcards() group = product->groups.front(); } QVERIFY(!!group); - QCOMPARE(group->files.size(), size_t(0)); + QCOMPARE(group->files.size(), expected.size()); // we assume all files are wildcards QVERIFY(!!group->wildcards); QStringList actualFilePaths; - for (const SourceArtifactPtr &artifact : group->wildcards->files) { + for (const SourceArtifactPtr &artifact : group->files) { QString str = artifact->absoluteFilePath; int idx = str.indexOf(m_wildcardsTestDirPath); if (idx != -1) diff --git a/tests/lspclient/CMakeLists.txt b/tests/lspclient/CMakeLists.txt new file mode 100644 index 000000000..ba6f3dbdc --- /dev/null +++ b/tests/lspclient/CMakeLists.txt @@ -0,0 +1,8 @@ +add_qbs_app(qbs_lspclient + DEPENDS + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network + qbscore + qtclsp + SOURCES lspclient.cpp + ) diff --git a/tests/lspclient/lspclient.cpp b/tests/lspclient/lspclient.cpp new file mode 100644 index 000000000..caf3e5916 --- /dev/null +++ b/tests/lspclient/lspclient.cpp @@ -0,0 +1,315 @@ +/**************************************************************************** +** +** Copyright (C) 2023 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <lsp/clientcapabilities.h> +#include <lsp/initializemessages.h> +#include <lsp/languagefeatures.h> + +#include <QBuffer> +#include <QCommandLineOption> +#include <QCommandLineParser> +#include <QCoreApplication> +#include <QLocalSocket> + +#include <cstdlib> +#include <iostream> + +enum class Command { GotoDefinition, }; + +class LspClient : public QObject +{ +public: + LspClient(Command command, const QString &socketPath, const QString &filePath, + int line, int column); + void start(); + +private: + void finishWithError(const QString &msg); + void exit(int code); + void initiateProtocol(); + void handleIncomingData(); + void sendMessage(const lsp::JsonObject &msg); + void sendMessage(const lsp::JsonRpcMessage &msg); + void handleCurrentMessage(); + void handleInitializeReply(); + void sendRequest(); + void handleResponse(); + void sendGotoDefinitionRequest(); + void handleGotoDefinitionResponse(); + lsp::DocumentUri uri() const; + lsp::DocumentUri::PathMapper mapper() const; + + const Command m_command; + const QString m_socketPath; + const QString m_filePath; + const int m_line; + const int m_column; + QLocalSocket m_socket; + QBuffer m_incomingData; + lsp::BaseMessage m_currentMessage; + QJsonObject m_messageObject; + + enum class State { Inactive, Connecting, Initializing, RunningCommand } + m_state = State::Inactive; +}; + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + const QCommandLineOption socketOption({"socket", "s"}, "The server socket to connect to.", + "socket"); + const QCommandLineOption gotoDefinitionOption( + {"goto-def", "g"}, "Go to definition from the specified location."); + QCommandLineParser parser; + parser.addOptions({socketOption, gotoDefinitionOption}); + parser.addHelpOption(); + parser.addPositionalArgument("location", "The location at which to operate.", + "<file>:<line>:<column>"); + parser.process(app); + + const auto complainAndExit = [&](const char *text) { + std::cerr << text << std::endl; + parser.showHelp(EXIT_FAILURE); + }; + + if (!parser.isSet(socketOption)) + complainAndExit("Socket must be specified."); + + // Initialized to suppress warning. TODO: In C++23, mark lambdas as noreturn instead. + Command command = Command::GotoDefinition; + + if (parser.isSet(gotoDefinitionOption)) + command = Command::GotoDefinition; + else + complainAndExit("Don't know what to do."); + + if (parser.positionalArguments().size() != 1) + complainAndExit("Need location."); + const auto complainAboutLocationString = [&] { complainAndExit("Invalid location."); }; + const QString loc = parser.positionalArguments().first(); + int sep1 = loc.indexOf(':'); + if (sep1 <= 0) + complainAboutLocationString(); + if (qbs::Internal::HostOsInfo::isWindowsHost()) { + sep1 = loc.indexOf(':', sep1 + 1); + if (sep1 < 0) + complainAboutLocationString(); + } + const int sep2 = loc.indexOf(':', sep1 + 1); + if (sep2 == -1 || sep2 == loc.size() - 1) + complainAboutLocationString(); + const auto extractNumber = [&](const QString &s) { + bool ok; + const int n = s.toInt(&ok); + if (!ok || n <= 0) + complainAboutLocationString(); + return n; + }; + const int line = extractNumber(loc.mid(sep1 + 1, sep2 - sep1 - 1)); + const int column = extractNumber(loc.mid(sep2 + 1)); + + LspClient client(command, parser.value(socketOption), + QDir::fromNativeSeparators(loc.left(sep1)), line, column); + QMetaObject::invokeMethod(&client, &LspClient::start, Qt::QueuedConnection); + + return app.exec(); +} + +LspClient::LspClient(Command command, const QString &socketPath, const QString &filePath, + int line, int column) + : m_command(command), m_socketPath(socketPath), m_filePath(filePath), + m_line(line), m_column(column) +{ + connect(&m_socket, &QLocalSocket::disconnected, this, [this] { + finishWithError("Server disconnected unexpectedly."); + }); + connect(&m_socket, &QLocalSocket::errorOccurred, this, [this] { + finishWithError(QString::fromLatin1("Socket error: %1").arg(m_socket.errorString())); + }); + connect(&m_socket, &QLocalSocket::connected, this, &LspClient::initiateProtocol); + connect(&m_socket, &QLocalSocket::readyRead, this, &LspClient::handleIncomingData); +} + +void LspClient::start() +{ + m_state = State::Connecting; + m_incomingData.open(QIODevice::ReadWrite | QIODevice::Append); + m_socket.connectToServer(m_socketPath); +} + +void LspClient::finishWithError(const QString &msg) +{ + std::cerr << qPrintable(msg) << std::endl; + m_socket.disconnectFromServer(); + exit(EXIT_FAILURE); +} + +void LspClient::exit(int code) +{ + m_socket.disconnect(this); + qApp->exit(code); +} + +void LspClient::initiateProtocol() +{ + if (m_state != State::Connecting) { + finishWithError(QString::fromLatin1("State should be %1, was %2.") + .arg(int(State::Connecting), int(m_state))); + return; + } + m_state = State::Initializing; + + lsp::DynamicRegistrationCapabilities definitionCaps; + definitionCaps.setDynamicRegistration(false); + lsp::TextDocumentClientCapabilities docCaps; + docCaps.setDefinition(definitionCaps); + lsp::ClientCapabilities clientCaps; + clientCaps.setTextDocument(docCaps); + lsp::InitializeParams initParams; + initParams.setCapabilities(clientCaps); + sendMessage(lsp::InitializeRequest(initParams)); +} + +void LspClient::handleIncomingData() +{ + const int pos = m_incomingData.pos(); + m_incomingData.write(m_socket.readAll()); + m_incomingData.seek(pos); + QString parseError; + lsp::BaseMessage::parse(&m_incomingData, parseError, m_currentMessage); + if (!parseError.isEmpty()) { + return finishWithError(QString::fromLatin1("Error parsing server message: %1.") + .arg(parseError)); + } + if (m_currentMessage.isComplete()) { + m_incomingData.buffer().remove(0, m_incomingData.pos()); + m_incomingData.seek(0); + handleCurrentMessage(); + m_currentMessage = {}; + m_messageObject = {}; + if (m_socket.state() == QLocalSocket::ConnectedState) + handleIncomingData(); + } +} + +void LspClient::sendMessage(const lsp::JsonObject &msg) +{ + sendMessage(lsp::JsonRpcMessage(msg)); +} + +void LspClient::sendMessage(const lsp::JsonRpcMessage &msg) +{ + lsp::BaseMessage baseMsg = msg.toBaseMessage(); + m_socket.write(baseMsg.header()); + m_socket.write(baseMsg.content); +} + +void LspClient::handleCurrentMessage() +{ + m_messageObject = lsp::JsonRpcMessage(m_currentMessage).toJsonObject(); + switch (m_state) { + case State::Inactive: + case State::Connecting: + finishWithError("Received message in non-connected state."); + break; + case State::Initializing: + handleInitializeReply(); + sendRequest(); + break; + case State::RunningCommand: + handleResponse(); + break; + } +} + +void LspClient::handleInitializeReply() +{ + lsp::ServerCapabilities serverCaps = lsp::InitializeRequest::Response( + m_messageObject).result().value_or(lsp::InitializeResult()).capabilities(); + const auto defProvider = serverCaps.definitionProvider(); + if (!defProvider) + return finishWithError("Expected definition provider."); + const bool * const defProviderValue = std::get_if<bool>(&(*defProvider)); + if (!defProviderValue || !*defProviderValue) + return finishWithError("Expected definition provider."); + sendMessage(lsp::InitializeNotification(lsp::InitializedParams())); +} + +void LspClient::sendRequest() +{ + m_state = State::RunningCommand; + switch (m_command) { + case Command::GotoDefinition: + return sendGotoDefinitionRequest(); + } +} + +void LspClient::handleResponse() +{ + switch (m_command) { + case Command::GotoDefinition: + return handleGotoDefinitionResponse(); + } +} + +void LspClient::sendGotoDefinitionRequest() +{ + const lsp::TextDocumentIdentifier doc(uri()); + const lsp::Position pos(m_line - 1, m_column - 1); + sendMessage(lsp::GotoDefinitionRequest({doc, pos})); +} + +void LspClient::handleGotoDefinitionResponse() +{ + const lsp::GotoResult result(lsp::GotoDefinitionRequest::Response(m_messageObject) + .result().value_or(lsp::GotoResult())); + QList<lsp::Utils::Link> links; + const auto loc2Link = [this](const lsp::Location &loc) { return loc.toLink(mapper()); }; + if (const auto loc = std::get_if<lsp::Location>(&result)) { + links << loc2Link(*loc); + } else if (const auto locs = std::get_if<QList<lsp::Location>>(&result)) { + links = lsp::Utils::transform(*locs, loc2Link); + } + for (const lsp::Utils::Link &link : std::as_const(links)) { + std::cout << qPrintable(link.targetFilePath) << ':' << link.targetLine << ':' + << (link.targetColumn + 1) << std::endl; + } + exit(EXIT_SUCCESS); +} + +lsp::DocumentUri LspClient::uri() const +{ + return lsp::DocumentUri::fromFilePath(lsp::Utils::FilePath::fromUserInput(m_filePath), + mapper()); +} + +lsp::DocumentUri::PathMapper LspClient::mapper() const +{ + return [](const lsp::Utils::FilePath &fp) { return fp; }; +} diff --git a/tests/lspclient/lspclient.qbs b/tests/lspclient/lspclient.qbs new file mode 100644 index 000000000..c89b9e0c0 --- /dev/null +++ b/tests/lspclient/lspclient.qbs @@ -0,0 +1,10 @@ +QbsApp { + name: "qbs_lspclient" + + Depends { name: "qtclsp" } + Depends { name: "Qt.network" } + + cpp.defines: base.filter(function(d) { return d !== "QT_NO_CAST_FROM_ASCII"; }) + + files: "lspclient.cpp" +} diff --git a/tests/tests.qbs b/tests/tests.qbs index 3cc757c8a..54bc80715 100644 --- a/tests/tests.qbs +++ b/tests/tests.qbs @@ -5,6 +5,7 @@ Project { "auto/auto.qbs", "benchmarker/benchmarker.qbs", "fuzzy-test/fuzzy-test.qbs", + "lspclient/lspclient.qbs", ] AutotestRunner { |