Skip to content

Commit 8727445

Browse files
authored
Add a framework for LSP testing. (#4841)
Also adds tests for exit and initialize, just basic things.
1 parent 809c532 commit 8727445

File tree

13 files changed

+255
-36
lines changed

13 files changed

+255
-36
lines changed

testing/file_test/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ they have an associated error. An exception is that the main test file may omit
8686
8787
Some keywords can be inserted for content:
8888
89+
- ```
90+
[[@LSP:<method>:<extra content>]]
91+
```
92+
93+
Produces JSON for an LSP method call, complete with `Content-Length` header.
94+
8995
- ```
9096
[[@TEST_NAME]]
9197
```

testing/file_test/file_test_base.cpp

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,16 @@ auto FileTestBase::ProcessTestFileAndRun(TestContext& context)
344344
llvm::PrettyStackTraceProgram stack_trace_entry(
345345
test_argv_for_stack_trace.size() - 1, test_argv_for_stack_trace.data());
346346

347+
// Execution must be serialized for either serial tests or console output.
348+
std::unique_lock<std::mutex> output_lock;
349+
if (output_mutex_ &&
350+
(context.capture_console_output || !AllowParallelRun())) {
351+
output_lock = std::unique_lock<std::mutex>(*output_mutex_);
352+
}
353+
347354
// Conditionally capture console output. We use a scope exit to ensure the
348355
// captures terminate even on run failures.
349-
std::unique_lock<std::mutex> output_lock;
350356
if (context.capture_console_output) {
351-
if (output_mutex_) {
352-
output_lock = std::unique_lock<std::mutex>(*output_mutex_);
353-
}
354357
CaptureStderr();
355358
CaptureStdout();
356359
}
@@ -514,6 +517,84 @@ struct SplitState {
514517
int file_index = 0;
515518
};
516519

520+
// Replaces the keyword at the given position. Returns the position to start a
521+
// find for the next keyword.
522+
static auto ReplaceContentKeywordAt(std::string* content, size_t keyword_pos,
523+
llvm::StringRef test_name, int* lsp_id)
524+
-> ErrorOr<size_t> {
525+
auto keyword = llvm::StringRef(*content).substr(keyword_pos);
526+
527+
// Line replacements aren't handled here.
528+
static constexpr llvm::StringLiteral Line = "[[@LINE";
529+
if (keyword.starts_with(Line)) {
530+
// Just move past the prefix to find the next one.
531+
return keyword_pos + Line.size();
532+
}
533+
534+
// Replaced with the actual test name.
535+
static constexpr llvm::StringLiteral TestName = "[[@TEST_NAME]]";
536+
if (keyword.starts_with(TestName)) {
537+
content->replace(keyword_pos, TestName.size(), test_name);
538+
return keyword_pos + test_name.size();
539+
}
540+
541+
// Reformatted as an LSP call with headers.
542+
static constexpr llvm::StringLiteral Lsp = "[[@LSP:";
543+
if (keyword.starts_with(Lsp)) {
544+
auto method_start = keyword_pos + Lsp.size();
545+
546+
static constexpr llvm::StringLiteral LspEnd = "]]";
547+
auto keyword_end = content->find("]]", method_start);
548+
if (keyword_end == std::string::npos) {
549+
return ErrorBuilder()
550+
<< "Missing `" << LspEnd << "` after `" << Lsp << "`";
551+
}
552+
553+
auto method_end = content->find(":", method_start);
554+
auto extra_content_start = method_end + 1;
555+
if (method_end == std::string::npos || method_end > keyword_end) {
556+
method_end = keyword_end;
557+
extra_content_start = keyword_end;
558+
}
559+
auto method = content->substr(method_start, method_end - method_start);
560+
561+
auto extra_content =
562+
content->substr(extra_content_start, keyword_end - extra_content_start);
563+
std::string extra_content_sep;
564+
if (!extra_content.empty()) {
565+
extra_content_sep = ",";
566+
if (!extra_content.starts_with("\n")) {
567+
extra_content_sep += " ";
568+
}
569+
}
570+
571+
// Form the JSON.
572+
std::string json;
573+
if (method == "exit") {
574+
if (!extra_content.empty()) {
575+
return Error("`[[@LSP:exit:` cannot include extra content");
576+
}
577+
json = R"({"jsonrpc": "2.0", "method": "exit"})";
578+
} else {
579+
json = llvm::formatv(
580+
R"({{"jsonrpc": "2.0", "id": "{0}", "method": "{1}"{2}{3}})",
581+
++(*lsp_id), method, extra_content_sep, extra_content)
582+
.str();
583+
}
584+
// Add the Content-Length header. The `2` accounts for extra newlines.
585+
auto json_with_header =
586+
llvm::formatv("Content-Length: {0}\n\n{1}\n", json.size() + 2, json)
587+
.str();
588+
// Insert the content.
589+
content->replace(keyword_pos, keyword_end + 2 - keyword_pos,
590+
json_with_header);
591+
return keyword_pos + json_with_header.size();
592+
}
593+
594+
return ErrorBuilder() << "Unexpected use of `[[@` at `"
595+
<< keyword.substr(0, 5) << "`";
596+
}
597+
517598
// Replaces the content keywords.
518599
//
519600
// TEST_NAME is the only content keyword at present, but we do validate that
@@ -543,20 +624,13 @@ static auto ReplaceContentKeywords(llvm::StringRef filename,
543624
test_name.consume_front("fail_");
544625
test_name.consume_front("todo_");
545626

627+
// A counter for LSP calls.
628+
int lsp_id = 0;
546629
while (keyword_pos != std::string::npos) {
547-
static constexpr llvm::StringLiteral TestName = "[[@TEST_NAME]]";
548-
auto keyword = llvm::StringRef(*content).substr(keyword_pos);
549-
if (keyword.starts_with(TestName)) {
550-
content->replace(keyword_pos, TestName.size(), test_name);
551-
keyword_pos += test_name.size();
552-
} else if (keyword.starts_with("[[@LINE")) {
553-
// Just move past the prefix to find the next one.
554-
keyword_pos += Prefix.size();
555-
} else {
556-
return ErrorBuilder()
557-
<< "Unexpected use of `[[@` at `" << keyword.substr(0, 5) << "`";
558-
}
559-
keyword_pos = content->find(Prefix, keyword_pos);
630+
CARBON_ASSIGN_OR_RETURN(
631+
auto keyword_end,
632+
ReplaceContentKeywordAt(content, keyword_pos, test_name, &lsp_id));
633+
keyword_pos = content->find(Prefix, keyword_end);
560634
}
561635
return Success();
562636
}

testing/file_test/file_test_base.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ class FileTestBase : public testing::Test {
116116
// Optionally allows children to provide extra replacements for autoupdate.
117117
virtual auto DoExtraCheckReplacements(std::string& /*check_line*/) -> void {}
118118

119+
// Whether to allow running the test in parallel, particularly for autoupdate.
120+
// This can be overridden to force some tests to be run serially. At any given
121+
// time, all parallel tests and a single non-parallel test will be allowed to
122+
// run.
123+
virtual auto AllowParallelRun() const -> bool { return true; }
124+
119125
// Runs a test and compares output. This keeps output split by line so that
120126
// issues are a little easier to identify by the different line.
121127
auto TestBody() -> void final;

testing/file_test/file_test_base_test.cpp

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -198,21 +198,6 @@ static auto TestEscaping(TestParams& params)
198198
return {{.success = true}};
199199
}
200200

201-
// Prints and returns expected results for stdin.carbon.
202-
static auto TestStdin(TestParams& params)
203-
-> ErrorOr<FileTestBaseTest::RunResult> {
204-
CARBON_CHECK(params.input_stream);
205-
constexpr int ReadSize = 256;
206-
char buf[ReadSize];
207-
while (feof(params.input_stream) == 0) {
208-
auto read = fread(&buf, sizeof(char), ReadSize, params.input_stream);
209-
if (read > 0) {
210-
params.error_stream.write(buf, read);
211-
}
212-
}
213-
return {{.success = true}};
214-
}
215-
216201
// Prints and returns expected results for unattached_multi_file.carbon.
217202
static auto TestUnattachedMultiFile(TestParams& params)
218203
-> ErrorOr<FileTestBaseTest::RunResult> {
@@ -258,6 +243,17 @@ static auto EchoFileContent(TestParams& params)
258243
buffer = remainder;
259244
}
260245
}
246+
if (params.input_stream) {
247+
params.error_stream << "--- STDIN:\n";
248+
constexpr int ReadSize = 1024;
249+
char buf[ReadSize];
250+
while (feof(params.input_stream) == 0) {
251+
auto read = fread(&buf, sizeof(char), ReadSize, params.input_stream);
252+
if (read > 0) {
253+
params.error_stream.write(buf, read);
254+
}
255+
}
256+
}
261257
return {{.success = true}};
262258
}
263259

@@ -287,8 +283,6 @@ auto FileTestBaseTest::Run(
287283
.Case("file_only_re_one_file.carbon", &TestFileOnlyREOneFile)
288284
.Case("file_only_re_multi_file.carbon", &TestFileOnlyREMultiFile)
289285
.Case("no_line_number.carbon", &TestNoLineNumber)
290-
.Case("stdin.carbon", &TestStdin)
291-
.Case("stdin_and_autoupdate_split.carbon", &TestStdin)
292286
.Case("unattached_multi_file.carbon", &TestUnattachedMultiFile)
293287
.Case("fail_multi_success_overall_fail.carbon",
294288
[&](TestParams&) {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
2+
// Exceptions. See /LICENSE for license information.
3+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
5+
// AUTOUPDATE
6+
// TIP: To test this file alone, run:
7+
// TIP: bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/lsp_calls.carbon
8+
// TIP: To dump output, run:
9+
// TIP: bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/lsp_calls.carbon
10+
11+
// --- STDIN
12+
[[@LSP:foo:]]
13+
[[@LSP:foo]]
14+
[[@LSP:bar:content]]
15+
[[@LSP:baz:
16+
multi
17+
line
18+
]]
19+
[[@LSP:exit]]
20+
21+
// --- AUTOUPDATE-SPLIT
22+
23+
// CHECK:STDERR: --- STDIN:
24+
// CHECK:STDERR: Content-Length: 48
25+
// CHECK:STDERR:
26+
// CHECK:STDERR: {"jsonrpc": "2.0", "id": "1", "method": "foo"}
27+
// CHECK:STDERR:
28+
// CHECK:STDERR: Content-Length: 48
29+
// CHECK:STDERR:
30+
// CHECK:STDERR: {"jsonrpc": "2.0", "id": "2", "method": "foo"}
31+
// CHECK:STDERR:
32+
// CHECK:STDERR: Content-Length: 57
33+
// CHECK:STDERR:
34+
// CHECK:STDERR: {"jsonrpc": "2.0", "id": "3", "method": "bar", content}
35+
// CHECK:STDERR:
36+
// CHECK:STDERR: Content-Length: 61
37+
// CHECK:STDERR:
38+
// CHECK:STDERR: {"jsonrpc": "2.0", "id": "4", "method": "baz",
39+
// CHECK:STDERR: multi
40+
// CHECK:STDERR: line
41+
// CHECK:STDERR: }
42+
// CHECK:STDERR:
43+
// CHECK:STDERR: Content-Length: 38
44+
// CHECK:STDERR:
45+
// CHECK:STDERR: {"jsonrpc": "2.0", "method": "exit"}
46+
// CHECK:STDERR:
47+
// CHECK:STDERR:
48+
// CHECK:STDOUT: 1 args: `default_args`

testing/file_test/testdata/stdin.carbon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ n
2020

2121
// --- AUTOUPDATE-SPLIT
2222

23+
// CHECK:STDERR: --- STDIN:
2324
// CHECK:STDERR:
2425
// CHECK:STDERR: S
2526
// CHECK:STDERR: t

toolchain/language_server/BUILD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ load("@rules_cc//cc:defs.bzl", "cc_library")
66

77
package(default_visibility = ["//visibility:public"])
88

9+
filegroup(
10+
name = "testdata",
11+
data = glob(["testdata/**/*.carbon"]),
12+
)
13+
914
cc_library(
1015
name = "language_server",
1116
srcs = ["language_server.cpp"],

toolchain/language_server/language_server.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "clang-tools-extra/clangd/LSPBinder.h"
88
#include "clang-tools-extra/clangd/Transport.h"
9+
#include "clang-tools-extra/clangd/support/Logger.h"
910
#include "common/raw_string_ostream.h"
1011
#include "toolchain/language_server/context.h"
1112
#include "toolchain/language_server/incoming_messages.h"
@@ -14,7 +15,12 @@
1415
namespace Carbon::LanguageServer {
1516

1617
auto Run(FILE* input_stream, llvm::raw_ostream& output_stream,
17-
llvm::raw_ostream& /*error_stream*/) -> ErrorOr<Success> {
18+
llvm::raw_ostream& error_stream) -> ErrorOr<Success> {
19+
// TODO: Consider implementing a custom logger that splits vlog to
20+
// vlog_stream when provided. For now, this disables verbose logging.
21+
clang::clangd::StreamLogger logger(error_stream, clang::clangd::Logger::Info);
22+
clang::clangd::LoggingSession logging_session(logger);
23+
1824
// Set up the connection.
1925
std::unique_ptr<clang::clangd::Transport> transport(
2026
clang::clangd::newJSONTransport(input_stream, output_stream,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
2+
// Exceptions. See /LICENSE for license information.
3+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
//
5+
// AUTOUPDATE
6+
// TIP: To test this file alone, run:
7+
// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/exit.carbon
8+
// TIP: To dump output, run:
9+
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/exit.carbon
10+
11+
// --- STDIN
12+
[[@LSP:exit]]
13+
14+
// --- AUTOUPDATE-SPLIT
15+
16+
// CHECK:STDOUT:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
2+
// Exceptions. See /LICENSE for license information.
3+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4+
//
5+
// AUTOUPDATE
6+
// TIP: To test this file alone, run:
7+
// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/fail_empty_stdin.carbon
8+
// TIP: To dump output, run:
9+
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/fail_empty_stdin.carbon
10+
11+
// --- STDIN
12+
// --- AUTOUPDATE-SPLIT
13+
14+
// CHECK:STDERR: error: Input/output error
15+
// CHECK:STDERR:
16+
// CHECK:STDOUT:

0 commit comments

Comments
 (0)