Skip to content
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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,14 @@ node_modules
.vscode-test/
*.vsix
src/test/examples/*/.tox

# Python
__pycache__
.pytest_cache

# VS Code
vscode.d.ts
vscode.proposed.testCoverage.d.ts

# Tox
.tox
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}/sample"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ but use at your own risk.

## Installing

Install the extension via the [Visual Studio
- Install the extension via the [Visual Studio
Marketplace](https://marketplace.visualstudio.com/items?itemName=the-compiler.python-tox)
or the [Open VSX
Registry](https://open-vsx.org/extension/the-compiler/python-tox). Also, make
sure that [tox](https://github.com/tox-dev/tox) is installed.
Registry](https://open-vsx.org/extension/the-compiler/python-tox)

Finally, run one of the [commands](#extension-commands) via the command palette
- make sure that [tox](https://github.com/tox-dev/tox) is installed.

- finally, run one of the [commands](#extension-commands) via the command palette
or bind them to a shortcut. No default shortcuts are provided.

For [VSpaceCode](https://vspacecode.github.io/), consider a configuration such as:
Expand Down
45 changes: 45 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@
"activationEvents": [
"onCommand:python-tox.select",
"onCommand:python-tox.selectMultiple",
"onCommand:python-tox.openDocs"
"onCommand:python-tox.openDocs",
"workspaceContains:tox.ini"
],
"enabledApiProposals": [
"testCoverage"
],
"main": "./out/extension.js",
"contributes": {
Expand All @@ -63,6 +67,9 @@
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"download-api": "vscode-dts dev",
"postdownload-api": "vscode-dts main",
"postinstall": "npm run download-api",
"pretest": "npm run compile && npm run lint",
"lint": "eslint src --ext ts --max-warnings=0",
"test": "node ./out/test/runTest.js",
Expand All @@ -82,6 +89,8 @@
"mocha": "^9.2.2",
"ts-mockito": "^2.6.1",
"typescript": "^4.6.3",
"vsce": "^2.7.0"
"vsce": "^2.7.0",
"vscode-test": "^1.5.2",
"vscode-dts": "^0.3.3"
}
}
1 change: 1 addition & 0 deletions sample/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest==7.1.1
10 changes: 10 additions & 0 deletions sample/tests/test_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def test_sample_1():
assert True, "Something went very wrong here..."


def test_sample_2():
assert type("Hello") is str, "Somehow the string is not a string."


def test_sample_3():
assert 3 == 3, "Math failed, please reboot."
30 changes: 30 additions & 0 deletions sample/tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
; tox docs: https://tox.wiki/en/latest/index.html

[tox]
envlist = py310
skipsdist = True

[testenv]
deps = -rrequirements.txt

[testenv:test-all]
commands =
pytest -vv tests/

[testenv:test-1]
commands =
pytest {posargs} tests/test_sample.py::test_sample_1

[testenv:test-2]
commands =
pytest {posargs} tests/test_sample.py::test_sample_2

[testenv:test-3]
commands =
pytest {posargs} tests/test_sample.py::test_sample_3

; Will be ignored from test UI
[testenv:test-{four,five}]
commands =
four: echo 4
five: echo 5
178 changes: 178 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,18 @@ function runTox(envs: string[], toxArguments: string, terminal: vscode.Terminal
terminal.sendText(terminalCommand);
}

/**
* Get a new terminal or use an existing one with the same name.
* @param projDir The directory of the project.
* @param name The name of the terminal
* @returns The terminal to run commands on.
*/
function getTerminal(projDir : string = findProjectDir(), name : string = "tox") : vscode.Terminal {
for (const terminal of vscode.window.terminals) {
if (terminal.name === name){
return terminal;
}
}
return vscode.window.createTerminal({"cwd": projDir, "name": name});
}

Expand Down Expand Up @@ -103,6 +114,173 @@ async function openDocumentationCommand() {
}

export function activate(context: vscode.ExtensionContext) {
const controller = vscode.tests.createTestController('toxTestController', 'Tox Testing');
context.subscriptions.push(controller);

controller.resolveHandler = async (test) => {
if (!test) {
await discoverAllFilesInWorkspace();
}
else {
await parseTestsInFileContents(test);
}
};

async function runHandler(
shouldDebug: boolean,
request: vscode.TestRunRequest,
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();
}

const runProfile = controller.createRunProfile(
'Run',
vscode.TestRunProfileKind.Run,
(request, token) => {
runHandler(false, request, token);
}
);

// When text documents are open, parse tests in them.
vscode.workspace.onDidOpenTextDocument(parseTestsInDocument);

// We could also listen to document changes to re-parse unsaved changes:
vscode.workspace.onDidSaveTextDocument(document => parseTestsInDocument(document));

/**
* In this function, we'll get the file TestItem if we've already found it,
* otherwise we'll create it with `canResolveChildren = true` to indicate it
* can be passed to the `controller.resolveHandler` to gets its children.
* @param uri The uri of the file to get or create
* @returns vscode.TestItem
*/
function getOrCreateFile(uri: vscode.Uri): vscode.TestItem
{
const existing = controller.items.get(uri.toString());
if (existing) {
return existing;
}

const file = controller.createTestItem(uri.toString(), uri.path.split('/').pop()!, uri);
controller.items.add(file);

file.canResolveChildren = true;
return file;
}

/**
* Parses for tests in the document.
* @param e The provided document
* @param filename The name of the file to look for. Default = tox.ini
*/
async function parseTestsInDocument(e: vscode.TextDocument, filename: string = 'tox.ini') {
if (e.uri.scheme === 'file' && e.uri.path.endsWith(filename)) {
const file = getOrCreateFile(e.uri);
const content = e.getText();

// Empty existing children
file.children.forEach(element => {
file.children.delete(element.id);
});

const listOfChildren = await parseTestsInFileContents(file, content);
listOfChildren.forEach((testItem) => {
file.children.add(testItem);
});
}
}

/**
* Parses the file to fill in the test.children from the contents
* @param file The file to parse
* @param contents The contents of the file
*/
async function parseTestsInFileContents(file: vscode.TestItem, contents?: string): Promise<vscode.TestItem[]> {

if (contents === undefined) {
const rawContent = await vscode.workspace.fs.readFile(file.uri!);
contents = new util.TextDecoder().decode(rawContent);
}

let listOfChildren: vscode.TestItem[] = [];

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);

// Excluding tox permutations for now
if (regexResult && !regexResult[2].includes('{')) {
let range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, regexResult[0].length));

const newTestItem = controller.createTestItem(regexResult[2], regexResult[2], file.uri);
newTestItem.range = range;

listOfChildren.push(newTestItem);
}
}

return listOfChildren;
}

async function discoverAllFilesInWorkspace() {
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
// that you only re-parse children that have been resolved in the past.
watcher.onDidChange(uri => parseTestsInFileContents(getOrCreateFile(uri)));
// 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;
})
);
}

context.subscriptions.push(
vscode.commands.registerCommand('python-tox.select', selectCommand),
vscode.commands.registerCommand('python-tox.selectMultiple', selectMultipleCommand),
Expand Down