| /*--------------------------------------------------------- | |
| * 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 ?? ''; | |
| } |