mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 13:00:58 +00:00
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:
parent
3d1f7201ad
commit
f82185934a
|
@ -46,7 +46,6 @@
|
|||
"treeItemCheckbox",
|
||||
"treeViewActiveItem",
|
||||
"treeViewReveal",
|
||||
"testInvalidateResults",
|
||||
"workspaceTrust",
|
||||
"telemetry",
|
||||
"windowActivity",
|
||||
|
|
88
src/vs/base/common/prefixTree.ts
Normal file
88
src/vs/base/common/prefixTree.ts
Normal 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;
|
||||
}
|
47
src/vs/base/test/common/prefixTree.test.ts
Normal file
47
src/vs/base/test/common/prefixTree.test.ts
Normal 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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
18
src/vscode-dts/vscode.d.ts
vendored
18
src/vscode-dts/vscode.d.ts
vendored
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue