/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** 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. ** ****************************************************************************/ #include #include #include #include #include #include using namespace LanguageServerProtocol; Q_DECLARE_METATYPE(QTextCodec *) Q_DECLARE_METATYPE(BaseMessage) Q_DECLARE_METATYPE(JsonRpcMessage) Q_DECLARE_METATYPE(DocumentUri) class tst_LanguageServerProtocol : public QObject { Q_OBJECT private slots: void initTestCase(); void baseMessageParse_data(); void baseMessageParse(); void baseMessageToData_data(); void baseMessageToData(); void fromJsonValue(); void toJsonObject_data(); void toJsonObject(); void jsonMessageToBaseMessage_data(); void jsonMessageToBaseMessage(); void jsonObject(); void documentUri_data(); void documentUri(); private: QByteArray defaultMimeType; QTextCodec *defaultCodec = nullptr; }; void tst_LanguageServerProtocol::initTestCase() { defaultMimeType = JsonRpcMessageHandler::jsonRpcMimeType(); defaultCodec = QTextCodec::codecForName("utf-8"); } void tst_LanguageServerProtocol::baseMessageParse_data() { QTest::addColumn("data"); QTest::addColumn("mimeType"); QTest::addColumn("content"); QTest::addColumn("complete"); QTest::addColumn("valid"); QTest::addColumn("error"); QTest::addColumn("codec"); QTest::addColumn("partial"); QTest::newRow("empty content") << QByteArray("") << defaultMimeType << QByteArray() << false // complete << false // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("garbage") << QByteArray("garbage\r\n") << defaultMimeType << QByteArray() << false // complete << false // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("minimum message") << QByteArray("Content-Length: 0\r\n" "\r\n") << defaultMimeType << QByteArray() << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("minimum message with content") << QByteArray("Content-Length: 3\r\n" "\r\n" "foo") << defaultMimeType << QByteArray("foo") << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("minimum message with incomplete content") << QByteArray("Content-Length: 6\r\n" "\r\n" "foo") << defaultMimeType << QByteArray("foo") << false // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("default mime type") << QByteArray("Content-Length: 0\r\n" "Content-Type: application/vscode-jsonrpc\r\n" "\r\n") << defaultMimeType << QByteArray() << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("default mime type and charset") << QByteArray("Content-Length: 0\r\n" "Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n" "\r\n") << defaultMimeType << QByteArray() << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); // For backwards compatibility it is highly recommended that a client and a server // treats the string utf8 as utf-8. (lsp documentation) QTest::newRow("default mime type and old charset") << QByteArray("Content-Length: 0\r\n" "Content-Type: application/vscode-jsonrpc; charset=utf8\r\n" "\r\n") << defaultMimeType << QByteArray() << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("non default mime type with default charset") << QByteArray("Content-Length: 0\r\n" "Content-Type: text/x-python\r\n" "\r\n") << QByteArray("text/x-python") << QByteArray() << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("non default mime type and charset") << QByteArray("Content-Length: 0\r\n" "Content-Type: text/x-python; charset=iso-8859-1\r\n" "\r\n") << QByteArray("text/x-python") << QByteArray() << true // complete << true // valid << false // errorMessage << QTextCodec::codecForName("iso-8859-1") << BaseMessage(); QTest::newRow("data after message") << QByteArray("Content-Length: 3\r\n" "\r\n" "foobar") << defaultMimeType << QByteArray("foo") << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("Unexpected header field") << QByteArray("Content-Length: 6\r\n" "Foo: bar\r\n" "\r\n" "foobar") << defaultMimeType << QByteArray("foobar") << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("Unexpected header line") << QByteArray("Content-Length: 6\r\n" "Foobar\r\n" "\r\n" "foobar") << defaultMimeType << QByteArray("foobar") << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("Unknown mimeType") << QByteArray("Content-Length: 6\r\n" "Content-Type: foobar\r\n" "\r\n" "foobar") << QByteArray("foobar") << QByteArray("foobar") << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("Unknown charset") << QByteArray("Content-Length: 6\r\n" "Content-Type: application/vscode-jsonrpc; charset=foobar\r\n" "\r\n" "foobar") << defaultMimeType << QByteArray("foobar") << true // complete << true // valid << true // errorMessage << defaultCodec << BaseMessage(); QTest::newRow("completing content") << QByteArray("bar") << defaultMimeType << QByteArray("foobar") << true // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(defaultMimeType, "foo", 6, defaultCodec); QTest::newRow("still incomplet content") << QByteArray("bar") << defaultMimeType << QByteArray("foobar") << false // complete << true // valid << false // errorMessage << defaultCodec << BaseMessage(defaultMimeType, "foo", 7, defaultCodec); } void tst_LanguageServerProtocol::baseMessageParse() { QFETCH(QByteArray, data); QFETCH(QByteArray, mimeType); QFETCH(QByteArray, content); QFETCH(bool, complete); QFETCH(bool, valid); QFETCH(QTextCodec *, codec); QFETCH(bool, error); QFETCH(BaseMessage, partial); QBuffer buffer(&data); buffer.open(QIODevice::ReadWrite); QString parseError; BaseMessage::parse(&buffer, parseError, partial); if (!parseError.isEmpty() && !error) // show message if there is an error message we do not expect QWARN(parseError.toLatin1()); QCOMPARE(!parseError.isEmpty(), error); QCOMPARE(partial.content, content); QCOMPARE(partial.isValid(), valid); QCOMPARE(partial.isComplete(), complete); QCOMPARE(partial.mimeType, mimeType); QVERIFY(partial.codec != nullptr); QVERIFY(codec != nullptr); QCOMPARE(partial.codec->mibEnum(), codec->mibEnum()); } void tst_LanguageServerProtocol::baseMessageToData_data() { QTest::addColumn("message"); QTest::addColumn("data"); QTest::newRow("empty") << BaseMessage(defaultMimeType, "") << QByteArray("Content-Length: 0\r\n" "\r\n"); QTest::newRow("content") << BaseMessage(defaultMimeType, "foo") << QByteArray("Content-Length: 3\r\n" "\r\n" "foo"); QTest::newRow("custom mime type") << BaseMessage("text/x-python", "") << QByteArray("Content-Length: 0\r\n" "Content-Type: text/x-python; charset=UTF-8\r\n" "\r\n"); QTextCodec *codec = QTextCodec::codecForName("iso-8859-1"); QTest::newRow("custom mime type and codec") << BaseMessage("text/x-python", "", 0, codec) << QByteArray("Content-Length: 0\r\n" "Content-Type: text/x-python; charset=ISO-8859-1\r\n" "\r\n"); QTest::newRow("custom codec") << BaseMessage(defaultMimeType, "", 0, codec) << QByteArray("Content-Length: 0\r\n" "Content-Type: application/vscode-jsonrpc; charset=ISO-8859-1\r\n" "\r\n"); } void tst_LanguageServerProtocol::baseMessageToData() { QFETCH(BaseMessage, message); QFETCH(QByteArray, data); QCOMPARE(message.toData(), data); } void tst_LanguageServerProtocol::fromJsonValue() { const QString strVal("foobar"); QCOMPARE(LanguageServerProtocol::fromJsonValue(QJsonValue(strVal)), strVal); const int intVal = 42; QCOMPARE(LanguageServerProtocol::fromJsonValue(QJsonValue(intVal)), intVal); const double doubleVal = 4.2; QCOMPARE(LanguageServerProtocol::fromJsonValue(QJsonValue(doubleVal)), doubleVal); const bool boolVal = false; QCOMPARE(LanguageServerProtocol::fromJsonValue(QJsonValue(boolVal)), boolVal); const QJsonArray array = QJsonArray::fromStringList({"foo", "bar"}); QCOMPARE(LanguageServerProtocol::fromJsonValue(array), array); QJsonObject object; object.insert("asd", "foo"); QCOMPARE(LanguageServerProtocol::fromJsonValue(object), object); } void tst_LanguageServerProtocol::toJsonObject_data() { QTest::addColumn("content"); QTest::addColumn("codec"); QTest::addColumn("error"); QTest::addColumn("expected"); QJsonObject tstObject; tstObject.insert("jsonrpc", "2.0"); QTest::newRow("empty") << QByteArray("") << defaultCodec << false << QJsonObject(); QTest::newRow("garbage") << QByteArray("foobar") << defaultCodec << true << QJsonObject(); QTest::newRow("empty object") << QByteArray("{}") << defaultCodec << false << QJsonObject(); QTest::newRow("object") << QByteArray(R"({"jsonrpc": "2.0"})") << defaultCodec << false << tstObject; QTextCodec *codec = QTextCodec::codecForName("iso-8859-1"); QJsonObject tstCodecObject; tstCodecObject.insert("foo", QString::fromLatin1("b\xe4r")); QTest::newRow("object88591") << QByteArray("{\"foo\": \"b\xe4r\"}") << codec << false << tstCodecObject; QTest::newRow("object and garbage") << QByteArray(R"({"jsonrpc": "2.0"} foobar)") << defaultCodec << true << QJsonObject(); // TODO can be improved QTest::newRow("empty array") << QByteArray("[]") << defaultCodec << true << QJsonObject(); QTest::newRow("null") << QByteArray("null") << defaultCodec << true << QJsonObject(); } void tst_LanguageServerProtocol::toJsonObject() { QFETCH(QByteArray, content); QFETCH(QTextCodec *, codec); QFETCH(bool, error); QFETCH(QJsonObject, expected); QString parseError; const QJsonObject object = JsonRpcMessageHandler::toJsonObject(content, codec, parseError); if (!error && !parseError.isEmpty()) QFAIL(parseError.toLocal8Bit().data()); QCOMPARE(object, expected); QCOMPARE(!parseError.isEmpty(), error); } void tst_LanguageServerProtocol::jsonMessageToBaseMessage_data() { QTest::addColumn("jsonMessage"); QTest::addColumn("baseMessage"); QTest::newRow("empty object") << JsonRpcMessage(QJsonObject()) << BaseMessage(JsonRpcMessageHandler::jsonRpcMimeType(), "{}"); QTest::newRow("key value pair") << JsonRpcMessage({{"key", "value"}}) << BaseMessage(JsonRpcMessageHandler::jsonRpcMimeType(), R"({"key":"value"})"); } void tst_LanguageServerProtocol::jsonMessageToBaseMessage() { QFETCH(JsonRpcMessage, jsonMessage); QFETCH(BaseMessage, baseMessage); QCOMPARE(jsonMessage.toBaseMessage(), baseMessage); } class JsonTestObject : public JsonObject { public: using JsonObject::JsonObject; using JsonObject::insert; using JsonObject::value; using JsonObject::contains; using JsonObject::find; using JsonObject::end; using JsonObject::remove; using JsonObject::keys; using JsonObject::typedValue; using JsonObject::optionalValue; using JsonObject::clientValue; using JsonObject::optionalClientValue; using JsonObject::array; using JsonObject::optionalArray; using JsonObject::clientArray; using JsonObject::optionalClientArray; using JsonObject::insertArray; using JsonObject::checkKey; using JsonObject::valueTypeString; using JsonObject::check; using JsonObject::checkType; using JsonObject::checkVal; using JsonObject::checkArray; using JsonObject::checkOptional; using JsonObject::checkOptionalArray; using JsonObject::errorString; using JsonObject::operator==; }; void tst_LanguageServerProtocol::jsonObject() { JsonTestObject obj; obj.insert("integer", 42); obj.insert("double", 42.42); obj.insert("bool", false); obj.insert("null", QJsonValue::Null); obj.insert("string", "foobar"); obj.insertArray("strings", QStringList{"foo", "bar"}); const JsonTestObject innerObj(obj); obj.insert("object", innerObj); QCOMPARE(obj.value("integer"), QJsonValue(42)); QCOMPARE(obj.value("double"), QJsonValue(42.42)); QCOMPARE(obj.value("bool"), QJsonValue(false)); QCOMPARE(obj.value("null"), QJsonValue(QJsonValue::Null)); QCOMPARE(obj.value("string"), QJsonValue("foobar")); QCOMPARE(obj.value("strings"), QJsonValue(QJsonArray({"foo", "bar"}))); QCOMPARE(obj.value("object"), QJsonValue(QJsonObject(innerObj))); QCOMPARE(obj.typedValue("integer"), 42); QCOMPARE(obj.typedValue("double"), 42.42); QCOMPARE(obj.typedValue("bool"), false); QCOMPARE(obj.typedValue("string"), QString("foobar")); QCOMPARE(obj.typedValue("object"), innerObj); QVERIFY(!obj.optionalValue("doesNotExist").has_value()); QVERIFY(obj.optionalValue("integer").has_value()); QCOMPARE(obj.optionalValue("integer").value_or(0), 42); QVERIFY(obj.clientValue("null").isNull()); QVERIFY(!obj.clientValue("integer").isNull()); QCOMPARE(obj.clientValue("integer").value(), 42); QVERIFY(!obj.optionalClientValue("doesNotExist").has_value()); QVERIFY(obj.optionalClientValue("null").has_value()); QVERIFY(obj.optionalClientValue("null").value().isNull()); QVERIFY(obj.optionalClientValue("integer").has_value()); QVERIFY(!obj.optionalClientValue("integer").value().isNull()); QCOMPARE(obj.optionalClientValue("integer").value().value(0), 42); QCOMPARE(obj.array("strings"), QList({"foo", "bar"})); QVERIFY(!obj.optionalArray("doesNotExist").has_value()); QVERIFY(obj.optionalArray("strings").has_value()); QCOMPARE(obj.optionalArray("strings").value_or(QList()), QList({"foo", "bar"})); QVERIFY(obj.clientArray("null").isNull()); QVERIFY(!obj.clientArray("strings").isNull()); QCOMPARE(obj.clientArray("strings").toList(), QList({"foo", "bar"})); QVERIFY(!obj.optionalClientArray("doesNotExist").has_value()); QVERIFY(obj.optionalClientArray("null").has_value()); QVERIFY(obj.optionalClientArray("null").value().isNull()); QVERIFY(obj.optionalClientArray("strings").has_value()); QVERIFY(!obj.optionalClientArray("strings").value().isNull()); QCOMPARE(obj.optionalClientArray("strings").value().toList(), QList({"foo", "bar"})); ErrorHierarchy errorHierarchy; QVERIFY(!obj.check(&errorHierarchy, "doesNotExist")); ErrorHierarchy errorDoesNotExists; errorDoesNotExists.setError(obj.errorString(QJsonValue::Double, QJsonValue::Undefined)); errorDoesNotExists.prependMember("doesNotExist"); QCOMPARE(errorHierarchy, errorDoesNotExists); errorHierarchy.clear(); QVERIFY(!obj.check(&errorHierarchy, "bool")); ErrorHierarchy errorWrongType; errorWrongType.setError(obj.errorString(QJsonValue::Double, QJsonValue::Bool)); errorWrongType.prependMember("bool"); QCOMPARE(errorHierarchy, errorWrongType); errorHierarchy.clear(); QVERIFY(obj.check(&errorHierarchy, "integer")); QVERIFY(errorHierarchy.isEmpty()); QVERIFY(obj.check(&errorHierarchy, "double")); QVERIFY(errorHierarchy.isEmpty()); QVERIFY(obj.check(&errorHierarchy, "bool")); QVERIFY(errorHierarchy.isEmpty()); QVERIFY(obj.check(&errorHierarchy, "null")); QVERIFY(errorHierarchy.isEmpty()); QVERIFY(obj.check(&errorHierarchy, "string")); QVERIFY(errorHierarchy.isEmpty()); } void tst_LanguageServerProtocol::documentUri_data() { QTest::addColumn("uri"); QTest::addColumn("isValid"); QTest::addColumn("fileName"); QTest::addColumn("string"); // '/' (fs root) is part of the file path const QString filePrefix = Utils::HostOsInfo::isWindowsHost() ? QString("file:///") : QString("file://"); QTest::newRow("empty uri") << DocumentUri() << false << Utils::FilePath() << QString(); QTest::newRow("home dir") << DocumentUri::fromFilePath(Utils::FilePath::fromString(QDir::homePath())) << true << Utils::FilePath::fromUserInput(QDir::homePath()) << QString(filePrefix + QDir::homePath()); const QString argv0 = QFileInfo(qApp->arguments().first()).absoluteFilePath(); const auto argv0FileName = Utils::FilePath::fromUserInput(argv0); QTest::newRow("argv0 file name") << DocumentUri::fromFilePath(argv0FileName) << true << argv0FileName << QString(filePrefix + QDir::fromNativeSeparators(argv0)); QTest::newRow("http") << DocumentUri::fromProtocol("https://www.qt.io/") << true << Utils::FilePath() << "https://www.qt.io/"; // depending on the OS the resulting path is different (made suitable for the file system) const QString winUserPercent("file:///C%3A/Users/"); const QString winUser = Utils::HostOsInfo::isWindowsHost() ? QString("C:\\Users\\") : QString("/C:/Users/"); QTest::newRow("percent encoding") << DocumentUri::fromProtocol(winUserPercent) << true << Utils::FilePath::fromUserInput(winUser) << QString(filePrefix + QDir::fromNativeSeparators(winUser)); } void tst_LanguageServerProtocol::documentUri() { QFETCH(DocumentUri, uri); QFETCH(bool, isValid); QFETCH(Utils::FilePath, fileName); QFETCH(QString, string); QCOMPARE(uri.isValid(), isValid); QCOMPARE(uri.toFilePath(), fileName); QCOMPARE(uri.toString(), string); } QTEST_MAIN(tst_LanguageServerProtocol) #include "tst_languageserverprotocol.moc"