[vscode-go] src/goModules: correctly handle gopath mode

671 views
Skip to first unread message

Ethan Reesor (Gerrit)

unread,
Aug 19, 2021, 11:10:53 PM8/19/21
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Ethan Reesor has uploaded this change for review.

View Change

src/goModules: correctly handle gopath mode

In certain circumstances, `go env GOPATH` can return 'NUL' or
'/dev/null'. This updates goModules to handle that case.

Change-Id: I487911b0ee8de28e88d7c558c11a49109956eb0c
---
M src/goMain.ts
A src/goTest/explore.ts
A src/goTest/resolve.ts
A src/goTest/run.ts
A src/goTest/utils.ts
A src/goTest/walk.ts
D src/goTestExplorer.ts
7 files changed, 1,201 insertions(+), 1,100 deletions(-)

diff --git a/src/goMain.ts b/src/goMain.ts
index 5f7d26d..8300c5b 100644
--- a/src/goMain.ts
+++ b/src/goMain.ts
@@ -114,7 +114,7 @@
import { resetSurveyConfig, showSurveyConfig, timeMinute } from './goSurvey';
import { ExtensionAPI } from './export';
import extensionAPI from './extensionAPI';
-import { isVscodeTestingAPIAvailable, TestExplorer } from './goTestExplorer';
+import { GoTestExplorer, isVscodeTestingAPIAvailable } from './goTest/explore';

export let buildDiagnosticCollection: vscode.DiagnosticCollection;
export let lintDiagnosticCollection: vscode.DiagnosticCollection;
@@ -337,11 +337,11 @@
);

if (isVscodeTestingAPIAvailable) {
- const testExplorer = TestExplorer.setup(ctx);
+ const testExplorer = GoTestExplorer.setup(ctx);

ctx.subscriptions.push(
vscode.commands.registerCommand('go.test.refresh', (args) => {
- if (args) testExplorer.resolve(args);
+ if (args) testExplorer.resolver.resolve(args);
})
);
}
diff --git a/src/goTest/explore.ts b/src/goTest/explore.ts
new file mode 100644
index 0000000..5ccd0cf
--- /dev/null
+++ b/src/goTest/explore.ts
@@ -0,0 +1,244 @@
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+import {
+ ConfigurationChangeEvent,
+ ExtensionContext,
+ Range,
+ TestController,
+ TestItem,
+ TestItemCollection,
+ TestRunProfileKind,
+ TextDocument,
+ TextDocumentChangeEvent,
+ Uri,
+ workspace,
+ WorkspaceFoldersChangeEvent
+} from 'vscode';
+import vscode = require('vscode');
+import { GoDocumentSymbolProvider } from '../goOutline';
+import { outputChannel } from '../goStatus';
+import { dispose, disposeIfEmpty, findItem, GoTest, Workspace } from './utils';
+import { GoTestResolver, ProvideSymbols } from './resolve';
+import { GoTestRunner } from './run';
+
+// 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;
+
+// Check whether the process is running as a test.
+function isInTest() {
+ return process.env.VSCODE_GO_IN_TEST === '1';
+}
+
+export class GoTestExplorer {
+ static setup(context: ExtensionContext): GoTestExplorer {
+ if (!isVscodeTestingAPIAvailable) throw new Error('VSCode Testing API is unavailable');
+
+ const ctrl = vscode.tests.createTestController('go', 'Go');
+ const getSym = new GoDocumentSymbolProvider().provideDocumentSymbols;
+ const inst = new this(workspace, ctrl, getSym);
+
+ context.subscriptions.push(
+ workspace.onDidChangeConfiguration(async (x) => {
+ try {
+ await inst.didChangeConfiguration(x);
+ } catch (error) {
+ if (isInTest()) throw error;
+ else outputChannel.appendLine(`Failed while handling 'onDidChangeConfiguration': ${error}`);
+ }
+ })
+ );
+
+ context.subscriptions.push(
+ workspace.onDidOpenTextDocument(async (x) => {
+ try {
+ await inst.didOpenTextDocument(x);
+ } catch (error) {
+ if (isInTest()) throw error;
+ else outputChannel.appendLine(`Failed while handling 'onDidOpenTextDocument': ${error}`);
+ }
+ })
+ );
+
+ context.subscriptions.push(
+ workspace.onDidChangeTextDocument(async (x) => {
+ try {
+ await inst.didChangeTextDocument(x);
+ } catch (error) {
+ if (isInTest()) throw error;
+ else outputChannel.appendLine(`Failed while handling 'onDidChangeTextDocument': ${error}`);
+ }
+ })
+ );
+
+ context.subscriptions.push(
+ workspace.onDidChangeWorkspaceFolders(async (x) => {
+ try {
+ await inst.didChangeWorkspaceFolders(x);
+ } catch (error) {
+ if (isInTest()) throw error;
+ else outputChannel.appendLine(`Failed while handling 'onDidChangeWorkspaceFolders': ${error}`);
+ }
+ })
+ );
+
+ const watcher = workspace.createFileSystemWatcher('**/*_test.go', false, true, false);
+ context.subscriptions.push(watcher);
+ context.subscriptions.push(
+ watcher.onDidCreate(async (x) => {
+ try {
+ await inst.didCreateFile(x);
+ } catch (error) {
+ if (isInTest()) throw error;
+ else outputChannel.appendLine(`Failed while handling 'FileSystemWatcher.onDidCreate': ${error}`);
+ }
+ })
+ );
+ context.subscriptions.push(
+ watcher.onDidDelete(async (x) => {
+ try {
+ await inst.didDeleteFile(x);
+ } catch (error) {
+ if (isInTest()) throw error;
+ else outputChannel.appendLine(`Failed while handling 'FileSystemWatcher.onDidDelete': ${error}`);
+ }
+ })
+ );
+
+ return inst;
+ }
+
+ public readonly resolver: GoTestResolver;
+
+ constructor(
+ private readonly workspace: Workspace,
+ private readonly ctrl: TestController,
+ provideDocumentSymbols: ProvideSymbols
+ ) {
+ const resolver = new GoTestResolver(workspace, ctrl, provideDocumentSymbols);
+ const runner = new GoTestRunner(workspace, ctrl, resolver);
+
+ this.resolver = resolver;
+
+ ctrl.resolveHandler = async (item) => {
+ try {
+ await resolver.resolve(item);
+ } catch (error) {
+ if (isInTest()) throw error;
+
+ const m = 'Failed to resolve tests';
+ outputChannel.appendLine(`${m}: ${error}`);
+ await vscode.window.showErrorMessage(m);
+ }
+ };
+
+ ctrl.createRunProfile(
+ 'go test',
+ TestRunProfileKind.Run,
+ async (request, token) => {
+ try {
+ await runner.run(request, token);
+ } catch (error) {
+ const m = 'Failed to execute tests';
+ outputChannel.appendLine(`${m}: ${error}`);
+ await vscode.window.showErrorMessage(m);
+ }
+ },
+ true
+ );
+ }
+
+ /* ***** Listeners ***** */
+
+ protected async didOpenTextDocument(doc: TextDocument) {
+ await this.documentUpdate(doc);
+ }
+
+ protected async didChangeTextDocument(e: TextDocumentChangeEvent) {
+ await this.documentUpdate(
+ e.document,
+ e.contentChanges.map((x) => x.range)
+ );
+ }
+
+ protected async didChangeWorkspaceFolders(e: WorkspaceFoldersChangeEvent) {
+ if (e.added.length > 0) {
+ await this.resolver.resolve();
+ }
+
+ if (e.removed.length === 0) {
+ return;
+ }
+
+ this.ctrl.items.forEach((item) => {
+ const uri = Uri.parse(item.id);
+ if (uri.query === 'package') {
+ return;
+ }
+
+ const ws = this.workspace.getWorkspaceFolder(uri);
+ if (!ws) {
+ dispose(item);
+ }
+ });
+ }
+
+ protected async didCreateFile(file: Uri) {
+ await this.documentUpdate(await this.workspace.openTextDocument(file));
+ }
+
+ protected async didDeleteFile(file: Uri) {
+ const id = GoTest.id(file, 'file');
+ function find(children: TestItemCollection): TestItem {
+ return findItem(children, (item) => {
+ if (item.id === id) {
+ return item;
+ }
+
+ const uri = Uri.parse(item.id);
+ if (!file.path.startsWith(uri.path)) {
+ return;
+ }
+
+ return find(item.children);
+ });
+ }
+
+ const found = find(this.ctrl.items);
+ if (found) {
+ dispose(found);
+ disposeIfEmpty(found.parent);
+ }
+ }
+
+ protected async didChangeConfiguration(e: ConfigurationChangeEvent) {
+ let update = false;
+ this.ctrl.items.forEach((item) => {
+ if (e.affectsConfiguration('go.testExplorerPackages', item.uri)) {
+ dispose(item);
+ update = true;
+ }
+ });
+
+ if (update) {
+ this.resolver.resolve();
+ }
+ }
+
+ // Handle opened documents, document changes, and file creation.
+ private async documentUpdate(doc: TextDocument, ranges?: Range[]) {
+ if (!doc.uri.path.endsWith('_test.go')) {
+ return;
+ }
+
+ if (doc.uri.scheme === 'git') {
+ // TODO(firelizzard18): When a workspace is reopened, VSCode passes us git: URIs. Why?
+ return;
+ }
+
+ await this.resolver.processDocument(doc, ranges);
+ }
+}
diff --git a/src/goTest/resolve.ts b/src/goTest/resolve.ts
new file mode 100644
index 0000000..c7fd8ab
--- /dev/null
+++ b/src/goTest/resolve.ts
@@ -0,0 +1,355 @@
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+import {
+ CancellationToken,
+ DocumentSymbol,
+ FileType,
+ Range,
+ SymbolKind,
+ TestController,
+ TestItem,
+ TextDocument,
+ Uri,
+ workspace,
+ WorkspaceFolder
+} from 'vscode';
+import path = require('path');
+import { getModFolderPath } from '../goModules';
+import { getCurrentGoPath } from '../util';
+import { getGoConfig } from '../config';
+import { dispose, disposeIfEmpty, FileSystem, GoTest, Workspace } from './utils';
+import { walk, WalkStop } from './walk';
+
+export type ProvideSymbols = (doc: TextDocument, token: CancellationToken) => Thenable<DocumentSymbol[]>;
+
+export class GoTestResolver {
+ constructor(
+ private readonly workspace: Workspace,
+ private readonly ctrl: TestController,
+ private readonly provideDocumentSymbols: ProvideSymbols
+ ) {}
+
+ get items() {
+ return this.ctrl.items;
+ }
+
+ async resolve(item?: TestItem) {
+ // Expand the root item - find all modules and workspaces
+ if (!item) {
+ // Dispose of package entries at the root if they are now part of a workspace folder
+ this.ctrl.items.forEach((item) => {
+ const uri = Uri.parse(item.id);
+ if (uri.query !== 'package') {
+ return;
+ }
+
+ if (this.workspace.getWorkspaceFolder(uri)) {
+ dispose(item);
+ }
+ });
+
+ // Create entries for all modules and workspaces
+ for (const folder of this.workspace.workspaceFolders || []) {
+ const found = await walkWorkspaces(this.workspace.fs, folder.uri);
+ let needWorkspace = false;
+ for (const [uri, isMod] of found.entries()) {
+ if (!isMod) {
+ needWorkspace = true;
+ continue;
+ }
+
+ await this.getModule(Uri.parse(uri));
+ }
+
+ // If the workspace folder contains any Go files not in a module, create a workspace entry
+ if (needWorkspace) {
+ await this.getWorkspace(folder);
+ }
+ }
+ return;
+ }
+
+ const uri = Uri.parse(item.id);
+
+ // The user expanded a module or workspace - find all packages
+ if (uri.query === 'module' || uri.query === 'workspace') {
+ await walkPackages(this.workspace.fs, uri, async (uri) => {
+ await this.getPackage(uri);
+ });
+ }
+
+ // The user expanded a module or package - find all files
+ if (uri.query === 'module' || uri.query === 'package') {
+ for (const [file, type] of await this.workspace.fs.readDirectory(uri)) {
+ if (type !== FileType.File || !file.endsWith('_test.go')) {
+ continue;
+ }
+
+ await this.getFile(Uri.joinPath(uri, file));
+ }
+ }
+
+ // The user expanded a file - find all functions
+ if (uri.query === 'file') {
+ const doc = await this.workspace.openTextDocument(uri.with({ query: '', fragment: '' }));
+ await this.processDocument(doc);
+ }
+
+ // TODO(firelizzard18): If uri.query is test or benchmark, this is where we
+ // would discover sub tests or benchmarks, if that is feasible.
+ }
+
+ // Create or Retrieve a sub test or benchmark. The ID will be of the form:
+ // file:///path/to/mod/file.go?test#TestXxx/A/B/C
+ getOrCreateSubTest(item: TestItem, name: string): TestItem {
+ const { fragment: parentName, query: kind } = Uri.parse(item.id);
+ const existing = this.getItem(item, item.uri, kind, `${parentName}/${name}`);
+ if (existing) return existing;
+
+ item.canResolveChildren = true;
+ const sub = this.createItem(name, item.uri, kind, `${parentName}/${name}`);
+ item.children.add(sub);
+ sub.range = item.range;
+ return sub;
+ }
+
+ // Processes a Go 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: TextDocument, ranges?: Range[]) {
+ const seen = new Set<string>();
+ const item = await this.getFile(doc.uri);
+ const symbols = await this.provideDocumentSymbols(doc, null);
+ for (const symbol of symbols) await this.processSymbol(doc.uri, item, seen, symbol);
+
+ item.children.forEach((child) => {
+ const uri = Uri.parse(child.id);
+ if (!seen.has(uri.fragment)) {
+ dispose(child);
+ return;
+ }
+
+ if (ranges?.some((r) => !!child.range.intersection(r))) {
+ child.children.forEach((x) => dispose(x));
+ }
+ });
+
+ disposeIfEmpty(item);
+ }
+
+ /* ***** Private ***** */
+
+ // Create an item.
+ private createItem(label: string, uri: Uri, kind: string, name?: string): TestItem {
+ return this.ctrl.createTestItem(GoTest.id(uri, kind, name), label, uri.with({ query: '', fragment: '' }));
+ }
+
+ // Retrieve an item.
+ private getItem(parent: TestItem | undefined, uri: Uri, kind: string, name?: string): TestItem {
+ return (parent?.children || this.ctrl.items).get(GoTest.id(uri, kind, name));
+ }
+
+ // Create or retrieve an item.
+ private getOrCreateItem(
+ parent: TestItem | undefined,
+ label: string,
+ uri: Uri,
+ kind: string,
+ name?: string
+ ): 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;
+ }
+
+ // If a test/benchmark with children is relocated, update the children's
+ // location.
+ private relocateChildren(item: TestItem) {
+ item.children.forEach((child) => {
+ child.range = item.range;
+ this.relocateChildren(child);
+ });
+ }
+
+ // Retrieve or create an item for a Go module.
+ private async getModule(uri: Uri): Promise<TestItem> {
+ const existing = this.getItem(null, uri, 'module');
+ if (existing) {
+ return existing;
+ }
+
+ // Use the module name as the label
+ const goMod = Uri.joinPath(uri, 'go.mod');
+ const contents = await this.workspace.fs.readFile(goMod);
+ const modLine = contents.toString().split('\n', 2)[0];
+ const match = modLine.match(/^module (?<name>.*?)(?:\s|\/\/|$)/);
+ const item = this.getOrCreateItem(null, match.groups.name, uri, 'module');
+ item.canResolveChildren = true;
+ return item;
+ }
+
+ // Retrieve or create an item for a workspace folder that is not a module.
+ private async getWorkspace(ws: WorkspaceFolder): Promise<TestItem> {
+ const existing = this.getItem(null, ws.uri, 'workspace');
+ if (existing) {
+ return existing;
+ }
+
+ // Use the workspace folder name as the label
+ const item = this.getOrCreateItem(null, ws.name, ws.uri, 'workspace');
+ item.canResolveChildren = true;
+ return item;
+ }
+
+ // Retrieve or create an item for a Go package.
+ private async getPackage(uri: Uri): Promise<TestItem> {
+ let item: TestItem;
+
+ const nested = getGoConfig(uri).get('testExplorerPackages') === 'nested';
+ const modDir = await getModFolderPath(uri, true);
+ const wsfolder = workspace.getWorkspaceFolder(uri);
+ if (modDir) {
+ // If the package is in a module, add it as a child of the module
+ let parent = await this.getModule(uri.with({ path: modDir, query: '', fragment: '' }));
+ if (uri.path === parent.uri.path) {
+ return parent;
+ }
+
+ if (nested) {
+ const bits = path.relative(parent.uri.path, uri.path).split(path.sep);
+ while (bits.length > 1) {
+ const dir = bits.shift();
+ const dirUri = uri.with({ path: path.join(parent.uri.path, dir), query: '', fragment: '' });
+ parent = this.getOrCreateItem(parent, dir, dirUri, 'package');
+ }
+ }
+
+ const label = uri.path.startsWith(parent.uri.path)
+ ? uri.path.substring(parent.uri.path.length + 1)
+ : uri.path;
+ item = this.getOrCreateItem(parent, label, uri, 'package');
+ } else if (wsfolder) {
+ // If the package is in a workspace folder, add it as a child of the workspace
+ const workspace = await this.getWorkspace(wsfolder);
+ const existing = this.getItem(workspace, uri, 'package');
+ if (existing) {
+ return existing;
+ }
+
+ const label = uri.path.startsWith(wsfolder.uri.path)
+ ? uri.path.substring(wsfolder.uri.path.length + 1)
+ : uri.path;
+ item = this.getOrCreateItem(workspace, label, uri, 'package');
+ } else {
+ // Otherwise, add it directly to the root
+ const existing = this.getItem(null, uri, 'package');
+ if (existing) {
+ return existing;
+ }
+
+ const srcPath = path.join(getCurrentGoPath(uri), 'src');
+ const label = uri.path.startsWith(srcPath) ? uri.path.substring(srcPath.length + 1) : uri.path;
+ item = this.getOrCreateItem(null, label, uri, 'package');
+ }
+
+ item.canResolveChildren = true;
+ return item;
+ }
+
+ // Retrieve or create an item for a Go file.
+ private async getFile(uri: Uri): Promise<TestItem> {
+ const dir = path.dirname(uri.path);
+ const pkg = await this.getPackage(uri.with({ path: dir, query: '', fragment: '' }));
+ const existing = this.getItem(pkg, uri, 'file');
+ if (existing) {
+ return existing;
+ }
+
+ const label = path.basename(uri.path);
+ const item = this.getOrCreateItem(pkg, label, uri, 'file');
+ item.canResolveChildren = true;
+ return item;
+ }
+
+ // Recursively process a Go AST symbol. If the symbol represents a 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(uri: Uri, file: TestItem, seen: Set<string>, symbol: DocumentSymbol) {
+ // Skip TestMain(*testing.M) - allow TestMain(*testing.T)
+ if (symbol.name === 'TestMain' && /\*testing.M\)/.test(symbol.detail)) {
+ return;
+ }
+
+ // Recursively process symbols that are nested
+ if (symbol.kind !== SymbolKind.Function) {
+ for (const sym of symbol.children) await this.processSymbol(uri, file, seen, sym);
+ return;
+ }
+
+ const match = symbol.name.match(/^(?<type>Test|Example|Benchmark)/);
+ if (!match) {
+ return;
+ }
+
+ seen.add(symbol.name);
+
+ const kind = match.groups.type.toLowerCase();
+ const existing = this.getItem(file, uri, kind, symbol.name);
+ if (existing) {
+ if (!existing.range.isEqual(symbol.range)) {
+ existing.range = symbol.range;
+ this.relocateChildren(existing);
+ }
+ return existing;
+ }
+
+ const item = this.getOrCreateItem(file, symbol.name, uri, kind, symbol.name);
+ item.range = symbol.range;
+ }
+}
+
+// Walk the workspace, looking for Go modules. Returns a map indicating paths
+// that are modules (value == true) and paths that are not modules but contain
+// Go files (value == false).
+async function walkWorkspaces(fs: FileSystem, uri: Uri): Promise<Map<string, boolean>> {
+ const found = new Map<string, boolean>();
+ await walk(fs, uri, async (dir, file, type) => {
+ if (type !== FileType.File) {
+ return;
+ }
+
+ if (file === 'go.mod') {
+ // BUG(firelizard18): This does not create a separate entry for
+ // modules within a module. Thus, tests in a module within another
+ // module will appear under the top-level module's tree. This may or
+ // may not be acceptable.
+ found.set(dir.toString(), true);
+ return WalkStop.Current;
+ }
+
+ if (file.endsWith('.go')) {
+ found.set(dir.toString(), false);
+ }
+ });
+ return found;
+}
+
+// Walk the workspace, calling the callback for any directory that contains a Go
+// test file.
+async function walkPackages(fs: FileSystem, uri: Uri, cb: (uri: Uri) => Promise<unknown>) {
+ await walk(fs, uri, async (dir, file) => {
+ if (file.endsWith('_test.go')) {
+ await cb(dir);
+ return WalkStop.Files;
+ }
+ });
+}
diff --git a/src/goTest/run.ts b/src/goTest/run.ts
new file mode 100644
index 0000000..0e65db0
--- /dev/null
+++ b/src/goTest/run.ts
@@ -0,0 +1,450 @@
+/*---------------------------------------------------------
+ * Copyright 2021 The Go Authors. All rights reserved.
+ * Licensed under the MIT License. See LICENSE in the project root for license information.
+ *--------------------------------------------------------*/
+import {
+ CancellationToken,
+ Location,
+ OutputChannel,
+ Position,
+ TestController,
+ TestItem,
+ TestMessage,
+ TestRun,
+ TestRunRequest,
+ Uri
+} from 'vscode';
+import vscode = require('vscode');
+import path = require('path');
+import { isModSupported } from '../goModules';
+import { getGoConfig } from '../config';
+import { getTestFlags, goTest, GoTestOutput } from '../testUtils';
+import { GoTestResolver } from './resolve';
+import { dispose, forEachAsync, Workspace } from './utils';
+
+export class GoTestRunner {
+ constructor(
+ private readonly workspace: Workspace,
+ private readonly ctrl: TestController,
+ private readonly resolver: GoTestResolver
+ ) {}
+
+ // Execute tests - TestController.runTest callback
+ async run(request: TestRunRequest, token: CancellationToken) {
+ const collected = new Map<string, CollectedTest[]>();
+ const docs = new Set<Uri>();
+ if (request.include) {
+ for (const item of request.include) {
+ await collectTests(this.resolver, item, true, request.exclude || [], collected, docs);
+ }
+ } else {
+ const promises: Promise<unknown>[] = [];
+ this.ctrl.items.forEach((item) => {
+ const p = collectTests(this.resolver, item, true, request.exclude || [], collected, docs);
+ promises.push(p);
+ });
+ await Promise.all(promises);
+ }
+
+ // Save all documents that contain a test we're about to run, to ensure `go
+ // test` has the latest changes
+ await Promise.all(this.workspace.textDocuments.filter((x) => docs.has(x.uri)).map((x) => x.save()));
+
+ let hasBench = false,
+ hasNonBench = false;
+ for (const items of collected.values()) {
+ for (const { item } of items) {
+ const uri = Uri.parse(item.id);
+ if (uri.query === 'benchmark') hasBench = true;
+ else hasNonBench = true;
+ }
+ }
+
+ const run = this.ctrl.createTestRun(request);
+ const outputChannel = new TestRunOutput(run);
+ for (const [dir, items] of collected.entries()) {
+ const uri = Uri.parse(dir);
+ const isMod = await isModSupported(uri, true);
+ const goConfig = getGoConfig(uri);
+ const flags = getTestFlags(goConfig);
+ const includeBench = getGoConfig(uri).get('testExplorerRunBenchmarks');
+
+ // Separate tests and benchmarks and mark them as queued for execution.
+ // Clear any sub tests/benchmarks generated by a previous run.
+ const tests: Record<string, TestItem> = {};
+ const benchmarks: Record<string, TestItem> = {};
+ for (const { item, explicitlyIncluded } of items) {
+ const uri = Uri.parse(item.id);
+ if (/[/#]/.test(uri.fragment)) {
+ // running sub-tests is not currently supported
+ vscode.window.showErrorMessage(`Cannot run ${uri.fragment} - running sub-tests is not supported`);
+ continue;
+ }
+
+ // When the user clicks the run button on a package, they expect all
+ // of the tests within that package to run - they probably don't
+ // want to run the benchmarks. So if a benchmark is not explicitly
+ // selected, don't run benchmarks. But the user may disagree, so
+ // behavior can be changed with `go.testExplorerRunBenchmarks`.
+ // However, if the user clicks the run button on a file or package
+ // that contains benchmarks and nothing else, they likely expect
+ // those benchmarks to run.
+ if (uri.query === 'benchmark' && !explicitlyIncluded && !includeBench && !(hasBench && !hasNonBench)) {
+ continue;
+ }
+
+ item.error = null;
+ run.enqueued(item);
+ item.canResolveChildren = false;
+ item.children.forEach((x) => dispose(x));
+
+ if (uri.query === 'benchmark') {
+ benchmarks[uri.fragment] = item;
+ } else {
+ tests[uri.fragment] = item;
+ }
+ }
+
+ const record = new Map<TestItem, string[]>();
+ const testFns = Object.keys(tests);
+ const benchmarkFns = Object.keys(benchmarks);
+ const concat = goConfig.get<boolean>('testExplorerConcatenateMessages');
+
+ // Run tests
+ if (testFns.length > 0) {
+ const complete = new Set<TestItem>();
+ const success = await goTest({
+ goConfig,
+ flags,
+ isMod,
+ outputChannel,
+ dir: uri.fsPath,
+ functions: testFns,
+ cancel: token,
+ goTestOutputConsumer: (e) =>
+ consumeGoTestEvent(this.resolver, run, tests, record, complete, concat, e)
+ });
+ if (!success) {
+ if (isBuildFailure(outputChannel.lines)) {
+ markComplete(tests, new Set(), (item) => {
+ run.errored(item, { message: 'Compilation failed' });
+ item.error = 'Compilation failed';
+ });
+ } else {
+ markComplete(tests, complete, (x) => run.skipped(x));
+ }
+ }
+ }
+
+ // Run benchmarks
+ if (benchmarkFns.length > 0) {
+ const complete = new Set<TestItem>();
+ const success = await goTest({
+ goConfig,
+ flags,
+ isMod,
+ outputChannel,
+ dir: uri.fsPath,
+ functions: benchmarkFns,
+ isBenchmark: true,
+ cancel: token,
+ goTestOutputConsumer: (e) => consumeGoBenchmarkEvent(this.resolver, run, benchmarks, complete, e)
+ });
+
+ // Explicitly complete any incomplete benchmarks (see test_events.md)
+ if (success) {
+ markComplete(benchmarks, complete, (x) => run.passed(x));
+ } else if (isBuildFailure(outputChannel.lines)) {
+ markComplete(benchmarks, new Set(), (item) => {
+ // TODO change to errored when that is added back
+ run.failed(item, { message: 'Compilation failed' });
+ item.error = 'Compilation failed';
+ });
+ } else {
+ markComplete(benchmarks, complete, (x) => run.skipped(x));
+ }
+ }
+ }
+
+ run.end();
+ }
+}
+
+type CollectedTest = { item: TestItem; explicitlyIncluded: boolean };
+
+// Recursively find all tests, benchmarks, and examples within a
+// module/package/etc, minus exclusions. Map tests to the package they are
+// defined in, and track files.
+async function collectTests(
+ resolver: GoTestResolver,
+ item: TestItem,
+ explicitlyIncluded: boolean,
+ excluded: TestItem[],
+ functions: Map<string, CollectedTest[]>,
+ docs: Set<Uri>
+) {
+ for (let i = item; i.parent; i = i.parent) {
+ if (excluded.indexOf(i) >= 0) {
+ return;
+ }
+ }
+
+ const uri = Uri.parse(item.id);
+ if (!uri.fragment) {
+ if (item.children.size === 0) {
+ await resolver.resolve(item);
+ }
+
+ await forEachAsync(item.children, (child) => {
+ return collectTests(resolver, child, false, excluded, functions, docs);
+ });
+ return;
+ }
+
+ const file = uri.with({ query: '', fragment: '' });
+ docs.add(file);
+
+ const dir = file.with({ path: path.dirname(uri.path) }).toString();
+ if (functions.has(dir)) {
+ functions.get(dir).push({ item, explicitlyIncluded });
+ } else {
+ functions.set(dir, [{ item, explicitlyIncluded }]);
+ }
+ return;
+}
+
+// TestRunOutput is a fake OutputChannel that forwards all test output to the test API
+// console.
+class TestRunOutput implements OutputChannel {
+ readonly name: string;
+ readonly lines: string[] = [];
+
+ constructor(private run: TestRun) {
+ this.name = `Test run at ${new Date()}`;
+ }
+
+ append(value: string) {
+ this.run.appendOutput(value);
+ }
+
+ appendLine(value: string) {
+ this.lines.push(value);
+ this.run.appendOutput(value + '\r\n');
+ }
+
+ clear() {}
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ show(...args: unknown[]) {}
+ hide() {}
+ dispose() {}
+}
+
+// Resolve a test name to a test item. If the test name is TestXxx/Foo, Foo is
+// created as a child of TestXxx. The same is true for TestXxx#Foo and
+// TestXxx/#Foo.
+function resolveTestName(
+ resolver: GoTestResolver,
+ tests: Record<string, TestItem>,
+ name: string
+): TestItem | undefined {
+ if (!name) {
+ return;
+ }
+
+ const parts = name.split(/[#/]+/);
+ let test = tests[parts[0]];
+ if (!test) {
+ return;
+ }
+
+ for (const part of parts.slice(1)) {
+ test = resolver.getOrCreateSubTest(test, part);
+ }
+ return test;
+}
+
+// Process benchmark events (see test_events.md)
+function consumeGoBenchmarkEvent(
+ resolver: GoTestResolver,
+ run: TestRun,
+ benchmarks: Record<string, TestItem>,
+ complete: Set<TestItem>,
+ e: GoTestOutput
+) {
+ if (e.Test) {
+ // Find (or create) the (sub)benchmark
+ const test = resolveTestName(resolver, benchmarks, e.Test);
+ if (!test) {
+ return;
+ }
+
+ switch (e.Action) {
+ case 'fail': // Failed
+ run.failed(test, { message: 'Failed' });
+ complete.add(test);
+ break;
+
+ case 'skip': // Skipped
+ run.skipped(test);
+ complete.add(test);
+ break;
+ }
+
+ return;
+ }
+
+ // Ignore anything that's not an output event
+ if (!e.Output) {
+ return;
+ }
+
+ // On start: "BenchmarkFooBar"
+ // On complete: "BenchmarkFooBar-4 123456 123.4 ns/op 123 B/op 12 allocs/op"
+
+ // Extract the benchmark name and status
+ const m = e.Output.match(/^(?<name>Benchmark[/\w]+)(?:-(?<procs>\d+)\s+(?<result>.*))?(?:$|\n)/);
+ if (!m) {
+ // If the output doesn't start with `BenchmarkFooBar`, ignore it
+ return;
+ }
+
+ // Find (or create) the (sub)benchmark
+ const test = resolveTestName(resolver, benchmarks, m.groups.name);
+ if (!test) {
+ return;
+ }
+
+ // If output includes benchmark results, the benchmark passed. If output
+ // only includes the benchmark name, the benchmark is running.
+ if (m.groups.result) {
+ run.passed(test);
+ complete.add(test);
+ vscode.commands.executeCommand('testing.showMostRecentOutput');
+ } else {
+ run.started(test);
+ }
+}
+
+// Pass any incomplete benchmarks (see test_events.md)
+function markComplete(items: Record<string, TestItem>, complete: Set<TestItem>, fn: (item: TestItem) => void) {
+ function mark(item: TestItem) {
+ if (!complete.has(item)) {
+ fn(item);
+ }
+ item.children.forEach((child) => mark(child));
+ }
+
+ for (const name in items) {
+ mark(items[name]);
+ }
+}
+
+// Process test events (see test_events.md)
+function consumeGoTestEvent(
+ resolver: GoTestResolver,
+ run: TestRun,
+ tests: Record<string, TestItem>,
+ record: Map<TestItem, string[]>,
+ complete: Set<TestItem>,
+ concat: boolean,
+ e: GoTestOutput
+) {
+ const test = resolveTestName(resolver, tests, e.Test);
+ if (!test) {
+ return;
+ }
+
+ switch (e.Action) {
+ case 'cont':
+ case 'pause':
+ // ignore
+ break;
+
+ case 'run':
+ run.started(test);
+ break;
+
+ case 'pass':
+ // TODO(firelizzard18): add messages on pass, once that capability
+ // is added.
+ complete.add(test);
+ run.passed(test, e.Elapsed * 1000);
+ break;
+
+ case 'fail': {
+ complete.add(test);
+ const messages = parseOutput(test, record.get(test) || []);
+
+ if (!concat) {
+ run.failed(test, messages, e.Elapsed * 1000);
+ break;
+ }
+
+ const merged = new Map<string, TestMessage>();
+ for (const { message, location } of messages) {
+ const loc = `${location.uri}:${location.range.start.line}`;
+ if (merged.has(loc)) {
+ merged.get(loc).message += '\n' + message;
+ } else {
+ merged.set(loc, { message, location });
+ }
+ }
+
+ run.failed(test, Array.from(merged.values()), e.Elapsed * 1000);
+ break;
+ }
+
+ case 'skip':
+ complete.add(test);
+ run.skipped(test);
+ break;
+
+ case 'output':
+ if (/^(=== RUN|\s*--- (FAIL|PASS): )/.test(e.Output)) {
+ break;
+ }
+
+ if (record.has(test)) record.get(test).push(e.Output);
+ else record.set(test, [e.Output]);
+ break;
+ }
+}
+
+function parseOutput(test: TestItem, output: string[]): TestMessage[] {
+ const messages: TestMessage[] = [];
+
+ const uri = Uri.parse(test.id);
+ const gotI = output.indexOf('got:\n');
+ const wantI = output.indexOf('want:\n');
+ if (uri.query === 'example' && gotI >= 0 && wantI >= 0) {
+ const got = output.slice(gotI + 1, wantI).join('');
+ const want = output.slice(wantI + 1).join('');
+ const message = TestMessage.diff('Output does not match', want, got);
+ message.location = new Location(test.uri, test.range.start);
+ messages.push(message);
+ output = output.slice(0, gotI);
+ }
+
+ let current: Location;
+ const dir = Uri.joinPath(test.uri, '..');
+ for (const line of output) {
+ const m = line.match(/^\s*(?<file>.*\.go):(?<line>\d+): ?(?<message>.*\n)$/);
+ if (m) {
+ const file = Uri.joinPath(dir, m.groups.file);
+ const ln = Number(m.groups.line) - 1; // VSCode uses 0-based line numbering (internally)
+ current = new Location(file, new Position(ln, 0));
+ messages.push({ message: m.groups.message, location: current });
+ } else if (current) {
+ messages.push({ message: line, location: current });
+ }
+ }
+
+ return messages;
+}
+
+function isBuildFailure(output: string[]): boolean {
+ const rePkg = /^# (?<pkg>[\w/.-]+)(?: \[(?<test>[\w/.-]+).test\])?/;
+
+ // TODO(firelizzard18): Add more sophisticated check for build failures?
+ return output.some((x) => rePkg.test(x));
+}
diff --git a/src/goTest/utils.ts b/src/goTest/utils.ts
new file mode 100644
index 0000000..9b106d6
--- /dev/null
+++ b/src/goTest/utils.ts
@@ -0,0 +1,71 @@
+import * as vscode from 'vscode';
+
+export class GoTest {
+ // Construct an ID for an item. Exported for tests.
+ // - Module: file:///path/to/mod?module
+ // - Package: file:///path/to/mod/pkg?package
+ // - File: file:///path/to/mod/file.go?file
+ // - Test: file:///path/to/mod/file.go?test#TestXxx
+ // - Benchmark: file:///path/to/mod/file.go?benchmark#BenchmarkXxx
+ // - Example: file:///path/to/mod/file.go?example#ExampleXxx
+ static id(uri: vscode.Uri, kind: string, name?: string): string {
+ uri = uri.with({ query: kind });
+ if (name) uri = uri.with({ fragment: name });
+ return uri.toString();
+ }
+}
+
+// The subset of vscode.FileSystem that is used by the test explorer.
+export type FileSystem = Pick<vscode.FileSystem, 'readFile' | 'readDirectory'>;
+
+// The subset of vscode.workspace that is used by the test explorer.
+export interface Workspace
+ extends Pick<typeof vscode.workspace, 'workspaceFolders' | 'getWorkspaceFolder' | 'textDocuments'> {
+ // use custom FS type
+ readonly fs: FileSystem;
+
+ // only include one overload
+ openTextDocument(uri: vscode.Uri): Thenable<vscode.TextDocument>;
+}
+
+export function findItem(
+ items: vscode.TestItemCollection,
+ fn: (item: vscode.TestItem) => vscode.TestItem | undefined
+): vscode.TestItem | undefined {
+ let found: vscode.TestItem | undefined;
+ items.forEach((item) => {
+ if (found) return;
+ found = fn(item);
+ });
+ return found;
+}
+
+export function forEachAsync<T>(
+ items: vscode.TestItemCollection,
+ fn: (item: vscode.TestItem) => Promise<T>
+): Promise<T[]> {
+ const promises: Promise<T>[] = [];
+ items.forEach((item) => promises.push(fn(item)));
+ return Promise.all(promises);
+}
+
+export function dispose(item: vscode.TestItem) {
+ 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.
+export function disposeIfEmpty(item: vscode.TestItem) {
+ // Don't dispose of empty top-level items
+ const uri = vscode.Uri.parse(item.id);
+ if (uri.query === 'module' || uri.query === 'workspace' || (uri.query === 'package' && !item.parent)) {
+ return;
+ }
+
+ if (item.children.size > 0) {
+ return;
+ }
+
+ dispose(item);
+ disposeIfEmpty(item.parent);
+}
diff --git a/src/goTest/walk.ts b/src/goTest/walk.ts
new file mode 100644
index 0000000..764d33a
--- /dev/null
+++ b/src/goTest/walk.ts
@@ -0,0 +1,78 @@
+import * as vscode from 'vscode';
+import { FileSystem } from './utils';
+
+// Reasons to stop walking, used by walk
+export enum WalkStop {
+ None = 0, // Don't stop
+ Abort, // Abort the walk
+ Current, // Stop walking the current directory
+ Files, // Skip remaining files
+ Directories // Skip remaining directories
+}
+
+// Recursively walk a directory, breadth first.
+export async function walk(
+ fs: FileSystem,
+ uri: vscode.Uri,
+ cb: (dir: vscode.Uri, file: string, type: vscode.FileType) => Promise<WalkStop | undefined>
+): Promise<void> {
+ let dirs = [uri];
+
+ // While there are directories to be scanned
+ while (dirs.length > 0) {
+ const d = dirs;
+ dirs = [];
+
+ outer: for (const uri of d) {
+ const dirs2 = [];
+ let skipFiles = false,
+ skipDirs = false;
+
+ // Scan the directory
+ inner: for (const [file, type] of await fs.readDirectory(uri)) {
+ if ((skipFiles && type === vscode.FileType.File) || (skipDirs && type === vscode.FileType.Directory)) {
+ continue;
+ }
+
+ // Ignore all dotfiles
+ if (file.startsWith('.')) {
+ continue;
+ }
+
+ if (type === vscode.FileType.Directory) {
+ dirs2.push(vscode.Uri.joinPath(uri, file));
+ }
+
+ const s = await cb(uri, file, type);
+ switch (s) {
+ case WalkStop.Abort:
+ // Immediately abort the entire walk
+ return;
+
+ case WalkStop.Current:
+ // Immediately abort the current directory
+ continue outer;
+
+ case WalkStop.Files:
+ // Skip all subsequent files in the current directory
+ skipFiles = true;
+ if (skipFiles && skipDirs) {
+ break inner;
+ }
+ break;
+
+ case WalkStop.Directories:
+ // Skip all subsequent directories in the current directory
+ skipDirs = true;
+ if (skipFiles && skipDirs) {
+ break inner;
+ }
+ break;
+ }
+ }
+
+ // Add subdirectories to the recursion list
+ dirs.push(...dirs2);
+ }
+ }
+}
diff --git a/src/goTestExplorer.ts b/src/goTestExplorer.ts
deleted file mode 100644
index 94a74a9..0000000
--- a/src/goTestExplorer.ts
+++ /dev/null
@@ -1,1097 +0,0 @@
-/*---------------------------------------------------------
- * Copyright 2021 The Go Authors. All rights reserved.
- * Licensed under the MIT License. See LICENSE in the project root for license information.
- *--------------------------------------------------------*/
-import {
- CancellationToken,
- ConfigurationChangeEvent,
- DocumentSymbol,
- ExtensionContext,
- FileType,
- Location,
- OutputChannel,
- Position,
- Range,
- SymbolKind,
- TestController,
- TestItem,
- TestItemCollection,
- TestMessage,
- TestRun,
- TestRunProfileKind,
- TestRunRequest,
- TextDocument,
- TextDocumentChangeEvent,
- Uri,
- workspace,
- WorkspaceFolder,
- WorkspaceFoldersChangeEvent
-} from 'vscode';
-import vscode = require('vscode');
-import path = require('path');
-import { getModFolderPath, isModSupported } from './goModules';
-import { getCurrentGoPath } from './util';
-import { GoDocumentSymbolProvider } from './goOutline';
-import { getGoConfig } from './config';
-import { getTestFlags, goTest, GoTestOutput } from './testUtils';
-import { outputChannel } from './goStatus';
-
-// 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;
-
-// eslint-disable-next-line @typescript-eslint/no-namespace
-export namespace TestExplorer {
- // exported for tests
-
- export type FileSystem = Pick<vscode.FileSystem, 'readFile' | 'readDirectory'>;
-
- export interface Workspace
- extends Pick<typeof vscode.workspace, 'workspaceFolders' | 'getWorkspaceFolder' | 'textDocuments'> {
- // use custom FS type
- readonly fs: FileSystem;
-
- // only include one overload
- openTextDocument(uri: Uri): Thenable<TextDocument>;
- }
-}
-
-async function doSafe<T>(context: string, p: Thenable<T> | (() => T | Thenable<T>), onError?: T): Promise<T> {
- try {
- if (typeof p === 'function') {
- return await p();
- } else {
- return await p;
- }
- } catch (error) {
- if (process.env.VSCODE_GO_IN_TEST === '1') {
- throw error;
- }
-
- // TODO internationalization?
- if (context === 'resolveHandler') {
- const m = 'Failed to resolve tests';
- outputChannel.appendLine(`${m}: ${error}`);
- await vscode.window.showErrorMessage(m);
- } else if (context === 'runHandler') {
- const m = 'Failed to execute tests';
- outputChannel.appendLine(`${m}: ${error}`);
- await vscode.window.showErrorMessage(m);
- } else if (/^did/.test(context)) {
- outputChannel.appendLine(`Failed while handling '${context}': ${error}`);
- } else {
- const m = 'An unknown error occurred';
- outputChannel.appendLine(`${m}: ${error}`);
- await vscode.window.showErrorMessage(m);
- }
- return onError;
- }
-}
-
-export class TestExplorer {
- static setup(context: ExtensionContext): TestExplorer {
- if (!isVscodeTestingAPIAvailable) throw new Error('VSCode Testing API is unavailable');
-
- const ctrl = vscode.tests.createTestController('go', 'Go');
- const getSym = new GoDocumentSymbolProvider().provideDocumentSymbols;
- const inst = new this(ctrl, workspace, getSym);
-
- context.subscriptions.push(
- workspace.onDidChangeConfiguration((x) =>
- doSafe('onDidChangeConfiguration', inst.didChangeConfiguration(x))
- )
- );
-
- context.subscriptions.push(
- workspace.onDidOpenTextDocument((x) => doSafe('onDidOpenTextDocument', inst.didOpenTextDocument(x)))
- );
-
- context.subscriptions.push(
- workspace.onDidChangeTextDocument((x) => doSafe('onDidChangeTextDocument', inst.didChangeTextDocument(x)))
- );
-
- context.subscriptions.push(
- workspace.onDidChangeWorkspaceFolders((x) =>
- doSafe('onDidChangeWorkspaceFolders', inst.didChangeWorkspaceFolders(x))
- )
- );
-
- const watcher = workspace.createFileSystemWatcher('**/*_test.go', false, true, false);
- context.subscriptions.push(watcher);
- context.subscriptions.push(watcher.onDidCreate((x) => doSafe('onDidCreate', inst.didCreateFile(x))));
- context.subscriptions.push(watcher.onDidDelete((x) => doSafe('onDidDelete', inst.didDeleteFile(x))));
-
- return inst;
- }
-
- constructor(
- public ctrl: TestController,
- public ws: TestExplorer.Workspace,
- public provideDocumentSymbols: (doc: TextDocument, token: CancellationToken) => Thenable<DocumentSymbol[]>
- ) {
- ctrl.resolveHandler = (item) => this.resolve(item);
- ctrl.createRunProfile('go test', TestRunProfileKind.Run, (rq, tok) => this.run(rq, tok), true);
- }
-
- /* ***** Interface (external) ***** */
-
- resolve(item?: TestItem) {
- return doSafe('resolveHandler', resolve(this, item));
- }
-
- run(request: TestRunRequest, token: CancellationToken) {
- return doSafe('runHandler', runTests(this, request, token));
- }
-
- /* ***** Interface (internal) ***** */
-
- // Create an item.
- createItem(label: string, uri: Uri, kind: string, name?: string): TestItem {
- return this.ctrl.createTestItem(testID(uri, kind, name), label, uri.with({ query: '', fragment: '' }));
- }
-
- // Retrieve an item.
- getItem(parent: TestItem | undefined, uri: Uri, kind: string, name?: string): TestItem {
- const items = getChildren(parent || this.ctrl.items);
- return items.get(testID(uri, kind, name));
- }
-
- // Create or retrieve an item.
- getOrCreateItem(parent: TestItem | undefined, label: string, uri: Uri, kind: string, name?: string): TestItem {
- const existing = this.getItem(parent, uri, kind, name);
- if (existing) return existing;
-
- const item = this.createItem(label, uri, kind, name);
- getChildren(parent || this.ctrl.items).add(item);
- return item;
- }
-
- // Create or Retrieve a sub test or benchmark. The ID will be of the form:
- // file:///path/to/mod/file.go?test#TestXxx/A/B/C
- getOrCreateSubTest(item: TestItem, name: string): TestItem {
- const { fragment: parentName, query: kind } = Uri.parse(item.id);
- const existing = this.getItem(item, item.uri, kind, `${parentName}/${name}`);
- if (existing) return existing;
-
- item.canResolveChildren = true;
- const sub = this.createItem(name, item.uri, kind, `${parentName}/${name}`);
- item.children.add(sub);
- sub.range = item.range;
- return sub;
- }
-
- /* ***** Listeners ***** */
-
- protected async didOpenTextDocument(doc: TextDocument) {
- await documentUpdate(this, doc);
- }
-
- protected async didChangeTextDocument(e: TextDocumentChangeEvent) {
- await documentUpdate(
- this,
- e.document,
- e.contentChanges.map((x) => x.range)
- );
- }
-
- protected async didChangeWorkspaceFolders(e: WorkspaceFoldersChangeEvent) {
- if (e.removed.length > 0) {
- for (const item of collect(this.ctrl.items)) {
- const uri = Uri.parse(item.id);
- if (uri.query === 'package') {
- continue;
- }
-
- const ws = this.ws.getWorkspaceFolder(uri);
- if (!ws) {
- dispose(item);
- }
- }
- }
-
- if (e.added) {
- await resolve(this);
- }
- }
-
- protected async didCreateFile(file: Uri) {
- await documentUpdate(this, await this.ws.openTextDocument(file));
- }
-
- protected async didDeleteFile(file: Uri) {
- const id = testID(file, 'file');
- function find(children: TestItemCollection): TestItem {
- for (const item of collect(children)) {
- if (item.id === id) {
- return item;
- }
-
- const uri = Uri.parse(item.id);
- if (!file.path.startsWith(uri.path)) {
- continue;
- }
-
- const found = find(item.children);
- if (found) {
- return found;
- }
- }
- }
-
- const found = find(this.ctrl.items);
- if (found) {
- dispose(found);
- disposeIfEmpty(found.parent);
- }
- }
-
- protected async didChangeConfiguration(e: ConfigurationChangeEvent) {
- let update = false;
- for (const item of collect(this.ctrl.items)) {
- if (e.affectsConfiguration('go.testExplorerPackages', item.uri)) {
- dispose(item);
- update = true;
- }
- }
-
- if (update) {
- resolve(this);
- }
- }
-}
-
-// Construct an ID for an item. Exported for tests.
-// - Module: file:///path/to/mod?module
-// - Package: file:///path/to/mod/pkg?package
-// - File: file:///path/to/mod/file.go?file
-// - Test: file:///path/to/mod/file.go?test#TestXxx
-// - Benchmark: file:///path/to/mod/file.go?benchmark#BenchmarkXxx
-// - Example: file:///path/to/mod/file.go?example#ExampleXxx
-export function testID(uri: Uri, kind: string, name?: string): string {
- uri = uri.with({ query: kind });
- if (name) uri = uri.with({ fragment: name });
- return uri.toString();
-}
-
-function collect(items: TestItemCollection): TestItem[] {
- const r: TestItem[] = [];
- items.forEach((i) => r.push(i));
- return r;
-}
-
-function getChildren(parent: TestItem | TestItemCollection): TestItemCollection {
- if ('children' in parent) {
- return parent.children;
- }
- return parent;
-}
-
-function dispose(item: TestItem) {
- 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.
-function disposeIfEmpty(item: TestItem) {
- // Don't dispose of empty top-level items
- const uri = Uri.parse(item.id);
- if (uri.query === 'module' || uri.query === 'workspace' || (uri.query === 'package' && !item.parent)) {
- return;
- }
-
- if (item.children.size > 0) {
- return;
- }
-
- dispose(item);
- disposeIfEmpty(item.parent);
-}
-
-// Dispose of the children of a test. Sub-tests and sub-benchmarks are
-// discovered emperically (from test output) not semantically (from code), so
-// there are situations where they must be discarded.
-function discardChildren(item: TestItem) {
- item.canResolveChildren = false;
- item.children.forEach(dispose);
-}
-
-// If a test/benchmark with children is relocated, update the children's
-// location.
-function relocateChildren(item: TestItem) {
- for (const child of collect(item.children)) {
- child.range = item.range;
- relocateChildren(child);
- }
-}
-
-// Retrieve or create an item for a Go module.
-async function getModule(expl: TestExplorer, uri: Uri): Promise<TestItem> {
- const existing = expl.getItem(null, uri, 'module');
- if (existing) {
- return existing;
- }
-
- // Use the module name as the label
- const goMod = Uri.joinPath(uri, 'go.mod');
- const contents = await expl.ws.fs.readFile(goMod);
- const modLine = contents.toString().split('\n', 2)[0];
- const match = modLine.match(/^module (?<name>.*?)(?:\s|\/\/|$)/);
- const item = expl.getOrCreateItem(null, match.groups.name, uri, 'module');
- item.canResolveChildren = true;
- return item;
-}
-
-// Retrieve or create an item for a workspace folder that is not a module.
-async function getWorkspace(expl: TestExplorer, ws: WorkspaceFolder): Promise<TestItem> {
- const existing = expl.getItem(null, ws.uri, 'workspace');
- if (existing) {
- return existing;
- }
-
- // Use the workspace folder name as the label
- const item = expl.getOrCreateItem(null, ws.name, ws.uri, 'workspace');
- item.canResolveChildren = true;
- return item;
-}
-
-// Retrieve or create an item for a Go package.
-async function getPackage(expl: TestExplorer, uri: Uri): Promise<TestItem> {
- let item: TestItem;
-
- const nested = getGoConfig(uri).get('testExplorerPackages') === 'nested';
- const modDir = await getModFolderPath(uri, true);
- const wsfolder = workspace.getWorkspaceFolder(uri);
- if (modDir) {
- // If the package is in a module, add it as a child of the module
- let parent = await getModule(expl, uri.with({ path: modDir, query: '', fragment: '' }));
- if (uri.path === parent.uri.path) {
- return parent;
- }
-
- if (nested) {
- const bits = path.relative(parent.uri.path, uri.path).split(path.sep);
- while (bits.length > 1) {
- const dir = bits.shift();
- const dirUri = uri.with({ path: path.join(parent.uri.path, dir), query: '', fragment: '' });
- parent = expl.getOrCreateItem(parent, dir, dirUri, 'package');
- }
- }
-
- const label = uri.path.startsWith(parent.uri.path) ? uri.path.substring(parent.uri.path.length + 1) : uri.path;
- item = expl.getOrCreateItem(parent, label, uri, 'package');
- } else if (wsfolder) {
- // If the package is in a workspace folder, add it as a child of the workspace
- const workspace = await getWorkspace(expl, wsfolder);
- const existing = expl.getItem(workspace, uri, 'package');
- if (existing) {
- return existing;
- }
-
- const label = uri.path.startsWith(wsfolder.uri.path)
- ? uri.path.substring(wsfolder.uri.path.length + 1)
- : uri.path;
- item = expl.getOrCreateItem(workspace, label, uri, 'package');
- } else {
- // Otherwise, add it directly to the root
- const existing = expl.getItem(null, uri, 'package');
- if (existing) {
- return existing;
- }
-
- const srcPath = path.join(getCurrentGoPath(uri), 'src');
- const label = uri.path.startsWith(srcPath) ? uri.path.substring(srcPath.length + 1) : uri.path;
- item = expl.getOrCreateItem(null, label, uri, 'package');
- }
-
- item.canResolveChildren = true;
- return item;
-}
-
-// Retrieve or create an item for a Go file.
-async function getFile(expl: TestExplorer, uri: Uri): Promise<TestItem> {
- const dir = path.dirname(uri.path);
- const pkg = await getPackage(expl, uri.with({ path: dir, query: '', fragment: '' }));
- const existing = expl.getItem(pkg, uri, 'file');
- if (existing) {
- return existing;
- }
-
- const label = path.basename(uri.path);
- const item = expl.getOrCreateItem(pkg, label, uri, 'file');
- item.canResolveChildren = true;
- return item;
-}
-
-// Recursively process a Go AST symbol. If the symbol represents a 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.
-async function processSymbol(expl: TestExplorer, uri: Uri, file: TestItem, seen: Set<string>, symbol: DocumentSymbol) {
- // Skip TestMain(*testing.M) - allow TestMain(*testing.T)
- if (symbol.name === 'TestMain' && /\*testing.M\)/.test(symbol.detail)) {
- return;
- }
-
- // Recursively process symbols that are nested
- if (symbol.kind !== SymbolKind.Function) {
- for (const sym of symbol.children) await processSymbol(expl, uri, file, seen, sym);
- return;
- }
-
- const match = symbol.name.match(/^(?<type>Test|Example|Benchmark)/);
- if (!match) {
- return;
- }
-
- seen.add(symbol.name);
-
- const kind = match.groups.type.toLowerCase();
- const existing = expl.getItem(file, uri, kind, symbol.name);
- if (existing) {
- if (!existing.range.isEqual(symbol.range)) {
- existing.range = symbol.range;
- relocateChildren(existing);
- }
- return existing;
- }
-
- const item = expl.getOrCreateItem(file, symbol.name, uri, kind, symbol.name);
- item.range = symbol.range;
-}
-
-// Processes a Go 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 function processDocument(expl: TestExplorer, doc: TextDocument, ranges?: Range[]) {
- const seen = new Set<string>();
- const item = await getFile(expl, doc.uri);
- const symbols = await expl.provideDocumentSymbols(doc, null);
- for (const symbol of symbols) await processSymbol(expl, doc.uri, item, seen, symbol);
-
- for (const child of collect(item.children)) {
- const uri = Uri.parse(child.id);
- if (!seen.has(uri.fragment)) {
- dispose(child);
- continue;
- }
-
- if (ranges?.some((r) => !!child.range.intersection(r))) {
- discardChildren(child);
- }
- }
-
- disposeIfEmpty(item);
-}
-
-// Reasons to stop walking
-enum WalkStop {
- None = 0, // Don't stop
- Abort, // Abort the walk
- Current, // Stop walking the current directory
- Files, // Skip remaining files
- Directories // Skip remaining directories
-}
-
-// Recursively walk a directory, breadth first.
-async function walk(
- fs: TestExplorer.FileSystem,
- uri: Uri,
- cb: (dir: Uri, file: string, type: FileType) => Promise<WalkStop | undefined>
-): Promise<void> {
- let dirs = [uri];
-
- // While there are directories to be scanned
- while (dirs.length > 0) {
- const d = dirs;
- dirs = [];
-
- outer: for (const uri of d) {
- const dirs2 = [];
- let skipFiles = false,
- skipDirs = false;
-
- // Scan the directory
- inner: for (const [file, type] of await fs.readDirectory(uri)) {
- if ((skipFiles && type === FileType.File) || (skipDirs && type === FileType.Directory)) {
- continue;
- }
-
- // Ignore all dotfiles
- if (file.startsWith('.')) {
- continue;
- }
-
- if (type === FileType.Directory) {
- dirs2.push(Uri.joinPath(uri, file));
- }
-
- const s = await cb(uri, file, type);
- switch (s) {
- case WalkStop.Abort:
- // Immediately abort the entire walk
- return;
-
- case WalkStop.Current:
- // Immediately abort the current directory
- continue outer;
-
- case WalkStop.Files:
- // Skip all subsequent files in the current directory
- skipFiles = true;
- if (skipFiles && skipDirs) {
- break inner;
- }
- break;
-
- case WalkStop.Directories:
- // Skip all subsequent directories in the current directory
- skipDirs = true;
- if (skipFiles && skipDirs) {
- break inner;
- }
- break;
- }
- }
-
- // Add subdirectories to the recursion list
- dirs.push(...dirs2);
- }
- }
-}
-
-// Walk the workspace, looking for Go modules. Returns a map indicating paths
-// that are modules (value == true) and paths that are not modules but contain
-// Go files (value == false).
-async function walkWorkspaces(fs: TestExplorer.FileSystem, uri: Uri): Promise<Map<string, boolean>> {
- const found = new Map<string, boolean>();
- await walk(fs, uri, async (dir, file, type) => {
- if (type !== FileType.File) {
- return;
- }
-
- if (file === 'go.mod') {
- // BUG(firelizard18): This does not create a separate entry for
- // modules within a module. Thus, tests in a module within another
- // module will appear under the top-level module's tree. This may or
- // may not be acceptable.
- found.set(dir.toString(), true);
- return WalkStop.Current;
- }
-
- if (file.endsWith('.go')) {
- found.set(dir.toString(), false);
- }
- });
- return found;
-}
-
-// Walk the workspace, calling the callback for any directory that contains a Go
-// test file.
-async function walkPackages(fs: TestExplorer.FileSystem, uri: Uri, cb: (uri: Uri) => Promise<unknown>) {
- await walk(fs, uri, async (dir, file) => {
- if (file.endsWith('_test.go')) {
- await cb(dir);
- return WalkStop.Files;
- }
- });
-}
-
-// Handle opened documents, document changes, and file creation.
-async function documentUpdate(expl: TestExplorer, doc: TextDocument, ranges?: Range[]) {
- if (!doc.uri.path.endsWith('_test.go')) {
- return;
- }
-
- if (doc.uri.scheme === 'git') {
- // TODO(firelizzard18): When a workspace is reopened, VSCode passes us git: URIs. Why?
- return;
- }
-
- await processDocument(expl, doc, ranges);
-}
-
-// TestController.resolveChildrenHandler callback
-async function resolve(expl: TestExplorer, item?: TestItem) {
- // Expand the root item - find all modules and workspaces
- if (!item) {
- // Dispose of package entries at the root if they are now part of a workspace folder
- for (const item of collect(expl.ctrl.items)) {
- const uri = Uri.parse(item.id);
- if (uri.query !== 'package') {
- continue;
- }
-
- if (expl.ws.getWorkspaceFolder(uri)) {
- dispose(item);
- }
- }
-
- // Create entries for all modules and workspaces
- for (const folder of expl.ws.workspaceFolders || []) {
- const found = await walkWorkspaces(expl.ws.fs, folder.uri);
- let needWorkspace = false;
- for (const [uri, isMod] of found.entries()) {
- if (!isMod) {
- needWorkspace = true;
- continue;
- }
-
- await getModule(expl, Uri.parse(uri));
- }
-
- // If the workspace folder contains any Go files not in a module, create a workspace entry
- if (needWorkspace) {
- await getWorkspace(expl, folder);
- }
- }
- return;
- }
-
- const uri = Uri.parse(item.id);
-
- // The user expanded a module or workspace - find all packages
- if (uri.query === 'module' || uri.query === 'workspace') {
- await walkPackages(expl.ws.fs, uri, async (uri) => {
- await getPackage(expl, uri);
- });
- }
-
- // The user expanded a module or package - find all files
- if (uri.query === 'module' || uri.query === 'package') {
- for (const [file, type] of await expl.ws.fs.readDirectory(uri)) {
- if (type !== FileType.File || !file.endsWith('_test.go')) {
- continue;
- }
-
- await getFile(expl, Uri.joinPath(uri, file));
- }
- }
-
- // The user expanded a file - find all functions
- if (uri.query === 'file') {
- const doc = await expl.ws.openTextDocument(uri.with({ query: '', fragment: '' }));
- await processDocument(expl, doc);
- }
-
- // TODO(firelizzard18): If uri.query is test or benchmark, this is where we
- // would discover sub tests or benchmarks, if that is feasible.
-}
-
-type CollectedTest = { item: TestItem; explicitlyIncluded: boolean };
-
-// Recursively find all tests, benchmarks, and examples within a
-// module/package/etc, minus exclusions. Map tests to the package they are
-// defined in, and track files.
-async function collectTests(
- expl: TestExplorer,
- item: TestItem,
- explicitlyIncluded: boolean,
- excluded: TestItem[],
- functions: Map<string, CollectedTest[]>,
- docs: Set<Uri>
-) {
- for (let i = item; i.parent; i = i.parent) {
- if (excluded.indexOf(i) >= 0) {
- return;
- }
- }
-
- const uri = Uri.parse(item.id);
- if (!uri.fragment) {
- if (item.children.size === 0) {
- await resolve(expl, item);
- }
-
- for (const child of collect(item.children)) {
- await collectTests(expl, child, false, excluded, functions, docs);
- }
- return;
- }
-
- const file = uri.with({ query: '', fragment: '' });
- docs.add(file);
-
- const dir = file.with({ path: path.dirname(uri.path) }).toString();
- if (functions.has(dir)) {
- functions.get(dir).push({ item, explicitlyIncluded });
- } else {
- functions.set(dir, [{ item, explicitlyIncluded }]);
- }
- return;
-}
-
-// TestRunOutput is a fake OutputChannel that forwards all test output to the test API
-// console.
-class TestRunOutput implements OutputChannel {
- readonly name: string;
- readonly lines: string[] = [];
-
- constructor(private run: TestRun) {
- this.name = `Test run at ${new Date()}`;
- }
-
- append(value: string) {
- this.run.appendOutput(value);
- }
-
- appendLine(value: string) {
- this.lines.push(value);
- this.run.appendOutput(value + '\r\n');
- }
-
- clear() {}
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- show(...args: unknown[]) {}
- hide() {}
- dispose() {}
-}
-
-// Resolve a test name to a test item. If the test name is TestXxx/Foo, Foo is
-// created as a child of TestXxx. The same is true for TestXxx#Foo and
-// TestXxx/#Foo.
-function resolveTestName(expl: TestExplorer, tests: Record<string, TestItem>, name: string): TestItem | undefined {
- if (!name) {
- return;
- }
-
- const parts = name.split(/[#/]+/);
- let test = tests[parts[0]];
- if (!test) {
- return;
- }
-
- for (const part of parts.slice(1)) {
- test = expl.getOrCreateSubTest(test, part);
- }
- return test;
-}
-
-// Process benchmark events (see test_events.md)
-function consumeGoBenchmarkEvent(
- expl: TestExplorer,
- run: TestRun,
- benchmarks: Record<string, TestItem>,
- complete: Set<TestItem>,
- e: GoTestOutput
-) {
- if (e.Test) {
- // Find (or create) the (sub)benchmark
- const test = resolveTestName(expl, benchmarks, e.Test);
- if (!test) {
- return;
- }
-
- switch (e.Action) {
- case 'fail': // Failed
- run.failed(test, { message: 'Failed' });
- complete.add(test);
- break;
-
- case 'skip': // Skipped
- run.skipped(test);
- complete.add(test);
- break;
- }
-
- return;
- }
-
- // Ignore anything that's not an output event
- if (!e.Output) {
- return;
- }
-
- // On start: "BenchmarkFooBar"
- // On complete: "BenchmarkFooBar-4 123456 123.4 ns/op 123 B/op 12 allocs/op"
-
- // Extract the benchmark name and status
- const m = e.Output.match(/^(?<name>Benchmark[/\w]+)(?:-(?<procs>\d+)\s+(?<result>.*))?(?:$|\n)/);
- if (!m) {
- // If the output doesn't start with `BenchmarkFooBar`, ignore it
- return;
- }
-
- // Find (or create) the (sub)benchmark
- const test = resolveTestName(expl, benchmarks, m.groups.name);
- if (!test) {
- return;
- }
-
- // If output includes benchmark results, the benchmark passed. If output
- // only includes the benchmark name, the benchmark is running.
- if (m.groups.result) {
- run.passed(test);
- complete.add(test);
- vscode.commands.executeCommand('testing.showMostRecentOutput');
- } else {
- run.started(test);
- }
-}
-
-// Pass any incomplete benchmarks (see test_events.md)
-function markComplete(items: Record<string, TestItem>, complete: Set<TestItem>, fn: (item: TestItem) => void) {
- function mark(item: TestItem) {
- if (!complete.has(item)) {
- fn(item);
- }
- for (const child of collect(item.children)) {
- mark(child);
- }
- }
-
- for (const name in items) {
- mark(items[name]);
- }
-}
-
-// Process test events (see test_events.md)
-function consumeGoTestEvent(
- expl: TestExplorer,
- run: TestRun,
- tests: Record<string, TestItem>,
- record: Map<TestItem, string[]>,
- complete: Set<TestItem>,
- concat: boolean,
- e: GoTestOutput
-) {
- const test = resolveTestName(expl, tests, e.Test);
- if (!test) {
- return;
- }
-
- switch (e.Action) {
- case 'cont':
- case 'pause':
- // ignore
- break;
-
- case 'run':
- run.started(test);
- break;
-
- case 'pass':
- // TODO(firelizzard18): add messages on pass, once that capability
- // is added.
- complete.add(test);
- run.passed(test, e.Elapsed * 1000);
- break;
-
- case 'fail': {
- complete.add(test);
- const messages = parseOutput(test, record.get(test) || []);
-
- if (!concat) {
- run.failed(test, messages, e.Elapsed * 1000);
- break;
- }
-
- const merged = new Map<string, TestMessage>();
- for (const { message, location } of messages) {
- const loc = `${location.uri}:${location.range.start.line}`;
- if (merged.has(loc)) {
- merged.get(loc).message += '\n' + message;
- } else {
- merged.set(loc, { message, location });
- }
- }
-
- run.failed(test, Array.from(merged.values()), e.Elapsed * 1000);
- break;
- }
-
- case 'skip':
- complete.add(test);
- run.skipped(test);
- break;
-
- case 'output':
- if (/^(=== RUN|\s*--- (FAIL|PASS): )/.test(e.Output)) {
- break;
- }
-
- if (record.has(test)) record.get(test).push(e.Output);
- else record.set(test, [e.Output]);
- break;
- }
-}
-
-function parseOutput(test: TestItem, output: string[]): TestMessage[] {
- const messages: TestMessage[] = [];
-
- const uri = Uri.parse(test.id);
- const gotI = output.indexOf('got:\n');
- const wantI = output.indexOf('want:\n');
- if (uri.query === 'example' && gotI >= 0 && wantI >= 0) {
- const got = output.slice(gotI + 1, wantI).join('');
- const want = output.slice(wantI + 1).join('');
- const message = TestMessage.diff('Output does not match', want, got);
- message.location = new Location(test.uri, test.range.start);
- messages.push(message);
- output = output.slice(0, gotI);
- }
-
- let current: Location;
- const dir = Uri.joinPath(test.uri, '..');
- for (const line of output) {
- const m = line.match(/^\s*(?<file>.*\.go):(?<line>\d+): ?(?<message>.*\n)$/);
- if (m) {
- const file = Uri.joinPath(dir, m.groups.file);
- const ln = Number(m.groups.line) - 1; // VSCode uses 0-based line numbering (internally)
- current = new Location(file, new Position(ln, 0));
- messages.push({ message: m.groups.message, location: current });
- } else if (current) {
- messages.push({ message: line, location: current });
- }
- }
-
- return messages;
-}
-
-function isBuildFailure(output: string[]): boolean {
- const rePkg = /^# (?<pkg>[\w/.-]+)(?: \[(?<test>[\w/.-]+).test\])?/;
-
- // TODO(firelizzard18): Add more sophisticated check for build failures?
- return output.some((x) => rePkg.test(x));
-}
-
-// Execute tests - TestController.runTest callback
-async function runTests(expl: TestExplorer, request: TestRunRequest, token: CancellationToken) {
- const collected = new Map<string, CollectedTest[]>();
- const docs = new Set<Uri>();
- if (request.include) {
- for (const item of request.include) {
- await collectTests(expl, item, true, request.exclude || [], collected, docs);
- }
- } else {
- const promises: Promise<unknown>[] = [];
- expl.ctrl.items.forEach((item) => {
- const p = collectTests(expl, item, true, request.exclude || [], collected, docs);
- promises.push(p);
- });
- await Promise.all(promises);
- }
-
- // Save all documents that contain a test we're about to run, to ensure `go
- // test` has the latest changes
- await Promise.all(expl.ws.textDocuments.filter((x) => docs.has(x.uri)).map((x) => x.save()));
-
- let hasBench = false,
- hasNonBench = false;
- for (const items of collected.values()) {
- for (const { item } of items) {
- const uri = Uri.parse(item.id);
- if (uri.query === 'benchmark') hasBench = true;
- else hasNonBench = true;
- }
- }
-
- const run = expl.ctrl.createTestRun(request);
- const outputChannel = new TestRunOutput(run);
- for (const [dir, items] of collected.entries()) {
- const uri = Uri.parse(dir);
- const isMod = await isModSupported(uri, true);
- const goConfig = getGoConfig(uri);
- const flags = getTestFlags(goConfig);
- const includeBench = getGoConfig(uri).get('testExplorerRunBenchmarks');
-
- // Separate tests and benchmarks and mark them as queued for execution.
- // Clear any sub tests/benchmarks generated by a previous run.
- const tests: Record<string, TestItem> = {};
- const benchmarks: Record<string, TestItem> = {};
- for (const { item, explicitlyIncluded } of items) {
- const uri = Uri.parse(item.id);
- if (/[/#]/.test(uri.fragment)) {
- // running sub-tests is not currently supported
- vscode.window.showErrorMessage(`Cannot run ${uri.fragment} - running sub-tests is not supported`);
- continue;
- }
-
- // When the user clicks the run button on a package, they expect all
- // of the tests within that package to run - they probably don't
- // want to run the benchmarks. So if a benchmark is not explicitly
- // selected, don't run benchmarks. But the user may disagree, so
- // behavior can be changed with `go.testExplorerRunBenchmarks`.
- // However, if the user clicks the run button on a file or package
- // that contains benchmarks and nothing else, they likely expect
- // those benchmarks to run.
- if (uri.query === 'benchmark' && !explicitlyIncluded && !includeBench && !(hasBench && !hasNonBench)) {
- continue;
- }
-
- item.error = null;
- run.enqueued(item);
- discardChildren(item);
-
- if (uri.query === 'benchmark') {
- benchmarks[uri.fragment] = item;
- } else {
- tests[uri.fragment] = item;
- }
- }
-
- const record = new Map<TestItem, string[]>();
- const testFns = Object.keys(tests);
- const benchmarkFns = Object.keys(benchmarks);
- const concat = goConfig.get<boolean>('testExplorerConcatenateMessages');
-
- // Run tests
- if (testFns.length > 0) {
- const complete = new Set<TestItem>();
- const success = await goTest({
- goConfig,
- flags,
- isMod,
- outputChannel,
- dir: uri.fsPath,
- functions: testFns,
- cancel: token,
- goTestOutputConsumer: (e) => consumeGoTestEvent(expl, run, tests, record, complete, concat, e)
- });
- if (!success) {
- if (isBuildFailure(outputChannel.lines)) {
- markComplete(tests, new Set(), (item) => {
- run.errored(item, { message: 'Compilation failed' });
- item.error = 'Compilation failed';
- });
- } else {
- markComplete(tests, complete, (x) => run.skipped(x));
- }
- }
- }
-
- // Run benchmarks
- if (benchmarkFns.length > 0) {
- const complete = new Set<TestItem>();
- const success = await goTest({
- goConfig,
- flags,
- isMod,
- outputChannel,
- dir: uri.fsPath,
- functions: benchmarkFns,
- isBenchmark: true,
- cancel: token,
- goTestOutputConsumer: (e) => consumeGoBenchmarkEvent(expl, run, benchmarks, complete, e)
- });
-
- // Explicitly complete any incomplete benchmarks (see test_events.md)
- if (success) {
- markComplete(benchmarks, complete, (x) => run.passed(x));
- } else if (isBuildFailure(outputChannel.lines)) {
- markComplete(benchmarks, new Set(), (item) => {
- // TODO change to errored when that is added back
- run.failed(item, { message: 'Compilation failed' });
- item.error = 'Compilation failed';
- });
- } else {
- markComplete(benchmarks, complete, (x) => run.skipped(x));
- }
- }
- }
-
- run.end();
-}

To view, visit change 343789. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: vscode-go
Gerrit-Branch: master
Gerrit-Change-Id: I487911b0ee8de28e88d7c558c11a49109956eb0c
Gerrit-Change-Number: 343789
Gerrit-PatchSet: 1
Gerrit-Owner: Ethan Reesor <ethan....@gmail.com>
Gerrit-MessageType: newchange

Ethan Reesor (Gerrit)

unread,
Aug 19, 2021, 11:15:35 PM8/19/21
to goph...@pubsubhelper.golang.org, Suzy Mueller, Hyang-Ah Hana Kim, golang-co...@googlegroups.com

Ethan Reesor abandoned this change.

View Change

Abandoned Something weird happened with my commit

To view, visit change 343789. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: vscode-go
Gerrit-Branch: master
Gerrit-Change-Id: I487911b0ee8de28e88d7c558c11a49109956eb0c
Gerrit-Change-Number: 343789
Gerrit-PatchSet: 1
Gerrit-Owner: Ethan Reesor <ethan....@gmail.com>
Gerrit-Reviewer: Hyang-Ah Hana Kim <hya...@gmail.com>
Gerrit-Reviewer: Suzy Mueller <suz...@golang.org>
Gerrit-MessageType: abandon
Reply all
Reply to author
Forward
0 new messages