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

Feature Task Integration #12

Merged
merged 3 commits into from
Jul 6, 2022
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ node_modules
.vscode-test/
*.vsix
src/test/examples/*/.tox

# Python venvs
.venv
17 changes: 16 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,27 @@
"Other"
],
"activationEvents": [
"onCommand:workbench.action.tasks.runTask",
"onCommand:python-tox.select",
"onCommand:python-tox.selectMultiple",
"onCommand:python-tox.openDocs"
],
"main": "./out/extension.js",
"contributes": {
"taskDefinitions": [
{
"type": "tox",
"required": [
"testenv"
],
"properties": {
"testenv": {
"type": "string",
"description": "The testenv to execute"
}
}
}
],
"commands": [
{
"command": "python-tox.select",
Expand Down Expand Up @@ -83,4 +98,4 @@
"typescript": "^4.6.3",
"vsce": "^2.7.0"
}
}
}
24 changes: 19 additions & 5 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import * as util from 'util';
import * as path from 'path';
import * as os from 'os';

import { ToxTaskProvider } from './toxTaskProvider';

let toxTaskProvider: vscode.Disposable | undefined;

const exec = util.promisify(child_process.exec);

function findProjectDir() {
Expand All @@ -26,7 +30,7 @@ function findProjectDir() {
}

async function getToxEnvs(projDir: string) {
const { stdout } = await exec('tox -a', {cwd: projDir});
const { stdout } = await exec('tox -a', { cwd: projDir });
return stdout.trim().split(os.EOL);
}

Expand All @@ -40,7 +44,7 @@ async function safeGetToxEnvs(projDir: string) {
}

function runTox(envs: string[], projDir: string) {
const term = vscode.window.createTerminal({"cwd": projDir, "name": "tox"});
const term = vscode.window.createTerminal({ "cwd": projDir, "name": "tox" });
const envArg = envs.join(",");
term.show(true); // preserve focus

Expand All @@ -65,7 +69,7 @@ async function selectCommand() {
if (!envs) {
return;
}
const selected = await vscode.window.showQuickPick(envs, {placeHolder: "tox environment"});
const selected = await vscode.window.showQuickPick(envs, { placeHolder: "tox environment" });
if (!selected) {
return;
}
Expand All @@ -78,7 +82,7 @@ async function selectMultipleCommand() {
if (!envs) {
return;
}
const selected = await vscode.window.showQuickPick(envs, {placeHolder: "tox environments", canPickMany: true});
const selected = await vscode.window.showQuickPick(envs, { placeHolder: "tox environments", canPickMany: true });
if (!selected) {
return;
}
Expand All @@ -90,14 +94,24 @@ async function openDocumentationCommand() {
}

export function activate(context: vscode.ExtensionContext) {
const workspaceTox = findProjectDir();
if (workspaceTox) {
toxTaskProvider = vscode.tasks.registerTaskProvider(ToxTaskProvider.toxType, new ToxTaskProvider(workspaceTox));
}
context.subscriptions.push(
vscode.commands.registerCommand('python-tox.select', selectCommand),
vscode.commands.registerCommand('python-tox.selectMultiple', selectMultipleCommand),
vscode.commands.registerCommand('python-tox.openDocs', openDocumentationCommand)
);
}

export function deactivate() {}
export function deactivate() {

if (toxTaskProvider) {
toxTaskProvider.dispose();
}

}

// For testing, before we move this to a utils.ts
export const _private = {
Expand Down
18 changes: 6 additions & 12 deletions src/test/suite/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,15 @@
import { strict as assert } from 'assert';
import { doesNotMatch, strict as assert } from 'assert';

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
import * as extension from '../../extension';
import * as path from 'path';
import * as fs from 'fs';
import * as util from 'util';

function getExampleDir(name: string) {
const dir = path.join(__dirname, '..', '..', '..', 'src', 'test', 'examples', name);
assert.ok(fs.existsSync(dir));
return dir;
}
import * as utils from './utils';

function getExampleFileUri(name: string, file: string) {
const dir = getExampleDir(name);
const dir = utils.getExampleDir(name);
const filePath = path.join(dir, file);
assert.ok(fs.existsSync(filePath));
return vscode.Uri.file(filePath);
Expand Down Expand Up @@ -44,19 +38,19 @@ suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');

test('getting tox environments', async () => {
const dir = getExampleDir("simple");
const dir = utils.getExampleDir("simple");
const envs = await extension._private.getToxEnvs(dir);
assert.deepEqual(envs, ["one", "two"]);
});

test('make sure we have all tox environments', async () => {
const dir = getExampleDir("allenvs");
const dir = utils.getExampleDir("allenvs");
const envs = await extension._private.getToxEnvs(dir);
assert.deepEqual(envs, ["one", "two", "three"]);
});

test('running tox', async () => {
const dir = getExampleDir("end2end");
const dir = utils.getExampleDir("end2end");

const tmpdir = path.join(dir, ".tox", "tmp");
const marker = path.join(tmpdir, "tox-did-run");
Expand Down
31 changes: 31 additions & 0 deletions src/test/suite/toxTaskProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { doesNotMatch, strict as assert } from 'assert';

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
import * as tasks from '../../toxTaskProvider';
import * as utils from './utils';

suite('ToxTaskProvider Test Suite', () => {

vscode.window.showInformationMessage('Start all tests.');

test('getting tox tasks', async() => {
const dir = utils.getExampleDir("allenvs");

const allEnvsWorkspaceFolder = {
uri: vscode.Uri.file(dir),
name: "AllEnvs",
index: 0,
};

vscode.workspace.updateWorkspaceFolders(0, null, allEnvsWorkspaceFolder);

const toxTaskProvider = new tasks.ToxTaskProvider(dir);
const toxTasks = await toxTaskProvider.provideTasks();
assert.equal(toxTasks?.length, 3);
assert.equal(toxTasks[0].name, "one");
assert.equal(toxTasks[1].name, "two");
assert.equal(toxTasks[2].name, "three");
});
});
9 changes: 9 additions & 0 deletions src/test/suite/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { doesNotMatch, strict as assert } from 'assert';
import * as fs from 'fs';
import * as path from 'path';

export function getExampleDir(name: string) {
const dir = path.join(__dirname, '..', '..', '..', 'src', 'test', 'examples', name);
assert.ok(fs.existsSync(dir));
return dir;
}
149 changes: 149 additions & 0 deletions src/toxTaskProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import * as path from 'path';
import * as fs from 'fs';
import * as cp from 'child_process';
import * as vscode from 'vscode';

export class ToxTaskProvider implements vscode.TaskProvider {
static readonly toxType = 'tox';
static readonly toxIni = 'tox.ini';
private toxPromise: Thenable<vscode.Task[]> | undefined = undefined;

constructor(workspaceRoot: string) {
const pattern = path.join(workspaceRoot, ToxTaskProvider.toxIni);
const fileWatcher = vscode.workspace.createFileSystemWatcher(pattern);
fileWatcher.onDidChange(() => this.toxPromise = undefined);
fileWatcher.onDidCreate(() => this.toxPromise = undefined);
fileWatcher.onDidDelete(() => this.toxPromise = undefined);
}

public provideTasks(): Thenable<vscode.Task[]> | undefined {
if (!this.toxPromise) {
this.toxPromise = getToxTestenvs();
}
return this.toxPromise;
}

public resolveTask(_task: vscode.Task): vscode.Task | undefined {
const testenv = _task.definition.testenv;
if (testenv) {
const definition: ToxTaskDefinition = <any>_task.definition;
return new vscode.Task(definition, _task.scope ?? vscode.TaskScope.Workspace, definition.testenv, ToxTaskProvider.toxType, new vscode.ShellExecution(`tox -e ${definition.testenv}`));
}
return undefined;
}
}

function exists(file: string): Promise<boolean> {
return new Promise<boolean>((resolve, _reject) => {
fs.exists(file, (value) => {
resolve(value);
});
});
}

function exec(command: string, options: cp.ExecOptions): Promise<{ stdout: string; stderr: string }> {
return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
cp.exec(command, options, (error, stdout, stderr) => {
if (error) {
reject({ error, stdout, stderr });
}
resolve({ stdout, stderr });
});
});
}

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
*/
testenv: string;
}

const buildNames: string[] = ['build', 'compile', 'watch'];
function isBuildTask(name: string): boolean {
for (const buildName of buildNames) {
if (name.indexOf(buildName) !== -1) {
return true;
}
}
return false;
}

const testNames: string[] = ['test'];
function isTestTask(name: string): boolean {
for (const testName of testNames) {
if (name.indexOf(testName) !== -1) {
return true;
}
}
return false;
}

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

if (!workspaceFolders || workspaceFolders.length === 0) {
return result;
}
for (const workspaceFolder of workspaceFolders) {
const folderString = workspaceFolder.uri.fsPath;
if (!folderString) {
continue;
}
const toxFile = path.join(folderString, ToxTaskProvider.toxIni);
if (!await exists(toxFile)) {
continue;
}

const commandLine = 'tox -a';
try {
const { stdout, stderr } = await exec(commandLine, { cwd: folderString });
if (stderr && stderr.length > 0) {
getOutputChannel().appendLine(stderr);
getOutputChannel().show(true);
}
if (stdout) {
const lines = stdout.split(/\r{0,1}\n/);
for (const line of lines) {
if (line.length === 0) {
continue;
}
const toxTestenv = line;
const kind: ToxTaskDefinition = {
type: ToxTaskProvider.toxType,
testenv: toxTestenv
};

const task = new vscode.Task(kind, workspaceFolder, toxTestenv, ToxTaskProvider.toxType, new vscode.ShellExecution(`tox -e ${toxTestenv}`));
result.push(task);
const lowerCaseLine = line.toLowerCase();
if (isBuildTask(lowerCaseLine)) {
task.group = vscode.TaskGroup.Build;
} else if (isTestTask(lowerCaseLine)) {
task.group = vscode.TaskGroup.Test;
}
}
}
} 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;
}