Skip to content

Commit

Permalink
testing: initial test coverage UI
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
connor4312 committed Nov 21, 2023
1 parent 0ce333b commit e09bee7
Show file tree
Hide file tree
Showing 24 changed files with 1,088 additions and 153 deletions.
5 changes: 3 additions & 2 deletions build/lib/stylelint/vscode-known-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@
"--z-index-notebook-scrollbar",
"--z-index-run-button-container",
"--z-index-notebook-sticky-scroll",
"--zoom-factor"
"--zoom-factor",
"--test-bar-width"
]
}
}
4 changes: 1 addition & 3 deletions src/vs/base/common/iterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,7 @@ export namespace Iterable {

export function* concat<T>(...iterables: Iterable<T>[]): Iterable<T> {
for (const iterable of iterables) {
for (const element of iterable) {
yield element;
}
yield* iterable;
}
}

Expand Down
48 changes: 36 additions & 12 deletions src/vs/base/common/prefixTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Iterable } from 'vs/base/common/iterator';

const unset = Symbol('unset');

export interface IPrefixTreeNode<T> {
/** Possible children of the node. */
children?: ReadonlyMap<string, Node<T>>;

/** The value if data exists for this node in the tree. Mutable. */
value: T | undefined;
}

/**
* A simple prefix tree implementation where a value is stored based on
* well-defined prefix segments.
Expand All @@ -17,14 +27,19 @@ export class WellDefinedPrefixTree<V> {
return this._size;
}

/** Gets the top-level nodes of the tree */
public get nodes(): Iterable<IPrefixTreeNode<V>> {
return this.root.children?.values() || Iterable.empty();
}

/** Inserts a new value in the prefix tree. */
insert(key: Iterable<string>, value: V): void {
this.opNode(key, n => n.value = value);
this.opNode(key, n => n._value = value);
}

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

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

const value = path[i].node.value;
const value = path[i].node._value;
if (value === unset) {
return; // not actually a real node
}
Expand All @@ -50,7 +65,7 @@ export class WellDefinedPrefixTree<V> {
for (; i > 0; i--) {
const parent = path[i - 1];
parent.node.children!.delete(path[i].part);
if (parent.node.children!.size > 0 || parent.node.value !== unset) {
if (parent.node.children!.size > 0 || parent.node._value !== unset) {
break;
}
}
Expand All @@ -70,7 +85,7 @@ export class WellDefinedPrefixTree<V> {
node = next;
}

return node.value === unset ? undefined : node.value;
return node._value === unset ? undefined : node._value;
}

/** Gets whether the tree has the key, or a parent of the key, already inserted. */
Expand All @@ -81,7 +96,7 @@ export class WellDefinedPrefixTree<V> {
if (!next) {
return false;
}
if (next.value !== unset) {
if (next._value !== unset) {
return true;
}

Expand Down Expand Up @@ -118,7 +133,7 @@ export class WellDefinedPrefixTree<V> {
node = next;
}

return node.value !== unset;
return node._value !== unset;
}

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

if (node.value === unset) {
if (node._value === unset) {
this._size++;
}

Expand All @@ -149,8 +164,8 @@ export class WellDefinedPrefixTree<V> {
const stack = [this.root];
while (stack.length > 0) {
const node = stack.pop()!;
if (node.value !== unset) {
yield node.value;
if (node._value !== unset) {
yield node._value;
}

if (node.children) {
Expand All @@ -162,7 +177,16 @@ export class WellDefinedPrefixTree<V> {
}
}

class Node<T> {
class Node<T> implements IPrefixTreeNode<T> {
public children?: Map<string, Node<T>>;
public value: T | typeof unset = unset;

public get value() {
return this._value === unset ? undefined : this._value;
}

public set value(value: T | undefined) {
this._value = value === undefined ? unset : value;
}

public _value: T | typeof unset = unset;
}
7 changes: 7 additions & 0 deletions src/vs/editor/common/core/position.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,11 @@ export class Position {
&& (typeof obj.column === 'number')
);
}

public toJSON(): IPosition {
return {
lineNumber: this.lineNumber,
column: this.column
};
}
}
1 change: 1 addition & 0 deletions src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,7 @@ declare namespace monaco {
* Test if `obj` is an `IPosition`.
*/
static isIPosition(obj: any): obj is IPosition;
toJSON(): IPosition;
}

/**
Expand Down
24 changes: 14 additions & 10 deletions src/vs/workbench/api/browser/mainThreadTesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { revive } from 'vs/base/common/marshalling';
import { ISettableObservable } from 'vs/base/common/observable';
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { CoverageDetails, ExtensionRunTestsRequest, IFileCoverage, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestRunProfileBitset, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';

@extHostNamedCustomer(MainContext.MainThreadTesting)
export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider {
Expand Down Expand Up @@ -124,17 +124,21 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
/**
* @inheritdoc
*/
$signalCoverageAvailable(runId: string, taskId: string): void {
$signalCoverageAvailable(runId: string, taskId: string, available: boolean): void {
this.withLiveRun(runId, run => {
const task = run.tasks.find(t => t.id === taskId);
if (!task) {
return;
}

(task.coverage as MutableObservableValue<TestCoverage>).value = new TestCoverage({
provideFileCoverage: async token => revive<IFileCoverage[]>(await this.proxy.$provideFileCoverage(runId, taskId, token)),
resolveFileCoverage: (i, token) => this.proxy.$resolveFileCoverage(runId, taskId, i, token),
});
const fn = available ? ((token: CancellationToken) => TestCoverage.load({
provideFileCoverage: async token => await this.proxy.$provideFileCoverage(runId, taskId, token)
.then(c => c.map(IFileCoverage.deserialize)),
resolveFileCoverage: (i, token) => this.proxy.$resolveFileCoverage(runId, taskId, i, token)
.then(d => d.map(CoverageDetails.deserialize)),
}, token)) : undefined;

(task.coverage as ISettableObservable<undefined | ((tkn: CancellationToken) => Promise<TestCoverage>)>).set(fn, undefined);
});
}

Expand Down
6 changes: 3 additions & 3 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2617,12 +2617,12 @@ export interface ExtHostTestingShape {
/** Expands a test item's children, by the given number of levels. */
$expandTest(testId: string, levels: number): Promise<void>;
/** Requests file coverage for a test run. Errors if not available. */
$provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise<Dto<IFileCoverage[]>>;
$provideFileCoverage(runId: string, taskId: string, token: CancellationToken): Promise<IFileCoverage.Serialized[]>;
/**
* Requests coverage details for the file index in coverage data for the run.
* Requires file coverage to have been previously requested via $provideFileCoverage.
*/
$resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise<CoverageDetails[]>;
$resolveFileCoverage(runId: string, taskId: string, fileIndex: number, token: CancellationToken): Promise<CoverageDetails.Serialized[]>;
/** Configures a test run config. */
$configureRunProfile(controllerId: string, configId: number): void;
/** Asks the controller to refresh its tests */
Expand Down Expand Up @@ -2693,7 +2693,7 @@ export interface MainThreadTestingShape {
/** Appends raw output to the test run.. */
$appendOutputToRun(runId: string, taskId: string, output: VSBuffer, location?: ILocationDto, testId?: string): void;
/** Triggered when coverage is added to test results. */
$signalCoverageAvailable(runId: string, taskId: string): void;
$signalCoverageAvailable(runId: string, taskId: string, available: boolean): void;
/** Signals a task in a test run started. */
$startedTestRunTask(runId: string, task: ITestRunTask): void;
/** Signals a task in a test run ended. */
Expand Down
Loading

0 comments on commit e09bee7

Please sign in to comment.