From d6c1669777290a2ab484123ca3d6f83992264b60 Mon Sep 17 00:00:00 2001 From: Steffen Hemer Date: Thu, 27 Apr 2023 10:36:09 +0200 Subject: [PATCH 1/5] testController: Add project/folder name to test tree If multiple projects with tox.ini's are present in the workspace, the test tree view contains just multiple 'tox.ini' entrys with no visible way to differentiate. Use the description field and fill with project/folder name. Cleaned some trailing whitespaces while at it. --- package.json | 2 +- src/run.ts | 2 +- src/testController.ts | 38 ++++++++++++++++++++------------------ 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 0110e6c..2ebf08d 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "python", "tox" ], - "version": "1.0.0", + "version": "1.0.1-dev", "license": "MIT", "publisher": "the-compiler", "repository": { diff --git a/src/run.ts b/src/run.ts index 152163a..db7feef 100644 --- a/src/run.ts +++ b/src/run.ts @@ -15,7 +15,7 @@ export function runTox(envs: string[], toxArguments: string, terminal: vscode.Te const envArg = envs.join(","); terminal.show(true); // preserve focus - // FIXME In theory, there's a command injection here, if an environment name + // FIXME: In theory, there's a command injection here, if an environment name // contains shell metacharacters. However: // - Escaping the argument in a shell-agnostic way is hard: // https://github.com/microsoft/vscode/blob/1.57.0/src/vs/workbench/contrib/debug/node/terminals.ts#L84-L211 diff --git a/src/testController.ts b/src/testController.ts index 87249cf..bbe8e6c 100644 --- a/src/testController.ts +++ b/src/testController.ts @@ -6,10 +6,10 @@ import { runTox } from './run'; export function create() { const controller = vscode.tests.createTestController('toxTestController', 'Tox Testing'); - controller.resolveHandler = async (test) => { + controller.resolveHandler = async (test) => { if (!test) { await discoverAllFilesInWorkspace(); - } + } else { await parseTestsInFileContents(test); } @@ -18,34 +18,34 @@ export function create() { async function runHandler( shouldDebug: boolean, request: vscode.TestRunRequest, - token: vscode.CancellationToken) + token: vscode.CancellationToken) { const run = controller.createTestRun(request); const queue: vscode.TestItem[] = []; - + if (request.include) { request.include.forEach(test => queue.push(test)); } - + while (queue.length > 0 && !token.isCancellationRequested) { const test = queue.pop()!; - + // Skip tests the user asked to exclude if (request.exclude?.includes(test)) { continue; } - + const start = Date.now(); try { const cwd = vscode.workspace.getWorkspaceFolder(test.uri!)!.uri.path; runTox([test.label], cwd); run.passed(test, Date.now() - start); - } + } catch (e: any) { run.failed(test, new vscode.TestMessage(e.message), Date.now() - start); } } - + // Make sure to end the run after all tests have been executed: run.end(); } @@ -62,7 +62,7 @@ export function create() { for (const document of vscode.workspace.textDocuments) { parseTestsInDocument(document); } - + // Check for tox.ini files when a new document is opened or saved. vscode.workspace.onDidOpenTextDocument(parseTestsInDocument); vscode.workspace.onDidSaveTextDocument(parseTestsInDocument); @@ -80,8 +80,10 @@ export function create() { if (existing) { return existing; } - - const file = controller.createTestItem(uri.toString(), uri.path.split('/').pop()!, uri); + + let splittedPath = uri.path.split('/'); + const file = controller.createTestItem(uri.toString(), splittedPath.pop()!, uri); + file.description = "(" + splittedPath.pop()! + ")"; controller.items.add(file); file.canResolveChildren = true; @@ -121,7 +123,7 @@ export function create() { for (let lineNo = 0; lineNo < lines.length; lineNo++) { let line = lines[lineNo]; - let regexResult = testRegex.exec(line); + let regexResult = testRegex.exec(line); if (!regexResult) { continue; } @@ -148,12 +150,12 @@ export function create() { if (!vscode.workspace.workspaceFolders) { return []; // handle the case of no open folders } - + return Promise.all( vscode.workspace.workspaceFolders.map(async workspaceFolder => { const pattern = new vscode.RelativePattern(workspaceFolder, 'tox.ini'); const watcher = vscode.workspace.createFileSystemWatcher(pattern); - + // When files are created, make sure there's a corresponding "file" node in the tree watcher.onDidCreate(uri => getOrCreateFile(uri)); // When files change, re-parse them. Note that you could optimize this so @@ -162,15 +164,15 @@ export function create() { // And, finally, delete TestItems for removed files. This is simple, since // we use the URI as the TestItem's ID. watcher.onDidDelete(uri => controller.items.delete(uri.toString())); - + for (const file of await vscode.workspace.findFiles(pattern)) { getOrCreateFile(file); } - + return watcher; }) ); } - return controller; + return controller; } From b718e8ae77d8ab3b1f1cbc0c1295b4de3c725e37 Mon Sep 17 00:00:00 2001 From: Steffen Hemer Date: Thu, 27 Apr 2023 10:49:35 +0200 Subject: [PATCH 2/5] toxTaskProvider: Collected tox calls at one place (in run) toxTaskProvider may re-use even more from run.ts. To get a better overview, at least collect the tox commands at a common place there. Use the same way to split the output of tox's testenv here as in run.ts Did not adopt the indentation style of toxTaskProvider (used four spaces instead of tabs) for cleaner diff. Signed-off-by: Steffen Hemer --- src/run.ts | 8 ++++++-- src/toxTaskProvider.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/run.ts b/src/run.ts index db7feef..01d353a 100644 --- a/src/run.ts +++ b/src/run.ts @@ -6,11 +6,15 @@ import { getTerminal } from './utils'; const exec = util.promisify(child_process.exec); +export const commandToxListAllEnvs = 'tox -a'; + export async function getToxEnvs(projDir: string) { - const { stdout } = await exec('tox -a', { cwd: projDir }); + const { stdout } = await exec(commandToxListAllEnvs, { cwd: projDir }); return stdout.trim().split(os.EOL); } +export const commandToxRun = 'tox -e'; + export function runTox(envs: string[], toxArguments: string, terminal: vscode.Terminal = getTerminal() ) { const envArg = envs.join(","); terminal.show(true); // preserve focus @@ -27,6 +31,6 @@ export function runTox(envs: string[], toxArguments: string, terminal: vscode.Te // - Real tox environment names are very unlikely to accidentally contain // such characters - in fact, using spaces in env names seems to not work // properly at all. - let terminalCommand = `tox -e ${envArg} ${toxArguments}`; + let terminalCommand = `${commandToxRun} ${envArg} ${toxArguments}`; terminal.sendText(terminalCommand); } diff --git a/src/toxTaskProvider.ts b/src/toxTaskProvider.ts index 68c3f2d..b32c342 100644 --- a/src/toxTaskProvider.ts +++ b/src/toxTaskProvider.ts @@ -4,7 +4,8 @@ import * as fs from 'fs'; import * as child_process from 'child_process'; import * as vscode from 'vscode'; import * as util from 'util'; - +import * as os from 'os'; +import { commandToxListAllEnvs, commandToxRun } from './run'; const exec = util.promisify(child_process.exec); @@ -37,7 +38,7 @@ export class ToxTaskProvider implements vscode.TaskProvider { _task.scope ?? vscode.TaskScope.Workspace, definition.testenv, ToxTaskProvider.toxType, - new vscode.ShellExecution(`tox -e ${definition.testenv}`) + new vscode.ShellExecution(`${commandToxRun} ${definition.testenv}`) ); } return undefined; @@ -90,16 +91,15 @@ async function getToxTestenvs(): Promise { continue; } - const commandLine = 'tox -a'; try { - const { stdout, stderr } = await exec(commandLine, { cwd: folderString }); + const { stdout, stderr } = await exec(commandToxListAllEnvs, { cwd: folderString }); if (stderr && stderr.length > 0) { const channel = getOutputChannel(); channel.appendLine(stderr); channel.show(true); } if (stdout) { - const lines = stdout.split(/\r?\n/); + const lines = stdout.trim().split(os.EOL); for (const line of lines) { if (line.length === 0) { continue; @@ -115,7 +115,7 @@ async function getToxTestenvs(): Promise { workspaceFolder, toxTestenv, ToxTaskProvider.toxType, - new vscode.ShellExecution(`tox -e ${toxTestenv}`) + new vscode.ShellExecution(`${commandToxRun} ${toxTestenv}`) ); task.group = inferTaskGroup(line.toLowerCase()); result.push(task); From 4b978b6cd7b7c8a4151879664ab62ba7e238f43b Mon Sep 17 00:00:00 2001 From: Steffen Hemer Date: Thu, 27 Apr 2023 12:03:34 +0200 Subject: [PATCH 3/5] toxTaskProvider: Re-use testenv parsing from run, move error checks there Instead of having two code sections with the same purpose (one with a more elaborate error handling), fuse them to one. --- src/run.ts | 33 +++++++++++++++++-- src/toxTaskProvider.ts | 75 ++++++++++++++---------------------------- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/run.ts b/src/run.ts index 01d353a..8163f49 100644 --- a/src/run.ts +++ b/src/run.ts @@ -9,8 +9,29 @@ const exec = util.promisify(child_process.exec); export const commandToxListAllEnvs = 'tox -a'; export async function getToxEnvs(projDir: string) { - const { stdout } = await exec(commandToxListAllEnvs, { cwd: projDir }); - return stdout.trim().split(os.EOL); + try { + const { stdout, stderr } = await exec(commandToxListAllEnvs, { cwd: projDir }); + if (stderr && stderr.length > 0) { + const channel = getOutputChannel(); + channel.appendLine(stderr); + channel.show(true); + } + if (stdout) { + return stdout.trim().split(os.EOL); + } + } catch (err: any) { + const channel = getOutputChannel(); + if (err.stderr) { + channel.appendLine(err.stderr); + } + if (err.stdout) { + channel.appendLine(err.stdout); + } + channel.appendLine('Auto detecting tox testenvs failed.'); + channel.show(true); + } + + return undefined; } export const commandToxRun = 'tox -e'; @@ -34,3 +55,11 @@ export function runTox(envs: string[], toxArguments: string, terminal: vscode.Te let terminalCommand = `${commandToxRun} ${envArg} ${toxArguments}`; terminal.sendText(terminalCommand); } + +let _channel: vscode.OutputChannel; +function getOutputChannel(): vscode.OutputChannel { + if (!_channel) { + _channel = vscode.window.createOutputChannel('Tox Auto Detection'); + } + return _channel; +} diff --git a/src/toxTaskProvider.ts b/src/toxTaskProvider.ts index b32c342..0d99ff6 100644 --- a/src/toxTaskProvider.ts +++ b/src/toxTaskProvider.ts @@ -4,8 +4,7 @@ import * as fs from 'fs'; import * as child_process from 'child_process'; import * as vscode from 'vscode'; import * as util from 'util'; -import * as os from 'os'; -import { commandToxListAllEnvs, commandToxRun } from './run'; +import { getToxEnvs, commandToxRun } from './run'; const exec = util.promisify(child_process.exec); @@ -24,7 +23,7 @@ export class ToxTaskProvider implements vscode.TaskProvider { public provideTasks(): Thenable | undefined { if (!this.toxPromise) { - this.toxPromise = getToxTestenvs(); + this.toxPromise = getToxTestTasks(); } return this.toxPromise; } @@ -45,14 +44,6 @@ export class ToxTaskProvider implements vscode.TaskProvider { } } -let _channel: vscode.OutputChannel; -function getOutputChannel(): vscode.OutputChannel { - if (!_channel) { - _channel = vscode.window.createOutputChannel('Tox Auto Detection'); - } - return _channel; -} - interface ToxTaskDefinition extends vscode.TaskDefinition { /** * The environment name @@ -72,7 +63,7 @@ function inferTaskGroup(taskName: string): vscode.TaskGroup | undefined { } } -async function getToxTestenvs(): Promise { +async function getToxTestTasks(): Promise { const workspaceFolders = vscode.workspace.workspaceFolders; const result: vscode.Task[] = []; @@ -91,47 +82,31 @@ async function getToxTestenvs(): Promise { continue; } - try { - const { stdout, stderr } = await exec(commandToxListAllEnvs, { cwd: folderString }); - if (stderr && stderr.length > 0) { - const channel = getOutputChannel(); - channel.appendLine(stderr); - channel.show(true); - } - if (stdout) { - const lines = stdout.trim().split(os.EOL); - for (const line of lines) { - if (line.length === 0) { - continue; - } - const toxTestenv = line; - const kind: ToxTaskDefinition = { - type: ToxTaskProvider.toxType, - testenv: toxTestenv - }; + const toxTestenvs = await getToxEnvs(folderString); + + if (toxTestenvs !== undefined) { + for (const toxTestenv of toxTestenvs) { - const task = new vscode.Task( - kind, - workspaceFolder, - toxTestenv, - ToxTaskProvider.toxType, - new vscode.ShellExecution(`${commandToxRun} ${toxTestenv}`) - ); - task.group = inferTaskGroup(line.toLowerCase()); - result.push(task); + if (toxTestenv.length === 0) { + continue; } + + const kind: ToxTaskDefinition = { + type: ToxTaskProvider.toxType, + testenv: toxTestenv + }; + + const task = new vscode.Task( + kind, + workspaceFolder, + toxTestenv, + ToxTaskProvider.toxType, + new vscode.ShellExecution(`${commandToxRun} ${toxTestenv}`) + ); + task.group = inferTaskGroup(toxTestenv.toLowerCase()); + result.push(task); } - } catch (err: any) { - const channel = getOutputChannel(); - if (err.stderr) { - channel.appendLine(err.stderr); - } - if (err.stdout) { - channel.appendLine(err.stdout); - } - channel.appendLine('Auto detecting tox testenvs failed.'); - channel.show(true); - } + } } return result; } From 64376b83b615cb15ce6c79ce73c255669542bfe5 Mon Sep 17 00:00:00 2001 From: Steffen Hemer Date: Thu, 27 Apr 2023 14:17:06 +0200 Subject: [PATCH 4/5] testController: Do not parse for testenv on its own but search for the listed testenv Instead of parsing the tox.ini file for testenv with a regex, testController uses the output from tox and tries to locate the listed testenvs (with the regex) within the file to add the test task marker. Additionally, it adds all testenvs that could not be located (i.e. the ones from permutations) to line zero. This lets them at least appear correctly in the tree view. --- src/testController.ts | 60 +++++++++++++++++++++++++++++------------- src/toxTaskProvider.ts | 2 +- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/testController.ts b/src/testController.ts index bbe8e6c..147e750 100644 --- a/src/testController.ts +++ b/src/testController.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as util from 'util'; -import { runTox } from './run'; +import { runTox, getToxEnvs } from './run'; export function create() { const controller = vscode.tests.createTestController('toxTestController', 'Tox Testing'); @@ -121,27 +121,51 @@ export function create() { const testRegex = /^(\[testenv):(.*)\]/gm; // made with https://regex101.com let lines = contents.split('\n'); - for (let lineNo = 0; lineNo < lines.length; lineNo++) { - let line = lines[lineNo]; - let regexResult = testRegex.exec(line); - if (!regexResult) { - continue; - } + const toxTests = await getToxEnvs(path.dirname(file.uri!.path)); - let envName = regexResult[2]; - if (envName.includes('{')) { - // Excluding tox permutations for now - continue; - } + if (toxTests !== undefined) { + for (let lineNo = 0; lineNo < lines.length; lineNo++) { + let line = lines[lineNo]; + let regexResult = testRegex.exec(line); + if (!regexResult) { + continue; + } - const newTestItem = controller.createTestItem(envName, envName, file.uri); - newTestItem.range = new vscode.Range( - new vscode.Position(lineNo, 0), - new vscode.Position(lineNo, regexResult[0].length) - ); + let envName = regexResult[2]; + if (envName.includes('{')) { + //FIXME: Excluding tox permutations for now, maybe just use the last permutation line to add all leftover toxTests? + continue; + } - listOfChildren.push(newTestItem); + for (let testNo = 0; testNo < toxTests.length; testNo++) { + let toxTest = toxTests[testNo]; + + if (toxTest === envName) { + const newTestItem = controller.createTestItem(envName, envName, file.uri); + newTestItem.range = new vscode.Range( + new vscode.Position(lineNo, 0), + new vscode.Position(lineNo, regexResult[0].length) + ); + listOfChildren.push(newTestItem); + //remove the toxTest for which a match was found with the regex + toxTests.splice(testNo,1); + //no need to go further through the list of toxTests if we found the respective lineNo + break; + } + } + } + + //add the remaining of the toxTests (that potentially are part of permutations) + for (let toxTest of toxTests) { + const newTestItem = controller.createTestItem(toxTest, toxTest, file.uri); + newTestItem.range = new vscode.Range( + new vscode.Position(0, 0), // ... to the beginning of the document + new vscode.Position(0, 0) + ); + listOfChildren.push(newTestItem); + } } + //FIXME: empty tox.ini produces a single test at line 0 with env name 'python' (tox -a lists this test!)??? return listOfChildren; } diff --git a/src/toxTaskProvider.ts b/src/toxTaskProvider.ts index 0d99ff6..99f78a2 100644 --- a/src/toxTaskProvider.ts +++ b/src/toxTaskProvider.ts @@ -106,7 +106,7 @@ async function getToxTestTasks(): Promise { task.group = inferTaskGroup(toxTestenv.toLowerCase()); result.push(task); } - } + } } return result; } From 118eaa639d6cc38baeb9d87a71f5b783e20d9ce6 Mon Sep 17 00:00:00 2001 From: Steffen Hemer Date: Fri, 28 Apr 2023 13:31:57 +0200 Subject: [PATCH 5/5] testController: Use a per-project terminal to run tests To run tests from tox.ini files from different projects, create (and re-use) separate terminals. Terminal identifier uses label and description of root test item (the tox.ini). --- src/testController.ts | 3 ++- src/utils.ts | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/testController.ts b/src/testController.ts index 147e750..4e8b3ca 100644 --- a/src/testController.ts +++ b/src/testController.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as util from 'util'; import { runTox, getToxEnvs } from './run'; +import { getTerminal, getRootParentLabelDesc } from './utils'; export function create() { const controller = vscode.tests.createTestController('toxTestController', 'Tox Testing'); @@ -38,7 +39,7 @@ export function create() { const start = Date.now(); try { const cwd = vscode.workspace.getWorkspaceFolder(test.uri!)!.uri.path; - runTox([test.label], cwd); + runTox([test.label], "", getTerminal(cwd, getRootParentLabelDesc(test))); run.passed(test, Date.now() - start); } catch (e: any) { diff --git a/src/utils.ts b/src/utils.ts index b45c1a7..c82a4dd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -34,3 +34,18 @@ export function getTerminal(projDir : string = findProjectDir(), name : string = } return vscode.window.createTerminal({"cwd": projDir, "name": name}); } + +/** + * Get the top-most parent label (+ description) for terminal name + * @param test The test to start from. + * @returns The label and description of the root test item. + */ +export function getRootParentLabelDesc(test: vscode.TestItem) : string { + let root = test; + + while (root.parent !== undefined){ + root = root.parent; + } + + return root.label + " " + root.description; // FIXME: return as tuple? +}