testing: finalize testInvalidateResults (#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.)
This commit is contained in:
Connor Peet 2023-07-18 11:14:25 -07:00 committed by GitHub
parent 3d1f7201ad
commit f82185934a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 177 additions and 42 deletions

View file

@ -46,7 +46,6 @@
"treeItemCheckbox",
"treeViewActiveItem",
"treeViewReveal",
"testInvalidateResults",
"workspaceTrust",
"telemetry",
"windowActivity",

View file

@ -0,0 +1,88 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const unset = Symbol('unset');
/**
* A simple prefix tree implementation where a value is stored based on
* well-defined prefix segments.
*/
export class WellDefinedPrefixTree<V> {
private readonly root = new Node<V>();
/** Inserts a new value in the prefix tree. */
insert(key: Iterable<string>, value: V): void {
let node = this.root;
for (const part of key) {
if (!node.children) {
const next = new Node<V>();
node.children = new Map([[part, next]]);
node = next;
} else if (!node.children.has(part)) {
const next = new Node<V>();
node.children.set(part, next);
node = next;
} else {
node = node.children.get(part)!;
}
}
node.value = value;
}
/** Gets a value from the tree. */
find(key: Iterable<string>): V | undefined {
let node = this.root;
for (const segment of key) {
const next = node.children?.get(segment);
if (!next) {
return undefined;
}
node = next;
}
return node.value === unset ? undefined : node.value;
}
/** Gets whether the tree has the key, or a parent of the key, already inserted. */
hasKeyOrParent(key: Iterable<string>): boolean {
let node = this.root;
for (const segment of key) {
const next = node.children?.get(segment);
if (!next) {
return false;
}
if (next.value !== unset) {
return true;
}
node = next;
}
return false;
}
/** Gets whether the tree has the given key or any children. */
hasKeyOrChildren(key: Iterable<string>): boolean {
let node = this.root;
for (const segment of key) {
const next = node.children?.get(segment);
if (!next) {
return false;
}
node = next;
}
return true;
}
}
class Node<T> {
public children?: Map<string, Node<T>>;
public value: T | typeof unset = unset;
}

View file

@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
import * as assert from 'assert';
suite('WellDefinedPrefixTree', () => {
let tree: WellDefinedPrefixTree<number>;
setup(() => {
tree = new WellDefinedPrefixTree<number>();
});
test('find', () => {
const key1 = ['foo', 'bar'];
const key2 = ['foo', 'baz'];
tree.insert(key1, 42);
tree.insert(key2, 43);
assert.strictEqual(tree.find(key1), 42);
assert.strictEqual(tree.find(key2), 43);
assert.strictEqual(tree.find(['foo', 'baz', 'bop']), undefined);
assert.strictEqual(tree.find(['foo']), undefined);
});
test('hasParentOfKey', () => {
const key = ['foo', 'bar'];
tree.insert(key, 42);
assert.strictEqual(tree.hasKeyOrParent(['foo', 'bar', 'baz']), true);
assert.strictEqual(tree.hasKeyOrParent(['foo', 'bar']), true);
assert.strictEqual(tree.hasKeyOrParent(['foo']), false);
assert.strictEqual(tree.hasKeyOrParent(['baz']), false);
});
test('hasKeyOrChildren', () => {
const key = ['foo', 'bar'];
tree.insert(key, 42);
assert.strictEqual(tree.hasKeyOrChildren([]), true);
assert.strictEqual(tree.hasKeyOrChildren(['foo']), true);
assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar']), true);
assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar', 'baz']), false);
});
});

View file

@ -18,6 +18,8 @@ import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResu
import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { extHostNamedCustomer, IExtHostContext } 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 {
@ -55,11 +57,19 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
/**
* @inheritdoc
*/
$markTestRetired(testId: string): void {
$markTestRetired(testIds: string[] | undefined): void {
let tree: WellDefinedPrefixTree<undefined> | undefined;
if (testIds) {
tree = new WellDefinedPrefixTree();
for (const id of testIds) {
tree.insert(TestId.fromString(id).path, undefined);
}
}
for (const result of this.resultService.results) {
// all non-live results are already entirely outdated
if (result instanceof LiveTestResult) {
result.markRetired(testId);
result.markRetired(tree);
}
}
}

View file

@ -2514,7 +2514,7 @@ export interface MainThreadTestingShape {
/** Signals that an extension-provided test run finished. */
$finishedExtensionTestRun(runId: string): void;
/** Marks a test (or controller) as retired in all results. */
$markTestRetired(testId: string): void;
$markTestRetired(testIds: string[] | undefined): void;
}
// --- proxy identifiers

View file

@ -29,7 +29,6 @@ import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants';
import { TestId, TestIdPathParts, TestPosition } from 'vs/workbench/contrib/testing/common/testId';
import { InvalidTestItemError } from 'vs/workbench/contrib/testing/common/testItemCollection';
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';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import type * as vscode from 'vscode';
interface ControllerInfo {
@ -141,10 +140,11 @@ export class ExtHostTesting implements ExtHostTestingShape {
return this.runTracker.createTestRun(controllerId, collection, request, name, persist);
},
invalidateTestResults: items => {
checkProposedApiEnabled(extension, 'testInvalidateResults');
for (const item of items instanceof Array ? items : [items]) {
const id = item ? TestId.fromExtHostTestItem(item, controllerId).toString() : controllerId;
this.proxy.$markTestRetired(id);
if (items === undefined) {
this.proxy.$markTestRetired(undefined);
} else {
const itemsArr = items instanceof Array ? items : [items];
this.proxy.$markTestRetired(itemsArr.map(i => TestId.fromExtHostTestItem(i!, controllerId).toString()));
}
},
set resolveHandler(fn) {

View file

@ -102,6 +102,7 @@ export class TestId {
/**
* Gets whether maybeChild is a child of maybeParent.
* todo@connor4312: review usages of this to see if using the WellDefinedPrefixTree is better
*/
public static isChild(maybeParent: string, maybeChild: string) {
return maybeChild.startsWith(maybeParent) && maybeChild[maybeParent.length] === TestIdPathParts.Delimiter;
@ -109,6 +110,7 @@ export class TestId {
/**
* Compares the position of the two ID strings.
* todo@connor4312: review usages of this to see if using the WellDefinedPrefixTree is better
*/
public static compare(a: string, b: string) {
if (a === b) {

View file

@ -8,6 +8,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { Emitter, Event } from 'vs/base/common/event';
import { Lazy } from 'vs/base/common/lazy';
import { language } from 'vs/base/common/platform';
import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree';
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
import { localize } from 'vs/nls';
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
@ -233,6 +234,7 @@ export class LiveTestResult implements ITestResult {
private readonly newTaskEmitter = new Emitter<number>();
private readonly endTaskEmitter = new Emitter<number>();
private readonly changeEmitter = new Emitter<TestResultItemChange>();
/** todo@connor4312: convert to a WellDefinedPrefixTree */
private readonly testById = new Map<string, TestResultItemWithChildren>();
private testMarkerCounter = 0;
private _completedAt?: number;
@ -436,9 +438,9 @@ export class LiveTestResult implements ITestResult {
/**
* Marks the test and all of its children in the run as retired.
*/
public markRetired(testId: string) {
public markRetired(testIds: WellDefinedPrefixTree<undefined> | undefined) {
for (const [id, test] of this.testById) {
if (!test.retired && id === testId || TestId.isChild(testId, id)) {
if (!test.retired && (!testIds || testIds.hasKeyOrParent(TestId.fromString(id).path))) {
test.retired = true;
this.changeEmitter.fire({ reason: TestResultItemChangeReason.ComputedStateChange, item: test, result: this });
}

View file

@ -86,7 +86,6 @@ export const allApiProposals = Object.freeze({
terminalDimensions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalDimensions.d.ts',
terminalQuickFixProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalQuickFixProvider.d.ts',
testCoverage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testCoverage.d.ts',
testInvalidateResults: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testInvalidateResults.d.ts',
testObserver: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts',
textSearchProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textSearchProvider.d.ts',
timeline: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.timeline.d.ts',

View file

@ -16304,6 +16304,24 @@ declare module 'vscode' {
*/
createTestItem(id: string, label: string, uri?: Uri): TestItem;
/**
* Marks an item's results as being outdated. This is commonly called when
* code or configuration changes and previous results should no longer
* be considered relevant. The same logic used to mark results as outdated
* may be used to drive {@link TestRunRequest.continuous continuous test runs}.
*
* If an item is passed to this method, test results for the item and all of
* its children will be marked as outdated. If no item is passed, then all
* test owned by the TestController will be marked as outdated.
*
* Any test runs started before the moment this method is called, including
* runs which may still be ongoing, will be marked as outdated and deprioritized
* in the editor's UI.
*
* @param item Item to mark as outdated. If undefined, all the controller's items are marked outdated.
*/
invalidateTestResults(items?: TestItem | readonly TestItem[]): void;
/**
* Unregisters the test controller, disposing of its associated tests
* and unpersisted results.

View file

@ -1,30 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// https://github.com/microsoft/vscode/issues/134970
declare module 'vscode' {
export interface TestController {
/**
* Marks an item's results as being outdated. This is commonly called when
* code or configuration changes and previous results should no longer
* be considered relevant. The same logic used to mark results as outdated
* may be used to drive {@link TestRunRequest.continuous continuous test runs}.
*
* If an item is passed to this method, test results for the item and all of
* its children will be marked as outdated. If no item is passed, then all
* test owned by the TestController will be marked as outdated.
*
* Any test runs started before the moment this method is called, including
* runs which may still be ongoing, will be marked as outdated and deprioritized
* in the editor's UI.
*
* @param item Item to mark as outdated. If undefined, all the controller's items are marked outdated.
*/
invalidateTestResults(items?: TestItem | readonly TestItem[]): void;
}
}