Skip to content

Commit 6662913

Browse files
authored
Add tests for LSP test discovery (#916)
* Add tests for LSP test discovery Adds comprehensive tests for LSP test discovery. Removes the capabilities cache since all it was doing was short circuiting a nested property lookup. * Address comments
1 parent e27ba74 commit 6662913

File tree

3 files changed

+243
-23
lines changed

3 files changed

+243
-23
lines changed

src/TestExplorer/LSPTestDiscovery.ts

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,26 @@ import {
1919
textDocumentTestsRequest,
2020
workspaceTestsRequest,
2121
} from "../sourcekit-lsp/lspExtensions";
22-
import { LanguageClientManager } from "../sourcekit-lsp/LanguageClientManager";
23-
import { LanguageClient } from "vscode-languageclient/node";
22+
import { InitializeResult, RequestType } from "vscode-languageclient/node";
2423
import { SwiftPackage, TargetType } from "../SwiftPackage";
24+
import { Converter } from "vscode-languageclient/lib/common/protocolConverter";
25+
26+
interface ILanguageClient {
27+
get initializeResult(): InitializeResult | undefined;
28+
get protocol2CodeConverter(): Converter;
29+
30+
sendRequest<P, R, E>(
31+
type: RequestType<P, R, E>,
32+
params: P,
33+
token?: vscode.CancellationToken
34+
): Promise<R>;
35+
}
36+
37+
interface ILanguageClientManager {
38+
useLanguageClient<Return>(process: {
39+
(client: ILanguageClient, cancellationToken: vscode.CancellationToken): Promise<Return>;
40+
}): Promise<Return>;
41+
}
2542

2643
/**
2744
* Used to augment test discovery via `swift test --list-tests`.
@@ -34,7 +51,7 @@ import { SwiftPackage, TargetType } from "../SwiftPackage";
3451
export class LSPTestDiscovery {
3552
private capCache = new Map<string, boolean>();
3653

37-
constructor(private languageClient: LanguageClientManager) {}
54+
constructor(private languageClient: ILanguageClientManager) {}
3855

3956
/**
4057
* Return a list of tests in the supplied document.
@@ -82,43 +99,34 @@ export class LSPTestDiscovery {
8299
* above the supplied `minVersion`.
83100
*/
84101
private checkExperimentalCapability(
85-
client: LanguageClient,
102+
client: ILanguageClient,
86103
method: string,
87104
minVersion: number
88105
) {
89-
const capKey = `${method}:${minVersion}`;
90-
const cachedCap = this.capCache.get(capKey);
91-
if (cachedCap !== undefined) {
92-
return cachedCap;
93-
}
94-
95106
const experimentalCapability = client.initializeResult?.capabilities.experimental;
96107
if (!experimentalCapability) {
97108
throw new Error(`${method} requests not supported`);
98109
}
99110
const targetCapability = experimentalCapability[method];
100-
const canUse = (targetCapability?.version ?? -1) >= minVersion;
101-
this.capCache.set(capKey, canUse);
102-
return canUse;
111+
return (targetCapability?.version ?? -1) >= minVersion;
103112
}
104113

105114
/**
106115
* Convert from `LSPTestItem[]` to `TestDiscovery.TestClass[]`,
107116
* updating the format of the location.
108117
*/
109118
private transformToTestClass(
110-
client: LanguageClient,
119+
client: ILanguageClient,
111120
swiftPackage: SwiftPackage,
112121
input: LSPTestItem[]
113122
): TestDiscovery.TestClass[] {
114123
return input.map(item => {
115124
const location = client.protocol2CodeConverter.asLocation(item.location);
116-
const id = this.transformId(item, location, swiftPackage);
117125
return {
118126
...item,
119-
id: id,
120-
location: location,
127+
id: this.transformId(item, location, swiftPackage),
121128
children: this.transformToTestClass(client, swiftPackage, item.children),
129+
location,
122130
};
123131
});
124132
}
@@ -141,10 +149,6 @@ export class LSPTestDiscovery {
141149
.find(target => swiftPackage.getTarget(location.uri.fsPath) === target);
142150

143151
const id = target !== undefined ? `${target.c99name}.${item.id}` : item.id;
144-
if (item.style === "XCTest") {
145-
return id.replace(/\(\)$/, "");
146-
} else {
147-
return id;
148-
}
152+
return item.style === "XCTest" ? id.replace(/\(\)$/, "") : id;
149153
}
150154
}

src/sourcekit-lsp/lspExtensions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export interface LSPTestItem {
110110
*
111111
* For a test suite, this may contain the individual test cases or nested suites.
112112
*/
113-
children: [LSPTestItem];
113+
children: LSPTestItem[];
114114

115115
/**
116116
* Tags associated with this test item.
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the VS Code Swift open source project
4+
//
5+
// Copyright (c) 2024 the VS Code Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of VS Code Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import * as assert from "assert";
16+
import * as vscode from "vscode";
17+
import * as ls from "vscode-languageserver-protocol";
18+
import * as p2c from "vscode-languageclient/lib/common/protocolConverter";
19+
import { beforeEach } from "mocha";
20+
import { InitializeResult, RequestType } from "vscode-languageclient";
21+
import { LSPTestDiscovery } from "../../../src/TestExplorer/LSPTestDiscovery";
22+
import { SwiftPackage, Target, TargetType } from "../../../src/SwiftPackage";
23+
import { TestClass } from "../../../src/TestExplorer/TestDiscovery";
24+
import { SwiftToolchain } from "../../../src/toolchain/toolchain";
25+
import {
26+
LSPTestItem,
27+
textDocumentTestsRequest,
28+
workspaceTestsRequest,
29+
} from "../../../src/sourcekit-lsp/lspExtensions";
30+
31+
class TestLanguageClient {
32+
private responses = new Map<string, unknown>();
33+
private responseVersions = new Map<string, number>();
34+
35+
setResponse<P, R, E>(type: RequestType<P, R, E>, response: R) {
36+
this.responses.set(type.method, response);
37+
}
38+
39+
setResponseVersion<P, R, E>(type: RequestType<P, R, E>, version: number) {
40+
this.responseVersions.set(type.method, version);
41+
}
42+
43+
get initializeResult(): InitializeResult | undefined {
44+
return {
45+
capabilities: {
46+
experimental: {
47+
"textDocument/tests": {
48+
version: this.responseVersions.get("textDocument/tests") ?? 999,
49+
},
50+
"workspace/tests": {
51+
version: this.responseVersions.get("workspace/tests") ?? 999,
52+
},
53+
},
54+
},
55+
};
56+
}
57+
get protocol2CodeConverter(): p2c.Converter {
58+
return p2c.createConverter(undefined, true, true);
59+
}
60+
61+
sendRequest<P, R, E>(type: RequestType<P, R, E>): Promise<R> {
62+
const response = this.responses.get(type.method) as R | undefined;
63+
return response ? Promise.resolve(response) : Promise.reject("Method not implemented");
64+
}
65+
}
66+
67+
suite("LSPTestDiscovery Suite", () => {
68+
let client: TestLanguageClient;
69+
let discoverer: LSPTestDiscovery;
70+
let pkg: SwiftPackage;
71+
const file = vscode.Uri.file("file:///some/file.swift");
72+
73+
beforeEach(async () => {
74+
pkg = await SwiftPackage.create(file, await SwiftToolchain.create());
75+
client = new TestLanguageClient();
76+
discoverer = new LSPTestDiscovery({
77+
useLanguageClient(process) {
78+
return process(client, new vscode.CancellationTokenSource().token);
79+
},
80+
});
81+
});
82+
83+
suite("Empty responses", () => {
84+
test(textDocumentTestsRequest.method, async () => {
85+
client.setResponse(textDocumentTestsRequest, []);
86+
87+
const testClasses = await discoverer.getDocumentTests(pkg, file);
88+
89+
assert.deepStrictEqual(testClasses, []);
90+
});
91+
92+
test(workspaceTestsRequest.method, async () => {
93+
client.setResponse(workspaceTestsRequest, []);
94+
95+
const testClasses = await discoverer.getWorkspaceTests(pkg);
96+
97+
assert.deepStrictEqual(testClasses, []);
98+
});
99+
});
100+
101+
suite("Unsupported LSP version", () => {
102+
test(textDocumentTestsRequest.method, async () => {
103+
client.setResponseVersion(textDocumentTestsRequest, 0);
104+
105+
await assert.rejects(() => discoverer.getDocumentTests(pkg, file));
106+
});
107+
108+
test(workspaceTestsRequest.method, async () => {
109+
client.setResponseVersion(workspaceTestsRequest, 0);
110+
111+
await assert.rejects(() => discoverer.getWorkspaceTests(pkg));
112+
});
113+
114+
test("missing experimental capabiltity", async () => {
115+
Object.defineProperty(client, "initializeResult", {
116+
get: () => ({ capabilities: {} }),
117+
});
118+
119+
await assert.rejects(() => discoverer.getWorkspaceTests(pkg));
120+
});
121+
122+
test("missing specific capability", async () => {
123+
Object.defineProperty(client, "initializeResult", {
124+
get: () => ({ capabilities: { experimental: {} } }),
125+
});
126+
127+
await assert.rejects(() => discoverer.getWorkspaceTests(pkg));
128+
});
129+
});
130+
131+
suite("Non empty responses", () => {
132+
let items: LSPTestItem[];
133+
let expected: TestClass[];
134+
135+
beforeEach(() => {
136+
items = [
137+
{
138+
id: "topLevelTest()",
139+
label: "topLevelTest()",
140+
disabled: false,
141+
style: "swift-testing",
142+
tags: [],
143+
location: ls.Location.create(
144+
file.fsPath,
145+
ls.Range.create(ls.Position.create(1, 0), ls.Position.create(2, 0))
146+
),
147+
children: [],
148+
},
149+
];
150+
151+
expected = items.map(item => ({
152+
...item,
153+
location: client.protocol2CodeConverter.asLocation(item.location),
154+
children: [],
155+
}));
156+
});
157+
158+
test(textDocumentTestsRequest.method, async () => {
159+
client.setResponse(textDocumentTestsRequest, items);
160+
161+
const testClasses = await discoverer.getDocumentTests(pkg, file);
162+
163+
assert.deepStrictEqual(testClasses, expected);
164+
});
165+
166+
test(workspaceTestsRequest.method, async () => {
167+
client.setResponse(workspaceTestsRequest, items);
168+
169+
const testClasses = await discoverer.getWorkspaceTests(pkg);
170+
171+
assert.deepStrictEqual(testClasses, expected);
172+
});
173+
174+
test("converts LSP XCTest IDs", async () => {
175+
items = items.map(item => ({ ...item, style: "XCTest" }));
176+
expected = expected.map(item => ({
177+
...item,
178+
id: "topLevelTest",
179+
style: "XCTest",
180+
}));
181+
182+
client.setResponse(workspaceTestsRequest, items);
183+
184+
const testClasses = await discoverer.getWorkspaceTests(pkg);
185+
186+
assert.deepStrictEqual(testClasses, expected);
187+
});
188+
189+
test("Prepends test target to ID", async () => {
190+
const testTargetName = "TestTargetC99Name";
191+
expected = expected.map(item => ({
192+
...item,
193+
id: `${testTargetName}.topLevelTest()`,
194+
}));
195+
196+
client.setResponse(workspaceTestsRequest, items);
197+
198+
const target: Target = {
199+
c99name: testTargetName,
200+
name: testTargetName,
201+
path: file.fsPath,
202+
type: TargetType.test,
203+
sources: [],
204+
};
205+
pkg.getTargets = () => [target];
206+
pkg.getTarget = () => target;
207+
208+
const testClasses = await discoverer.getWorkspaceTests(pkg);
209+
210+
assert.deepStrictEqual(
211+
testClasses.map(({ id }) => id),
212+
expected.map(({ id }) => id)
213+
);
214+
});
215+
});
216+
});

0 commit comments

Comments
 (0)