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

Add an option to execute a task before running/debugging tests. #192

Closed
wants to merge 1 commit into from
Closed
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 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Not good enough for you?!: Edit your `.vscode/`[settings.json] file according to
| `test.runtimeLimit` | [seconds] Test executable is running in a process. In case of an infinite loop it will run forever unless this parameter is set. It applies instantly. (0 means infinite) |
| `test.parallelExecutionLimit` | Maximizes the number of the parallel test executions. (It applies instantly.) Note: If your executables depend on the **same resource** then this **could cause a problem**. |
| `test.parallelExecutionOfExecutableLimit` | Maximizes the number of the parallel execution of executables. To enable this just for specific executables use the `testMate.cpp.test.advancedExecutables` -> `parallelizationLimit`. The `testMate.cpp.test.parallelExecutionLimit` is a global limit and this is a local one. Note: If your **test cases** depend on the same resource then this **could cause a problem**. |
| `test.dependsOnTask` | Name of task to execute before running/debugging tests. The task should be defined like any other task in vscode (e.g. in tasks.json). If the task exits with a non-zero code, execution of tests will be halted. The same variables as in [debug.configTemplate] (except `${args*}`) will be substituted before running the task, with an additional one: `${execs}`, a string of quoted executable paths separated by spaces (e.g. `"/path/to/exec1" "/path/to/exec2"`). |
| `discovery.gracePeriodForMissing` | [seconds] Test executables are being watched (only inside the workspace directory). In case of one recompiles it will try to preserve the test states. If compilation reaches timeout it will drop the suite. |
| `discovery.retireDebounceLimit` | [milisec] Retire events will be held back for the given duration. (Reload is required) |
| `discovery.runtimeLimit` | [seconds] The timeout of the test-executable used to identify it (Calls the exec with `--help`). |
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,12 @@
"default": 1,
"minimum": 1
},
"testMate.cpp.test.dependsOnTask": {
"markdownDescription": "Name of task to execute before running/debugging tests.",
"scope": "resource",
"type": "string",
"default": ""
},
"testMate.cpp.discovery.gracePeriodForMissing": {
"markdownDescription": "[seconds] Test executables are being watched (only inside the workspace directory). In case of one recompiles it will try to preserve the test states. If compilation reaches timeout it will drop the suite.",
"scope": "resource",
Expand Down
4 changes: 4 additions & 0 deletions src/Configurations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ export class Configurations {
}
}

public getDependsOnTask(): string | undefined {
return this._new.get<string | undefined>('test.dependsOnTask', undefined);
}

public getExecWatchTimeout(): number {
const res = this._getNewOrOldOrDefAndMigrate<number>('discovery.gracePeriodForMissing', 10) * 1000;
return res;
Expand Down
2 changes: 1 addition & 1 deletion src/Suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export class Suite implements TestSuiteInfo {
}

/** If the return value is not empty then we should run the parent */
public collectTestToRun(tests: readonly string[], isParentIn: boolean): AbstractTest[] {
public collectTestToRun(tests: readonly string[], isParentIn: boolean = false): AbstractTest[] {
const isCurrParentIn = isParentIn || tests.indexOf(this.id) != -1;

return this.children
Expand Down
137 changes: 104 additions & 33 deletions src/TestAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inspect } from 'util';
import { sep as osPathSeparator } from 'path';
import { sep as osPathSeparator, basename } from 'path';
import * as vscode from 'vscode';
import {
TestEvent,
Expand Down Expand Up @@ -405,16 +405,21 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable {
this._rootSuite.cancel();
}

public run(tests: string[]): Promise<void> {
public async run(tests: string[]): Promise<void> {
if (this._mainTaskQueue.size > 0) {
this._log.info(
"Run is busy. Your test maybe in an infinite loop: Try to limit the test's timeout with: defaultRunningTimeoutSec config option!",
);
}

return this._mainTaskQueue.then(() => {
return this._rootSuite.run(tests).catch((reason: Error) => this._log.exceptionS(reason));
});
return this._mainTaskQueue
.then(async () => {
await this._executeDependsOnTaskIfDefined(this._getRunnableToTestMap(tests));
await this._rootSuite.run(tests);
})
.catch(err => {
this._log.exceptionS(err);
});
}

public async debug(tests: string[]): Promise<void> {
Expand All @@ -425,15 +430,7 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable {

this._log.info('Using debug');

const runnableToTestMap = tests
.map(t => this._rootSuite.findTestById(t))
.reduce((runnableToTestMap, test) => {
if (test === undefined) return runnableToTestMap;
const arr = runnableToTestMap.find(x => x[0] === test.runnable);
if (arr) arr[1].push(test!);
else runnableToTestMap.push([test.runnable, [test]]);
return runnableToTestMap;
}, new Array<[AbstractRunnable, Readonly<AbstractTest>[]]>());
const runnableToTestMap = this._getRunnableToTestMap(tests);

if (runnableToTestMap.length !== 1) {
this._log.error('unsupported executable count', tests);
Expand All @@ -451,16 +448,6 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable {
this._log.debugS('debugConfigTemplate', debugConfigTemplate);
this._log.infoSWithTags('Using debug', { debugConfigTemplateSource });

const label = runnableTests.length > 1 ? `(${runnableTests.length} tests)` : runnableTests[0].label;

const suiteLabels =
runnableTests.length > 1
? ''
: [...runnableTests[0].route()]
.filter((v, i, a) => i < a.length - 1)
.map(s => s.label)
.join(' ← ');

const argsArray = runnable.getDebugParams(runnableTests, configuration.getDebugBreakOnFailure());

if (runnableTests.length === 1 && runnableTests[0] instanceof Catch2Test) {
Expand All @@ -472,7 +459,7 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable {

const items: QuickPickItem[] = [
{
label: label,
label: this._getRunnableLabel(runnableTests),
sectionStack: [],
description: 'Select the section combo you wish to debug or choose this to debug all of it.',
},
Expand Down Expand Up @@ -514,19 +501,15 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable {
}

const varToResolve: ResolveRulePair<string | object>[] = [
...this._variableToValue,
['${suitelabel}', suiteLabels], // deprecated
['${suiteLabel}', suiteLabels],
['${label}', label],
['${exec}', runnable.properties.path],
['${args}', argsArray], // deprecated
['${argsArray}', argsArray],
['${argsStr}', '"' + argsArray.map(a => a.replace('"', '\\"')).join('" "') + '"'],
['${cwd}', runnable.properties.options.cwd!],
['${envObj}', Object.assign(Object.assign({}, process.env), runnable.properties.options.env!)],
];

const debugConfig = resolveVariables(debugConfigTemplate, varToResolve);
const debugConfig = resolveVariables(
this._resolveVariablesForRunnables(debugConfigTemplate, runnableToTestMap),
varToResolve,
);

// we dont know better :(
// https://github.com/Microsoft/vscode/issues/70125
Expand All @@ -538,6 +521,8 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable {

return this._mainTaskQueue
.then(async () => {
await this._executeDependsOnTaskIfDefined(runnableToTestMap);

let terminateConn: vscode.Disposable | undefined;

const terminated = new Promise<void>(resolve => {
Expand Down Expand Up @@ -574,7 +559,93 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable {
});
}

private async _executeDependsOnTaskIfDefined(runnableToTestMap: Array<[AbstractRunnable, Readonly<AbstractTest>[]]>) {
const dependsOnTaskName = this._getConfiguration().getDependsOnTask();
if (!dependsOnTaskName) {
return;
}

const tasks = await vscode.tasks.fetchTasks();
const dependsOnTask = tasks.find(task => task.name === dependsOnTaskName);
if (!dependsOnTask) {
throw Error(`Could not find task with name "${dependsOnTaskName}" defined in testMate.cpp.test.dependsOnTask.`);
}

const task = this._resolveVariablesForRunnables(dependsOnTask, runnableToTestMap);
const label =
runnableToTestMap.length > 1
? `${runnableToTestMap.length}-runnables`
: basename(runnableToTestMap[0][0].properties.path);
task.name += '-' + label;

const taskPromise = new Promise<number>(resolve => {
const subscriptionDisposable = vscode.tasks.onDidEndTaskProcess((event: vscode.TaskProcessEndEvent) => {
if (event.execution.task.name === task.name) {
subscriptionDisposable.dispose();
resolve(event.exitCode);
}
});
});

this._log.info('startDependsOnTask');
await vscode.tasks.executeTask(task);
this._log.info('dependsOnTaskStarted');

const taskExitCode = await taskPromise;
this._log.info('dependsOnTaskTerminated');
if (taskExitCode != 0) {
throw Error(`Depends on Task "${task.name}" exited with failure exit code ${taskExitCode}.`);
}
}

private _getConfiguration(): Configurations {
return new Configurations(this._log, this.workspaceFolder.uri);
}

private _getRunnableToTestMap(tests: string[]): Array<[AbstractRunnable, Readonly<AbstractTest>[]]> {
return this._rootSuite.collectTestToRun(tests).reduce((runnableToTestMap, test) => {
if (test === undefined) return runnableToTestMap;
const arr = runnableToTestMap.find(x => x[0] === test.runnable);
if (arr) arr[1].push(test!);
else runnableToTestMap.push([test.runnable, [test]]);
return runnableToTestMap;
}, new Array<[AbstractRunnable, Readonly<AbstractTest>[]]>());
}

private _getRunnableLabel(runnableTests: Readonly<AbstractTest>[]) {
return runnableTests.length > 1 ? `(${runnableTests.length} tests)` : runnableTests[0].label;
}

private _resolveVariablesForRunnables<T>(
value: T,
runnableToTestMap: Array<[AbstractRunnable, Readonly<AbstractTest>[]]>,
): T {
const runnables: AbstractRunnable[] = [];
const allRunnableTests: Readonly<AbstractTest>[] = [];
runnableToTestMap.forEach(([runnable, runnableTests]) => {
runnables.push(runnable);
allRunnableTests.push(...runnableTests);
});

const suiteLabels =
allRunnableTests.length > 1
? ''
: [...allRunnableTests[0].route()]
.filter((v, i, a) => i < a.length - 1)
.map(s => s.label)
.join(' ← ');

const varToResolve: ResolveRulePair<string | object>[] = [
...this._variableToValue,
['${suitelabel}', suiteLabels], // deprecated
['${suiteLabel}', suiteLabels],
['${label}', this._getRunnableLabel(allRunnableTests)],
['${exec}', runnables[0].properties.path],
['${execs}', runnables.map(runnable => `"${runnable.properties.path}"`).join(' ')],
['${cwd}', runnables[0].properties.options.cwd!],
['${envObj}', Object.assign(Object.assign({}, process.env), runnables[0].properties.options.env!)],
];

return resolveVariables(value, varToResolve);
}
}
2 changes: 1 addition & 1 deletion src/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function _mapAllStrings<T>(value: T, mapperFunc: (s: string) => any): T {
return ((value as any[]).map((v: any) => _mapAllStrings(v, mapperFunc)) as unknown) as T;
} else if (typeof value === 'object') {
// eslint-disable-next-line
const newValue: any = {};
const newValue: T = Object.create(value as any);
for (const prop in value) {
const val = _mapAllStrings(value[prop], mapperFunc);
if (val !== undefined) newValue[prop] = val;
Expand Down