Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] Use VSCode test system #353

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,920 changes: 1,894 additions & 26 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"license": "MIT",
"icon": "testexplorer_dark.png",
"engines": {
"vscode": "^1.45.1"
"vscode": "^1.64.0"
},
"categories": [
"Programming Languages"
Expand Down Expand Up @@ -301,11 +301,11 @@
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.5",
"@types/sinon": "^9.0.4",
"@types/vscode": "^1.45.1",
"@types/vscode": "^1.64.0",
"mocha": "^7.1.2",
"sinon": "^9.0.2",
"tslint": "^6.1.2",
"typescript": "^3.9.3",
"typescript": "^4.5.5",
"vscode-test": "^1.3.0"
},
"dependencies": {
Expand All @@ -317,4 +317,4 @@
"extensionDependencies": [
"ms-dotnettools.csharp"
]
}
}
6 changes: 3 additions & 3 deletions src/buildTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export function buildTree(parsedNames: IParsedName[]): ITestTreeNode {
for (let i = 0; i < parsedName.segments.length - 1; i++) {
const segment = parsedName.segments[i];

const part = parsedName.fullName.substr(segment.start, segment.end - segment.start);
const fullName = parsedName.fullName.substr(0, segment.end);
const part = parsedName.fullName.slice(segment.start, segment.end);
const fullName = parsedName.fullName.slice(0, segment.end);
if (!currentNode.subTrees.has(part)) {
const newTree: ITestTreeNode = {
fullName,
Expand All @@ -36,7 +36,7 @@ export function buildTree(parsedNames: IParsedName[]): ITestTreeNode {
}

const lastSegment = parsedName.segments[parsedName.segments.length - 1];
const testName = parsedName.fullName.substr(lastSegment.start, lastSegment.end - lastSegment.start);
const testName = parsedName.fullName.slice(lastSegment.start, lastSegment.end);
currentNode.tests.push(testName);
}
return root;
Expand Down
8 changes: 5 additions & 3 deletions src/executor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"use strict";
import { ChildProcess, exec } from "child_process";
import { ChildProcess, exec, ExecException } from "child_process";
import { platform } from "os";
import * as vscode from "vscode";
import { Debug, IDebugRunnerInfo } from "./debug";
import { Logger } from "./logger";

type ExecCallback = (error: ExecException | null, stdOut: string, stdErr: string) => void;

export class Executor {

public static runInTerminal(command: string, cwd?: string, addNewLine: boolean = true, terminal: string = ".NET Test Explorer"): void {
Expand All @@ -18,7 +20,7 @@ export class Executor {
this.terminals[terminal].sendText(command, addNewLine);
}

public static exec(command: string, callback, cwd?: string, addToProcessList?: boolean) {
public static exec(command: string, callback: ExecCallback, cwd?: string, addToProcessList?: boolean) {
// DOTNET_CLI_UI_LANGUAGE does not seem to be respected when passing it as a parameter to the exec
// function so we set the variable here instead
process.env.DOTNET_CLI_UI_LANGUAGE = "en";
Expand All @@ -45,7 +47,7 @@ export class Executor {
return childProcess;
}

public static debug(command: string, callback, cwd?: string, addToProcessList?: boolean) {
public static debug(command: string, callback: ExecCallback, cwd?: string, addToProcessList?: boolean) {
// DOTNET_CLI_UI_LANGUAGE does not seem to be respected when passing it as a parameter to the exec
// function so we set the variable here instead
process.env.DOTNET_CLI_UI_LANGUAGE = "en";
Expand Down
168 changes: 167 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
"use strict";

import { exec, execFile } from "child_process";
import { mkdtemp, rmdir, unlink } from "fs/promises";
import { tmpdir } from "os";
import path = require("path");
import * as vscode from "vscode";
import { AppInsights } from "./appInsights";
import { AppInsightsClient } from "./appInsightsClient";
import { buildTree, ITestTreeNode, mergeSingleItemTrees } from "./buildTree";
import { DotnetTestExplorer } from "./dotnetTestExplorer";
import { Executor } from "./executor";
import { FindTestInContext } from "./findTestInContext";
import { GotoTest } from "./gotoTest";
import { LeftClickTest } from "./leftClickTest";
import { Logger } from "./logger";
import { parseTestName } from "./parseTestName";
import { Problems } from "./problems";
import { StatusBar } from "./statusBar";
import { TestCommands } from "./testCommands";
import { TestDirectories } from "./testDirectories";
import { TestNode } from "./testNode";
import { parseResults } from "./testResultsFile";
import { TestStatusCodeLensProvider } from "./testStatusCodeLensProvider";
import { Utility } from "./utility";
import { Watch } from "./watch";
Expand All @@ -29,10 +36,169 @@ export function activate(context: vscode.ExtensionContext) {
const leftClickTest = new LeftClickTest();
const appInsights = new AppInsights(testCommands, testDirectories);

const controller = vscode.tests.createTestController("helloWorldTests", "Hello World Tests");

const env = {
...process.env,
DOTNET_CLI_UI_LANGUAGE: "en",
VSTEST_HOST_DEBUG: "0",
};

for (const folder of vscode.workspace.workspaceFolders) {
const options = {
cwd: folder.uri.fsPath,
env
}
execFile("dotnet", ["test", "--list-tests", "--verbosity=quiet"], options, (error, stdout, stderr) => {
console.log(stdout);
if (error) {
console.error(error);
// some error happened
// TODO: log it (properly)
return;
}

const lines = stdout.split(/\n\r?|\r/);
const rawTests = lines.filter(line => /^ /.test(line));
const parsedTestNames = rawTests.map(x => parseTestName(x.trim()));
// const rootTree = mergeSingleItemTrees(buildTree(parsedTestNames));
const rootTree = buildTree(parsedTestNames);

// convert the tree into tests
const generateNode = (tree: ITestTreeNode) => {
const treeNode = controller.createTestItem(tree.fullName, tree.name);
for (const subTree of tree.subTrees.values()) {
treeNode.children.add(generateNode(subTree));
}
for (const test of tree.tests) {
treeNode.children.add(controller.createTestItem(tree.fullName + "." + test, test));
}

return treeNode;
}

const rootNode = generateNode(rootTree);
rootNode.label = folder.name;
controller.items.add(rootNode);
});
}

controller.createRunProfile("Run", vscode.TestRunProfileKind.Run, async (request, token) => {
const run = controller.createTestRun(request, "My test run", true);
const wait = () => new Promise(resolve => setTimeout(resolve, 1000));

const createFilterArg = (item: vscode.TestItem, negate: boolean) => {
const fullMatch = item.children.size === 0;
const operator = (negate ? "!" : "") + (fullMatch ? "=" : "~");
const fullyQualifiedName = item.id.replaceAll(/\(.*\)/g, "");
return `FullyQualifiedName${operator}${fullyQualifiedName}`;
}

const includeFilters = request.include?.map(item => createFilterArg(item, false));
const excludeFilters = request.exclude.map(item => createFilterArg(item, true));

const toBeJoined = [...excludeFilters];
if (includeFilters) {
toBeJoined.push("(" + includeFilters.join("|") + ")")
}
const joinedFilters = toBeJoined.join("&");

const filterArgs = joinedFilters.length > 0 ? ["--filter", joinedFilters] : []
const resultsFolder = path.join(tmpdir(), await mkdtemp("test-explorer"));
const resultsFile = path.join(resultsFolder, "test-results.trx");
const loggerArgs = ["--logger", "trx;LogFileName=" + resultsFile]

try {
const output = await new Promise((resolve, reject) => execFile(
"dotnet", ["test", ...filterArgs, ...loggerArgs], {
env,
cwd: vscode.workspace.workspaceFolders[0].uri.fsPath
}, (error, stdOut, stdErr) => {
// if (error) reject(error);
// else
resolve(stdOut);
}));

const results = await parseResults(resultsFile);

for (const result of results) {
const parsedName = parseTestName(result.fullName);
let item = controller.items.get("");
for (const segment of parsedName.segments) {
const segmentString = parsedName.fullName.substring(0, segment.end);
item = item.children.get(segmentString);
if (item === undefined) {
// TODO: need to unfold folded items
console.error("no such test node:", result.fullName, result);
console.error("error at:", segmentString);
}
}
if (item === undefined) {
console.error("no such test:", result.fullName, result);
}
if (result.outcome === "Failed")
run.failed(item, { message: result.message });
else if (result.outcome === "NotExecuted")
run.skipped(item);
else if (result.outcome === "Passed")
run.passed(item);
else
console.log("unexpected value for outcome: " + result.outcome);
}
run.end();
}
finally {
// await unlink(resultsFile);
await rmdir(resultsFolder);
}
});

controller.createRunProfile("Debug", vscode.TestRunProfileKind.Debug, async (request, token) => {
const run = controller.createTestRun(request, "My test run", true);
const wait = () => new Promise(resolve => setTimeout(resolve, 1000));

let tests = request.include ?? controller.items;
tests.forEach(test => {
run.enqueued(test);
})
await wait();
tests.forEach(test => {
run.started(test);
})
await wait();
tests.forEach(test => {
run.passed(test);
});
run.end();
});

controller.createRunProfile("Watch", vscode.TestRunProfileKind.Run, async (request, token) => {
const run = controller.createTestRun(request, "My test run", true);
const wait = () => new Promise(resolve => setTimeout(resolve, 100));

let tests = request.include ?? controller.items;
while (!token.isCancellationRequested) {
tests.forEach(test => {
run.enqueued(test);
})
await wait();
tests.forEach(test => {
run.started(test);
})
await wait();
tests.forEach(test => {
run.passed(test);
});
await wait();
}
run.end();
});

Logger.Log("Starting extension");

testDirectories.parseTestDirectories();

context.subscriptions.push(controller);
context.subscriptions.push(problems);
context.subscriptions.push(statusBar);
context.subscriptions.push(testCommands);
Expand Down Expand Up @@ -64,7 +230,7 @@ export function activate(context: vscode.ExtensionContext) {
{ language: "csharp", scheme: "file" },
codeLensProvider));
context.subscriptions.push(vscode.languages.registerCodeLensProvider(
{language: "fsharp", scheme: "file" },
{ language: "fsharp", scheme: "file" },
codeLensProvider
));

Expand Down
7 changes: 3 additions & 4 deletions src/testCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,7 @@ export class TestCommands implements Disposable {
.window
.showErrorMessage("Build failed. Fix your build and try to run the test(s) again", "Re-run test(s)",)
.then(selection => {
if (selection !== undefined)
{
if (selection !== undefined) {
vscode.commands.executeCommand("dotnet-test-explorer.rerunLastCommand");
}
});;
Expand All @@ -228,7 +227,7 @@ export class TestCommands implements Disposable {
this.isRunning = false;
}

private runBuildCommandForSpecificDirectory(testDirectoryPath: string): Promise<any> {
private runBuildCommandForSpecificDirectory(testDirectoryPath: string): Promise<void> {
return new Promise((resolve, reject) => {

if (Utility.skipBuild) {
Expand All @@ -247,7 +246,7 @@ export class TestCommands implements Disposable {
});
}

private runTestCommandForSpecificDirectory(testDirectoryPath: string, testName: string, isSingleTest: boolean, index: number, debug?: boolean): Promise<any[]> {
private runTestCommandForSpecificDirectory(testDirectoryPath: string, testName: string, isSingleTest: boolean, index: number, debug?: boolean): Promise<void> {

const trxTestName = index + ".trx";

Expand Down
8 changes: 4 additions & 4 deletions src/testDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function executeDotnetTest(testDirectoryPath: string, dotnetTestOptions: string)

Logger.Log(`Executing ${command} in ${testDirectoryPath}`);

Executor.exec(command, (err: Error, stdout: string, stderr: string) => {
Executor.exec(command, (err, stdout, stderr) => {
if (err) {
Logger.LogError(`Error while executing ${command}`, stdout);

Expand Down Expand Up @@ -111,8 +111,8 @@ function extractAssemblyPaths(testCommandStdout: string): string[] {
do {
match = testRunLineRegex.exec(testCommandStdout);
if (match) {
const assemblyPath = match.find((capture, i) => capture && i != 0); // first capture group is the whole match
results.push(assemblyPath);
const assemblyPath = match.find((capture, i) => capture && i !== 0); // first capture group is the whole match
results.push(assemblyPath!);
}
}
while (match);
Expand Down Expand Up @@ -178,7 +178,7 @@ function executeDotnetVstest(assemblyPaths: string[], listTestsTargetPath: strin

Executor.exec(
command,
(err: Error, stdout: string, stderr: string) => {
(err, stdout, stderr) => {
if (err) {
Logger.LogError(`Error while executing ${command}.`, err);

Expand Down
2 changes: 1 addition & 1 deletion test/fsxunittests/FSharpTests.fsproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion test/mstest/MSTestTests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion test/nunit/NunitTests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading