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

support parallel test execution #260

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@
"default": {},
"description": "Specify the list of additional environment variables used for running tests."
},
"mesonbuild.testJobs": {
"type": "integer",
"default": -1,
"minimum": -1,
"description": "Specify the maximum number of tests executed in parallel. -1 for number of CPUs, 0 for unlimited."
},
"mesonbuild.benchmarkOptions": {
"type": "array",
"default": [
Expand Down
142 changes: 110 additions & 32 deletions src/tests.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
import * as os from "os";
import * as vscode from "vscode";
import { ExecResult, exec, extensionConfiguration } from "./utils";
import { Tests, DebugEnvironmentConfiguration } from "./types";
import { ExecResult, exec, extensionConfiguration, getTargetName } from "./utils";
import { Targets, Test, Tests, DebugEnvironmentConfiguration } from "./types";
import { getMesonTests, getMesonTargets } from "./introspection";
import { workspaceState } from "./extension";

// this is far from complete, but should suffice for the
// "test is made of a single executable is made of a single source file" usecase.
function findSourceOfTest(test: Test, targets: Targets): vscode.Uri | undefined {
const test_exe = test.cmd.at(0);
if (!test_exe) {
return undefined;
}

// the meson target such that it is of meson type executable()
// and produces the binary that the test() executes.
const testDependencyTarget = targets.find((target) => {
const depend = test.depends.find((depend) => {
return depend == target.id && target.type == "executable";
});
return depend && test_exe == target.filename.at(0);
});

// the first source file belonging to the target.
const path = testDependencyTarget?.target_sources
?.find((elem) => {
return elem.sources;
})
?.sources?.at(0);
return path ? vscode.Uri.file(path) : undefined;
}

export async function rebuildTests(controller: vscode.TestController) {
let tests = await getMesonTests(workspaceState.get<string>("mesonbuild.buildDir")!);
const buildDir = workspaceState.get<string>("mesonbuild.buildDir")!;
const tests = await getMesonTests(buildDir);
const targets = await getMesonTargets(buildDir);

controller.items.forEach((item) => {
if (!tests.some((test) => item.id == test.name)) {
Expand All @@ -14,7 +43,8 @@ export async function rebuildTests(controller: vscode.TestController) {
});

for (let testDescr of tests) {
let testItem = controller.createTestItem(testDescr.name, testDescr.name);
const testSourceFile = findSourceOfTest(testDescr, targets);
const testItem = controller.createTestItem(testDescr.name, testDescr.name, testSourceFile);
controller.items.add(testItem);
}
}
Expand All @@ -25,39 +55,87 @@ export async function testRunHandler(
token: vscode.CancellationToken,
) {
const run = controller.createTestRun(request, undefined, false);
const queue: vscode.TestItem[] = [];
const parallelTests: vscode.TestItem[] = [];
const sequentialTests: vscode.TestItem[] = [];

const buildDir = workspaceState.get<string>("mesonbuild.buildDir")!;
const mesonTests = await getMesonTests(buildDir);

function testAdder(test: vscode.TestItem) {
const mesonTest = mesonTests.find((mesonTest) => {
return mesonTest.name == test.id;
})!;
if (mesonTest.is_parallel) {
parallelTests.push(test);
} else {
sequentialTests.push(test);
}
// this way the total number of runs shows up from the beginning,
// instead of incrementing as individual runs finish
run.enqueued(test);
}
if (request.include) {
request.include.forEach((test) => queue.push(test));
request.include.forEach(testAdder);
} else {
controller.items.forEach((test) => queue.push(test));
controller.items.forEach(testAdder);
}

const buildDir = workspaceState.get<string>("mesonbuild.buildDir")!;

for (let test of queue) {
function dispatchTest(test: vscode.TestItem) {
run.started(test);
let starttime = Date.now();
try {
await exec(
extensionConfiguration("mesonPath"),
["test", "-C", buildDir, "--print-errorlog", `"${test.id}"`],
extensionConfiguration("testEnvironment"),
);
let duration = Date.now() - starttime;
run.passed(test, duration);
} catch (e) {
const execResult = e as ExecResult;

run.appendOutput(execResult.stdout);
let duration = Date.now() - starttime;
if (execResult.error?.code == 125) {
vscode.window.showErrorMessage("Failed to build tests. Results will not be updated");
run.errored(test, new vscode.TestMessage(execResult.stderr));
} else {
run.failed(test, new vscode.TestMessage(execResult.stderr), duration);
}
return exec(
extensionConfiguration("mesonPath"),
["test", "-C", buildDir, "--print-errorlog", `"${test.id}"`],
extensionConfiguration("testEnvironment"),
).then(
(onfulfilled) => {
run.passed(test, onfulfilled.time_ms);
},
(onrejected) => {
const execResult = onrejected as ExecResult;

let stdout = execResult.stdout;
if (os.platform() != "win32") {
stdout = stdout.replace(/\n/g, "\r\n");
}
run.appendOutput(stdout, undefined, test);
if (execResult.error?.code == 125) {
vscode.window.showErrorMessage("Failed to build tests. Results will not be updated");
run.errored(test, new vscode.TestMessage(execResult.stderr));
} else {
run.failed(test, new vscode.TestMessage(execResult.stderr), execResult.time_ms);
}
},
);
}

const running_tests: Promise<void>[] = [];
const max_running: number = (() => {
const jobs_config = extensionConfiguration("testJobs");
switch (jobs_config) {
case -1:
return os.cpus().length;
case 0:
return Number.MAX_SAFE_INTEGER;
default:
return jobs_config;
}
})();

for (const test of parallelTests) {
const running_test = dispatchTest(test).finally(() => {
running_tests.splice(running_tests.indexOf(running_test), 1);
});

running_tests.push(running_test);

if (running_tests.length >= max_running) {
await Promise.race(running_tests);
}
}
await Promise.all(running_tests);

for (const test of sequentialTests) {
await dispatchTest(test);
}

run.end();
Expand Down Expand Up @@ -89,8 +167,8 @@ export async function testDebugHandler(
);

let args = ["compile", "-C", buildDir];
requiredTargets.forEach((target) => {
args.push(target.name);
requiredTargets.forEach(async (target) => {
args.push(await getTargetName(target));
});

try {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface ExtensionConfiguration {
setupOptions: string[];
testOptions: string[];
testEnvironment: { [key: string]: string };
testJobs: number;
benchmarkOptions: string[];
buildFolder: string;
mesonPath: string;
Expand Down
11 changes: 8 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { extensionPath, workspaceState } from "./extension";
export interface ExecResult {
stdout: string;
stderr: string;
time_ms: number; // runtime in milliseconds
error?: cp.ExecFileException;
}

Expand All @@ -32,11 +33,13 @@ export async function exec(
options.env = { ...(options.env ?? process.env), ...extraEnv };
}
return new Promise<ExecResult>((resolve, reject) => {
let time_ms = Date.now();
cp.execFile(command, args, options, (error, stdout, stderr) => {
time_ms = Date.now() - time_ms;
if (error) {
reject({ error, stdout, stderr });
reject({ stdout, stderr, time: time_ms, error });
} else {
resolve({ stdout, stderr });
resolve({ stdout, stderr, time_ms });
}
});
});
Expand All @@ -49,8 +52,10 @@ export async function execFeed(
stdin: string,
) {
return new Promise<ExecResult>((resolve) => {
let time = Date.now();
const p = cp.execFile(command, args, options, (error, stdout, stderr) => {
resolve({ stdout, stderr, error: error ? error : undefined });
time = Date.now() - time;
resolve({ stdout, stderr, time_ms: time, error: error ?? undefined });
});

p.stdin?.write(stdin);
Expand Down
Loading