Skip to content

Commit 24faa6b

Browse files
committed
src/goTestExplorer: improve readability
Change function names and add comments.
1 parent 77f3623 commit 24faa6b

File tree

1 file changed

+87
-32
lines changed

1 file changed

+87
-32
lines changed

src/goTestExplorer.ts

Lines changed: 87 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function setupTestExplorer(context: ExtensionContext) {
7373
const found = find(ctrl.root);
7474
if (found) {
7575
found.dispose();
76-
removeIfEmpty(found.parent);
76+
disposeIfEmpty(found.parent);
7777
}
7878
});
7979

@@ -99,17 +99,26 @@ export function setupTestExplorer(context: ExtensionContext) {
9999
);
100100
}
101101

102+
// Construct an ID for an item.
103+
// - Module: file:///path/to/mod?module
104+
// - Package: file:///path/to/mod/pkg?package
105+
// - File: file:///path/to/mod/file.go?file
106+
// - Test: file:///path/to/mod/file.go?test#TestXxx
107+
// - Benchmark: file:///path/to/mod/file.go?benchmark#BenchmarkXxx
108+
// - Example: file:///path/to/mod/file.go?example#ExampleXxx
102109
function testID(uri: Uri, kind: string, name?: string): string {
103110
uri = uri.with({ query: kind });
104111
if (name) uri = uri.with({ fragment: name });
105112
return uri.toString();
106113
}
107114

115+
// Retrieve a child item.
108116
function getItem(parent: TestItem, uri: Uri, kind: string, name?: string): TestItem | undefined {
109117
return parent.children.get(testID(uri, kind, name));
110118
}
111119

112-
function createItem(
120+
// Create or Retrieve a child item.
121+
function getOrCreateItem(
113122
ctrl: TestController,
114123
parent: TestItem,
115124
label: string,
@@ -126,7 +135,9 @@ function createItem(
126135
return ctrl.createTestItem(id, label, parent, uri.with({ query: '', fragment: '' }));
127136
}
128137

129-
function createSubItem(ctrl: TestController, item: TestItem, name: string): TestItem {
138+
// Create or Retrieve a sub test or benchmark. The ID will be of the form:
139+
// file:///path/to/mod/file.go?test#TestXxx/A/B/C
140+
function getOrCreateSubTest(ctrl: TestController, item: TestItem, name: string): TestItem {
130141
let uri = Uri.parse(item.id);
131142
uri = uri.with({ fragment: `${uri.fragment}/${name}` });
132143
const existing = item.children.get(uri.toString());
@@ -141,7 +152,9 @@ function createSubItem(ctrl: TestController, item: TestItem, name: string): Test
141152
return sub;
142153
}
143154

144-
function removeIfEmpty(item: TestItem) {
155+
// Dispose of the item if it has no children, recursively. This facilitates
156+
// cleaning up package/file trees that contain no tests.
157+
function disposeIfEmpty(item: TestItem) {
145158
// Don't dispose of the root
146159
if (!item.parent) {
147160
return;
@@ -158,9 +171,10 @@ function removeIfEmpty(item: TestItem) {
158171
}
159172

160173
item.dispose();
161-
removeIfEmpty(item.parent);
174+
disposeIfEmpty(item.parent);
162175
}
163176

177+
// Retrieve or create an item for a Go module.
164178
async function getModule(ctrl: TestController, uri: Uri): Promise<TestItem> {
165179
const existing = getItem(ctrl.root, uri, 'module');
166180
if (existing) {
@@ -172,25 +186,27 @@ async function getModule(ctrl: TestController, uri: Uri): Promise<TestItem> {
172186
const contents = await workspace.fs.readFile(goMod);
173187
const modLine = contents.toString().split('\n', 2)[0];
174188
const match = modLine.match(/^module (?<name>.*?)(?:\s|\/\/|$)/);
175-
const item = createItem(ctrl, ctrl.root, match.groups.name, uri, 'module');
189+
const item = getOrCreateItem(ctrl, ctrl.root, match.groups.name, uri, 'module');
176190
item.canResolveChildren = true;
177191
item.runnable = true;
178192
return item;
179193
}
180194

195+
// Retrieve or create an item for a workspace folder that is not a module.
181196
async function getWorkspace(ctrl: TestController, ws: WorkspaceFolder): Promise<TestItem> {
182197
const existing = getItem(ctrl.root, ws.uri, 'workspace');
183198
if (existing) {
184199
return existing;
185200
}
186201

187202
// Use the workspace folder name as the label
188-
const item = createItem(ctrl, ctrl.root, ws.name, ws.uri, 'workspace');
203+
const item = getOrCreateItem(ctrl, ctrl.root, ws.name, ws.uri, 'workspace');
189204
item.canResolveChildren = true;
190205
item.runnable = true;
191206
return item;
192207
}
193208

209+
// Retrieve or create an item for a Go package.
194210
async function getPackage(ctrl: TestController, uri: Uri): Promise<TestItem> {
195211
let item: TestItem;
196212

@@ -210,7 +226,7 @@ async function getPackage(ctrl: TestController, uri: Uri): Promise<TestItem> {
210226
}
211227

212228
const label = uri.path.startsWith(modUri.path) ? uri.path.substring(modUri.path.length + 1) : uri.path;
213-
item = createItem(ctrl, module, label, uri, 'package');
229+
item = getOrCreateItem(ctrl, module, label, uri, 'package');
214230
} else if (wsfolder) {
215231
// If the package is in a workspace folder, add it as a child of the workspace
216232
const workspace = await getWorkspace(ctrl, wsfolder);
@@ -222,7 +238,7 @@ async function getPackage(ctrl: TestController, uri: Uri): Promise<TestItem> {
222238
const label = uri.path.startsWith(wsfolder.uri.path)
223239
? uri.path.substring(wsfolder.uri.path.length + 1)
224240
: uri.path;
225-
item = createItem(ctrl, workspace, label, uri, 'package');
241+
item = getOrCreateItem(ctrl, workspace, label, uri, 'package');
226242
} else {
227243
// Otherwise, add it directly to the root
228244
const existing = getItem(ctrl.root, uri, 'package');
@@ -232,14 +248,15 @@ async function getPackage(ctrl: TestController, uri: Uri): Promise<TestItem> {
232248

233249
const srcPath = path.join(getCurrentGoPath(uri), 'src');
234250
const label = uri.path.startsWith(srcPath) ? uri.path.substring(srcPath.length + 1) : uri.path;
235-
item = createItem(ctrl, ctrl.root, label, uri, 'package');
251+
item = getOrCreateItem(ctrl, ctrl.root, label, uri, 'package');
236252
}
237253

238254
item.canResolveChildren = true;
239255
item.runnable = true;
240256
return item;
241257
}
242258

259+
// Retrieve or create an item for a Go file.
243260
async function getFile(ctrl: TestController, uri: Uri): Promise<TestItem> {
244261
const dir = path.dirname(uri.path);
245262
const pkg = await getPackage(ctrl, uri.with({ path: dir }));
@@ -249,12 +266,16 @@ async function getFile(ctrl: TestController, uri: Uri): Promise<TestItem> {
249266
}
250267

251268
const label = path.basename(uri.path);
252-
const item = createItem(ctrl, pkg, label, uri, 'file');
269+
const item = getOrCreateItem(ctrl, pkg, label, uri, 'file');
253270
item.canResolveChildren = true;
254271
item.runnable = true;
255272
return item;
256273
}
257274

275+
// Recursively process a Go AST symbol. If the symbol represents a test,
276+
// benchmark, or example function, a test item will be created for it, if one
277+
// does not already exist. If the symbol is not a function and contains
278+
// children, those children will be processed recursively.
258279
async function processSymbol(
259280
ctrl: TestController,
260281
uri: Uri,
@@ -286,14 +307,20 @@ async function processSymbol(
286307
return existing;
287308
}
288309

289-
const item = createItem(ctrl, file, symbol.name, uri, kind, symbol.name);
310+
const item = getOrCreateItem(ctrl, file, symbol.name, uri, kind, symbol.name);
290311
item.range = symbol.range;
291312
item.runnable = true;
292313
// item.debuggable = true;
293314
symbols.set(item, symbol);
294315
}
295316

296-
async function loadFileTests(ctrl: TestController, doc: TextDocument) {
317+
// Processes a Go document, calling processSymbol for each symbol in the
318+
// document.
319+
//
320+
// Any previously existing tests that no longer have a corresponding symbol in
321+
// the file will be disposed. If the document contains no tests, it will be
322+
// disposed.
323+
async function processDocument(ctrl: TestController, doc: TextDocument) {
297324
const seen = new Set<string>();
298325
const item = await getFile(ctrl, doc.uri);
299326
const symbols = await new GoDocumentSymbolProvider().provideDocumentSymbols(doc, null);
@@ -306,18 +333,19 @@ async function loadFileTests(ctrl: TestController, doc: TextDocument) {
306333
}
307334
}
308335

309-
removeIfEmpty(item);
336+
disposeIfEmpty(item);
310337
}
311338

339+
// Reasons to stop walking
312340
enum WalkStop {
313-
None = 0,
314-
Abort,
315-
Current,
316-
Files,
317-
Directories
341+
None = 0, // Don't stop
342+
Abort, // Abort the walk
343+
Current, // Stop walking the current directory
344+
Files, // Skip remaining files
345+
Directories // Skip remaining directories
318346
}
319347

320-
// Recursively walk a directory, breadth first
348+
// Recursively walk a directory, breadth first.
321349
async function walk(
322350
uri: Uri,
323351
cb: (dir: Uri, file: string, type: FileType) => Promise<WalkStop | undefined>
@@ -383,7 +411,10 @@ async function walk(
383411
}
384412
}
385413

386-
async function walkWorkspaces(uri: Uri) {
414+
// Walk the workspace, looking for Go modules. Returns a map indicating paths
415+
// that are modules (value == true) and paths that are not modules but contain
416+
// Go files (value == false).
417+
async function walkWorkspaces(uri: Uri): Promise<Map<string, boolean>> {
387418
const found = new Map<string, boolean>();
388419
await walk(uri, async (dir, file, type) => {
389420
if (type !== FileType.File) {
@@ -402,6 +433,8 @@ async function walkWorkspaces(uri: Uri) {
402433
return found;
403434
}
404435

436+
// Walk the workspace, calling the callback for any directory that contains a Go
437+
// test file.
405438
async function walkPackages(uri: Uri, cb: (uri: Uri) => Promise<unknown>) {
406439
await walk(uri, async (dir, file) => {
407440
if (file.endsWith('_test.go')) {
@@ -411,6 +444,7 @@ async function walkPackages(uri: Uri, cb: (uri: Uri) => Promise<unknown>) {
411444
});
412445
}
413446

447+
// Handle opened documents, document changes, and file creation.
414448
async function documentUpdate(ctrl: TestController, doc: TextDocument) {
415449
if (!doc.uri.path.endsWith('_test.go')) {
416450
return;
@@ -421,10 +455,12 @@ async function documentUpdate(ctrl: TestController, doc: TextDocument) {
421455
return;
422456
}
423457

424-
await loadFileTests(ctrl, doc);
458+
await processDocument(ctrl, doc);
425459
}
426460

461+
// TestController.resolveChildrenHandler callback
427462
async function resolveChildren(ctrl: TestController, item: TestItem) {
463+
// The user expanded the root item - find all modules and workspaces
428464
if (!item.parent) {
429465
// Dispose of package entries at the root if they are now part of a workspace folder
430466
const items = Array.from(ctrl.root.children.values());
@@ -461,15 +497,16 @@ async function resolveChildren(ctrl: TestController, item: TestItem) {
461497
}
462498

463499
const uri = Uri.parse(item.id);
500+
501+
// The user expanded a module or workspace - find all packages
464502
if (uri.query === 'module' || uri.query === 'workspace') {
465-
// Create entries for all packages in the module or workspace
466503
await walkPackages(uri, async (uri) => {
467504
await getPackage(ctrl, uri);
468505
});
469506
}
470507

508+
// The user expanded a module or package - find all files
471509
if (uri.query === 'module' || uri.query === 'package') {
472-
// Create entries for all test files in the package
473510
for (const [file, type] of await workspace.fs.readDirectory(uri)) {
474511
if (type !== FileType.File || !file.endsWith('_test.go')) {
475512
continue;
@@ -479,13 +516,19 @@ async function resolveChildren(ctrl: TestController, item: TestItem) {
479516
}
480517
}
481518

519+
// The user expanded a file - find all functions
482520
if (uri.query === 'file') {
483-
// Create entries for all test functions in a file
484521
const doc = await workspace.openTextDocument(uri.with({ query: '', fragment: '' }));
485-
await loadFileTests(ctrl, doc);
522+
await processDocument(ctrl, doc);
486523
}
524+
525+
// TODO(firelizzard18): If uri.query is test or benchmark, this is where we
526+
// would discover sub tests or benchmarks, if that is feasible.
487527
}
488528

529+
// Recursively find all tests, benchmarks, and examples within a
530+
// module/package/etc, minus exclusions. Map tests to the package they are
531+
// defined in, and track files.
489532
async function collectTests(
490533
ctrl: TestController,
491534
item: TestItem,
@@ -523,6 +566,8 @@ async function collectTests(
523566
return;
524567
}
525568

569+
// TestRunOutput is a fake OutputChannel that forwards all test output to the test API
570+
// console.
526571
class TestRunOutput<T> implements OutputChannel {
527572
readonly name: string;
528573
constructor(private run: TestRun<T>) {
@@ -544,6 +589,9 @@ class TestRunOutput<T> implements OutputChannel {
544589
dispose() {}
545590
}
546591

592+
// Resolve a test name to a test item. If the test name is TestXxx/Foo, Foo is
593+
// created as a child of TestXxx. The same is true for TestXxx#Foo and
594+
// TestXxx/#Foo.
547595
function resolveTestName(ctrl: TestController, tests: Record<string, TestItem>, name: string): TestItem | undefined {
548596
if (!name) {
549597
return;
@@ -556,12 +604,12 @@ function resolveTestName(ctrl: TestController, tests: Record<string, TestItem>,
556604
}
557605

558606
for (const part of parts.slice(1)) {
559-
test = createSubItem(ctrl, test, part);
607+
test = getOrCreateSubTest(ctrl, test, part);
560608
}
561609
return test;
562610
}
563611

564-
// Process benchmark test events (see test_events.md)
612+
// Process benchmark events (see test_events.md)
565613
function consumeGoBenchmarkEvent<T>(
566614
ctrl: TestController,
567615
run: TestRun<T>,
@@ -643,6 +691,7 @@ function passBenchmarks<T>(run: TestRun<T>, items: Record<string, TestItem>, com
643691
}
644692
}
645693

694+
// Process test events (see test_events.md)
646695
function consumeGoTestEvent<T>(
647696
ctrl: TestController,
648697
run: TestRun<T>,
@@ -687,6 +736,8 @@ function consumeGoTestEvent<T>(
687736
}
688737
}
689738

739+
// Search recorded test output for `file.go:123: Foo bar` and attach a message
740+
// to the corresponding location.
690741
function processRecordedOutput<T>(run: TestRun<T>, test: TestItem, output: string[]) {
691742
// mostly copy and pasted from https://gitlab.com/firelizzard/vscode-go-test-adapter/-/blob/733443d229df68c90145a5ae7ed78ca64dec6f43/src/tests.ts
692743
type message = { all: string; error?: string };
@@ -729,14 +780,16 @@ function processRecordedOutput<T>(run: TestRun<T>, test: TestItem, output: strin
729780
}
730781
}
731782

783+
// Execute tests - TestController.runTest callback
732784
async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
733785
const collected = new Map<string, TestItem[]>();
734786
const docs = new Set<Uri>();
735787
for (const item of request.tests) {
736788
await collectTests(ctrl, item, request.exclude, collected, docs);
737789
}
738790

739-
// Ensure `go test` has the latest changes
791+
// Save all documents that contain a test we're about to run, to ensure `go
792+
// test` has the latest changes
740793
await Promise.all(
741794
Array.from(docs).map((uri) => {
742795
workspace.openTextDocument(uri).then((doc) => doc.save());
@@ -751,12 +804,13 @@ async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
751804
const isMod = await isModSupported(uri, true);
752805
const flags = getTestFlags(goConfig);
753806

807+
// Separate tests and benchmarks and mark them as queued for execution.
808+
// Clear any sub tests/benchmarks generated by a previous run.
754809
const tests: Record<string, TestItem> = {};
755810
const benchmarks: Record<string, TestItem> = {};
756811
for (const item of items) {
757812
run.setState(item, TestResultState.Queued);
758813

759-
// Clear any dynamic subtests generated by a previous run
760814
item.canResolveChildren = false;
761815
Array.from(item.children.values()).forEach((x) => x.dispose());
762816

@@ -772,8 +826,8 @@ async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
772826
const testFns = Object.keys(tests);
773827
const benchmarkFns = Object.keys(benchmarks);
774828

829+
// Run tests
775830
if (testFns.length > 0) {
776-
// Run tests
777831
await goTest({
778832
goConfig,
779833
flags,
@@ -785,8 +839,8 @@ async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
785839
});
786840
}
787841

842+
// Run benchmarks
788843
if (benchmarkFns.length > 0) {
789-
// Run benchmarks
790844
const complete = new Set<TestItem>();
791845
await goTest({
792846
goConfig,
@@ -803,6 +857,7 @@ async function runTest<T>(ctrl: TestController, request: TestRunRequest<T>) {
803857
passBenchmarks(run, benchmarks, complete);
804858
}
805859

860+
// Create test messages
806861
for (const [test, output] of record.entries()) {
807862
processRecordedOutput(run, test, output);
808863
}

0 commit comments

Comments
 (0)