first provisional test provider sample

This commit is contained in:
Connor Peet
2020-11-19 15:37:31 -08:00
parent cf591da016
commit f2703585ef
14 changed files with 1766 additions and 0 deletions

View File

@ -0,0 +1,2 @@
vscode.d.ts
vscode.proposed.d.ts

View File

@ -0,0 +1,20 @@
/**@type {import('eslint').Linter.Config} */
// eslint-disable-next-line no-undef
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'semi': [2, "always"],
'@typescript-eslint/no-unused-vars': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-non-null-assertion': 0,
}
};

7
test-provider-sample/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
out
node_modules
.vscode-test/
*.vsix
/vscode.d.ts
/vscode.proposed.d.ts

View File

@ -0,0 +1,22 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}/sample"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "npm: watch"
}
]
}

View File

@ -0,0 +1,3 @@
{
"prettier.printWidth": 120
}

20
test-provider-sample/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,20 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@ -0,0 +1,26 @@
# Test Provider Sample
This sample demonstrates usage of the Test Provider API. It looks for tests as additions in `.md` files, with heading as groups, for example:
```
# Easy Math
2 + 2 = 4 // this test will pass
2 + 2 = 5 // this test will fail
# Harder Math
230230 + 5819123 = 6049353
```
## VS Code API
todo
## Running the Sample
- Run `npm install` in terminal to install dependencies
- Run the `Run Extension` target in the Debug View. This will:
- Start a task `npm: watch` to compile the code
- Run the extension in a new VS Code window
- Create a `test.txt` file containing the given content

1287
test-provider-sample/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
{
"enableProposedApi": true,
"name": "test-provider-sample",
"displayName": "test-provider-sample",
"description": "Sample showing how to use Proposed API",
"version": "0.0.1",
"publisher": "vscode-samples",
"repository": "https://github.com/Microsoft/vscode-extension-samples",
"engines": {
"vscode": "^1.51.0"
},
"categories": [
"Other"
],
"activationEvents": [
"*"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "extension.helloWorld",
"title": "Hello World"
}
]
},
"prettier": {
"printWidth": 120,
"singleQuote": true,
"arrowParens": "avoid"
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"lint": "eslint . --ext .ts,.tsx",
"watch": "tsc -watch -p ./",
"download-api": "vscode-dts dev",
"postdownload-api": "vscode-dts master",
"postinstall": "npm run download-api"
},
"devDependencies": {
"@types/node": "^12.12.0",
"@typescript-eslint/eslint-plugin": "^3.0.2",
"@typescript-eslint/parser": "^3.0.2",
"eslint": "^7.1.0",
"typescript": "^4.0.2",
"vscode-dts": "^0.3.1"
}
}

View File

@ -0,0 +1,8 @@
# Easy Math
2 + 2 = 4 // this test will pass
2 + 2 = 5 // this test will fail
# Harder Math
230230 + 5819123 = 6049353

View File

@ -0,0 +1,16 @@
import * as vscode from 'vscode';
import { TestCodeLensProvider } from './testCodeLens';
import { MathTestProvider } from './testProvider';
export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.test.registerTestProvider(new MathTestProvider()),
vscode.languages.registerCodeLensProvider({ pattern: '**/*.*' }, new TestCodeLensProvider()),
vscode.commands.registerCommand('test-provider-sample.runTests', async tests => {
await vscode.test.runTests({ tests, debug: false });
vscode.window.showInformationMessage('Test run complete');
}),
);
}

View File

@ -0,0 +1,82 @@
import * as vscode from 'vscode';
/**
* Create code lenses for tests in the current document.
*/
export class TestCodeLensProvider implements vscode.CodeLensProvider {
private readonly changeEmitter = new vscode.EventEmitter<void>();
public readonly onDidChangeCodeLenses = this.changeEmitter.event;
private currentObserver?: {
observer: vscode.TestObserver;
document: vscode.TextDocument;
};
constructor() {
vscode.workspace.onDidCloseTextDocument(doc => {
if (doc === this.currentObserver?.document) {
this.disposeCurrentObserver();
}
});
}
provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult<vscode.CodeLens[]> {
if (this.currentObserver?.document !== document) {
this.disposeCurrentObserver();
}
if (!this.currentObserver) {
this.currentObserver = { document, observer: vscode.test.createDocumentTestObserver(document) };
this.currentObserver.observer.onDidChangeTest(() => this.changeEmitter.fire());
}
return testsToLenses(document, this.currentObserver.observer.tests);
}
private disposeCurrentObserver() {
if (this.currentObserver) {
this.currentObserver.observer.dispose();
this.currentObserver = undefined;
}
}
}
const testsToLenses = (document: vscode.TextDocument, tests: Iterable<vscode.TestItem>) => {
const lenses: vscode.CodeLens[] = [];
const queue = [tests];
while (queue.length) {
for (const test of queue.pop()!) {
if (test.location && test.location.uri.toString() === document.uri.toString()) {
lenses.push(new TestCodeLens(test, test.location.range));
}
if (test.children) {
queue.push(test.children);
}
}
}
return lenses;
};
class TestCodeLens extends vscode.CodeLens {
constructor(public readonly test: vscode.TestItem, range: vscode.Range) {
super(range, {
title: statePrefix(test) + test.label,
command: 'test-provider-sample.runTests',
arguments: [[test]],
});
}
}
const statePrefix = (test: vscode.TestItem) => {
switch (test.state.runState) {
case vscode.TestRunState.Running:
return 'running: ';
case vscode.TestRunState.Passed:
return 'passed: ';
case vscode.TestRunState.Failed:
return 'failed: ';
default:
return 'Click to run: ';
}
};

View File

@ -0,0 +1,212 @@
import { TextDecoder } from 'util';
import * as vscode from 'vscode';
const textDecoder = new TextDecoder('utf-8');
export class MathTestProvider implements vscode.TestProvider {
/**
* @inheritdoc
*/
public createWorkspaceTestHierarchy(workspaceFolder: vscode.WorkspaceFolder): vscode.TestHierarchy<vscode.TestItem> {
const root = new TestRoot();
const pattern = new vscode.RelativePattern(workspaceFolder, '**/*.md');
const changeTestEmitter = new vscode.EventEmitter<vscode.TestItem>();
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
watcher.onDidCreate(async uri => await updateTestsInFile(root, uri, changeTestEmitter));
watcher.onDidChange(async uri => await updateTestsInFile(root, uri, changeTestEmitter));
watcher.onDidDelete(uri => {
removeTestsForFile(root, uri);
changeTestEmitter.fire(root);
});
const onDidDiscoverInitialTests = new vscode.EventEmitter<void>();
vscode.workspace
.findFiles(pattern, undefined, undefined)
.then(files => Promise.all(files.map(file => updateTestsInFile(root, file, changeTestEmitter))))
.then(() => onDidDiscoverInitialTests.fire());
return {
root,
onDidChangeTest: changeTestEmitter.event,
onDidDiscoverInitialTests: onDidDiscoverInitialTests.event,
dispose: () => watcher.dispose(),
};
}
/**
* @inheritdoc
*/
public createDocumentTestHierarchy(document: vscode.TextDocument): vscode.TestHierarchy<vscode.TestItem> {
const root = new TestRoot();
const file = new TestFile(document.uri);
root.children.push(file);
const changeTestEmitter = new vscode.EventEmitter<vscode.TestItem>();
file.updateTestsFromText(document.getText(), changeTestEmitter);
const listener = vscode.workspace.onDidChangeTextDocument(evt => {
if (evt.document === document) {
file.updateTestsFromText(document.getText(), changeTestEmitter);
}
});
const onDidDiscoverInitialTests = new vscode.EventEmitter<void>();
setTimeout(() => onDidDiscoverInitialTests.fire(), 0);
return {
root,
onDidChangeTest: changeTestEmitter.event,
onDidDiscoverInitialTests: onDidDiscoverInitialTests.event,
dispose: () => listener.dispose(),
};
}
/**
* @inheritdoc
*/
public async runTests(options: vscode.TestRunOptions) {
await this.runTestTree(options.tests);
}
private async runTestTree(tests: vscode.TestItem[]) {
for (const test of tests) {
if (test instanceof TestCase) {
await test.run();
}
if (test.children) {
this.runTestTree(test.children);
}
}
}
}
const removeTestsForFile = (root: TestRoot, uri: vscode.Uri) => {
root.children = root.children.filter(file => file.uri.toString() !== uri.toString());
};
const updateTestsInFile = async (root: TestRoot, uri: vscode.Uri, emitter: vscode.EventEmitter<vscode.TestItem>) => {
let testFile = root.children.find(file => file.uri.toString() === uri.toString());
const changeTarget = testFile ?? root;
if (!testFile) {
testFile = new TestFile(uri);
root.children.push(testFile);
await new Promise(resolve => setTimeout(resolve, Math.random() * 300 + 200));
}
await testFile.updateTestsFromFs(emitter);
emitter.fire(changeTarget);
};
const testRe = /^([0-9]+)\s*\+\s*([0-9]+)\s*=\s*([0-9]+)/;
const headingRe = /^(#+)\s*(.+)$/;
class TestRoot implements vscode.TestItem {
public readonly label = 'Markdown Tests';
public readonly state = new vscode.TestState(vscode.TestRunState.Unset);
public children = [] as TestFile[];
}
class TestFile implements vscode.TestItem {
public readonly label = this.uri.path.split('/').pop()!;
public children: (TestHeading | TestCase)[] = [];
public state = new vscode.TestState(vscode.TestRunState.Unset);
constructor(public readonly uri: vscode.Uri) {}
public async updateTestsFromFs(updateEmitter: vscode.EventEmitter<vscode.TestItem>) {
let text: string;
try {
const rawContent = await vscode.workspace.fs.readFile(this.uri);
text = textDecoder.decode(rawContent);
} catch (e) {
console.warn(`Error providing tests for ${this.uri.fsPath}`, e);
return;
}
this.updateTestsFromText(text, updateEmitter);
}
public updateTestsFromText(text: string, updateEmitter: vscode.EventEmitter<vscode.TestItem>) {
const lines = text.split('\n');
const ancestors: (TestFile | TestHeading)[] = [this];
this.children = [];
for (let lineNo = 0; lineNo < lines.length; lineNo++) {
const line = lines[lineNo];
const heading = headingRe.exec(line);
const test = testRe.exec(line);
if (test) {
const [, a, b, expected] = test!.map(Number);
const range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, test[0].length));
const tcase = new TestCase(a, b, expected, new vscode.Location(this.uri, range), updateEmitter);
ancestors[ancestors.length - 1].children.push(tcase);
continue;
}
if (heading) {
const [, pounds, name] = heading;
const level = pounds.length;
while (ancestors.length > level) {
ancestors.pop();
}
const range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, line.length));
ancestors.push(new TestHeading(level, name, new vscode.Location(this.uri, range)));
continue;
}
}
}
}
class TestHeading implements vscode.TestItem {
public readonly children: (TestHeading | TestCase)[] = [];
public state = new vscode.TestState(vscode.TestRunState.Unset);
constructor(
public readonly level: number,
public readonly label: string,
public readonly location: vscode.Location
) {}
}
class TestCase implements vscode.TestItem {
public get label() {
return `${this.a} + ${this.b} = ${this.expected}`;
}
public state = new vscode.TestState(vscode.TestRunState.Unset);
constructor(
private readonly a: number,
private readonly b: number,
private readonly expected: number,
public readonly location: vscode.Location,
private readonly updateEmitter: vscode.EventEmitter<vscode.TestItem>
) {}
async run() {
this.state = new vscode.TestState(vscode.TestRunState.Running);
this.updateEmitter.fire(this);
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 3000));
const actual = this.a + this.b;
if (actual === this.expected) {
this.state = new vscode.TestState(vscode.TestRunState.Passed);
} else {
this.state = new vscode.TestState(vscode.TestRunState.Failed, [
{
message: `Expected ${this.label}`,
expectedOutput: String(this.expected),
actualOutput: String(actual),
location: this.location,
},
]);
}
this.updateEmitter.fire(this);
}
}

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"lib": ["ES2019"],
"outDir": "out",
"sourceMap": true,
"strict": true,
"rootDir": "src"
},
"exclude": ["node_modules", ".vscode-test"]
}