Skip to content

Commit f821859

Browse files
authored
testing: finalize testInvalidateResults (microsoft#188180)
Also sends the tests as a bulk to the renderer, and implements a prefix tree for doing invalidation checks (which I plan to adopt elsewhere later on, perhaps in debt week.)
1 parent 3d1f720 commit f821859

File tree

11 files changed

+177
-42
lines changed

11 files changed

+177
-42
lines changed

extensions/vscode-api-tests/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
"treeItemCheckbox",
4747
"treeViewActiveItem",
4848
"treeViewReveal",
49-
"testInvalidateResults",
5049
"workspaceTrust",
5150
"telemetry",
5251
"windowActivity",

src/vs/base/common/prefixTree.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
const unset = Symbol('unset');
7+
8+
/**
9+
* A simple prefix tree implementation where a value is stored based on
10+
* well-defined prefix segments.
11+
*/
12+
export class WellDefinedPrefixTree<V> {
13+
private readonly root = new Node<V>();
14+
15+
/** Inserts a new value in the prefix tree. */
16+
insert(key: Iterable<string>, value: V): void {
17+
let node = this.root;
18+
for (const part of key) {
19+
if (!node.children) {
20+
const next = new Node<V>();
21+
node.children = new Map([[part, next]]);
22+
node = next;
23+
} else if (!node.children.has(part)) {
24+
const next = new Node<V>();
25+
node.children.set(part, next);
26+
node = next;
27+
} else {
28+
node = node.children.get(part)!;
29+
}
30+
31+
}
32+
33+
node.value = value;
34+
}
35+
36+
/** Gets a value from the tree. */
37+
find(key: Iterable<string>): V | undefined {
38+
let node = this.root;
39+
for (const segment of key) {
40+
const next = node.children?.get(segment);
41+
if (!next) {
42+
return undefined;
43+
}
44+
45+
node = next;
46+
}
47+
48+
return node.value === unset ? undefined : node.value;
49+
}
50+
51+
/** Gets whether the tree has the key, or a parent of the key, already inserted. */
52+
hasKeyOrParent(key: Iterable<string>): boolean {
53+
let node = this.root;
54+
for (const segment of key) {
55+
const next = node.children?.get(segment);
56+
if (!next) {
57+
return false;
58+
}
59+
if (next.value !== unset) {
60+
return true;
61+
}
62+
63+
node = next;
64+
}
65+
66+
return false;
67+
}
68+
69+
/** Gets whether the tree has the given key or any children. */
70+
hasKeyOrChildren(key: Iterable<string>): boolean {
71+
let node = this.root;
72+
for (const segment of key) {
73+
const next = node.children?.get(segment);
74+
if (!next) {
75+
return false;
76+
}
77+
78+
node = next;
79+
}
80+
81+
return true;
82+
}
83+
}
84+
85+
class Node<T> {
86+
public children?: Map<string, Node<T>>;
87+
public value: T | typeof unset = unset;
88+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
7+
import * as assert from 'assert';
8+
9+
suite('WellDefinedPrefixTree', () => {
10+
let tree: WellDefinedPrefixTree<number>;
11+
12+
setup(() => {
13+
tree = new WellDefinedPrefixTree<number>();
14+
});
15+
16+
test('find', () => {
17+
const key1 = ['foo', 'bar'];
18+
const key2 = ['foo', 'baz'];
19+
tree.insert(key1, 42);
20+
tree.insert(key2, 43);
21+
assert.strictEqual(tree.find(key1), 42);
22+
assert.strictEqual(tree.find(key2), 43);
23+
assert.strictEqual(tree.find(['foo', 'baz', 'bop']), undefined);
24+
assert.strictEqual(tree.find(['foo']), undefined);
25+
});
26+
27+
test('hasParentOfKey', () => {
28+
const key = ['foo', 'bar'];
29+
tree.insert(key, 42);
30+
31+
assert.strictEqual(tree.hasKeyOrParent(['foo', 'bar', 'baz']), true);
32+
assert.strictEqual(tree.hasKeyOrParent(['foo', 'bar']), true);
33+
assert.strictEqual(tree.hasKeyOrParent(['foo']), false);
34+
assert.strictEqual(tree.hasKeyOrParent(['baz']), false);
35+
});
36+
37+
38+
test('hasKeyOrChildren', () => {
39+
const key = ['foo', 'bar'];
40+
tree.insert(key, 42);
41+
42+
assert.strictEqual(tree.hasKeyOrChildren([]), true);
43+
assert.strictEqual(tree.hasKeyOrChildren(['foo']), true);
44+
assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar']), true);
45+
assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar', 'baz']), false);
46+
});
47+
});

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResu
1818
import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
1919
import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers';
2020
import { ExtHostContext, ExtHostTestingShape, ILocationDto, ITestControllerPatch, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
21+
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
22+
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
2123

2224
@extHostNamedCustomer(MainContext.MainThreadTesting)
2325
export class MainThreadTesting extends Disposable implements MainThreadTestingShape, ITestRootProvider {
@@ -55,11 +57,19 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
5557
/**
5658
* @inheritdoc
5759
*/
58-
$markTestRetired(testId: string): void {
60+
$markTestRetired(testIds: string[] | undefined): void {
61+
let tree: WellDefinedPrefixTree<undefined> | undefined;
62+
if (testIds) {
63+
tree = new WellDefinedPrefixTree();
64+
for (const id of testIds) {
65+
tree.insert(TestId.fromString(id).path, undefined);
66+
}
67+
}
68+
5969
for (const result of this.resultService.results) {
6070
// all non-live results are already entirely outdated
6171
if (result instanceof LiveTestResult) {
62-
result.markRetired(testId);
72+
result.markRetired(tree);
6373
}
6474
}
6575
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2514,7 +2514,7 @@ export interface MainThreadTestingShape {
25142514
/** Signals that an extension-provided test run finished. */
25152515
$finishedExtensionTestRun(runId: string): void;
25162516
/** Marks a test (or controller) as retired in all results. */
2517-
$markTestRetired(testId: string): void;
2517+
$markTestRetired(testIds: string[] | undefined): void;
25182518
}
25192519

25202520
// --- proxy identifiers

src/vs/workbench/api/common/extHostTesting.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants';
2929
import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId';
3030
import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection';
3131
import { AbstractIncrementalTestCollection, CoverageDetails, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, IStartControllerTestsResult, ITestItem, ITestItemContext, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestResultState, TestRunProfileBitset, TestsDiff, TestsDiffOp, isStartControllerTests } from 'vs/workbench/contrib/testing/common/testTypes';
32-
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
3332
import type * as vscode from 'vscode';
3433

3534
interface ControllerInfo {
@@ -141,10 +140,11 @@ export class ExtHostTesting implements ExtHostTestingShape {
141140
return this.runTracker.createTestRun(controllerId, collection, request, name, persist);
142141
},
143142
invalidateTestResults: items => {
144-
checkProposedApiEnabled(extension, 'testInvalidateResults');
145-
for (const item of items instanceof Array ? items : [items]) {
146-
const id = item ? TestId.fromExtHostTestItem(item, controllerId).toString() : controllerId;
147-
this.proxy.$markTestRetired(id);
143+
if (items === undefined) {
144+
this.proxy.$markTestRetired(undefined);
145+
} else {
146+
const itemsArr = items instanceof Array ? items : [items];
147+
this.proxy.$markTestRetired(itemsArr.map(i => TestId.fromExtHostTestItem(i!, controllerId).toString()));
148148
}
149149
},
150150
set resolveHandler(fn) {

src/vs/workbench/contrib/testing/common/testId.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,15 @@ export class TestId {
102102

103103
/**
104104
* Gets whether maybeChild is a child of maybeParent.
105+
* todo@connor4312: review usages of this to see if using the WellDefinedPrefixTree is better
105106
*/
106107
public static isChild(maybeParent: string, maybeChild: string) {
107108
return maybeChild.startsWith(maybeParent) && maybeChild[maybeParent.length] === TestIdPathParts.Delimiter;
108109
}
109110

110111
/**
111112
* Compares the position of the two ID strings.
113+
* todo@connor4312: review usages of this to see if using the WellDefinedPrefixTree is better
112114
*/
113115
public static compare(a: string, b: string) {
114116
if (a === b) {

src/vs/workbench/contrib/testing/common/testResult.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
88
import { Emitter, Event } from 'vs/base/common/event';
99
import { Lazy } from 'vs/base/common/lazy';
1010
import { language } from 'vs/base/common/platform';
11+
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
1112
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
1213
import { localize } from 'vs/nls';
1314
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
@@ -233,6 +234,7 @@ export class LiveTestResult implements ITestResult {
233234
private readonly newTaskEmitter = new Emitter<number>();
234235
private readonly endTaskEmitter = new Emitter<number>();
235236
private readonly changeEmitter = new Emitter<TestResultItemChange>();
237+
/** todo@connor4312: convert to a WellDefinedPrefixTree */
236238
private readonly testById = new Map<string, TestResultItemWithChildren>();
237239
private testMarkerCounter = 0;
238240
private _completedAt?: number;
@@ -436,9 +438,9 @@ export class LiveTestResult implements ITestResult {
436438
/**
437439
* Marks the test and all of its children in the run as retired.
438440
*/
439-
public markRetired(testId: string) {
441+
public markRetired(testIds: WellDefinedPrefixTree<undefined> | undefined) {
440442
for (const [id, test] of this.testById) {
441-
if (!test.retired && id === testId || TestId.isChild(testId, id)) {
443+
if (!test.retired && (!testIds || testIds.hasKeyOrParent(TestId.fromString(id).path))) {
442444
test.retired = true;
443445
this.changeEmitter.fire({ reason: TestResultItemChangeReason.ComputedStateChange, item: test, result: this });
444446
}

src/vs/workbench/services/extensions/common/extensionsApiProposals.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export const allApiProposals = Object.freeze({
8686
terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts',
8787
terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts',
8888
testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts',
89-
testInvalidateResults: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts',
9089
testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts',
9190
textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts',
9291
timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts',

src/vscode-dts/vscode.d.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16304,6 +16304,24 @@ declare module 'vscode' {
1630416304
*/
1630516305
createTestItem(id: string, label: string, uri?: Uri): TestItem;
1630616306

16307+
/**
16308+
* Marks an item's results as being outdated. This is commonly called when
16309+
* code or configuration changes and previous results should no longer
16310+
* be considered relevant. The same logic used to mark results as outdated
16311+
* may be used to drive {@link TestRunRequest.continuous continuous test runs}.
16312+
*
16313+
* If an item is passed to this method, test results for the item and all of
16314+
* its children will be marked as outdated. If no item is passed, then all
16315+
* test owned by the TestController will be marked as outdated.
16316+
*
16317+
* Any test runs started before the moment this method is called, including
16318+
* runs which may still be ongoing, will be marked as outdated and deprioritized
16319+
* in the editor's UI.
16320+
*
16321+
* @param item Item to mark as outdated. If undefined, all the controller's items are marked outdated.
16322+
*/
16323+
invalidateTestResults(items?: TestItem | readonly TestItem[]): void;
16324+
1630716325
/**
1630816326
* Unregisters the test controller, disposing of its associated tests
1630916327
* and unpersisted results.

0 commit comments

Comments
 (0)