mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-06-13 07:10:26 +08:00
first provisional test provider sample
This commit is contained in:
2
test-provider-sample/.eslintignore
Normal file
2
test-provider-sample/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
vscode.d.ts
|
||||
vscode.proposed.d.ts
|
||||
20
test-provider-sample/.eslintrc.js
Normal file
20
test-provider-sample/.eslintrc.js
Normal 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
7
test-provider-sample/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
out
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
|
||||
/vscode.d.ts
|
||||
/vscode.proposed.d.ts
|
||||
22
test-provider-sample/.vscode/launch.json
vendored
Normal file
22
test-provider-sample/.vscode/launch.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
3
test-provider-sample/.vscode/settings.json
vendored
Normal file
3
test-provider-sample/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"prettier.printWidth": 120
|
||||
}
|
||||
20
test-provider-sample/.vscode/tasks.json
vendored
Normal file
20
test-provider-sample/.vscode/tasks.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
26
test-provider-sample/README.md
Normal file
26
test-provider-sample/README.md
Normal 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
1287
test-provider-sample/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
test-provider-sample/package.json
Normal file
49
test-provider-sample/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
8
test-provider-sample/sample/test.md
Normal file
8
test-provider-sample/sample/test.md
Normal 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
|
||||
16
test-provider-sample/src/extension.ts
Normal file
16
test-provider-sample/src/extension.ts
Normal 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');
|
||||
}),
|
||||
);
|
||||
}
|
||||
82
test-provider-sample/src/testCodeLens.ts
Normal file
82
test-provider-sample/src/testCodeLens.ts
Normal 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: ';
|
||||
}
|
||||
};
|
||||
212
test-provider-sample/src/testProvider.ts
Normal file
212
test-provider-sample/src/testProvider.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
12
test-provider-sample/tsconfig.json
Normal file
12
test-provider-sample/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user