From d8e357291ac92448497ac9570e4d2452099db259 Mon Sep 17 00:00:00 2001 From: Borja Zarco Date: Sat, 9 May 2020 11:30:07 -0400 Subject: [PATCH] Add an option to execute a task before running/debugging tests. This change introduces "test.dependsOnTask", a config setting to define the name of a user defined task (e.g. in tasks.json) to be executed before running/debugging tests. --- README.md | 1 + package.json | 6 ++ src/Configurations.ts | 4 ++ src/Suite.ts | 2 +- src/TestAdapter.ts | 137 ++++++++++++++++++++++++++++++++---------- src/Util.ts | 2 +- 6 files changed, 117 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9a708bc0..33f64dc7 100644 --- a/README.md +++ b/README.md @@ -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`). | diff --git a/package.json b/package.json index c15a3c7b..004b77a4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Configurations.ts b/src/Configurations.ts index 3db9b99c..901861cb 100644 --- a/src/Configurations.ts +++ b/src/Configurations.ts @@ -431,6 +431,10 @@ export class Configurations { } } + public getDependsOnTask(): string | undefined { + return this._new.get('test.dependsOnTask', undefined); + } + public getExecWatchTimeout(): number { const res = this._getNewOrOldOrDefAndMigrate('discovery.gracePeriodForMissing', 10) * 1000; return res; diff --git a/src/Suite.ts b/src/Suite.ts index 7aa7e95e..15234c8a 100644 --- a/src/Suite.ts +++ b/src/Suite.ts @@ -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 diff --git a/src/TestAdapter.ts b/src/TestAdapter.ts index a369bbf8..89392bd7 100644 --- a/src/TestAdapter.ts +++ b/src/TestAdapter.ts @@ -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, @@ -405,16 +405,21 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable { this._rootSuite.cancel(); } - public run(tests: string[]): Promise { + public async run(tests: string[]): Promise { 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 { @@ -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[]]>()); + const runnableToTestMap = this._getRunnableToTestMap(tests); if (runnableToTestMap.length !== 1) { this._log.error('unsupported executable count', tests); @@ -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) { @@ -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.', }, @@ -514,19 +501,15 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable { } const varToResolve: ResolveRulePair[] = [ - ...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 @@ -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(resolve => { @@ -574,7 +559,93 @@ export class TestAdapter implements api.TestAdapter, vscode.Disposable { }); } + private async _executeDependsOnTaskIfDefined(runnableToTestMap: Array<[AbstractRunnable, Readonly[]]>) { + 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(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[]]> { + 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[]]>()); + } + + private _getRunnableLabel(runnableTests: Readonly[]) { + return runnableTests.length > 1 ? `(${runnableTests.length} tests)` : runnableTests[0].label; + } + + private _resolveVariablesForRunnables( + value: T, + runnableToTestMap: Array<[AbstractRunnable, Readonly[]]>, + ): T { + const runnables: AbstractRunnable[] = []; + const allRunnableTests: Readonly[] = []; + 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[] = [ + ...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); + } } diff --git a/src/Util.ts b/src/Util.ts index f8095d76..d3aca543 100644 --- a/src/Util.ts +++ b/src/Util.ts @@ -55,7 +55,7 @@ function _mapAllStrings(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;