blob: de41d2a57eca1ee7df0b91afb9dd94dc9a1a4c7c [file] [log] [blame]
import * as vscode from "vscode";
import type * as lc from "vscode-languageclient/node";
import * as ra from "./lsp_ext";
import type { Ctx } from "./ctx";
import { startDebugSession } from "./debug";
export const prepareTestExplorer = (
ctx: Ctx,
testController: vscode.TestController,
client: lc.LanguageClient,
) => {
let currentTestRun: vscode.TestRun | undefined;
let idToTestMap: Map<string, vscode.TestItem> = new Map();
const fileToTestMap: Map<string, vscode.TestItem[]> = new Map();
const idToRunnableMap: Map<string, ra.Runnable> = new Map();
testController.createRunProfile(
"Run Tests",
vscode.TestRunProfileKind.Run,
async (request: vscode.TestRunRequest, cancelToken: vscode.CancellationToken) => {
if (currentTestRun) {
await client.sendNotification(ra.abortRunTest);
while (currentTestRun) {
await new Promise((resolve) => setTimeout(resolve, 1));
}
}
currentTestRun = testController.createTestRun(request);
cancelToken.onCancellationRequested(async () => {
await client.sendNotification(ra.abortRunTest);
});
const include = request.include?.map((x) => x.id);
const exclude = request.exclude?.map((x) => x.id);
await client.sendRequest(ra.runTest, { include, exclude });
},
true,
undefined,
false,
);
testController.createRunProfile(
"Debug Tests",
vscode.TestRunProfileKind.Debug,
async (request: vscode.TestRunRequest) => {
if (request.include?.length !== 1 || request.exclude?.length !== 0) {
await vscode.window.showErrorMessage("You can debug only one test at a time");
return;
}
const id = request.include[0]!.id;
const runnable = idToRunnableMap.get(id);
if (!runnable) {
await vscode.window.showErrorMessage("You can debug only one test at a time");
return;
}
await startDebugSession(ctx, runnable);
},
true,
undefined,
false,
);
const deleteTest = (item: vscode.TestItem, parentList: vscode.TestItemCollection) => {
parentList.delete(item.id);
idToTestMap.delete(item.id);
idToRunnableMap.delete(item.id);
if (item.uri) {
fileToTestMap.set(
item.uri.toString(),
fileToTestMap.get(item.uri.toString())!.filter((t) => t.id !== item.id),
);
}
};
const addTest = (item: ra.TestItem) => {
const parentList = item.parent
? idToTestMap.get(item.parent)!.children
: testController.items;
const oldTest = parentList.get(item.id);
const uri = item.textDocument?.uri ? vscode.Uri.parse(item.textDocument?.uri) : undefined;
const range =
item.range &&
new vscode.Range(
new vscode.Position(item.range.start.line, item.range.start.character),
new vscode.Position(item.range.end.line, item.range.end.character),
);
if (oldTest) {
if (oldTest.uri?.toString() === uri?.toString()) {
oldTest.range = range;
return;
}
deleteTest(oldTest, parentList);
}
const iconToVscodeMap = {
package: "package",
module: "symbol-module",
test: "beaker",
};
const test = testController.createTestItem(
item.id,
`$(${iconToVscodeMap[item.kind]}) ${item.label}`,
uri,
);
test.range = range;
test.canResolveChildren = item.canResolveChildren;
idToTestMap.set(item.id, test);
if (uri) {
if (!fileToTestMap.has(uri.toString())) {
fileToTestMap.set(uri.toString(), []);
}
fileToTestMap.get(uri.toString())!.push(test);
}
if (item.runnable) {
idToRunnableMap.set(item.id, item.runnable);
}
parentList.add(test);
};
const addTestGroup = (testsAndScope: ra.DiscoverTestResults) => {
const { tests, scope, scopeFile } = testsAndScope;
const testSet: Set<string> = new Set();
for (const test of tests) {
addTest(test);
testSet.add(test.id);
}
// FIXME(hack_recover_crate_name): We eagerly resolve every test if we got a lazy top level response (detected
// by checking that `scope` is empty). This is not a good thing and wastes cpu and memory unnecessarily, so we
// should remove it.
if (!scope && !scopeFile) {
for (const test of tests) {
void testController.resolveHandler!(idToTestMap.get(test.id));
}
}
if (scope) {
const recursivelyRemove = (tests: vscode.TestItemCollection) => {
for (const [_, test] of tests) {
if (!testSet.has(test.id)) {
deleteTest(test, tests);
} else {
recursivelyRemove(test.children);
}
}
};
for (const root of scope) {
recursivelyRemove(idToTestMap.get(root)!.children);
}
}
if (scopeFile) {
const removeByFile = (file: vscode.Uri) => {
const testsToBeRemoved = (fileToTestMap.get(file.toString()) || []).filter(
(t) => !testSet.has(t.id),
);
for (const test of testsToBeRemoved) {
const parentList = test.parent?.children || testController.items;
deleteTest(test, parentList);
}
};
for (const file of scopeFile) {
removeByFile(vscode.Uri.parse(file.uri));
}
}
};
ctx.pushClientCleanup(
client.onNotification(ra.discoveredTests, (results) => {
addTestGroup(results);
}),
);
ctx.pushClientCleanup(
client.onNotification(ra.endRunTest, () => {
currentTestRun!.end();
currentTestRun = undefined;
}),
);
ctx.pushClientCleanup(
client.onNotification(ra.appendOutputToRunTest, (output) => {
currentTestRun!.appendOutput(`${output}\r\n`);
}),
);
ctx.pushClientCleanup(
client.onNotification(ra.changeTestState, (results) => {
const test = idToTestMap.get(results.testId)!;
if (results.state.tag === "failed") {
currentTestRun!.failed(test, new vscode.TestMessage(results.state.message));
} else if (results.state.tag === "passed") {
currentTestRun!.passed(test);
} else if (results.state.tag === "started") {
currentTestRun!.started(test);
} else if (results.state.tag === "skipped") {
currentTestRun!.skipped(test);
} else if (results.state.tag === "enqueued") {
currentTestRun!.enqueued(test);
}
}),
);
testController.resolveHandler = async (item) => {
const results = await client.sendRequest(ra.discoverTest, { testId: item?.id });
addTestGroup(results);
};
testController.refreshHandler = async () => {
testController.items.forEach((t) => {
testController.items.delete(t.id);
});
idToTestMap = new Map();
await testController.resolveHandler!(undefined);
};
};