mirror of
https://github.com/microsoft/vscode-extension-samples.git
synced 2026-04-27 16:55:44 +08:00
update to new testing api
This commit is contained in:
29
test-provider-sample/src/parser.ts
Normal file
29
test-provider-sample/src/parser.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const testRe = /^([0-9]+)\s*([+*/-])\s*([0-9]+)\s*=\s*([0-9]+)/;
|
||||
const headingRe = /^(#+)\s*(.+)$/;
|
||||
|
||||
export const parseMarkdown = (text: string, events: {
|
||||
onTest(range: vscode.Range, a: number, operator: string, b: number, expected: number): void;
|
||||
onHeading(range: vscode.Range, name: string, depth: number): void;
|
||||
}) => {
|
||||
const lines = text.split('\n');
|
||||
|
||||
for (let lineNo = 0; lineNo < lines.length; lineNo++) {
|
||||
const line = lines[lineNo];
|
||||
const test = testRe.exec(line);
|
||||
if (test) {
|
||||
const [, a, operator, b, expected] = test;
|
||||
const range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, test[0].length));
|
||||
events.onTest(range, Number(a), operator, Number(b), Number(expected));
|
||||
continue;
|
||||
}
|
||||
|
||||
const heading = headingRe.exec(line);
|
||||
if (heading) {
|
||||
const [, pounds, name] = heading;
|
||||
const range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, line.length));
|
||||
events.onHeading(range, name, Number(pounds));
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,75 +1,38 @@
|
||||
import { TextDecoder } from 'util';
|
||||
import * as vscode from 'vscode';
|
||||
import { parseMarkdown } from './parser';
|
||||
|
||||
const textDecoder = new TextDecoder('utf-8');
|
||||
|
||||
type MarkdownTestItem = WorkspaceTestRoot | TestFile | TestHeading | TestCase;
|
||||
|
||||
export class MathTestProvider implements vscode.TestProvider {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public provideWorkspaceTestHierarchy(workspaceFolder: vscode.WorkspaceFolder, token: vscode.CancellationToken): 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);
|
||||
});
|
||||
token.onCancellationRequested(() => watcher.dispose());
|
||||
|
||||
const discoveredInitialTests = vscode.workspace
|
||||
.findFiles(pattern, undefined, undefined)
|
||||
.then(files => Promise.all(files.map(file => updateTestsInFile(root, file, changeTestEmitter))));
|
||||
|
||||
return {
|
||||
root,
|
||||
onDidChangeTest: changeTestEmitter.event,
|
||||
discoveredInitialTests,
|
||||
};
|
||||
public provideWorkspaceTestRoot(workspaceFolder: vscode.WorkspaceFolder) {
|
||||
return new WorkspaceTestRoot(workspaceFolder, `${workspaceFolder.uri.toString()}: `);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public provideDocumentTestHierarchy(document: vscode.TextDocument, token: vscode.CancellationToken): 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());
|
||||
|
||||
const listener = vscode.workspace.onDidChangeTextDocument(evt => {
|
||||
if (evt.document === document) {
|
||||
file.updateTestsFromText(document.getText());
|
||||
changeTestEmitter.fire(file);
|
||||
}
|
||||
});
|
||||
token.onCancellationRequested(() => listener.dispose());
|
||||
|
||||
return {
|
||||
root,
|
||||
onDidChangeTest: changeTestEmitter.event,
|
||||
discoveredInitialTests: Promise.resolve(),
|
||||
};
|
||||
public provideDocumentTestRoot(document: vscode.TextDocument) {
|
||||
return new DocumentTestRoot(document, `${document.uri.toString()}: `);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public async runTests(run: vscode.TestRun, cancellation: vscode.CancellationToken) {
|
||||
const runTests = async (tests: Iterable<vscode.TestItem>) => {
|
||||
public async runTests(run: vscode.TestRun<MarkdownTestItem>, cancellation: vscode.CancellationToken) {
|
||||
const runTests = async (tests: Iterable<MarkdownTestItem>) => {
|
||||
for (const test of tests) {
|
||||
if (run.exclude?.includes(test)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (test instanceof TestCase) {
|
||||
if (cancellation.isCancellationRequested) {
|
||||
if (cancellation.isCancellationRequested) {
|
||||
run.setState(test, new vscode.TestState(vscode.TestResult.Skipped));
|
||||
} else {
|
||||
run.setState(test, new vscode.TestState(vscode.TestResult.Running));
|
||||
@ -85,106 +48,155 @@ export class MathTestProvider implements vscode.TestProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const removeTestsForFile = (root: TestRoot, uri: vscode.Uri) => {
|
||||
root.children = root.children.filter(file => file.uri.toString() !== uri.toString());
|
||||
};
|
||||
class WorkspaceTestRoot extends vscode.TestItem<TestFile> {
|
||||
public readonly parent = undefined;
|
||||
|
||||
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);
|
||||
constructor(private readonly workspaceFolder: vscode.WorkspaceFolder, prefix = '') {
|
||||
super(prefix + 'markdown', 'Markdown Tests', true);
|
||||
}
|
||||
|
||||
if ((await testFile.updateTestsFromFs()) === 0) {
|
||||
removeTestsForFile(root, uri);
|
||||
emitter.fire(root);
|
||||
} else {
|
||||
emitter.fire(changeTarget);
|
||||
public discoverChildren(progress: vscode.Progress<{ busy: boolean }>, token: vscode.CancellationToken) {
|
||||
const pattern = new vscode.RelativePattern(this.workspaceFolder, '**/*.md');
|
||||
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
|
||||
watcher.onDidCreate(uri =>
|
||||
this.children.add(new TestFile(uri, this, getContentFromFilesystem))
|
||||
);
|
||||
watcher.onDidChange(uri => this.children.get(uri.toString())?.refresh());
|
||||
watcher.onDidDelete(uri => this.children.delete(uri.toString()));
|
||||
token.onCancellationRequested(() => watcher.dispose());
|
||||
|
||||
vscode.workspace.findFiles(pattern).then(files => {
|
||||
for (const file of files) {
|
||||
this.children.add(new TestFile(file, this, getContentFromFilesystem));
|
||||
}
|
||||
|
||||
progress.report({ busy: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentTestRoot extends vscode.TestItem<TestFile> {
|
||||
public readonly parent = undefined;
|
||||
|
||||
constructor(private readonly document: vscode.TextDocument, prefix = '') {
|
||||
super(prefix + 'markdown', 'Markdown Tests', true);
|
||||
}
|
||||
|
||||
public discoverChildren(progress: vscode.Progress<{ busy: boolean }>, token: vscode.CancellationToken) {
|
||||
const file = new TestFile(this.document.uri, this, () =>
|
||||
Promise.resolve(this.document.getText())
|
||||
);
|
||||
this.children.add(file);
|
||||
|
||||
const changeListener = vscode.workspace.onDidChangeTextDocument(e => {
|
||||
if (e.document === this.document) {
|
||||
file.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
token.onCancellationRequested(() => changeListener.dispose());
|
||||
file.refresh().then(() => progress.report({ busy: false }));
|
||||
}
|
||||
}
|
||||
|
||||
let generationCounter = 0;
|
||||
|
||||
const getContentFromFilesystem = async (uri: vscode.Uri) => {
|
||||
try {
|
||||
const rawContent = await vscode.workspace.fs.readFile(uri);
|
||||
return textDecoder.decode(rawContent);
|
||||
} catch (e) {
|
||||
console.warn(`Error providing tests for ${uri.fsPath}`, e);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
type Operator = '+' | '-' | '*' | '/';
|
||||
|
||||
const testRe = /^([0-9]+)\s*([+*/-])\s*([0-9]+)\s*=\s*([0-9]+)/;
|
||||
const headingRe = /^(#+)\s*(.+)$/;
|
||||
|
||||
class TestRoot extends vscode.TestItem {
|
||||
public children: TestFile[] = [];
|
||||
|
||||
constructor() {
|
||||
super('markdown', 'Markdown Tests');
|
||||
}
|
||||
}
|
||||
|
||||
class TestFile extends vscode.TestItem {
|
||||
public children: (TestHeading | TestCase)[] = [];
|
||||
|
||||
constructor(public readonly uri: vscode.Uri) {
|
||||
super(`markdown/${uri.toString()}`, uri.path.split('/').pop()!);
|
||||
class TestFile extends vscode.TestItem<TestHeading | TestCase> {
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri,
|
||||
public parent: WorkspaceTestRoot | DocumentTestRoot,
|
||||
private readonly getContent: (uri: vscode.Uri) => Promise<string>,
|
||||
) {
|
||||
super(uri.toString(), uri.path.split('/').pop()!, true);
|
||||
}
|
||||
|
||||
public async updateTestsFromFs() {
|
||||
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;
|
||||
}
|
||||
|
||||
return this.updateTestsFromText(text);
|
||||
public discoverChildren(progress: vscode.Progress<{ busy: boolean }>, token: vscode.CancellationToken) {
|
||||
this.refresh().then(() => progress.report({ busy: false }));
|
||||
}
|
||||
|
||||
public updateTestsFromText(text: string) {
|
||||
const lines = text.split('\n');
|
||||
/**
|
||||
* Parses the tests from the input text, and updates the tests contained
|
||||
* by this file to be those from the text,
|
||||
*/
|
||||
public async refresh() {
|
||||
const ancestors: (TestFile | TestHeading)[] = [this];
|
||||
let discovered = 0;
|
||||
this.children = [];
|
||||
const thisGeneration = generationCounter++;
|
||||
|
||||
for (let lineNo = 0; lineNo < lines.length; lineNo++) {
|
||||
const line = lines[lineNo];
|
||||
const heading = headingRe.exec(line);
|
||||
parseMarkdown(await this.getContent(this.uri), {
|
||||
onTest: (range, a, operator, b, expected) => {
|
||||
const parent = ancestors[ancestors.length - 1];
|
||||
const tcase = new TestCase(Number(a), operator as Operator, Number(b), Number(expected), new vscode.Location(this.uri, range), thisGeneration, parent);
|
||||
|
||||
const test = testRe.exec(line);
|
||||
if (test) {
|
||||
const [, a, operator, b, expected] = test;
|
||||
const range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, test[0].length));
|
||||
const tcase = new TestCase(Number(a), operator as Operator, Number(b), Number(expected), new vscode.Location(this.uri, range));
|
||||
ancestors[ancestors.length - 1].children.push(tcase);
|
||||
discovered++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (heading) {
|
||||
const [, pounds, name] = heading;
|
||||
const level = pounds.length;
|
||||
while (ancestors.length > level) {
|
||||
const existing = parent.children.get(tcase.id);
|
||||
if (existing) {
|
||||
existing.generation = thisGeneration;
|
||||
} else {
|
||||
parent.children.add(tcase);
|
||||
}
|
||||
},
|
||||
onHeading: (range, name, depth) => {
|
||||
while (ancestors.length > depth) {
|
||||
ancestors.pop();
|
||||
}
|
||||
const range = new vscode.Range(new vscode.Position(lineNo, 0), new vscode.Position(lineNo, line.length));
|
||||
const thead = new TestHeading(level, name, new vscode.Location(this.uri, range));
|
||||
ancestors[ancestors.length - 1].children.push(thead);
|
||||
ancestors.push(thead);
|
||||
continue;
|
||||
|
||||
const parent = ancestors[ancestors.length - 1];
|
||||
const thead = new TestHeading(name, new vscode.Location(this.uri, range), thisGeneration, parent);
|
||||
const existing = parent.children.get(thead.id);
|
||||
if (existing instanceof TestHeading) {
|
||||
ancestors.push(existing);
|
||||
existing.generation = thisGeneration;
|
||||
} else {
|
||||
parent.children.add(thead);
|
||||
ancestors.push(thead);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.prune(thisGeneration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes tests that were deleted from the source. Each test suite and case
|
||||
* has a 'generation' counter which is updated each time we discover it. This
|
||||
* is called after discovery is finished to remove any children who are no
|
||||
* longer in this generation.
|
||||
*/
|
||||
private prune(thisGeneration: number) {
|
||||
const queue: (TestHeading | TestFile)[] = [this];
|
||||
for (const parent of queue) {
|
||||
for (const child of parent.children) {
|
||||
if (child.generation < thisGeneration) {
|
||||
parent.children.delete(child);
|
||||
} else if (child instanceof TestHeading) {
|
||||
queue.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return discovered;
|
||||
}
|
||||
}
|
||||
|
||||
class TestHeading extends vscode.TestItem {
|
||||
public readonly children: (TestHeading | TestCase)[] = [];
|
||||
class TestHeading extends vscode.TestItem<TestHeading | TestCase> {
|
||||
public readonly level: number = this.parent instanceof TestFile ? 1 : this.parent.level + 1;
|
||||
|
||||
constructor(
|
||||
public readonly level: number,
|
||||
label: string,
|
||||
public readonly location: vscode.Location,
|
||||
public generation: number,
|
||||
public readonly parent: TestFile | TestHeading,
|
||||
) {
|
||||
super(`markdown/${location.uri.toString()}/${label}`, label);
|
||||
super(`markdown/${location.uri.toString()}/${label}`, label, true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,8 +207,10 @@ class TestCase extends vscode.TestItem {
|
||||
private readonly b: number,
|
||||
private readonly expected: number,
|
||||
public readonly location: vscode.Location,
|
||||
public generation: number,
|
||||
public readonly parent: TestHeading | TestFile,
|
||||
) {
|
||||
super( `markdown/${location.uri.toString()}/${a} + ${b} = ${expected}`, `${a} + ${b} = ${expected}`);
|
||||
super(`markdown/${location.uri.toString()}/${a} + ${b} = ${expected}`, `${a} + ${b} = ${expected}`, false);
|
||||
}
|
||||
|
||||
async run(): Promise<vscode.TestState> {
|
||||
|
||||
Reference in New Issue
Block a user