diff --git a/test-provider-sample/README.md b/test-provider-sample/README.md index 8477b635..ea23fc36 100644 --- a/test-provider-sample/README.md +++ b/test-provider-sample/README.md @@ -23,4 +23,4 @@ todo - 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 +- Create a `test.md` file containing the given content diff --git a/test-provider-sample/sample/test2.md b/test-provider-sample/sample/test2.md new file mode 100644 index 00000000..83795dd5 --- /dev/null +++ b/test-provider-sample/sample/test2.md @@ -0,0 +1,4 @@ + +# hi + +2 + 2 = 4 // this test will pass diff --git a/test-provider-sample/src/extension.ts b/test-provider-sample/src/extension.ts index 37e277ee..3ae12d51 100644 --- a/test-provider-sample/src/extension.ts +++ b/test-provider-sample/src/extension.ts @@ -1,21 +1,10 @@ import * as vscode from 'vscode'; -import { TestCodeLensProvider } from './testCodeLens'; import { MathTestProvider } from './testProvider'; -import { TestTreeDataProvider } from './testTreeView'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.test.registerTestProvider(new MathTestProvider()), - // Sample code lens implementation: - vscode.languages.registerCodeLensProvider({ pattern: '**/*.*' }, new TestCodeLensProvider()), - - // Sample tree view: - vscode.window.createTreeView('sampleTestExplorerView', { - treeDataProvider: new TestTreeDataProvider(), - showCollapseAll: true, - }), - vscode.commands.registerCommand('test-provider-sample.runTests', async tests => { await vscode.test.runTests({ tests: tests instanceof Array ? tests : [tests], debug: false }); vscode.window.showInformationMessage('Test run complete'); diff --git a/test-provider-sample/src/testCodeLens.ts b/test-provider-sample/src/testCodeLens.ts deleted file mode 100644 index 575653da..00000000 --- a/test-provider-sample/src/testCodeLens.ts +++ /dev/null @@ -1,82 +0,0 @@ -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(); - - 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 { - 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) => { - 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: '; - } -}; diff --git a/test-provider-sample/src/testProvider.ts b/test-provider-sample/src/testProvider.ts index a4331acc..c172946d 100644 --- a/test-provider-sample/src/testProvider.ts +++ b/test-provider-sample/src/testProvider.ts @@ -66,26 +66,31 @@ export class MathTestProvider implements vscode.TestProvider { /** * @inheritdoc */ - public async runTests(options: vscode.TestRunOptions) { - const queue: (() => Promise)[] = []; - await this.gatherTestTree(options.tests, queue); + public async runTests(options: vscode.TestRunOptions, cancellation: vscode.CancellationToken) { + const queue = await this.gatherTestTree(options.tests); - for (const runTest of queue) { - await runTest(); + while (queue.length && !cancellation.isCancellationRequested) { + await queue.shift()!.run(); + } + + while (queue.length) { + queue.shift()!.cancel(); } } - private async gatherTestTree(tests: vscode.TestItem[], queue: (() => Promise)[]) { + private async gatherTestTree(tests: vscode.TestItem[], queue: ({ run(): Promise; cancel(): void; })[] = []) { for (const test of tests) { if (test instanceof TestCase) { test.markQueued(); - queue.push(() => test.run()); + queue.push({ run: () => test.run(), cancel: () => test.markCancelled() }); } if (test.children) { this.gatherTestTree(test.children, queue); } } + + return queue; } } @@ -99,11 +104,14 @@ const updateTestsInFile = async (root: TestRoot, uri: vscode.Uri, emitter: vscod 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); + if (await testFile.updateTestsFromFs(emitter) === 0) { + removeTestsForFile(root, uri); + emitter.fire(root); + } else { + emitter.fire(changeTarget); + } }; const testRe = /^([0-9]+)\s*\+\s*([0-9]+)\s*=\s*([0-9]+)/; @@ -133,12 +141,13 @@ class TestFile implements vscode.TestItem { return; } - this.updateTestsFromText(text, updateEmitter); + return this.updateTestsFromText(text, updateEmitter); } public updateTestsFromText(text: string, updateEmitter: vscode.EventEmitter) { const lines = text.split('\n'); const ancestors: (TestFile | TestHeading)[] = [this]; + let discovered = 0; this.children = []; for (let lineNo = 0; lineNo < lines.length; lineNo++) { @@ -151,6 +160,7 @@ class TestFile implements vscode.TestItem { 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); + discovered++; continue; } @@ -165,6 +175,8 @@ class TestFile implements vscode.TestItem { continue; } } + + return discovered; } } @@ -195,8 +207,13 @@ class TestCase implements vscode.TestItem { private readonly updateEmitter: vscode.EventEmitter ) {} - async markQueued() { - this.state = new vscode.TestState(vscode.TestRunState.Running); + markQueued() { + this.state = new vscode.TestState(vscode.TestRunState.Queued); + this.updateEmitter.fire(this); + } + + markCancelled() { + this.state = new vscode.TestState(vscode.TestRunState.Skipped); this.updateEmitter.fire(this); } diff --git a/test-provider-sample/src/testTreeView.ts b/test-provider-sample/src/testTreeView.ts deleted file mode 100644 index 34e79faa..00000000 --- a/test-provider-sample/src/testTreeView.ts +++ /dev/null @@ -1,129 +0,0 @@ -import * as vscode from 'vscode'; - -export class TestTreeDataProvider implements vscode.TreeDataProvider { - public static readonly viewId = 'sampleTestExplorerView'; - - private readonly didChangeEmitter = new vscode.EventEmitter(); - private observers?: vscode.TestObserver[]; - - constructor() { - vscode.workspace.onDidChangeWorkspaceFolders(() => { - this.disposeExistingObservers(); - }); - - let stopOngoingProgress: undefined | (() => void); - this.didChangeEmitter.event(() => { - const anyRunningTest = this.observers?.some(o => - o.tests.some(t => getComputedState(t) === vscode.TestRunState.Running) - ); - - if (stopOngoingProgress && !anyRunningTest) { - stopOngoingProgress(); - stopOngoingProgress = undefined; - } else if (!stopOngoingProgress && anyRunningTest) { - vscode.window.withProgress( - { location: { viewId: TestTreeDataProvider.viewId } }, - () => new Promise(resolve => (stopOngoingProgress = resolve)) - ); - } - }); - } - - /** - * @inheritdoc - */ - public readonly onDidChangeTreeData = this.didChangeEmitter.event; - - /** - * @inheritdoc - */ - getTreeItem(test: vscode.TestItem): vscode.TreeItem { - const item = new vscode.TreeItem( - test.label, - test.children?.length ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None - ); - item.description = test.description; - const state = getComputedState(test); - item.iconPath = getStateIcon(state); - item.contextValue = (test.runnable !== false ? 'runnable-' : '') + (test.debuggable !== true ? 'debuggable-' : ''); - if (test.location) { - item.command = { - title: 'Go to test location', - command: 'vscode.open', - arguments: [test.location.uri, { selection: test.location.range }] - }; - } - - return item; - } - - /** - * @inheritdoc - */ - getChildren(element?: vscode.TestItem): vscode.ProviderResult { - if (!this.observers) { - this.observers = []; - for (const folder of vscode.workspace.workspaceFolders ?? []) { - const observer = vscode.test.createWorkspaceTestObserver(folder); - observer.onDidChangeTest(evt => this.didChangeEmitter.fire(evt.commonChangeAncestor)); - this.observers.push(observer); - } - } - - return element ? element.children : this.observers.flatMap(o => o.tests); - } - - public dispose() { - this.disposeExistingObservers(); - } - - private disposeExistingObservers() { - if (this.observers) { - for (const d of this.observers) { - d.dispose(); - } - this.observers = undefined; - } - } -} - -const statePriorities: { [S in vscode.TestRunState]: number } = { - [vscode.TestRunState.Running]: 0, - [vscode.TestRunState.Failed]: 1, - [vscode.TestRunState.Errored]: 2, - [vscode.TestRunState.Running]: 3, - [vscode.TestRunState.Passed]: 4, - [vscode.TestRunState.Unset]: 5, - [vscode.TestRunState.Skipped]: 6, -}; - -/** - * Gets the state that should be displayed in the tree for a test item. - */ -const getComputedState = (item: vscode.TestItem): vscode.TestRunState => { - let bestChildState: vscode.TestRunState = item.state.runState; - let bestChildScore = statePriorities[bestChildState]; - for (const child of item.children ?? []) { - const state = getComputedState(child); - const score = statePriorities[state]; - if (score < bestChildScore) { - bestChildScore = score; - bestChildState = state; - } - } - - return bestChildState; -}; - -const getStateIcon = (state: vscode.TestRunState) => { - switch (state) { - case vscode.TestRunState.Failed: - return new vscode.ThemeIcon('close'); - case vscode.TestRunState.Running: - return new vscode.ThemeIcon('sync'); - case vscode.TestRunState.Passed: - return new vscode.ThemeIcon('check'); - default: - return undefined; - } -};