-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support select single test run (#22)
* feat: Support select single test run
- Loading branch information
Showing
15 changed files
with
597 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import * as vscode from 'vscode'; | ||
import { IDEExtensionContext } from '../context'; | ||
import { MoveTestResolver } from './resolve'; | ||
import { MoveTestRunner } from './run'; | ||
import { isInTest } from './utils'; | ||
import { Logger } from '../log'; | ||
|
||
// Set true only if the Testing API is available (VSCode version >= 1.59). | ||
export const isVscodeTestingAPIAvailable = | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
'object' === typeof (vscode as any).tests && 'function' === typeof (vscode as any).tests.createTestController; | ||
|
||
export class MoveTestExplorer { | ||
static setup(ctx: IDEExtensionContext) { | ||
if (!isVscodeTestingAPIAvailable) throw new Error('VSCode Testing API is unavailable'); | ||
|
||
const ctrl = vscode.tests.createTestController('move', 'Move Test Explorer'); | ||
const inst = new this(ctx, ctrl); | ||
|
||
// Process already open editors | ||
vscode.window.visibleTextEditors.forEach((ed) => { | ||
inst.documentUpdate(ed.document); | ||
}); | ||
|
||
ctx.vscode.subscriptions.push( | ||
vscode.workspace.onDidOpenTextDocument(async (x) => { | ||
try { | ||
await inst.didOpenTextDocument(x); | ||
} catch (error) { | ||
if (isInTest()) throw error; | ||
else ctx.logger.info(`Failed while handling 'onDidOpenTextDocument': ${error}`); | ||
} | ||
}) | ||
); | ||
|
||
ctx.vscode.subscriptions.push( | ||
vscode.workspace.onDidChangeTextDocument(async (x) => { | ||
try { | ||
await inst.didChangeTextDocument(x); | ||
} catch (error) { | ||
if (isInTest()) throw error; | ||
else ctx.logger.info(`Failed while handling 'onDidChangeTextDocument': ${error}`); | ||
} | ||
}) | ||
); | ||
|
||
ctx.vscode.subscriptions.push(ctrl); | ||
|
||
// register commands | ||
ctx.vscode.subscriptions.push( | ||
vscode.commands.registerCommand('starcoin.tests', () => { | ||
return inst.allItems; | ||
}) | ||
); | ||
|
||
return inst; | ||
} | ||
|
||
private readonly ctx: IDEExtensionContext; | ||
private readonly logger: Logger; | ||
public readonly resolver: MoveTestResolver; | ||
private readonly runner: MoveTestRunner; | ||
|
||
private constructor(ctx: IDEExtensionContext, ctrl: vscode.TestController) { | ||
this.ctx = ctx; | ||
this.logger = ctx.logger; | ||
|
||
this.resolver = new MoveTestResolver(ctx, ctrl); | ||
this.runner = new MoveTestRunner(ctx, ctrl); | ||
} | ||
|
||
/* ***** Private ***** */ | ||
|
||
// Handle opened documents, document changes, and file creation. | ||
private async documentUpdate(doc: vscode.TextDocument, ranges?: vscode.Range[]) { | ||
if (!doc.uri.path.endsWith('.move')) { | ||
return; | ||
} | ||
|
||
this.logger.info(`documentUpdate Processing ${doc.uri.fsPath}`); | ||
|
||
// If we don't do this, then we attempt to resolve tests in virtual | ||
// documents such as those created by the Git, GitLens, and GitHub PR | ||
// extensions | ||
if (doc.uri.scheme !== 'file') { | ||
return; | ||
} | ||
|
||
await this.resolver.processDocument(doc, ranges); | ||
} | ||
|
||
/* ***** Listeners ***** */ | ||
|
||
protected async didOpenTextDocument(doc: vscode.TextDocument) { | ||
await this.documentUpdate(doc); | ||
} | ||
|
||
protected async didChangeTextDocument(e: vscode.TextDocumentChangeEvent) { | ||
await this.documentUpdate( | ||
e.document, | ||
e.contentChanges.map((x) => x.range) | ||
); | ||
} | ||
|
||
get items() { | ||
return this.resolver.items; | ||
} | ||
|
||
get allItems() { | ||
function* it(coll: vscode.TestItemCollection): Generator<vscode.TestItem> { | ||
const arr: vscode.TestItem[] = []; | ||
coll.forEach((x) => arr.push(x)); | ||
|
||
for (const item of arr) { | ||
yield item; | ||
yield* it(item.children); | ||
} | ||
} | ||
|
||
return it(this.items); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { MoveTestExplorer } from './explore'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import * as path from 'path'; | ||
import * as vscode from 'vscode'; | ||
import { IDEExtensionContext } from '../context'; | ||
import { MoveTestKind, genMoveTestId, parseMoveTestId } from './utils'; | ||
|
||
export class MoveTestResolver { | ||
private ctx: IDEExtensionContext; | ||
private ctrl: vscode.TestController; | ||
public readonly all = new Map<string, vscode.TestItem>(); | ||
|
||
constructor(ctx: IDEExtensionContext, ctrl: vscode.TestController) { | ||
this.ctx = ctx; | ||
this.ctrl = ctrl; | ||
} | ||
|
||
public async provideDocumentSymbols(document: vscode.TextDocument): Promise<vscode.DocumentSymbol[]> { | ||
const symbols: vscode.DocumentSymbol[] | undefined = await vscode.commands.executeCommand( | ||
'vscode.executeDocumentSymbolProvider', | ||
document.uri | ||
); | ||
|
||
if (!symbols || symbols.length === 0) { | ||
return []; | ||
} | ||
|
||
return symbols; | ||
} | ||
|
||
get items() { | ||
return this.ctrl.items; | ||
} | ||
|
||
// Processes a Move document, calling processSymbol for each symbol in the | ||
// document. | ||
// | ||
// Any previously existing tests that no longer have a corresponding symbol in | ||
// the file will be disposed. If the document contains no tests, it will be | ||
// disposed. | ||
async processDocument(doc: vscode.TextDocument, ranges: vscode.Range[] | undefined) { | ||
const seen = new Set<string>(); | ||
const item = await this.getFile(doc.uri); | ||
const symbols = await this.provideDocumentSymbols(doc); | ||
|
||
for (const symbol of symbols) { | ||
await this.processSymbol(doc, item, seen, [], symbol); | ||
} | ||
|
||
item.children.forEach((child) => { | ||
const { name } = parseMoveTestId(child.id); | ||
if (!name || !seen.has(name)) { | ||
this.dispose(child); | ||
return; | ||
} | ||
|
||
if (ranges?.some((r: vscode.Range) => !!child.range?.intersection(r))) { | ||
item.children.forEach((x) => this.dispose(x)); | ||
} | ||
}); | ||
|
||
this.disposeIfEmpty(item); | ||
} | ||
|
||
// Create an item. | ||
private createItem(label: string, uri: vscode.Uri, kind: MoveTestKind, name?: string): vscode.TestItem { | ||
const id = genMoveTestId(uri, kind, name); | ||
const item = this.ctrl.createTestItem(id, label, uri.with({ query: '', fragment: '' })); | ||
this.all.set(id, item); | ||
return item; | ||
} | ||
|
||
// Retrieve an item. | ||
private getItem( | ||
parent: vscode.TestItem | undefined, | ||
uri: vscode.Uri, | ||
kind: MoveTestKind, | ||
name?: string | ||
): vscode.TestItem | undefined { | ||
return (parent?.children || this.ctrl.items).get(genMoveTestId(uri, kind, name)); | ||
} | ||
|
||
// Create or retrieve an item. | ||
private getOrCreateItem( | ||
parent: vscode.TestItem | undefined, | ||
label: string, | ||
uri: vscode.Uri, | ||
kind: MoveTestKind, | ||
name?: string | ||
): vscode.TestItem { | ||
const existing = this.getItem(parent, uri, kind, name); | ||
if (existing) return existing; | ||
|
||
const item = this.createItem(label, uri, kind, name); | ||
(parent?.children || this.ctrl.items).add(item); | ||
return item; | ||
} | ||
|
||
// Retrieve or create an item for a Go file. | ||
private async getFile(uri: vscode.Uri): Promise<vscode.TestItem> { | ||
const label = path.basename(uri.path); | ||
const item = this.getOrCreateItem(undefined, label, uri, 'file'); | ||
item.canResolveChildren = true; | ||
return item; | ||
} | ||
|
||
private dispose(item: vscode.TestItem) { | ||
this.all.delete(item.id); | ||
item.parent?.children.delete(item.id); | ||
} | ||
|
||
// Dispose of the item if it has no children, recursively. This facilitates | ||
// cleaning up package/file trees that contain no tests. | ||
private disposeIfEmpty(item: vscode.TestItem | undefined) { | ||
if (!item) return; | ||
// Don't dispose of empty top-level items | ||
const { kind } = parseMoveTestId(item.id); | ||
if (kind === 'module') { | ||
return; | ||
} | ||
|
||
if (item.children.size > 0) { | ||
return; | ||
} | ||
|
||
this.dispose(item); | ||
this.disposeIfEmpty(item.parent); | ||
} | ||
|
||
// Recursively process a Go AST symbol. If the symbol represents a test, fuzz test, | ||
// benchmark, or example function, a test item will be created for it, if one | ||
// does not already exist. If the symbol is not a function and contains | ||
// children, those children will be processed recursively. | ||
private async processSymbol( | ||
doc: vscode.TextDocument, | ||
file: vscode.TestItem, | ||
seen: Set<string>, | ||
parents: Array<string>, | ||
symbol: vscode.DocumentSymbol | ||
) { | ||
// Recursively process symbols that are nested | ||
if (symbol.kind !== vscode.SymbolKind.Function && symbol.kind !== vscode.SymbolKind.Method) { | ||
const parentName = symbol.name; | ||
|
||
for (const sym of symbol.children) { | ||
await this.processSymbol(doc, file, seen, parents.concat(parentName), sym); | ||
} | ||
|
||
return; | ||
} | ||
|
||
const match = symbol.detail.indexOf('test') > 0; | ||
if (!match) { | ||
return; | ||
} | ||
|
||
const longName = parents.concat(symbol.name).join('::'); | ||
seen.add(longName); | ||
const item = this.getOrCreateItem(file, symbol.name, doc.uri, 'func', longName); | ||
item.range = symbol.range; | ||
this.all.set(item.id, item); | ||
} | ||
} |
Oops, something went wrong.