Skip to content

Use tox testenv listing for generating the tree view #34

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

Open
wants to merge 5 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"python",
"tox"
],
"version": "1.0.0",
"version": "1.0.1-dev",
"license": "MIT",
"publisher": "the-compiler",
"repository": {
Expand Down
41 changes: 37 additions & 4 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
99 changes: 63 additions & 36 deletions src/testController.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Expand All @@ -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();
}
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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
Expand All @@ -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;
}
75 changes: 25 additions & 50 deletions src/toxTaskProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -23,7 +23,7 @@ export class ToxTaskProvider implements vscode.TaskProvider {

public provideTasks(): Thenable<vscode.Task[]> | undefined {
if (!this.toxPromise) {
this.toxPromise = getToxTestenvs();
this.toxPromise = getToxTestTasks();
}
return this.toxPromise;
}
Expand All @@ -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
Expand All @@ -71,7 +63,7 @@ function inferTaskGroup(taskName: string): vscode.TaskGroup | undefined {
}
}

async function getToxTestenvs(): Promise<vscode.Task[]> {
async function getToxTestTasks(): Promise<vscode.Task[]> {
const workspaceFolders = vscode.workspace.workspaceFolders;
const result: vscode.Task[] = [];

Expand All @@ -90,47 +82,30 @@ async function getToxTestenvs(): Promise<vscode.Task[]> {
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;
Expand Down
15 changes: 15 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}