Skip to content

Commit

Permalink
Support select single test run (#22)
Browse files Browse the repository at this point in the history
* feat: Support select single test run
  • Loading branch information
yubing744 authored Jul 9, 2022
1 parent 4844c2c commit cb94ce1
Show file tree
Hide file tree
Showing 15 changed files with 597 additions and 35 deletions.
5 changes: 4 additions & 1 deletion src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ export * from './mpm_commands';
type CommandCallback<T extends unknown[]> = (...args: T) => Promise<unknown> | unknown;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CommandFactory<T extends unknown[] = any[]> = (ideCtx: IDEExtensionContext) => CommandCallback<T>;
export type CommandFactory<T extends unknown[] = any[]> = (
ideCtx: IDEExtensionContext,
...args: any
) => CommandCallback<T>;

export function createRegisterCommand(ctx: IDEExtensionContext) {
return function registerCommand(name: string, fn: CommandFactory) {
Expand Down
13 changes: 13 additions & 0 deletions src/commands/mpm_commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,19 @@ export const mpmTestFile: CommandFactory = (ctx: IDEExtensionContext) => {
};
};

export const mpmTestFunction = (ctx: IDEExtensionContext) => {
return async (filter: string): Promise<void> => {
const document = window.activeTextEditor?.document;
if (!document) {
throw new Error('No document opened');
}

return mpmExecute(ctx, 'testUnit', 'package test', Marker.None, {
shellArgs: ['--filter', filter]
});
};
};

export const mpmPublish: CommandFactory = (ctx: IDEExtensionContext) => {
return async (): Promise<void> => {
return mpmExecute(ctx, 'publish', 'sandbox publish', Marker.None);
Expand Down
2 changes: 1 addition & 1 deletion src/download/commons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ function isZip(release: Release): boolean {
* @returns
*/
export function hasBinary(loader: Downloader): boolean {
return fs.existsSync(loader.executatePath);
return fs.existsSync(loader.executatePath) && fs.existsSync(loader.versionPath);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as vscode from 'vscode';

import { IDEExtensionContext } from './context';
import { Logger } from './log';
import { MoveTestExplorer } from './move-test';
import * as commands from './commands';

/**
Expand Down Expand Up @@ -37,11 +38,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {

await commands.checkAndUpdateAll(ideCtx)();
await commands.startLanguageServer(ideCtx)();
MoveTestExplorer.setup(ideCtx);

registerCommand('starcoin.build', commands.mpmBuild);
registerCommand('starcoin.testUnit', commands.mpmTestUnit);
registerCommand('starcoin.testIntegration', commands.mpmTestIntegration);
registerCommand('starcoin.testFile', commands.mpmTestFile);
registerCommand('starcoin.testFunction', commands.mpmTestFunction);
registerCommand('starcoin.publish', commands.mpmPublish);
registerCommand('starcoin.doctor', commands.mpmDoctor);
registerCommand('starcoin.checkCompatibility', commands.mpmCheckCompatibility);
Expand Down
122 changes: 122 additions & 0 deletions src/move-test/explore.ts
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);
}
}
1 change: 1 addition & 0 deletions src/move-test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MoveTestExplorer } from './explore';
161 changes: 161 additions & 0 deletions src/move-test/resolve.ts
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);
}
}
Loading

0 comments on commit cb94ce1

Please sign in to comment.