Skip to content

Commit 6f98706

Browse files
firelizzard18hyangah
authored andcommitted
src/goTest: add view for profiles
Updates #1641 Change-Id: I83233d4a64e98bc196d060cc1251a079eaeec22e Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/345470 Reviewed-by: Hyang-Ah Hana Kim <[email protected]> Trust: Hyang-Ah Hana Kim <[email protected]> Trust: Carlos Amedee <[email protected]> Run-TryBot: Hyang-Ah Hana Kim <[email protected]> TryBot-Result: kokoro <[email protected]>
1 parent b82ed6c commit 6f98706

File tree

7 files changed

+152
-43
lines changed

7 files changed

+152
-43
lines changed

docs/test-explorer.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Test explorer implementation (src/goTest)
2+
3+
## Mapping tests
4+
5+
`TestItem`s themselves cannot be used with `Map`s. For non-primitive (object)
6+
keys, Map uses strict equality. Two objects are only strictly equal to each
7+
other if they are the exact same object. Because of this, `TestItem`s cannot be
8+
used as map keys, as the extension host may provide different objects for the
9+
same test. Therefore, if we want to use `TestItem`s as a map key, we must use
10+
their ID instead.

package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@
100100
"onCommand:go.run.modinit",
101101
"onDebugInitialConfigurations",
102102
"onDebugResolve:go",
103-
"onWebviewPanel:welcomeGo"
103+
"onWebviewPanel:welcomeGo",
104+
"onView:go.test.profile"
104105
],
105106
"main": "./dist/goMain.js",
106107
"capabilities": {
@@ -2520,6 +2521,17 @@
25202521
"group": "profile"
25212522
}
25222523
]
2524+
},
2525+
"views": {
2526+
"test": [
2527+
{
2528+
"id": "go.test.profile",
2529+
"name": "Profiles",
2530+
"contextualTitle": "Go",
2531+
"icon": "$(graph)",
2532+
"when": "go.hasProfiles"
2533+
}
2534+
]
25232535
}
25242536
}
25252537
}

src/goTest/explore.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export class GoTestExplorer {
4141
);
4242

4343
context.subscriptions.push(ctrl);
44+
context.subscriptions.push(vscode.window.registerTreeDataProvider('go.test.profile', inst.profiler.view));
4445

4546
context.subscriptions.push(
4647
vscode.commands.registerCommand('go.test.refresh', async (item) => {
@@ -219,7 +220,7 @@ export class GoTestExplorer {
219220

220221
const ws = this.workspace.getWorkspaceFolder(item.uri);
221222
if (!ws) {
222-
dispose(item);
223+
dispose(this.resolver, item);
223224
}
224225
});
225226
}
@@ -246,16 +247,16 @@ export class GoTestExplorer {
246247

247248
const found = find(this.ctrl.items);
248249
if (found) {
249-
dispose(found);
250-
disposeIfEmpty(found.parent);
250+
dispose(this.resolver, found);
251+
disposeIfEmpty(this.resolver, found.parent);
251252
}
252253
}
253254

254255
protected async didChangeConfiguration(e: ConfigurationChangeEvent) {
255256
let update = false;
256257
this.ctrl.items.forEach((item) => {
257258
if (e.affectsConfiguration('go.testExplorerPackages', item.uri)) {
258-
dispose(item);
259+
dispose(this.resolver, item);
259260
update = true;
260261
}
261262
});

src/goTest/profile.ts

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22
* Copyright 2021 The Go Authors. All rights reserved.
33
* Licensed under the MIT License. See LICENSE in the project root for license information.
44
*--------------------------------------------------------*/
5-
import { Memento, TestItem, Uri } from 'vscode';
5+
import {
6+
EventEmitter,
7+
Memento,
8+
Range,
9+
TestItem,
10+
TextDocumentShowOptions,
11+
TreeDataProvider,
12+
TreeItem,
13+
TreeItemCollapsibleState,
14+
Uri
15+
} from 'vscode';
616
import vscode = require('vscode');
717
import { getTempFilePath } from '../util';
818
import { GoTestResolver } from './resolve';
@@ -13,7 +23,10 @@ const optionsMemento = 'testProfilingOptions';
1323
const defaultOptions: ProfilingOptions = { kind: 'cpu' };
1424

1525
export class GoTestProfiler {
16-
private readonly lastRunFor = new Map<string, Run>();
26+
public readonly view = new ProfileTreeDataProvider(this);
27+
28+
// Maps test IDs to profile files. See docs/test-explorer.md for details.
29+
private readonly runs = new Map<string, File[]>();
1730

1831
constructor(private readonly resolver: GoTestResolver, private readonly workspaceState: Memento) {}
1932

@@ -24,27 +37,28 @@ export class GoTestProfiler {
2437
this.workspaceState.update(optionsMemento, v);
2538
}
2639

27-
preRun(options: ProfilingOptions, items: TestItem[]): string[] {
40+
preRun(options: ProfilingOptions, item: TestItem): string[] {
2841
const kind = Kind.get(options.kind);
29-
if (!kind) {
30-
items.forEach((x) => this.lastRunFor.delete(x.id));
31-
return [];
32-
}
42+
if (!kind) return [];
3343

3444
const flags = [];
35-
const run = new Run(items, kind);
36-
flags.push(run.file.flag);
37-
items.forEach((x) => this.lastRunFor.set(x.id, run));
45+
const run = new File(kind, item);
46+
flags.push(run.flag);
47+
if (this.runs.has(item.id)) this.runs.get(item.id).unshift(run);
48+
else this.runs.set(item.id, [run]);
3849
return flags;
3950
}
4051

4152
postRun() {
4253
// Update the list of tests that have profiles.
43-
vscode.commands.executeCommand('setContext', 'go.profiledTests', Array.from(this.lastRunFor.keys()));
54+
vscode.commands.executeCommand('setContext', 'go.profiledTests', Array.from(this.runs.keys()));
55+
vscode.commands.executeCommand('setContext', 'go.hasProfiles', this.runs.size > 0);
56+
57+
this.view.didRun();
4458
}
4559

4660
hasProfileFor(id: string): boolean {
47-
return this.lastRunFor.has(id);
61+
return this.runs.has(id);
4862
}
4963

5064
async configure(): Promise<ProfilingOptions | undefined> {
@@ -68,13 +82,31 @@ export class GoTestProfiler {
6882
return;
6983
}
7084

71-
const run = this.lastRunFor.get(item.id);
72-
if (!run) {
73-
await vscode.window.showErrorMessage(`${name} was not profiled the last time it was run`);
85+
const runs = this.runs.get(item.id);
86+
if (!runs || runs.length === 0) {
87+
await vscode.window.showErrorMessage(`${name} has not been profiled`);
7488
return;
7589
}
7690

77-
await run.file.show();
91+
await runs[0].show();
92+
}
93+
94+
// Tests that have been profiled
95+
get tests() {
96+
const items = Array.from(this.runs.keys());
97+
items.sort((a: string, b: string) => {
98+
const aWhen = this.runs.get(a)[0].when.getTime();
99+
const bWhen = this.runs.get(b)[0].when.getTime();
100+
return bWhen - aWhen;
101+
});
102+
103+
// Filter out any tests that no longer exist
104+
return items.map((x) => this.resolver.all.get(x)).filter((x) => x);
105+
}
106+
107+
// Profiles associated with the given test
108+
get(item: TestItem) {
109+
return this.runs.get(item.id) || [];
78110
}
79111
}
80112

@@ -103,23 +135,20 @@ class Kind {
103135
static readonly Block = new Kind('block', 'Block', '--blockprofile');
104136
}
105137

106-
class Run {
138+
class File {
107139
private static nextID = 0;
108140

141+
public readonly id = File.nextID++;
109142
public readonly when = new Date();
110-
public readonly id = Run.nextID++;
111-
public readonly file: File;
112143

113-
constructor(public readonly targets: TestItem[], kind: Kind) {
114-
this.file = new File(this, kind);
115-
}
116-
}
144+
constructor(public readonly kind: Kind, public readonly target: TestItem) {}
117145

118-
class File {
119-
constructor(public readonly run: Run, public readonly kind: Kind) {}
146+
get label() {
147+
return `${this.kind.label} @ ${this.when.toTimeString()}`;
148+
}
120149

121150
get name() {
122-
return `profile-${this.run.id}.${this.kind.id}.prof`;
151+
return `profile-${this.id}.${this.kind.id}.prof`;
123152
}
124153

125154
get flag(): string {
@@ -134,3 +163,48 @@ class File {
134163
await vscode.window.showTextDocument(this.uri);
135164
}
136165
}
166+
167+
type TreeElement = TestItem | File;
168+
169+
class ProfileTreeDataProvider implements TreeDataProvider<TreeElement> {
170+
private readonly didChangeTreeData = new EventEmitter<void | TreeElement>();
171+
public readonly onDidChangeTreeData = this.didChangeTreeData.event;
172+
173+
constructor(private readonly profiler: GoTestProfiler) {}
174+
175+
didRun() {
176+
this.didChangeTreeData.fire();
177+
}
178+
179+
getTreeItem(element: TreeElement): TreeItem {
180+
if (element instanceof File) {
181+
const item = new TreeItem(element.label);
182+
item.contextValue = 'file';
183+
item.command = {
184+
title: 'Open',
185+
command: 'vscode.open',
186+
arguments: [element.uri]
187+
};
188+
return item;
189+
}
190+
191+
const item = new TreeItem(element.label, TreeItemCollapsibleState.Collapsed);
192+
item.contextValue = 'test';
193+
const options: TextDocumentShowOptions = {
194+
preserveFocus: false,
195+
selection: new Range(element.range.start, element.range.start)
196+
};
197+
item.command = {
198+
title: 'Go to test',
199+
command: 'vscode.open',
200+
arguments: [element.uri, options]
201+
};
202+
return item;
203+
}
204+
205+
getChildren(element?: TreeElement): TreeElement[] {
206+
if (!element) return this.profiler.tests;
207+
if (element instanceof File) return [];
208+
return this.profiler.get(element);
209+
}
210+
}

src/goTest/resolve.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface TestSuite {
3737
}
3838

3939
export class GoTestResolver {
40+
public readonly all = new Map<string, TestItem>();
4041
public readonly isDynamicSubtest = new WeakSet<TestItem>();
4142
public readonly isTestMethod = new WeakSet<TestItem>();
4243
public readonly isTestSuiteFunc = new WeakSet<TestItem>();
@@ -75,7 +76,7 @@ export class GoTestResolver {
7576
}
7677

7778
if (this.workspace.getWorkspaceFolder(item.uri)) {
78-
dispose(item);
79+
dispose(this, item);
7980
}
8081
});
8182

@@ -212,16 +213,16 @@ export class GoTestResolver {
212213
item.children.forEach((child) => {
213214
const { name } = GoTest.parseId(child.id);
214215
if (!seen.has(name)) {
215-
dispose(child);
216+
dispose(this, child);
216217
return;
217218
}
218219

219220
if (ranges?.some((r) => !!child.range.intersection(r))) {
220-
item.children.forEach(dispose);
221+
item.children.forEach((x) => dispose(this, x));
221222
}
222223
});
223224

224-
disposeIfEmpty(item);
225+
disposeIfEmpty(this, item);
225226
}
226227

227228
/* ***** Private ***** */
@@ -233,7 +234,10 @@ export class GoTestResolver {
233234

234235
// Create an item.
235236
private createItem(label: string, uri: Uri, kind: GoTestKind, name?: string): TestItem {
236-
return this.ctrl.createTestItem(GoTest.id(uri, kind, name), label, uri.with({ query: '', fragment: '' }));
237+
const id = GoTest.id(uri, kind, name);
238+
const item = this.ctrl.createTestItem(id, label, uri.with({ query: '', fragment: '' }));
239+
this.all.set(id, item);
240+
return item;
237241
}
238242

239243
// Retrieve an item.

src/goTest/run.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export class GoTestRunner {
200200
// Remove subtests created dynamically from test output
201201
item.children.forEach((child) => {
202202
if (this.resolver.isDynamicSubtest.has(child)) {
203-
dispose(child);
203+
dispose(this.resolver, child);
204204
}
205205
});
206206

@@ -327,15 +327,21 @@ export class GoTestRunner {
327327
}
328328

329329
private async runGoTest(config: RunConfig): Promise<boolean> {
330-
const { run, options, pkg, functions, record, concat, flags, ...rest } = config;
330+
const { run, options, pkg, functions, record, concat, ...rest } = config;
331331
if (Object.keys(functions).length === 0) return true;
332332

333+
if (options.kind) {
334+
if (Object.keys(functions).length > 1) {
335+
throw new Error('Profiling more than one test at once is unsupported');
336+
}
337+
rest.flags.push(...this.profiler.preRun(options, Object.values(functions)[0]));
338+
}
339+
333340
const complete = new Set<TestItem>();
334341
const outputChannel = new TestRunOutput(run);
335342

336343
const success = await goTest({
337344
...rest,
338-
flags: [...flags, ...this.profiler.preRun(options, Object.values(functions))],
339345
outputChannel,
340346
dir: pkg.uri.fsPath,
341347
functions: Object.keys(functions),

src/goTest/utils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See LICENSE in the project root for license information.
44
*--------------------------------------------------------*/
55
import * as vscode from 'vscode';
6+
import { GoTestResolver } from './resolve';
67

78
// GoTestKind indicates the Go construct represented by a test item.
89
//
@@ -91,13 +92,14 @@ export function forEachAsync<T>(
9192
return Promise.all(promises);
9293
}
9394

94-
export function dispose(item: vscode.TestItem) {
95+
export function dispose(resolver: GoTestResolver, item: vscode.TestItem) {
96+
resolver.all.delete(item.id);
9597
item.parent.children.delete(item.id);
9698
}
9799

98100
// Dispose of the item if it has no children, recursively. This facilitates
99101
// cleaning up package/file trees that contain no tests.
100-
export function disposeIfEmpty(item: vscode.TestItem) {
102+
export function disposeIfEmpty(resolver: GoTestResolver, item: vscode.TestItem) {
101103
// Don't dispose of empty top-level items
102104
const { kind } = GoTest.parseId(item.id);
103105
if (kind === 'module' || kind === 'workspace' || (kind === 'package' && !item.parent)) {
@@ -108,6 +110,6 @@ export function disposeIfEmpty(item: vscode.TestItem) {
108110
return;
109111
}
110112

111-
dispose(item);
112-
disposeIfEmpty(item.parent);
113+
dispose(resolver, item);
114+
disposeIfEmpty(resolver, item.parent);
113115
}

0 commit comments

Comments
 (0)