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..8163f49 100644 --- a/src/run.ts +++ b/src/run.ts @@ -6,16 +6,41 @@ 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 }); - 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'; + export function runTox(envs: string[], toxArguments: string, terminal: vscode.Terminal = getTerminal() ) { 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 @@ -27,6 +52,14 @@ 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); } + +let _channel: vscode.OutputChannel; +function getOutputChannel(): vscode.OutputChannel { + if (!_channel) { + _channel = vscode.window.createOutputChannel('Tox Auto Detection'); + } + return _channel; +} diff --git a/src/testController.ts b/src/testController.ts index 87249cf..4e8b3ca 100644 --- a/src/testController.ts +++ b/src/testController.ts @@ -1,15 +1,16 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as util from 'util'; -import { runTox } from './run'; +import { runTox, getToxEnvs } from './run'; +import { getTerminal, getRootParentLabelDesc } from './utils'; 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 +19,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); + runTox([test.label], "", getTerminal(cwd, getRootParentLabelDesc(test))); 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 +63,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 +81,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; @@ -119,27 +122,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; + } + + 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; + } - 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) - ); + 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; + } + } + } - listOfChildren.push(newTestItem); + //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; } @@ -148,12 +175,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 +189,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; } diff --git a/src/toxTaskProvider.ts b/src/toxTaskProvider.ts index 68c3f2d..99f78a2 100644 --- a/src/toxTaskProvider.ts +++ b/src/toxTaskProvider.ts @@ -4,7 +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 { getToxEnvs, commandToxRun } from './run'; const exec = util.promisify(child_process.exec); @@ -23,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; } @@ -37,21 +37,13 @@ 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; } } -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 @@ -71,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[] = []; @@ -90,47 +82,30 @@ async function getToxTestenvs(): Promise { continue; } - const commandLine = 'tox -a'; - try { - const { stdout, stderr } = await exec(commandLine, { cwd: folderString }); - if (stderr && stderr.length > 0) { - const channel = getOutputChannel(); - channel.appendLine(stderr); - channel.show(true); - } - if (stdout) { - const lines = stdout.split(/\r?\n/); - 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); - const task = new vscode.Task( - kind, - workspaceFolder, - toxTestenv, - ToxTaskProvider.toxType, - new vscode.ShellExecution(`tox -e ${toxTestenv}`) - ); - task.group = inferTaskGroup(line.toLowerCase()); - result.push(task); + if (toxTestenvs !== undefined) { + for (const toxTestenv of toxTestenvs) { + + 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; 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? +}