Skip to content

Commit 65f61f8

Browse files
authored
Ensure test group exists before trying to add examples (#2275)
1 parent 7733b8d commit 65f61f8

2 files changed

Lines changed: 96 additions & 39 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import * as assert from "assert";
2+
3+
import * as vscode from "vscode";
4+
import { CodeLens } from "vscode-languageclient/node";
5+
6+
import { TestController } from "../../testController";
7+
import { Command } from "../../common";
8+
9+
import { FAKE_TELEMETRY } from "./fakeTelemetry";
10+
11+
suite("TestController", () => {
12+
const context = {
13+
extensionMode: vscode.ExtensionMode.Test,
14+
subscriptions: [],
15+
workspaceState: {
16+
get: (_name: string) => undefined,
17+
update: (_name: string, _value: any) => Promise.resolve(),
18+
},
19+
} as unknown as vscode.ExtensionContext;
20+
21+
test("createTestItems doesn't break when there's a missing group", () => {
22+
const controller = new TestController(
23+
context,
24+
FAKE_TELEMETRY,
25+
() => undefined,
26+
);
27+
28+
const codeLensItems: CodeLens[] = [
29+
{
30+
range: new vscode.Range(0, 0, 10, 10),
31+
command: {
32+
title: "Run",
33+
command: Command.RunTest,
34+
arguments: [
35+
"test/fake_test.rb",
36+
"test_do_something",
37+
"bundle exec ruby -Itest test/fake_test.rb --name FakeTest#test_do_something",
38+
{
39+
/* eslint-disable @typescript-eslint/naming-convention */
40+
start_line: 0,
41+
start_column: 0,
42+
end_line: 10,
43+
end_column: 10,
44+
/* eslint-enable @typescript-eslint/naming-convention */
45+
},
46+
],
47+
},
48+
data: {
49+
type: "test",
50+
// eslint-disable-next-line @typescript-eslint/naming-convention
51+
group_id: 100,
52+
kind: "example",
53+
},
54+
},
55+
];
56+
57+
assert.doesNotThrow(() => {
58+
controller.createTestItems(codeLensItems);
59+
});
60+
});
61+
});

vscode/src/testController.ts

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ import { Workspace } from "./workspace";
88

99
const asyncExec = promisify(exec);
1010

11+
interface CodeLensData {
12+
type: string;
13+
// eslint-disable-next-line @typescript-eslint/naming-convention
14+
group_id: number;
15+
id?: number;
16+
kind: string;
17+
}
18+
1119
export class TestController {
1220
private readonly testController: vscode.TestController;
1321
private readonly testCommands: WeakMap<vscode.TestItem, string>;
@@ -72,8 +80,7 @@ export class TestController {
7280
this.testCommands.delete(test);
7381
});
7482

75-
const groupIdMap: Record<string, vscode.TestItem> = {};
76-
let classTest: vscode.TestItem;
83+
const groupIdMap: Map<number, vscode.TestItem> = new Map();
7784

7885
const uri = vscode.Uri.from({
7986
scheme: "file",
@@ -88,12 +95,9 @@ export class TestController {
8895
uri,
8996
);
9097

91-
if (res.data?.kind) {
92-
testItem.tags = [new vscode.TestTag(res.data.kind)];
93-
} else if (name.startsWith("test_")) {
94-
// Older Ruby LSP versions may not include 'kind' so we try infer it from the name.
95-
testItem.tags = [new vscode.TestTag("example")];
96-
}
98+
const data: CodeLensData = res.data;
99+
100+
testItem.tags = [new vscode.TestTag(data.kind)];
97101

98102
this.testCommands.set(testItem, command);
99103

@@ -102,40 +106,32 @@ export class TestController {
102106
new vscode.Position(location.end_line, location.end_column),
103107
);
104108

105-
// If the test has a group_id, the server supports code lens hierarchy
106-
if ("group_id" in res.data) {
107-
// If it has an id, it's a group
108-
if (res.data?.id) {
109-
// Add group to the map
110-
groupIdMap[res.data.id] = testItem;
111-
testItem.canResolveChildren = true;
112-
113-
if (res.data.group_id) {
114-
// Add nested group to its parent group
115-
groupIdMap[res.data.group_id].children.add(testItem);
116-
} else {
117-
// Or add it to the top-level
118-
this.testController.items.add(testItem);
119-
}
120-
// Otherwise, it's a test
121-
} else {
122-
// Add test to its parent group
123-
groupIdMap[res.data.group_id].children.add(testItem);
124-
testItem.tags = [...testItem.tags, this.debugTag];
125-
}
126-
// If the server doesn't support code lens hierarchy, all groups are top-level
109+
// If it has an id, it's a group. Otherwise, it's a test example
110+
if (data.id) {
111+
// Add group to the map
112+
groupIdMap.set(data.id, testItem);
113+
testItem.canResolveChildren = true;
127114
} else {
128-
// Add test methods as children to the test class so it appears nested in Test explorer
129-
// and running the test class will run all of the test methods
130-
// eslint-disable-next-line no-lonely-if
131-
if (testItem.tags.find((tag) => tag.id === "example")) {
132-
testItem.tags = [...testItem.tags, this.debugTag];
133-
classTest.children.add(testItem);
115+
// Set example tags
116+
testItem.tags = [...testItem.tags, this.debugTag];
117+
}
118+
119+
// Examples always have a `group_id`. Groups may or may not have it
120+
if (data.group_id) {
121+
// Add nested group to its parent group
122+
const group = groupIdMap.get(data.group_id);
123+
124+
// If there's a mistake on the server or in an addon, a code lens may be produced for a non-existing group
125+
if (group) {
126+
group.children.add(testItem);
134127
} else {
135-
classTest = testItem;
136-
classTest.canResolveChildren = true;
137-
this.testController.items.add(testItem);
128+
this.currentWorkspace()?.outputChannel.error(
129+
`Test example "${name}" is attached to group_id ${data.group_id}, but that group does not exist`,
130+
);
138131
}
132+
} else {
133+
// Or add it to the top-level
134+
this.testController.items.add(testItem);
139135
}
140136
});
141137
}

0 commit comments

Comments
 (0)