blob: 6a4c6f6059031abcd1f686355b6e86f04487af9b [file] [log] [blame]
/*---------------------------------------------------------
* Copyright 2021 The Go Authors. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------*/
import * as vscode from 'vscode';
import { GoTestResolver } from './resolve';
// GoTestKind indicates the Go construct represented by a test item.
//
// - A 'module' is a folder that contains a go.mod file
// - A 'workspace' is a VSCode workspace folder that contains .go files outside
// of a module
// - A 'package' is a folder that contains .go files (and is not a module)
// - A 'file' is a file ending with _test.go
// - A 'test' is a Go test, e.g. func TestXxx(t *testing.T)
// - A 'benchmark' is a Go benchmark, e.g. func BenchmarkXxx(t *testing.B)
// - A 'fuzz' is a Fuzz test, e.g., func TestFuzz(f *testing.F)
// - An 'example' is a Go example, e.g. func ExampleXxx()
//
// The top-level test item for a workspace folder is always either a module or a
// workspace. If the user opens a file (containing tests) that is not contained
// within any workspace folder, a top-level package will be created as a parent
// of that file.
export type GoTestKind = 'module' | 'workspace' | 'package' | 'file' | 'test' | 'benchmark' | 'fuzz' | 'example';
export class GoTest {
// Constructs an ID for an item. The ID of a test item consists of the URI
// for the relevant file or folder with the URI query set to the test item
// kind (see GoTestKind) and the URI fragment set to the function name, if
// the item represents a test, benchmark, or example function.
//
// - Module: file:///path/to/mod?module
// - Workspace: file:///path/to/src?workspace
// - 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
// - Fuzz: file:///path/to/mod/file.go?test#FuzzXxx
// - Example: file:///path/to/mod/file.go?example#ExampleXxx
static id(uri: vscode.Uri, kind: GoTestKind, name?: string): string {
uri = uri.with({ query: kind });
if (name) uri = uri.with({ fragment: name });
return uri.toString();
}
// Parses the ID as a URI and extracts the kind and name.
//
// The URI of the relevant file or folder should be retrieved wil
// TestItem.uri.
static parseId(id: string): { kind: GoTestKind; name?: string } {
const u = vscode.Uri.parse(id);
const kind = u.query as GoTestKind;
const name = u.fragment;
return { kind, name };
}
}
// Check whether the process is running as a test.
export function isInTest() {
return process.env.VSCODE_GO_IN_TEST === '1';
}
// 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(resolver: GoTestResolver, item: vscode.TestItem) {
resolver.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.
export function disposeIfEmpty(resolver: GoTestResolver, item: vscode.TestItem | undefined) {
if (!item) return;
// Don't dispose of empty top-level items
const { kind } = GoTest.parseId(item.id);
if (kind === 'module' || kind === 'workspace' || (kind === 'package' && !item.parent)) {
return;
}
if (item.children.size > 0) {
return;
}
dispose(resolver, item);
disposeIfEmpty(resolver, item.parent);
}
// The 'name' group captures the module name, and the unnamed group ignores any comment that might follow the name.
const moduleNameRegex = /^module.(?<name>.*?)(?:\s|\/\/|$)/mu;
export function findModuleName(goModContent: string): string {
const match = goModContent.toString().match(moduleNameRegex);
if (match === null) {
throw new Error('failed to find module name in go.mod');
}
return match.groups?.name ?? '';
}