From 1146920bc9ed9bef2b5bb91b3cdec4700eb09881 Mon Sep 17 00:00:00 2001 From: Yong He Date: Wed, 8 Jun 2022 11:54:27 -0700 Subject: Add smoke test for language server. (#2266) --- tools/slang-test/slang-test-main.cpp | 273 +++++++++++++++++++++++++++++++++++ tools/slang-test/test-context.cpp | 24 +++ tools/slang-test/test-context.h | 1 + 3 files changed, 298 insertions(+) (limited to 'tools') diff --git a/tools/slang-test/slang-test-main.cpp b/tools/slang-test/slang-test-main.cpp index f0d92b810..753ae5569 100644 --- a/tools/slang-test/slang-test-main.cpp +++ b/tools/slang-test/slang-test-main.cpp @@ -31,6 +31,7 @@ #include "../../source/compiler-core/slang-downstream-compiler.h" #include "../../source/compiler-core/slang-nvrtc-compiler.h" +#include "../../source/compiler-core/slang-language-server-protocol.h" #define STB_IMAGE_IMPLEMENTATION #include "external/stb/stb_image.h" @@ -1329,6 +1330,276 @@ TestResult runDocTest(TestContext* context, TestInput& input) return result; } +TestResult runLanguageServerTest(TestContext* context, TestInput& input) +{ + RefPtr connection; + if (SLANG_FAILED(context->createLanguageServerJSONRPCConnection(connection))) + { + return TestResult::Fail; + } + if (context->isCollectingRequirements()) + { + connection->sendCall(LanguageServerProtocol::ExitParams::methodName, JSONValue::makeInt(0)); + return TestResult::Pass; + } + LanguageServerProtocol::InitializeParams initParams; + LanguageServerProtocol::WorkspaceFolder wsFolder; + wsFolder.name = "test"; + String fullPath; + Path::getCanonical(input.filePath, fullPath); + wsFolder.uri = URI::fromLocalFilePath(Path::getParentDirectory(fullPath).getUnownedSlice()).uri; + initParams.workspaceFolders.add(wsFolder); + if (SLANG_FAILED(connection->sendCall( + LanguageServerProtocol::InitializeParams::methodName, &initParams, JSONValue::makeInt(0)))) + { + return TestResult::Fail; + } + if (SLANG_FAILED(connection->waitForResult(-1))) + { + return TestResult::Fail; + } + + LanguageServerProtocol::InitializeResult initResult; + if (SLANG_FAILED(connection->getMessage(&initResult))) + { + return TestResult::Fail; + } + + // Send open document call. + String testFileContent; + + if (SLANG_FAILED(File::readAllText(input.filePath, testFileContent))) + { + return TestResult::Fail; + } + + LanguageServerProtocol::DidOpenTextDocumentParams openDocParams; + openDocParams.textDocument.version = 0; + openDocParams.textDocument.uri = URI::fromLocalFilePath(fullPath.getUnownedSlice()).uri; + openDocParams.textDocument.text = testFileContent; + connection->sendCall( + LanguageServerProtocol::DidOpenTextDocumentParams::methodName, + &openDocParams, + JSONValue::makeInt(1)); + List diagnostics; + bool diagnosticsReceived = false; + auto waitForNonDiagnosticResponse = [&]() -> SlangResult + { + repeat: + if (SLANG_FAILED(connection->waitForResult(10000))) + return SLANG_FAIL; + if (connection->getMessageType() == JSONRPCMessageType::Call) + { + JSONRPCCall call; + connection->getRPC(&call); + if (call.method == "textDocument/publishDiagnostics") + { + diagnosticsReceived = true; + LanguageServerProtocol::PublishDiagnosticsParams arg; + if (SLANG_FAILED(connection->getMessage(&arg))) + return SLANG_FAIL; + diagnostics.add(arg); + goto repeat; + } + } + return SLANG_OK; + }; + + List lines; + StringUtil::calcLines(testFileContent.getUnownedSlice(), lines); + + StringBuilder actualOutputSB; + auto parseLocation = [&](UnownedStringSlice text, Index startPos, Int& linePos, Int& colPos) + { + linePos = StringUtil::parseIntAndAdvancePos(text.trimStart(), startPos); + startPos++; + colPos = StringUtil::parseIntAndAdvancePos(text.trimStart(), startPos); + return startPos; + }; + int callId = 2; + for (auto line : lines) + { + if (line.startsWith("//COMPLETE:")) + { + auto arg = line.tail(UnownedStringSlice("//COMPLETE:").getLength()); + Int linePos, colPos; + parseLocation(arg, 0, linePos, colPos); + + LanguageServerProtocol::CompletionParams params; + params.position.line = int(linePos - 1); + params.position.character = int(colPos - 1); + params.textDocument.uri = openDocParams.textDocument.uri; + if (SLANG_FAILED(connection->sendCall( + LanguageServerProtocol::CompletionParams::methodName, + ¶ms, + JSONValue::makeInt(callId++)))) + { + return TestResult::Fail; + } + if (SLANG_FAILED(waitForNonDiagnosticResponse())) + return TestResult::Fail; + actualOutputSB << "--------\n"; + LanguageServerProtocol::NullResponse nullResponse; + List completionItems; + if (SLANG_SUCCEEDED(connection->getMessage(&nullResponse))) + { + actualOutputSB << "null\n"; + } + else if (SLANG_SUCCEEDED(connection->getMessage(&completionItems))) + { + for (auto item : completionItems) + { + actualOutputSB << item.label << ": " << item.kind << " " << item.detail << " "; + for (auto ch : item.commitCharacters) + actualOutputSB << ch; + actualOutputSB << "\n"; + } + } + } + else if (line.startsWith("//SIGNATURE:")) + { + auto arg = line.tail(UnownedStringSlice("//SIGNATURE:").getLength()); + Int linePos, colPos; + parseLocation(arg, 0, linePos, colPos); + + LanguageServerProtocol::SignatureHelpParams params; + params.position.line = int(linePos - 1); + params.position.character = int(colPos - 1); + params.textDocument.uri = openDocParams.textDocument.uri; + if (SLANG_FAILED(connection->sendCall( + LanguageServerProtocol::SignatureHelpParams::methodName, + ¶ms, + JSONValue::makeInt(callId++)))) + { + return TestResult::Fail; + } + if (SLANG_FAILED(waitForNonDiagnosticResponse())) + return TestResult::Fail; + actualOutputSB << "--------\n"; + LanguageServerProtocol::NullResponse nullResponse; + LanguageServerProtocol::SignatureHelp sigInfo; + if (SLANG_SUCCEEDED(connection->getMessage(&nullResponse))) + { + actualOutputSB << "null\n"; + } + else if (SLANG_SUCCEEDED(connection->getMessage(&sigInfo))) + { + actualOutputSB << "activeParameter: " << sigInfo.activeParameter << "\n"; + actualOutputSB << "activeSignature: " << sigInfo.activeSignature << "\n"; + for (auto item : sigInfo.signatures) + { + actualOutputSB << item.label << ":"; + for (auto param : item.parameters) + { + actualOutputSB << " (" << param.label[0] << "," << param.label[1] << ")"; + } + actualOutputSB << "\n"; + actualOutputSB << item.documentation.value << "\n"; + } + } + } + else if (line.startsWith("//HOVER:")) + { + auto arg = line.tail(UnownedStringSlice("//HOVER:").getLength()); + Int linePos, colPos; + parseLocation(arg, 0, linePos, colPos); + + LanguageServerProtocol::HoverParams params; + params.position.line = int(linePos - 1); + params.position.character = int(colPos - 1); + params.textDocument.uri = openDocParams.textDocument.uri; + if (SLANG_FAILED(connection->sendCall( + LanguageServerProtocol::HoverParams::methodName, + ¶ms, + JSONValue::makeInt(callId++)))) + { + return TestResult::Fail; + } + if (SLANG_FAILED(waitForNonDiagnosticResponse())) + return TestResult::Fail; + actualOutputSB << "--------\n"; + LanguageServerProtocol::NullResponse nullResponse; + LanguageServerProtocol::Hover hover; + if (SLANG_SUCCEEDED(connection->getMessage(&nullResponse))) + { + actualOutputSB << "null\n"; + } + else if (SLANG_SUCCEEDED(connection->getMessage(&hover))) + { + actualOutputSB << "range: " << hover.range.start.line << "," + << hover.range.start.character << " - " << hover.range.end.line + << "," << hover.range.end.character; + actualOutputSB << "\ncontent:\n" << hover.contents.value << "\n"; + } + } + else if (line.startsWith("//DIAGNOSTICS")) + { + if (!diagnosticsReceived) + { + waitForNonDiagnosticResponse(); + } + actualOutputSB << "--------\n"; + for (auto item : diagnostics) + { + actualOutputSB << item.uri << "\n"; + for (auto msg : item.diagnostics) + { + actualOutputSB << msg.range.start.line << "," << msg.range.start.character + << "-" << msg.range.end.line << "," << msg.range.end.character + << " " << msg.message; + } + } + } + } + connection->sendCall(LanguageServerProtocol::ExitParams::methodName, JSONValue::makeInt(0)); + + auto outputStem = input.outputStem; + String expectedOutputPath = outputStem + ".expected.txt"; + String expectedOutput; + + Slang::File::readAllText(expectedOutputPath, expectedOutput); + expectedOutput = expectedOutput.trim(); + + TestResult result = TestResult::Pass; + + auto actualOutput = actualOutputSB.ProduceString(); + + // Redact absolute file names from actualOutput + List outputLines; + StringUtil::calcLines(actualOutput.getUnownedSlice(), outputLines); + StringBuilder redactedSB; + for (auto line : outputLines) + { + Index extIdx = line.indexOf(UnownedStringSlice(".slang")); + if (extIdx == -1) + { + redactedSB << line << "\n"; + continue; + } + redactedSB << "{REDACTED}" << line.tail(extIdx) << "\n"; + } + + actualOutput = redactedSB.ProduceString().trim(); + + if (!_areResultsEqual(input.testOptions->type, expectedOutput, actualOutput)) + { + context->getTestReporter()->dumpOutputDifference(expectedOutput, actualOutput); + result = TestResult::Fail; + } + + // If the test failed, then we write the actual output to a file + // so that we can easily diff it from the command line and + // diagnose the problem. + if (result == TestResult::Fail) + { + String actualOutputPath = outputStem + ".actual"; + Slang::File::writeAllText(actualOutputPath, actualOutput); + + context->getTestReporter()->dumpOutputDifference(expectedOutput, actualOutput); + } + return result; +} + TestResult runSimpleTest(TestContext* context, TestInput& input) { // need to execute the stand-alone Slang compiler on the file, and compare its output to what we expect @@ -2985,6 +3256,8 @@ static const TestCommandInfo s_testCommandInfos[] = { "PERFORMANCE_PROFILE", &runPerformanceProfile, 0 }, { "COMPILE", &runCompile, 0 }, { "DOC", &runDocTest, 0 }, + { "LANG_SERVER", &runLanguageServerTest, 0}, + }; const TestCommandInfo* _findTestCommandInfoByCommand(const UnownedStringSlice& name) diff --git a/tools/slang-test/test-context.cpp b/tools/slang-test/test-context.cpp index cdafbc30d..6bf7406e6 100644 --- a/tools/slang-test/test-context.cpp +++ b/tools/slang-test/test-context.cpp @@ -171,6 +171,30 @@ SlangResult TestContext::_createJSONRPCConnection(RefPtr& out return SLANG_OK; } +SlangResult TestContext::createLanguageServerJSONRPCConnection(RefPtr& out) +{ + RefPtr process; + + { + CommandLine cmdLine; + cmdLine.setExecutableLocation(ExecutableLocation(exeDirectoryPath, "slangd")); + SLANG_RETURN_ON_FAIL(Process::create(cmdLine, Process::Flag::AttachDebugger, process)); + } + + Stream* writeStream = process->getStream(StdStreamType::In); + RefPtr readStream( + new BufferedReadStream(process->getStream(StdStreamType::Out))); + + RefPtr connection = new HTTPPacketConnection(readStream, writeStream); + RefPtr rpcConnection = new JSONRPCConnection; + + SLANG_RETURN_ON_FAIL( + rpcConnection->init(connection, JSONRPCConnection::CallStyle::Object, process)); + + out = rpcConnection; + + return SLANG_OK; +} void TestContext::destroyRPCConnection() { diff --git a/tools/slang-test/test-context.h b/tools/slang-test/test-context.h index adb3e5ad2..284206efb 100644 --- a/tools/slang-test/test-context.h +++ b/tools/slang-test/test-context.h @@ -162,6 +162,7 @@ class TestContext void setTestReporter(TestReporter* reporter); TestReporter* getTestReporter(); + SlangResult createLanguageServerJSONRPCConnection(Slang::RefPtr& out); std::mutex mutex; -- cgit v1.2.3