Skip to content

Commit e09bee7

Browse files
committed
testing: initial test coverage UI
This continues on the coverage API I started a few years ago. It adds initial integration where a "Show Test Coverage" tree item is shown in the Test Results view, which then opens a dedicated Test Coverage view. The Test Coverage view is a fairly basic tree view following the draft design, with further improvements to come. The 'bars' widget is also built in a reusable way such that it can be integrated into the explorer, as this was a popular ask both inside and outside the team. For #123713.
1 parent 0ce333b commit e09bee7

24 files changed

+1088
-153
lines changed

build/lib/stylelint/vscode-known-variables.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,7 @@
791791
"--z-index-notebook-scrollbar",
792792
"--z-index-run-button-container",
793793
"--z-index-notebook-sticky-scroll",
794-
"--zoom-factor"
794+
"--zoom-factor",
795+
"--test-bar-width"
795796
]
796-
}
797+
}

src/vs/base/common/iterator.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ export namespace Iterable {
8484

8585
export function* concat<T>(...iterables: Iterable<T>[]): Iterable<T> {
8686
for (const iterable of iterables) {
87-
for (const element of iterable) {
88-
yield element;
89-
}
87+
yield* iterable;
9088
}
9189
}
9290

src/vs/base/common/prefixTree.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,18 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { Iterable } from 'vs/base/common/iterator';
7+
68
const unset = Symbol('unset');
79

10+
export interface IPrefixTreeNode<T> {
11+
/** Possible children of the node. */
12+
children?: ReadonlyMap<string, Node<T>>;
13+
14+
/** The value if data exists for this node in the tree. Mutable. */
15+
value: T | undefined;
16+
}
17+
818
/**
919
* A simple prefix tree implementation where a value is stored based on
1020
* well-defined prefix segments.
@@ -17,14 +27,19 @@ export class WellDefinedPrefixTree<V> {
1727
return this._size;
1828
}
1929

30+
/** Gets the top-level nodes of the tree */
31+
public get nodes(): Iterable<IPrefixTreeNode<V>> {
32+
return this.root.children?.values() || Iterable.empty();
33+
}
34+
2035
/** Inserts a new value in the prefix tree. */
2136
insert(key: Iterable<string>, value: V): void {
22-
this.opNode(key, n => n.value = value);
37+
this.opNode(key, n => n._value = value);
2338
}
2439

2540
/** Mutates a value in the prefix tree. */
2641
mutate(key: Iterable<string>, mutate: (value?: V) => V): void {
27-
this.opNode(key, n => n.value = mutate(n.value === unset ? undefined : n.value));
42+
this.opNode(key, n => n._value = mutate(n._value === unset ? undefined : n._value));
2843
}
2944

3045
/** Deletes a node from the prefix tree, returning the value it contained. */
@@ -41,7 +56,7 @@ export class WellDefinedPrefixTree<V> {
4156
i++;
4257
}
4358

44-
const value = path[i].node.value;
59+
const value = path[i].node._value;
4560
if (value === unset) {
4661
return; // not actually a real node
4762
}
@@ -50,7 +65,7 @@ export class WellDefinedPrefixTree<V> {
5065
for (; i > 0; i--) {
5166
const parent = path[i - 1];
5267
parent.node.children!.delete(path[i].part);
53-
if (parent.node.children!.size > 0 || parent.node.value !== unset) {
68+
if (parent.node.children!.size > 0 || parent.node._value !== unset) {
5469
break;
5570
}
5671
}
@@ -70,7 +85,7 @@ export class WellDefinedPrefixTree<V> {
7085
node = next;
7186
}
7287

73-
return node.value === unset ? undefined : node.value;
88+
return node._value === unset ? undefined : node._value;
7489
}
7590

7691
/** Gets whether the tree has the key, or a parent of the key, already inserted. */
@@ -81,7 +96,7 @@ export class WellDefinedPrefixTree<V> {
8196
if (!next) {
8297
return false;
8398
}
84-
if (next.value !== unset) {
99+
if (next._value !== unset) {
85100
return true;
86101
}
87102

@@ -118,7 +133,7 @@ export class WellDefinedPrefixTree<V> {
118133
node = next;
119134
}
120135

121-
return node.value !== unset;
136+
return node._value !== unset;
122137
}
123138

124139
private opNode(key: Iterable<string>, fn: (node: Node<V>) => void): void {
@@ -137,7 +152,7 @@ export class WellDefinedPrefixTree<V> {
137152
}
138153
}
139154

140-
if (node.value === unset) {
155+
if (node._value === unset) {
141156
this._size++;
142157
}
143158

@@ -149,8 +164,8 @@ export class WellDefinedPrefixTree<V> {
149164
const stack = [this.root];
150165
while (stack.length > 0) {
151166
const node = stack.pop()!;
152-
if (node.value !== unset) {
153-
yield node.value;
167+
if (node._value !== unset) {
168+
yield node._value;
154169
}
155170

156171
if (node.children) {
@@ -162,7 +177,16 @@ export class WellDefinedPrefixTree<V> {
162177
}
163178
}
164179

165-
class Node<T> {
180+
class Node<T> implements IPrefixTreeNode<T> {
166181
public children?: Map<string, Node<T>>;
167-
public value: T | typeof unset = unset;
182+
183+
public get value() {
184+
return this._value === unset ? undefined : this._value;
185+
}
186+
187+
public set value(value: T | undefined) {
188+
this._value = value === undefined ? unset : value;
189+
}
190+
191+
public _value: T | typeof unset = unset;
168192
}

src/vs/editor/common/core/position.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,11 @@ export class Position {
174174
&& (typeof obj.column === 'number')
175175
);
176176
}
177+
178+
public toJSON(): IPosition {
179+
return {
180+
lineNumber: this.lineNumber,
181+
column: this.column
182+
};
183+
}
177184
}

src/vs/monaco.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,7 @@ declare namespace monaco {
621621
* Test if `obj` is an `IPosition`.
622622
*/
623623
static isIPosition(obj: any): obj is IPosition;
624+
toJSON(): IPosition;
624625
}
625626

626627
/**

src/vs/workbench/api/browser/mainThreadTesting.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ import { VSBuffer } from 'vs/base/common/buffer';
77
import { CancellationToken } from 'vs/base/common/cancellation';
88
import { Event } from 'vs/base/common/event';
99
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
10-
import { revive } from 'vs/base/common/marshalling';
10+
import { ISettableObservable } from 'vs/base/common/observable';
11+
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
1112
import { URI } from 'vs/base/common/uri';
1213
import { Range } from 'vs/editor/common/core/range';
1314
import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
14-
import { ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
1515
import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
16+
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
1617
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
1718
import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
1819
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
1920
import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
20-
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
21+
import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
22+
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
2123
import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
22-
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
23-
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
2424

2525
@extHostNamedCustomer(MainContext.MainThreadTesting)
2626
export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider {
@@ -124,17 +124,21 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
124124
/**
125125
* @inheritdoc
126126
*/
127-
$signalCoverageAvailable(runId: string, taskId: string): void {
127+
$signalCoverageAvailable(runId: string, taskId: string, available: boolean): void {
128128
this.withLiveRun(runId, run => {
129129
const task = run.tasks.find(t => t.id === taskId);
130130
if (!task) {
131131
return;
132132
}
133133

134-
(task.coverage as MutableObservableValue<TestCoverage>).value = new TestCoverage({
135-
provideFileCoverage: async token => revive<IFileCoverage[]>(await this.proxy.$provideFileCoverage(runId, taskId, token)),
136-
resolveFileCoverage: (i, token) => this.proxy.$resolveFileCoverage(runId, taskId, i, token),
137-
});
134+
const fn = available ? ((token: CancellationToken) => TestCoverage.load({
135+
provideFileCoverage: async token => await this.proxy.$provideFileCoverage(runId, taskId, token)
136+
.then(c => c.map(IFileCoverage.deserialize)),
137+
resolveFileCoverage: (i, token) => this.proxy.$resolveFileCoverage(runId, taskId, i, token)
138+
.then(d => d.map(CoverageDetails.deserialize)),
139+
}, token)) : undefined;
140+
141+
(task.coverage as ISettableObservable<undefined | ((tkn: CancellationToken) => Promise<TestCoverage>)>).set(fn, undefined);
138142
});
139143
}
140144

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2617,12 +2617,12 @@ export interface ExtHostTestingShape {
26172617
/** Expands a test item's children, by the given number of levels. */
26182618
$expandTest(testId: string, levels: number): Promise<void>;
26192619
/** Requests file coverage for a test run. Errors if not available. */
2620-
$provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise<Dto<IFileCoverage[]>>;
2620+
$provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise<IFileCoverage.Serialized[]>;
26212621
/**
26222622
* Requests coverage details for the file index in coverage data for the run.
26232623
* Requires file coverage to have been previously requested via $provideFileCoverage.
26242624
*/
2625-
$resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise<CoverageDetails[]>;
2625+
$resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise<CoverageDetails.Serialized[]>;
26262626
/** Configures a test run config. */
26272627
$configureRunProfile(controllerId: string, configId: number): void;
26282628
/** Asks the controller to refresh its tests */
@@ -2693,7 +2693,7 @@ export interface MainThreadTestingShape {
26932693
/** Appends raw output to the test run.. */
26942694
$appendOutputToRun(runId: string, taskId: string, output: VSBuffer, location?: ILocationDto, testId?: string): void;
26952695
/** Triggered when coverage is added to test results. */
2696-
$signalCoverageAvailable(runId: string, taskId: string): void;
2696+
$signalCoverageAvailable(runId: string, taskId: string, available: boolean): void;
26972697
/** Signals a task in a test run started. */
26982698
$startedTestRunTask(runId: string, task: ITestRunTask): void;
26992699
/** Signals a task in a test run ended. */

0 commit comments

Comments
 (0)