mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
testing: improve test children API
This changeset results from the discussion in and fixes #126987. Migration for these changes should take about 15-20 minutes. - `createTestItem` no longer takes a parent. Instead, it creates a free- floating test item, which can be added as a child of a parent. - The `TestItem.children` is now a `TestItemCollection`, a set-like interface that also allows replacing items (intelligently diffing them internally) wholesale. This removes the need for the "generation counter" used in samples previously. - There is no longer a `root` on the test controller, but instead an `items` property which is the same `TestItemCollection` - The `tests` in the `TestRunRequest` has been replaced with an `include` property. If undefined, the extension should run all tests. (Since there is no longer a root to reference). Here's some example migrations: - https://github.com/microsoft/vscode-extension-samples/commit/3fad3d66c110107946f827daedd7759cad7c4358 - https://github.com/microsoft/vscode-selfhost-test-provider/commit/3aff74631605dfd4120b98014f54ff744f292fe0
This commit is contained in:
Vendored
+1
@@ -198,6 +198,7 @@
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch VS Code Internal",
|
||||
"trace": true,
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/scripts/code.bat"
|
||||
},
|
||||
|
||||
Vendored
+53
-38
@@ -1788,6 +1788,16 @@ declare module 'vscode' {
|
||||
*/
|
||||
export function createTestObserver(): TestObserver;
|
||||
|
||||
/**
|
||||
* Creates a new managed {@link TestItem} instance. It can be added into
|
||||
* the {@link TestItem.children} of an existing item, or into the
|
||||
* {@link TestController.items}.
|
||||
* @param id Unique identifier for the TestItem.
|
||||
* @param label Human-readable label of the test item.
|
||||
* @param uri URI this TestItem is associated with. May be a file or directory.
|
||||
*/
|
||||
export function createTestItem(id: string, label: string, uri?: Uri): TestItem;
|
||||
|
||||
/**
|
||||
* List of test results stored by the editor, sorted in descending
|
||||
* order by their `completedAt` time.
|
||||
@@ -1937,8 +1947,8 @@ declare module 'vscode' {
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* Root test item. Tests in the workspace should be added as children of
|
||||
* the root. The extension controls when to add these, although the
|
||||
* Available test items. Tests in the workspace should be added in this
|
||||
* collection. The extension controls when to add these, although the
|
||||
* editor may request children using the {@link resolveChildrenHandler},
|
||||
* and the extension should add tests for a file when
|
||||
* {@link vscode.workspace.onDidOpenTextDocument} fires in order for
|
||||
@@ -1948,9 +1958,7 @@ declare module 'vscode' {
|
||||
* as files change. See {@link resolveChildrenHandler} for details around
|
||||
* for the lifecycle of watches.
|
||||
*/
|
||||
// todo@API a little weird? what is its label, id, busy state etc? Can I dispose this?
|
||||
// todo@API allow createTestItem-calls without parent and simply treat them as root (similar to createSourceControlResourceGroup)
|
||||
readonly root: TestItem;
|
||||
readonly items: TestItemCollection;
|
||||
|
||||
/**
|
||||
* Creates a configuration used for running tests. Extensions must create
|
||||
@@ -1962,28 +1970,11 @@ declare module 'vscode' {
|
||||
*/
|
||||
createRunConfiguration(label: string, group: TestRunConfigurationGroup, runHandler: TestRunHandler, isDefault?: boolean): TestRunConfiguration;
|
||||
|
||||
/**
|
||||
* Creates a new managed {@link TestItem} instance as a child of this
|
||||
* one.
|
||||
* @param id Unique identifier for the TestItem.
|
||||
* @param label Human-readable label of the test item.
|
||||
* @param parent Parent of the item. This is required; top-level items
|
||||
* should be created as children of the {@link root}.
|
||||
* @param uri URI this TestItem is associated with. May be a file or directory.
|
||||
* @param data Custom data to be stored in {@link TestItem.data}
|
||||
*/
|
||||
createTestItem(
|
||||
id: string,
|
||||
label: string,
|
||||
parent: TestItem,
|
||||
uri?: Uri,
|
||||
): TestItem;
|
||||
|
||||
/**
|
||||
* A function provided by the extension that the editor may call to request
|
||||
* children of a test item, if the {@link TestItem.canExpand} is `true`.
|
||||
* When called, the item should discover children and call
|
||||
* {@link TestController.createTestItem} as children are discovered.
|
||||
* {@link vscode.test.createTestItem} as children are discovered.
|
||||
*
|
||||
* The item in the explorer will automatically be marked as "busy" until
|
||||
* the function returns or the returned thenable resolves.
|
||||
@@ -2030,11 +2021,12 @@ declare module 'vscode' {
|
||||
*/
|
||||
export class TestRunRequest {
|
||||
/**
|
||||
* Array of specific tests to run. The controllers should run all of the
|
||||
* given tests and all children of the given tests, excluding any tests
|
||||
* that appear in {@link TestRunRequest.exclude}.
|
||||
* Filter for specific tests to run. If given, the extension should run all
|
||||
* of the given tests and all children of the given tests, excluding
|
||||
* any tests that appear in {@link TestRunRequest.exclude}. If this is
|
||||
* not given, then the extension should simply run all tests.
|
||||
*/
|
||||
tests: TestItem[];
|
||||
include?: TestItem[];
|
||||
|
||||
/**
|
||||
* An array of tests the user has marked as excluded in the editor. May be
|
||||
@@ -2051,11 +2043,11 @@ declare module 'vscode' {
|
||||
configuration?: TestRunConfiguration;
|
||||
|
||||
/**
|
||||
* @param tests Array of specific tests to run.
|
||||
* @param tests Array of specific tests to run, or undefined to run all tests
|
||||
* @param exclude Tests to exclude from the run
|
||||
* @param configuration The run configuration used for this request.
|
||||
*/
|
||||
constructor(tests: readonly TestItem[], exclude?: readonly TestItem[], configuration?: TestRunConfiguration);
|
||||
constructor(include?: readonly TestItem[], exclude?: readonly TestItem[], configuration?: TestRunConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2115,6 +2107,34 @@ declare module 'vscode' {
|
||||
end(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of test items, found in {@link TestItem.children} and
|
||||
* {@link TestController.items}.
|
||||
*/
|
||||
export interface TestItemCollection {
|
||||
/**
|
||||
* A read-only array of all the test items children. Can be retrieved, or
|
||||
* set in order to replace children in the collection.
|
||||
*/
|
||||
all: readonly TestItem[];
|
||||
|
||||
/**
|
||||
* Adds the test item to the children. If an item with the same ID already
|
||||
* exists, it'll be replaced.
|
||||
*/
|
||||
add(item: TestItem): void;
|
||||
|
||||
/**
|
||||
* Removes the a single test item from the collection.
|
||||
*/
|
||||
remove(itemId: string): void;
|
||||
|
||||
/**
|
||||
* Efficiently gets a test item by ID, if it exists, in the children.
|
||||
*/
|
||||
get(itemId: string): TestItem | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A test item is an item shown in the "test explorer" view. It encompasses
|
||||
* both a suite and a test, since they have almost or identical capabilities.
|
||||
@@ -2135,12 +2155,12 @@ declare module 'vscode' {
|
||||
/**
|
||||
* A mapping of children by ID to the associated TestItem instances.
|
||||
*/
|
||||
//todo@API use array over es6-map
|
||||
readonly children: ReadonlyMap<string, TestItem>;
|
||||
readonly children: TestItemCollection;
|
||||
|
||||
/**
|
||||
* The parent of this item, given in {@link TestController.createTestItem}.
|
||||
* This is undefined only for the {@link TestController.root}.
|
||||
* The parent of this item, given in {@link vscode.test.createTestItem}.
|
||||
* This is undefined top-level items in the `TestController`, and for
|
||||
* items that aren't yet assigned to a parent.
|
||||
*/
|
||||
readonly parent?: TestItem;
|
||||
|
||||
@@ -2193,11 +2213,6 @@ declare module 'vscode' {
|
||||
* Extensions should generally not override this method.
|
||||
*/
|
||||
invalidateResults(): void;
|
||||
|
||||
/**
|
||||
* Removes the test and its children from the tree.
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -346,6 +346,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostTesting.createTestController(provider, label);
|
||||
},
|
||||
createTestItem(id, label, uri) {
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostTesting.createTestItem(id, label, uri);
|
||||
},
|
||||
createTestObserver() {
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostTesting.createTestObserver();
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mapFind } from 'vs/base/common/arrays';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
@@ -19,19 +18,22 @@ import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { ExtHostTestingShape, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
|
||||
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
||||
import { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi';
|
||||
import * as Convert from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { TestItemImpl, TestRunConfigurationGroup, TestRunRequest } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { TestRunConfigurationGroup, TestRunRequest } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { SingleUseTestCollection, TestPosition } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
|
||||
import { AbstractIncrementalTestCollection, CoverageDetails, IFileCoverage, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestIdWithSrc, ITestItem, RunTestForControllerRequest, TestRunConfigurationBitset, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import type * as vscode from 'vscode';
|
||||
|
||||
interface ControllerInfo {
|
||||
controller: vscode.TestController,
|
||||
configurations: Map<number, vscode.TestRunConfiguration>,
|
||||
collection: SingleUseTestCollection,
|
||||
}
|
||||
|
||||
export class ExtHostTesting implements ExtHostTestingShape {
|
||||
private readonly resultsChangedEmitter = new Emitter<void>();
|
||||
private readonly controllers = new Map</* controller ID */ string, {
|
||||
controller: vscode.TestController,
|
||||
configurations: Map<number, vscode.TestRunConfiguration>,
|
||||
collection: SingleUseTestCollection,
|
||||
}>();
|
||||
private readonly controllers = new Map</* controller ID */ string, ControllerInfo>();
|
||||
private readonly proxy: MainThreadTestingShape;
|
||||
private readonly runTracker: TestRunCoordinator;
|
||||
private readonly observer: TestObservers;
|
||||
@@ -61,12 +63,13 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
const proxy = this.proxy;
|
||||
|
||||
const controller: vscode.TestController = {
|
||||
root: collection.root,
|
||||
items: collection.root.children,
|
||||
get label() {
|
||||
return label;
|
||||
},
|
||||
set label(value: string) {
|
||||
label = value;
|
||||
collection.root.label = value;
|
||||
proxy.$updateControllerLabel(controllerId, label);
|
||||
},
|
||||
get id() {
|
||||
@@ -85,14 +88,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
return config;
|
||||
},
|
||||
createTestRun: (request, name, persist = true) => {
|
||||
return this.runTracker.createTestRun(controllerId, request, name, persist);
|
||||
},
|
||||
createTestItem(id: string, label: string, parent: vscode.TestItem, uri: vscode.Uri, data?: unknown) {
|
||||
if (!(parent instanceof TestItemImpl)) {
|
||||
throw new Error(`The "parent" passed in for TestItem ${id} is invalid`);
|
||||
}
|
||||
|
||||
return new TestItemImpl(id, label, uri, data, parent);
|
||||
return this.runTracker.createTestRun(controllerId, collection, request, name, persist);
|
||||
},
|
||||
set resolveChildrenHandler(fn) {
|
||||
collection.resolveHandler = fn;
|
||||
@@ -108,10 +104,14 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
},
|
||||
};
|
||||
|
||||
// back compat:
|
||||
(controller as any).createTestITem = this.createTestItem.bind(this);
|
||||
|
||||
proxy.$registerTestController(controllerId, label);
|
||||
disposable.add(toDisposable(() => proxy.$unregisterTestController(controllerId)));
|
||||
|
||||
this.controllers.set(controllerId, { controller, collection, configurations });
|
||||
const info: ControllerInfo = { controller, collection, configurations };
|
||||
this.controllers.set(controllerId, info);
|
||||
disposable.add(toDisposable(() => this.controllers.delete(controllerId)));
|
||||
|
||||
disposable.add(collection.onDidGenerateDiff(diff => proxy.$publishDiff(controllerId, diff)));
|
||||
@@ -119,6 +119,13 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
return controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements vscode.test.createTestItem
|
||||
*/
|
||||
public createTestItem(id: string, label: string, uri?: vscode.Uri) {
|
||||
return new TestItemImpl(id, label, uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements vscode.test.createTestObserver
|
||||
*/
|
||||
@@ -136,26 +143,19 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
throw new Error('The request passed to `vscode.test.runTests` must include a configuration');
|
||||
}
|
||||
|
||||
if (!req.tests.length) {
|
||||
return;
|
||||
const controller = this.controllers.get(config.controllerId);
|
||||
if (!controller) {
|
||||
throw new Error('Controller not found');
|
||||
}
|
||||
|
||||
const testListToProviders = (tests: ReadonlyArray<vscode.TestItem>) =>
|
||||
tests
|
||||
.map(this.getInternalTestForReference, this)
|
||||
.filter(isDefined)
|
||||
.map(t => ({ controllerId: t.controllerId, testId: t.item.extId, configId: config }));
|
||||
|
||||
await this.proxy.$runTests({
|
||||
targets: [{
|
||||
testIds: req.tests.map(t => t.id),
|
||||
testIds: req.include?.map(t => t.id) ?? [controller.collection.root.id],
|
||||
profileGroup: configGroupToBitset[config.group],
|
||||
profileId: config.configId,
|
||||
controllerId: config.controllerId,
|
||||
}],
|
||||
exclude: req.exclude
|
||||
? testListToProviders(req.exclude).map(t => ({ testId: t.testId, controllerId: t.controllerId }))
|
||||
: undefined,
|
||||
exclude: req.exclude?.map(t => ({ testId: t.id, controllerId: config.controllerId })),
|
||||
}, token);
|
||||
}
|
||||
|
||||
@@ -252,7 +252,11 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
configuration,
|
||||
);
|
||||
|
||||
const tracker = this.runTracker.prepareForMainThreadTestRun(publicReq, TestRunDto.fromInternal(req), token);
|
||||
const tracker = this.runTracker.prepareForMainThreadTestRun(
|
||||
publicReq,
|
||||
TestRunDto.fromInternal(req, lookup.collection),
|
||||
token,
|
||||
);
|
||||
|
||||
try {
|
||||
await configuration.runHandler(publicReq, token);
|
||||
@@ -275,14 +279,6 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||
this.runTracker.cancelRunById(runId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the internal test item associated with the reference from the extension.
|
||||
*/
|
||||
private getInternalTestForReference(test: vscode.TestItem) {
|
||||
return mapFind(this.controllers.values(), ({ collection }) => collection.getTestByReference(test))
|
||||
?? this.observer.getMirroredTestDataByReference(test);
|
||||
}
|
||||
}
|
||||
|
||||
class TestRunTracker extends Disposable {
|
||||
@@ -396,7 +392,7 @@ export class TestRunCoordinator {
|
||||
/**
|
||||
* Implements the public `createTestRun` API.
|
||||
*/
|
||||
public createTestRun(controllerId: string, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun {
|
||||
public createTestRun(controllerId: string, collection: SingleUseTestCollection, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRun {
|
||||
const existing = this.tracked.get(request);
|
||||
if (existing) {
|
||||
return existing.createRun(name);
|
||||
@@ -404,14 +400,14 @@ export class TestRunCoordinator {
|
||||
|
||||
// If there is not an existing tracked extension for the request, start
|
||||
// a new, detached session.
|
||||
const dto = TestRunDto.fromPublic(controllerId, request);
|
||||
const dto = TestRunDto.fromPublic(controllerId, collection, request);
|
||||
const config = tryGetConfigFromTestRunReq(request);
|
||||
this.proxy.$startedExtensionTestRun({
|
||||
controllerId,
|
||||
config: config && { group: configGroupToBitset[config.group], id: config.configId },
|
||||
exclude: request.exclude?.map(t => t.id) ?? [],
|
||||
id: dto.id,
|
||||
tests: request.tests.map(t => t.id),
|
||||
include: request.include?.map(t => t.id) ?? [collection.root.id],
|
||||
persist
|
||||
});
|
||||
|
||||
@@ -441,41 +437,44 @@ const tryGetConfigFromTestRunReq = (request: vscode.TestRunRequest) => {
|
||||
};
|
||||
|
||||
export class TestRunDto {
|
||||
public static fromPublic(controllerId: string, request: vscode.TestRunRequest) {
|
||||
public static fromPublic(controllerId: string, collection: SingleUseTestCollection, request: vscode.TestRunRequest) {
|
||||
return new TestRunDto(
|
||||
controllerId,
|
||||
generateUuid(),
|
||||
new Set(request.tests.map(t => t.id)),
|
||||
request.include && new Set(request.include.map(t => t.id)),
|
||||
new Set(request.exclude?.map(t => t.id) ?? Iterable.empty()),
|
||||
collection,
|
||||
);
|
||||
}
|
||||
|
||||
public static fromInternal(request: RunTestForControllerRequest) {
|
||||
public static fromInternal(request: RunTestForControllerRequest, collection: SingleUseTestCollection) {
|
||||
return new TestRunDto(
|
||||
request.controllerId,
|
||||
request.runId,
|
||||
new Set(request.testIds),
|
||||
request.testIds.includes(collection.root.id) ? undefined : new Set(request.testIds),
|
||||
new Set(request.excludeExtIds),
|
||||
collection,
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly controllerId: string,
|
||||
public readonly id: string,
|
||||
private readonly include: ReadonlySet<string>,
|
||||
private readonly include: ReadonlySet<string> | undefined,
|
||||
private readonly exclude: ReadonlySet<string>,
|
||||
public readonly colllection: SingleUseTestCollection,
|
||||
) { }
|
||||
|
||||
public isIncluded(test: vscode.TestItem) {
|
||||
for (let t: vscode.TestItem | undefined = test; t; t = t.parent) {
|
||||
if (this.include.has(t.id)) {
|
||||
if (this.include?.has(t.id)) {
|
||||
return true;
|
||||
} else if (this.exclude.has(t.id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return this.include === undefined; // default to true if running all tests with include=undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -624,6 +623,12 @@ class TestRunImpl implements vscode.TestRun {
|
||||
test = test.parent;
|
||||
}
|
||||
|
||||
const root = this.#req.colllection.root;
|
||||
if (!sent.has(root.id)) {
|
||||
sent.add(root.id);
|
||||
chain.unshift(Convert.TestItem.from(root));
|
||||
}
|
||||
|
||||
this.#proxy.$addTestsToRun(this.#req.controllerId, this.#req.id, chain);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,50 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { TestItemImpl } from 'vs/workbench/api/common/extHostTypes';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export const enum ExtHostTestItemEventType {
|
||||
NewChild,
|
||||
Disposed,
|
||||
export const enum ExtHostTestItemEventOp {
|
||||
Upsert,
|
||||
RemoveChild,
|
||||
Invalidated,
|
||||
SetProp,
|
||||
Bulk,
|
||||
}
|
||||
|
||||
export interface ITestItemUpsertChild {
|
||||
op: ExtHostTestItemEventOp.Upsert;
|
||||
item: TestItemImpl;
|
||||
}
|
||||
|
||||
export interface ITestItemRemoveChild {
|
||||
op: ExtHostTestItemEventOp.RemoveChild;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ITestItemInvalidated {
|
||||
op: ExtHostTestItemEventOp.Invalidated;
|
||||
}
|
||||
|
||||
export interface ITestItemSetProp {
|
||||
op: ExtHostTestItemEventOp.SetProp;
|
||||
key: keyof vscode.TestItem;
|
||||
value: any;
|
||||
}
|
||||
export interface ITestItemBulkReplace {
|
||||
op: ExtHostTestItemEventOp.Bulk;
|
||||
ops: (ITestItemUpsertChild | ITestItemRemoveChild)[];
|
||||
}
|
||||
|
||||
export type ExtHostTestItemEvent =
|
||||
| [evt: ExtHostTestItemEventType.NewChild, item: TestItemImpl]
|
||||
| [evt: ExtHostTestItemEventType.Disposed]
|
||||
| [evt: ExtHostTestItemEventType.Invalidated]
|
||||
| [evt: ExtHostTestItemEventType.SetProp, key: keyof vscode.TestItem, value: any];
|
||||
| ITestItemUpsertChild
|
||||
| ITestItemRemoveChild
|
||||
| ITestItemInvalidated
|
||||
| ITestItemSetProp
|
||||
| ITestItemBulkReplace;
|
||||
|
||||
export interface IExtHostTestItemApi {
|
||||
children: Map<string, TestItemImpl>;
|
||||
bus: Emitter<ExtHostTestItemEvent>;
|
||||
parent?: TestItemImpl;
|
||||
listener?: (evt: ExtHostTestItemEvent) => void;
|
||||
}
|
||||
|
||||
const eventPrivateApis = new WeakMap<TestItemImpl, IExtHostTestItemApi>();
|
||||
@@ -35,9 +59,216 @@ const eventPrivateApis = new WeakMap<TestItemImpl, IExtHostTestItemApi>();
|
||||
export const getPrivateApiFor = (impl: TestItemImpl) => {
|
||||
let api = eventPrivateApis.get(impl);
|
||||
if (!api) {
|
||||
api = { children: new Map(), bus: new Emitter() };
|
||||
api = {};
|
||||
eventPrivateApis.set(impl, api);
|
||||
}
|
||||
|
||||
return api;
|
||||
};
|
||||
|
||||
const testItemPropAccessor = <K extends keyof vscode.TestItem>(
|
||||
api: IExtHostTestItemApi,
|
||||
key: K,
|
||||
defaultValue: vscode.TestItem[K],
|
||||
equals: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean
|
||||
) => {
|
||||
let value = defaultValue;
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get() {
|
||||
return value;
|
||||
},
|
||||
set(newValue: vscode.TestItem[K]) {
|
||||
if (!equals(value, newValue)) {
|
||||
value = newValue;
|
||||
api.listener?.({ op: ExtHostTestItemEventOp.SetProp, key, value: newValue });
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
type WritableProps = Pick<vscode.TestItem, 'range' | 'label' | 'description' | 'canResolveChildren' | 'busy' | 'error'>;
|
||||
|
||||
const strictEqualComparator = <T>(a: T, b: T) => a === b;
|
||||
|
||||
const propComparators: { [K in keyof Required<WritableProps>]: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean } = {
|
||||
range: (a, b) => {
|
||||
if (a === b) { return true; }
|
||||
if (!a || !b) { return false; }
|
||||
return a.isEqual(b);
|
||||
},
|
||||
label: strictEqualComparator,
|
||||
description: strictEqualComparator,
|
||||
busy: strictEqualComparator,
|
||||
error: strictEqualComparator,
|
||||
canResolveChildren: strictEqualComparator
|
||||
};
|
||||
|
||||
const writablePropKeys = Object.keys(propComparators) as (keyof Required<WritableProps>)[];
|
||||
|
||||
const makePropDescriptors = (api: IExtHostTestItemApi, label: string): { [K in keyof Required<WritableProps>]: PropertyDescriptor } => ({
|
||||
range: testItemPropAccessor(api, 'range', undefined, propComparators.range),
|
||||
label: testItemPropAccessor(api, 'label', label, propComparators.label),
|
||||
description: testItemPropAccessor(api, 'description', undefined, propComparators.description),
|
||||
canResolveChildren: testItemPropAccessor(api, 'canResolveChildren', false, propComparators.canResolveChildren),
|
||||
busy: testItemPropAccessor(api, 'busy', false, propComparators.busy),
|
||||
error: testItemPropAccessor(api, 'error', undefined, propComparators.error),
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a partial test item containing the writable properties in B that
|
||||
* are different from A.
|
||||
*/
|
||||
export const diffTestItems = (a: vscode.TestItem, b: vscode.TestItem) => {
|
||||
const output = new Map<keyof WritableProps, unknown>();
|
||||
for (const key of writablePropKeys) {
|
||||
const cmp = propComparators[key] as (a: unknown, b: unknown) => boolean;
|
||||
if (!cmp(a[key], b[key])) {
|
||||
output.set(key, b[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
export class DuplicateTestItemError extends Error {
|
||||
constructor(id: string) {
|
||||
super(`Attempted to insert a duplicate test item ID ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidTestItemError extends Error {
|
||||
constructor(id: string) {
|
||||
super(`TestItem with ID "${id}" is invalid. Make sure to create it from the createTestItem method.`);
|
||||
}
|
||||
}
|
||||
|
||||
export const createTestItemCollection = (owningItem: TestItemImpl):
|
||||
vscode.TestItemCollection & { toJSON(): readonly vscode.TestItem[] } => {
|
||||
const api = getPrivateApiFor(owningItem);
|
||||
let all: readonly TestItemImpl[] | undefined;
|
||||
let mapped = new Map<string, TestItemImpl>();
|
||||
|
||||
return {
|
||||
/** @inheritdoc */
|
||||
get all() {
|
||||
if (!all) {
|
||||
all = Object.freeze([...mapped.values()]);
|
||||
}
|
||||
|
||||
return all;
|
||||
},
|
||||
|
||||
/** @inheritdoc */
|
||||
set all(items: readonly vscode.TestItem[]) {
|
||||
const newMapped = new Map<string, TestItemImpl>();
|
||||
const toDelete = new Set(mapped.keys());
|
||||
const bulk: ITestItemBulkReplace = { op: ExtHostTestItemEventOp.Bulk, ops: [] };
|
||||
|
||||
for (const item of items) {
|
||||
if (!(item instanceof TestItemImpl)) {
|
||||
throw new InvalidTestItemError(item.id);
|
||||
}
|
||||
|
||||
if (newMapped.has(item.id)) {
|
||||
throw new DuplicateTestItemError(item.id);
|
||||
}
|
||||
|
||||
newMapped.set(item.id, item);
|
||||
toDelete.delete(item.id);
|
||||
bulk.ops.push({ op: ExtHostTestItemEventOp.Upsert, item });
|
||||
}
|
||||
|
||||
for (const id of toDelete.keys()) {
|
||||
bulk.ops.push({ op: ExtHostTestItemEventOp.RemoveChild, id });
|
||||
}
|
||||
|
||||
api.listener?.(bulk);
|
||||
|
||||
// important mutations come after firing, so if an error happens no
|
||||
// changes will be "saved":
|
||||
mapped = newMapped;
|
||||
all = undefined;
|
||||
},
|
||||
|
||||
|
||||
/** @inheritdoc */
|
||||
add(item: vscode.TestItem) {
|
||||
if (!(item instanceof TestItemImpl)) {
|
||||
throw new InvalidTestItemError(item.id);
|
||||
}
|
||||
|
||||
mapped.set(item.id, item);
|
||||
all = undefined;
|
||||
api.listener?.({ op: ExtHostTestItemEventOp.Upsert, item });
|
||||
},
|
||||
|
||||
/** @inheritdoc */
|
||||
remove(id: string) {
|
||||
if (mapped.delete(id)) {
|
||||
all = undefined;
|
||||
api.listener?.({ op: ExtHostTestItemEventOp.RemoveChild, id });
|
||||
}
|
||||
},
|
||||
|
||||
/** @inheritdoc */
|
||||
get(itemId: string) {
|
||||
return mapped.get(itemId);
|
||||
},
|
||||
|
||||
/** JSON serialization function. */
|
||||
toJSON() {
|
||||
return this.all;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export class TestItemImpl implements vscode.TestItem {
|
||||
public readonly id!: string;
|
||||
public readonly uri!: vscode.Uri | undefined;
|
||||
public readonly children!: vscode.TestItemCollection;
|
||||
public readonly parent!: TestItemImpl | undefined;
|
||||
|
||||
public range!: vscode.Range | undefined;
|
||||
public description!: string | undefined;
|
||||
public label!: string;
|
||||
public error!: string | vscode.MarkdownString;
|
||||
public busy!: boolean;
|
||||
public canResolveChildren!: boolean;
|
||||
|
||||
/**
|
||||
* Note that data is deprecated and here for back-compat only
|
||||
*/
|
||||
constructor(id: string, label: string, uri: vscode.Uri | undefined) {
|
||||
const api = getPrivateApiFor(this);
|
||||
|
||||
Object.defineProperties(this, {
|
||||
id: {
|
||||
value: id,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
},
|
||||
uri: {
|
||||
value: uri,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
},
|
||||
parent: {
|
||||
enumerable: false,
|
||||
get() { return api.parent; },
|
||||
},
|
||||
children: {
|
||||
value: createTestItemCollection(this),
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
},
|
||||
...makePropDescriptors(api, label),
|
||||
});
|
||||
}
|
||||
|
||||
/** @deprecated back compat */
|
||||
public invalidateResults() {
|
||||
getPrivateApiFor(this).listener?.({ op: ExtHostTestItemEventOp.Invalidated });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands';
|
||||
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
||||
import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook';
|
||||
import { getPrivateApiFor, TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi';
|
||||
import { SaveReason } from 'vs/workbench/common/editor';
|
||||
import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
|
||||
@@ -1665,7 +1666,6 @@ export namespace TestItem {
|
||||
label: item.label,
|
||||
uri: URI.revive(item.uri),
|
||||
range: Range.to(item.range || undefined),
|
||||
dispose: () => undefined,
|
||||
invalidateResults: () => undefined,
|
||||
canResolveChildren: false,
|
||||
busy: false,
|
||||
@@ -1673,17 +1673,19 @@ export namespace TestItem {
|
||||
};
|
||||
}
|
||||
|
||||
export function to(item: ITestItem, parent?: vscode.TestItem): types.TestItemImpl {
|
||||
const testItem = new types.TestItemImpl(item.extId, item.label, URI.revive(item.uri), undefined, parent);
|
||||
export function to(item: ITestItem): TestItemImpl {
|
||||
const testItem = new TestItemImpl(item.extId, item.label, URI.revive(item.uri));
|
||||
testItem.range = Range.to(item.range || undefined);
|
||||
testItem.description = item.description || undefined;
|
||||
return testItem;
|
||||
}
|
||||
|
||||
export function toItemFromContext(context: ITestItemContext): types.TestItemImpl {
|
||||
let node: types.TestItemImpl | undefined;
|
||||
export function toItemFromContext(context: ITestItemContext): TestItemImpl {
|
||||
let node: TestItemImpl | undefined;
|
||||
for (const test of context.tests) {
|
||||
node = to(test.item, node);
|
||||
const next = to(test.item);
|
||||
getPrivateApiFor(next).parent = node;
|
||||
node = next;
|
||||
}
|
||||
|
||||
return node!;
|
||||
|
||||
@@ -7,14 +7,13 @@ import { asArray, coalesceInPlace, equals } from 'vs/base/common/arrays';
|
||||
import { illegalArgument } from 'vs/base/common/errors';
|
||||
import { IRelativePattern } from 'vs/base/common/glob';
|
||||
import { MarkdownString as BaseMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { ReadonlyMapView, ResourceMap } from 'vs/base/common/map';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { Mimes, normalizeMimeType } from 'vs/base/common/mime';
|
||||
import { isArray, isStringArray } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files';
|
||||
import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { getPrivateApiFor, ExtHostTestItemEventType, IExtHostTestItemApi } from 'vs/workbench/api/common/extHostTestingPrivateApi';
|
||||
import { CellEditType, ICellPartialMetadataEdit, IDocumentMetadataEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import type * as vscode from 'vscode';
|
||||
|
||||
@@ -3302,130 +3301,21 @@ export enum TestMessageSeverity {
|
||||
Hint = 3
|
||||
}
|
||||
|
||||
const testItemPropAccessor = <K extends keyof vscode.TestItem>(
|
||||
api: IExtHostTestItemApi,
|
||||
key: K,
|
||||
defaultValue: vscode.TestItem[K],
|
||||
equals: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean
|
||||
) => {
|
||||
let value = defaultValue;
|
||||
return {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get() {
|
||||
return value;
|
||||
},
|
||||
set(newValue: vscode.TestItem[K]) {
|
||||
if (!equals(value, newValue)) {
|
||||
value = newValue;
|
||||
api.bus.fire([ExtHostTestItemEventType.SetProp, key, newValue]);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const strictEqualComparator = <T>(a: T, b: T) => a === b;
|
||||
const rangeComparator = (a: vscode.Range | undefined, b: vscode.Range | undefined) => {
|
||||
if (a === b) { return true; }
|
||||
if (!a || !b) { return false; }
|
||||
return a.isEqual(b);
|
||||
};
|
||||
|
||||
export enum TestRunConfigurationGroup {
|
||||
Run = 1,
|
||||
Debug = 2,
|
||||
Coverage = 3,
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class TestRunRequest implements vscode.TestRunRequest {
|
||||
constructor(
|
||||
public readonly tests: vscode.TestItem[],
|
||||
public readonly include?: vscode.TestItem[],
|
||||
public readonly exclude?: vscode.TestItem[] | undefined,
|
||||
public readonly configuration?: vscode.TestRunConfiguration,
|
||||
) { }
|
||||
}
|
||||
|
||||
export class TestItemImpl implements vscode.TestItem {
|
||||
public readonly id!: string;
|
||||
public readonly uri!: vscode.Uri | undefined;
|
||||
public readonly children!: ReadonlyMap<string, TestItemImpl>;
|
||||
public readonly parent!: TestItemImpl | undefined;
|
||||
|
||||
public range!: vscode.Range | undefined;
|
||||
public description!: string | undefined;
|
||||
public label!: string;
|
||||
public error!: string | vscode.MarkdownString;
|
||||
public busy!: boolean;
|
||||
public canResolveChildren!: boolean;
|
||||
|
||||
/**
|
||||
* Note that data is deprecated and here for back-compat only
|
||||
*/
|
||||
constructor(id: string, label: string, uri: vscode.Uri | undefined, public data: any, parent: vscode.TestItem | undefined) {
|
||||
const api = getPrivateApiFor(this);
|
||||
|
||||
Object.defineProperties(this, {
|
||||
id: {
|
||||
value: id,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
},
|
||||
uri: {
|
||||
value: uri,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
},
|
||||
parent: {
|
||||
enumerable: false,
|
||||
value: parent,
|
||||
writable: false,
|
||||
},
|
||||
children: {
|
||||
value: new ReadonlyMapView(api.children),
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
},
|
||||
range: testItemPropAccessor(api, 'range', undefined, rangeComparator),
|
||||
label: testItemPropAccessor(api, 'label', label, strictEqualComparator),
|
||||
description: testItemPropAccessor(api, 'description', undefined, strictEqualComparator),
|
||||
canResolveChildren: testItemPropAccessor(api, 'canResolveChildren', false, strictEqualComparator),
|
||||
busy: testItemPropAccessor(api, 'busy', false, strictEqualComparator),
|
||||
error: testItemPropAccessor(api, 'error', undefined, strictEqualComparator),
|
||||
});
|
||||
|
||||
if (parent) {
|
||||
if (!(parent instanceof TestItemImpl)) {
|
||||
throw new Error(`The "parent" passed in for TestItem ${id} is invalid`);
|
||||
}
|
||||
|
||||
const parentApi = getPrivateApiFor(parent);
|
||||
if (parentApi.children.has(id)) {
|
||||
throw new Error(`Attempted to insert a duplicate test item ID ${id}`);
|
||||
}
|
||||
|
||||
parentApi.children.set(id, this);
|
||||
parentApi.bus.fire([ExtHostTestItemEventType.NewChild, this]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated back compat */
|
||||
public invalidate() {
|
||||
return this.invalidateResults();
|
||||
}
|
||||
|
||||
public invalidateResults() {
|
||||
getPrivateApiFor(this).bus.fire([ExtHostTestItemEventType.Invalidated]);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this.parent) {
|
||||
getPrivateApiFor(this.parent).children.delete(this.id);
|
||||
}
|
||||
|
||||
getPrivateApiFor(this).bus.fire([ExtHostTestItemEventType.Disposed]);
|
||||
}
|
||||
}
|
||||
|
||||
@es5ClassCompat
|
||||
export class TestMessage implements vscode.TestMessage {
|
||||
public severity = TestMessageSeverity.Error;
|
||||
|
||||
@@ -14,8 +14,8 @@ export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, lo
|
||||
export const testingRunIcon = registerIcon('testing-run-icon', Codicon.run, localize('testingRunIcon', 'Icon of the "run test" action.'));
|
||||
export const testingRunAllIcon = registerIcon('testing-run-all-icon', Codicon.runAll, localize('testingRunAllIcon', 'Icon of the "run all tests" action.'));
|
||||
// todo: https://github.com/microsoft/vscode-codicons/issues/72
|
||||
export const testingDebugAllIcon = registerIcon('testing-debug-all-icon', Codicon.debugAlt, localize('testingDebugAllIcon', 'Icon of the "debug all tests" action.'));
|
||||
export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAlt, localize('testingDebugIcon', 'Icon of the "debug test" action.'));
|
||||
export const testingDebugAllIcon = registerIcon('testing-debug-all-icon', Codicon.debugAltSmall, localize('testingDebugAllIcon', 'Icon of the "debug all tests" action.'));
|
||||
export const testingDebugIcon = registerIcon('testing-debug-icon', Codicon.debugAltSmall, localize('testingDebugIcon', 'Icon of the "debug test" action.'));
|
||||
export const testingCancelIcon = registerIcon('testing-cancel-icon', Codicon.debugStop, localize('testingCancelIcon', 'Icon to cancel ongoing test runs.'));
|
||||
export const testingFilterIcon = registerIcon('testing-filter', Codicon.filter, localize('filterIcon', 'Icon for the \'Filter\' action in the testing view.'));
|
||||
export const testingAutorunIcon = registerIcon('testing-autorun', Codicon.debugRerun, localize('autoRunIcon', 'Icon for the \'Autorun\' toggle in the testing view.'));
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
import { Barrier, isThenable, RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { assertNever } from 'vs/base/common/types';
|
||||
import { ExtHostTestItemEvent, ExtHostTestItemEventType, getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi';
|
||||
import { diffTestItems, ExtHostTestItemEvent, ExtHostTestItemEventOp, getPrivateApiFor, TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi';
|
||||
import * as Convert from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { TestItemImpl } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { applyTestItemUpdate, InternalTestItem, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { applyTestItemUpdate, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
|
||||
type TestItemRaw = Convert.TestItem.Raw;
|
||||
|
||||
@@ -23,7 +21,9 @@ export interface IHierarchyProvider {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export interface OwnedCollectionTestItem extends InternalTestItem {
|
||||
export interface OwnedCollectionTestItem {
|
||||
expand: TestItemExpandState;
|
||||
parent: string | null;
|
||||
actual: TestItemImpl;
|
||||
/**
|
||||
* Number of levels of items below this one that are expanded. May be infinite.
|
||||
@@ -52,7 +52,7 @@ export const enum TestPosition {
|
||||
* for test trees. Internally it indexes tests by their extension ID in
|
||||
* a map.
|
||||
*/
|
||||
export class TestTree<T extends InternalTestItem> {
|
||||
export class TestTree<T extends OwnedCollectionTestItem> {
|
||||
private readonly map = new Map<string, T>();
|
||||
private readonly _roots = new Set<T>();
|
||||
public readonly roots: ReadonlySet<T> = this._roots;
|
||||
@@ -69,11 +69,11 @@ export class TestTree<T extends InternalTestItem> {
|
||||
* @throws if a duplicate item is inserted
|
||||
*/
|
||||
public add(test: T) {
|
||||
if (this.map.has(test.item.extId)) {
|
||||
throw new Error(`Attempted to insert a duplicate test item ID ${test.item.extId}`);
|
||||
if (this.map.has(test.actual.id)) {
|
||||
throw new Error(`Attempted to insert a duplicate test item ID ${test.actual.id}`);
|
||||
}
|
||||
|
||||
this.map.set(test.item.extId, test);
|
||||
this.map.set(test.actual.id, test);
|
||||
if (!test.parent) {
|
||||
this._roots.add(test);
|
||||
}
|
||||
@@ -150,12 +150,11 @@ export class TestTree<T extends InternalTestItem> {
|
||||
* @private
|
||||
*/
|
||||
export class SingleUseTestCollection extends Disposable {
|
||||
protected readonly testItemToInternal = new Map<TestItemRaw, OwnedCollectionTestItem>();
|
||||
private readonly debounceSendDiff = this._register(new RunOnceScheduler(() => this.flushDiff(), 200));
|
||||
private readonly diffOpEmitter = this._register(new Emitter<TestsDiff>());
|
||||
private _resolveHandler?: (item: TestItemRaw) => Promise<void> | void;
|
||||
|
||||
public readonly root = new TestItemImpl(`${this.controllerId}Root`, this.controllerId, undefined, undefined, undefined);
|
||||
public readonly root = new TestItemImpl(`${this.controllerId}Root`, this.controllerId, undefined);
|
||||
public readonly tree = new TestTree<OwnedCollectionTestItem>();
|
||||
protected diff: TestsDiff = [];
|
||||
|
||||
@@ -163,7 +162,7 @@ export class SingleUseTestCollection extends Disposable {
|
||||
private readonly controllerId: string,
|
||||
) {
|
||||
super();
|
||||
this.addItemInner(this.root, null);
|
||||
this.upsertItem(this.root, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +170,7 @@ export class SingleUseTestCollection extends Disposable {
|
||||
*/
|
||||
public set resolveHandler(handler: undefined | ((item: TestItemRaw) => void)) {
|
||||
this._resolveHandler = handler;
|
||||
for (const test of this.testItemToInternal.values()) {
|
||||
for (const test of this.tree) {
|
||||
this.updateExpandability(test);
|
||||
}
|
||||
}
|
||||
@@ -181,18 +180,6 @@ export class SingleUseTestCollection extends Disposable {
|
||||
*/
|
||||
public readonly onDidGenerateDiff = this.diffOpEmitter.event;
|
||||
|
||||
public get roots() {
|
||||
return Iterable.filter(this.testItemToInternal.values(), t => t.parent === null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets test information by its reference, if it was defined and still exists
|
||||
* in this extension host.
|
||||
*/
|
||||
public getTestByReference(item: TestItemRaw) {
|
||||
return this.testItemToInternal.get(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a diff of all changes that have been made, and clears the diff queue.
|
||||
*/
|
||||
@@ -257,8 +244,8 @@ export class SingleUseTestCollection extends Disposable {
|
||||
}
|
||||
|
||||
public override dispose() {
|
||||
for (const item of this.testItemToInternal.values()) {
|
||||
getPrivateApiFor(item.actual).bus.dispose();
|
||||
for (const item of this.tree) {
|
||||
getPrivateApiFor(item.actual).listener = undefined;
|
||||
}
|
||||
|
||||
this.diff = [];
|
||||
@@ -268,21 +255,27 @@ export class SingleUseTestCollection extends Disposable {
|
||||
private onTestItemEvent(internal: OwnedCollectionTestItem, evt: ExtHostTestItemEvent) {
|
||||
const extId = internal?.actual.id;
|
||||
|
||||
switch (evt[0]) {
|
||||
case ExtHostTestItemEventType.Invalidated:
|
||||
switch (evt.op) {
|
||||
case ExtHostTestItemEventOp.Invalidated:
|
||||
this.pushDiff([TestDiffOpType.Retire, extId]);
|
||||
break;
|
||||
|
||||
case ExtHostTestItemEventType.Disposed:
|
||||
this.removeItem(internal);
|
||||
case ExtHostTestItemEventOp.RemoveChild:
|
||||
this.removeItem(evt.id);
|
||||
break;
|
||||
|
||||
case ExtHostTestItemEventType.NewChild:
|
||||
this.addItemInner(evt[1], internal);
|
||||
case ExtHostTestItemEventOp.Upsert:
|
||||
this.upsertItem(evt.item, internal);
|
||||
break;
|
||||
|
||||
case ExtHostTestItemEventType.SetProp:
|
||||
const [_, key, value] = evt;
|
||||
case ExtHostTestItemEventOp.Bulk:
|
||||
for (const op of evt.ops) {
|
||||
this.onTestItemEvent(internal, op);
|
||||
}
|
||||
break;
|
||||
|
||||
case ExtHostTestItemEventOp.SetProp:
|
||||
const { key, value } = evt;
|
||||
switch (key) {
|
||||
case 'canResolveChildren':
|
||||
this.updateExpandability(internal);
|
||||
@@ -299,54 +292,88 @@ export class SingleUseTestCollection extends Disposable {
|
||||
}
|
||||
break;
|
||||
default:
|
||||
assertNever(evt[0]);
|
||||
assertNever(evt);
|
||||
}
|
||||
}
|
||||
|
||||
private addItemInner(actual: TestItemRaw, parent: OwnedCollectionTestItem | null) {
|
||||
private upsertItem(actual: TestItemRaw, parent: OwnedCollectionTestItem | null) {
|
||||
if (!(actual instanceof TestItemImpl)) {
|
||||
throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`);
|
||||
}
|
||||
|
||||
if (this.testItemToInternal.has(actual)) {
|
||||
throw new Error(`Attempted to add a single TestItem ${actual.id} multiple times to the tree`);
|
||||
|
||||
// If the item already exists under a different parent, remove it.
|
||||
let internal = this.tree.get(actual.id);
|
||||
if (internal && internal.parent !== parent?.actual.id) {
|
||||
(internal.actual.parent ?? this.root).children.remove(actual.id);
|
||||
internal = undefined;
|
||||
}
|
||||
|
||||
if (this.tree.has(actual.id)) {
|
||||
throw new Error(`Attempted to insert a duplicate test item ID ${actual.id}`);
|
||||
// Case 1: a brand new item
|
||||
if (!internal) {
|
||||
const parentId = parent ? parent.actual.id : null;
|
||||
// always expand root node to know if there are tests (and whether to show the welcome view)
|
||||
const pExpandLvls = parent ? parent.expandLevels : 1;
|
||||
internal = {
|
||||
actual,
|
||||
parent: parentId,
|
||||
expandLevels: pExpandLvls /* intentionally undefined or 0 */ ? pExpandLvls - 1 : undefined,
|
||||
expand: TestItemExpandState.NotExpandable, // updated by `connectItemAndChildren`
|
||||
};
|
||||
|
||||
this.tree.add(internal);
|
||||
this.pushDiff([
|
||||
TestDiffOpType.Add,
|
||||
{ parent: parentId, controllerId: this.controllerId, expand: internal.expand, item: Convert.TestItem.from(actual) },
|
||||
]);
|
||||
|
||||
this.connectItemAndChildren(actual, internal, parent);
|
||||
return;
|
||||
}
|
||||
|
||||
const parentId = parent ? parent.item.extId : null;
|
||||
// always expand root node to know if there are tests (and whether to show the welcome view)
|
||||
const pExpandLvls = parent ? parent.expandLevels : 1;
|
||||
const internal: OwnedCollectionTestItem = {
|
||||
actual,
|
||||
parent: parentId,
|
||||
item: Convert.TestItem.from(actual),
|
||||
expandLevels: pExpandLvls /* intentionally undefined or 0 */ ? pExpandLvls - 1 : undefined,
|
||||
expand: TestItemExpandState.NotExpandable, // updated by `updateExpandability` down below
|
||||
controllerId: this.controllerId,
|
||||
};
|
||||
// Case 2: re-insertion of an existing item, no-op
|
||||
if (internal.actual === actual) {
|
||||
this.connectItem(actual, internal, parent); // re-connect in case the parent changed
|
||||
return; // no-op
|
||||
}
|
||||
|
||||
this.tree.add(internal);
|
||||
this.testItemToInternal.set(actual, internal);
|
||||
this.pushDiff([
|
||||
TestDiffOpType.Add,
|
||||
{ parent: parentId, controllerId: this.controllerId, expand: internal.expand, item: internal.item },
|
||||
]);
|
||||
// Case 3: upsert of an existing item by ID, with a new instance
|
||||
const oldChildren = internal.actual.children.all;
|
||||
const oldActual = internal.actual;
|
||||
const changedProps = diffTestItems(oldActual, actual);
|
||||
getPrivateApiFor(oldActual).listener = undefined;
|
||||
|
||||
internal.actual = actual;
|
||||
internal.expand = TestItemExpandState.NotExpandable; // updated by `connectItemAndChildren`
|
||||
for (const [key, value] of changedProps) {
|
||||
this.onTestItemEvent(internal, { op: ExtHostTestItemEventOp.SetProp, key, value });
|
||||
}
|
||||
|
||||
this.connectItemAndChildren(actual, internal, parent);
|
||||
|
||||
// Remove any children still referencing the old parent that aren't
|
||||
// included in the new one. Note that children might have moved to a new
|
||||
// parent, so the parent ID check is done.
|
||||
for (const child of oldChildren) {
|
||||
if (!actual.children.get(child.id) && this.tree.get(child.id)?.parent === actual.id) {
|
||||
this.removeItem(child.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private connectItem(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | null) {
|
||||
const api = getPrivateApiFor(actual);
|
||||
api.bus.event(this.onTestItemEvent.bind(this, internal));
|
||||
|
||||
// important that this comes after binding the event bus otherwise we
|
||||
// might miss a synchronous discovery completion
|
||||
api.parent = parent && parent.actual !== this.root ? parent.actual : undefined;
|
||||
api.listener = evt => this.onTestItemEvent(internal, evt);
|
||||
this.updateExpandability(internal);
|
||||
}
|
||||
|
||||
private connectItemAndChildren(actual: TestItemImpl, internal: OwnedCollectionTestItem, parent: OwnedCollectionTestItem | null) {
|
||||
this.connectItem(actual, internal, parent);
|
||||
|
||||
// Discover any existing children that might have already been added
|
||||
for (const child of api.children.values()) {
|
||||
if (!this.testItemToInternal.has(child)) {
|
||||
this.addItemInner(child, internal);
|
||||
}
|
||||
for (const child of actual.children.all) {
|
||||
this.upsertItem(child, internal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +418,7 @@ export class SingleUseTestCollection extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
const asyncChildren = [...internal.actual.children.values()]
|
||||
const asyncChildren = internal.actual.children.all
|
||||
.map(c => this.expand(c.id, levels))
|
||||
.filter(isThenable);
|
||||
|
||||
@@ -443,19 +470,24 @@ export class SingleUseTestCollection extends Disposable {
|
||||
this.pushDiff([TestDiffOpType.Update, { extId: internal.actual.id, expand: internal.expand }]);
|
||||
}
|
||||
|
||||
private removeItem(internal: OwnedCollectionTestItem) {
|
||||
this.pushDiff([TestDiffOpType.Remove, internal.actual.id]);
|
||||
private removeItem(childId: string) {
|
||||
const childItem = this.tree.get(childId);
|
||||
if (!childItem) {
|
||||
throw new Error('attempting to remove non-existent child');
|
||||
}
|
||||
|
||||
const queue: (OwnedCollectionTestItem | undefined)[] = [internal];
|
||||
this.pushDiff([TestDiffOpType.Remove, childId]);
|
||||
|
||||
const queue: (OwnedCollectionTestItem | undefined)[] = [childItem];
|
||||
while (queue.length) {
|
||||
const item = queue.pop();
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.tree.delete(item.item.extId);
|
||||
this.testItemToInternal.delete(item.actual);
|
||||
for (const child of item.actual.children.values()) {
|
||||
getPrivateApiFor(item.actual).listener = undefined;
|
||||
this.tree.delete(item.actual.id);
|
||||
for (const child of item.actual.children.all) {
|
||||
queue.push(this.tree.get(child.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { TestMessageSeverity, TestResultState } from 'vs/workbench/api/common/extHostTypes';
|
||||
|
||||
export { TestResultState } from 'vs/workbench/api/common/extHostTypes';
|
||||
|
||||
export interface ITestIdWithSrc {
|
||||
testId: string;
|
||||
controllerId: string;
|
||||
@@ -74,7 +76,7 @@ export interface ResolvedTestRunRequest {
|
||||
*/
|
||||
export interface ExtensionRunTestsRequest {
|
||||
id: string;
|
||||
tests: string[];
|
||||
include: string[];
|
||||
exclude: string[];
|
||||
controllerId: string;
|
||||
config?: { group: TestRunConfigurationBitset, id: number };
|
||||
|
||||
@@ -156,7 +156,7 @@ export class TestResultService implements ITestResultService {
|
||||
profileGroup: config.group,
|
||||
profileId: config.profileId,
|
||||
controllerId: req.controllerId,
|
||||
testIds: req.tests,
|
||||
testIds: req.include,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TestItemImpl, TestResultState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi';
|
||||
import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/mainThreadTestCollection';
|
||||
import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection';
|
||||
|
||||
export * as Convert from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
export { TestItemImpl, TestResultState } from 'vs/workbench/api/common/extHostTypes';
|
||||
export { TestItemImpl } from 'vs/workbench/api/common/extHostTestingPrivateApi';
|
||||
|
||||
/**
|
||||
* Gets a main thread test collection initialized with the given set of
|
||||
@@ -29,17 +29,18 @@ export const testStubs = {
|
||||
collection.root.canResolveChildren = true;
|
||||
collection.resolveHandler = item => {
|
||||
if (item === collection.root) {
|
||||
const a = new TestItemImpl(idPrefix + 'a', 'a', URI.file('/'), undefined, collection.root);
|
||||
const a = new TestItemImpl(idPrefix + 'a', 'a', URI.file('/'));
|
||||
a.canResolveChildren = true;
|
||||
new TestItemImpl(idPrefix + 'b', 'b', URI.file('/'), undefined, collection.root);
|
||||
const b = new TestItemImpl(idPrefix + 'b', 'b', URI.file('/'));
|
||||
item.children.all = [a, b];
|
||||
} else if (item.id === idPrefix + 'a') {
|
||||
new TestItemImpl(idPrefix + 'aa', 'aa', URI.file('/'), undefined, item);
|
||||
new TestItemImpl(idPrefix + 'ab', 'ab', URI.file('/'), undefined, item);
|
||||
item.children.all = [
|
||||
new TestItemImpl(idPrefix + 'aa', 'aa', URI.file('/')),
|
||||
new TestItemImpl(idPrefix + 'ab', 'ab', URI.file('/')),
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
return collection;
|
||||
},
|
||||
};
|
||||
|
||||
export const ReExportedTestRunState = TestResultState;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
|
||||
|
||||
export { TestResultState } from 'vs/workbench/api/common/extHostTypes';
|
||||
|
||||
export type TreeStateNode = { statusNode: true; state: TestResultState; priority: number };
|
||||
|
||||
/**
|
||||
|
||||
+7
-6
@@ -7,8 +7,9 @@ import * as assert from 'assert';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation';
|
||||
import { TestDiffOpType, TestItemExpandState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { TestResultState } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
|
||||
import { Convert, TestItemImpl, TestResultState } from 'vs/workbench/contrib/testing/common/testStubs';
|
||||
import { Convert, TestItemImpl } from 'vs/workbench/contrib/testing/common/testStubs';
|
||||
import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree';
|
||||
|
||||
class TestHierarchicalByLocationProjection extends HierarchicalByLocationProjection {
|
||||
@@ -53,10 +54,10 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => {
|
||||
harness.flush();
|
||||
harness.pushDiff([
|
||||
TestDiffOpType.Add,
|
||||
{ controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'c', undefined, undefined, undefined)) },
|
||||
{ controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'c', undefined)) },
|
||||
], [
|
||||
TestDiffOpType.Add,
|
||||
{ controllerId: 'ctrl2', parent: 'c', expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'ca', undefined, undefined, undefined)) },
|
||||
{ controllerId: 'ctrl2', parent: 'c', expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'ca', undefined)) },
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(harness.flush(), [
|
||||
@@ -74,7 +75,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => {
|
||||
{ e: 'b' }
|
||||
]);
|
||||
|
||||
new TestItemImpl('ac', 'ac', undefined, undefined, harness.c.root.children.get('id-a')!);
|
||||
harness.c.root.children.get('id-a')!.children.add(new TestItemImpl('ac', 'ac', undefined));
|
||||
|
||||
assert.deepStrictEqual(harness.flush(), [
|
||||
{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] },
|
||||
@@ -91,7 +92,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => {
|
||||
{ e: 'b' }
|
||||
]);
|
||||
|
||||
harness.c.root.children.get('id-a')!.children.get('id-ab')!.dispose();
|
||||
harness.c.root.children.get('id-a')!.children.remove('id-ab');
|
||||
|
||||
assert.deepStrictEqual(harness.flush(), [
|
||||
{ e: 'a', children: [{ e: 'aa' }] },
|
||||
@@ -104,7 +105,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => {
|
||||
resultsService.getStateById = () => [undefined, resultInState(TestResultState.Failed)];
|
||||
|
||||
const resultInState = (state: TestResultState): TestResultItem => ({
|
||||
item: harness.c.itemToInternal.get(harness.c.root.children.get('id-a')!)!.item,
|
||||
item: Convert.TestItem.from(harness.c.tree.get('id-a')!.actual),
|
||||
parent: 'id-root',
|
||||
tasks: [],
|
||||
retired: false,
|
||||
|
||||
+5
-5
@@ -42,10 +42,10 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => {
|
||||
harness.flush();
|
||||
harness.pushDiff([
|
||||
TestDiffOpType.Add,
|
||||
{ controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'root2', undefined, undefined, undefined)) },
|
||||
{ controllerId: 'ctrl2', parent: null, expand: TestItemExpandState.Expanded, item: Convert.TestItem.from(new TestItemImpl('c', 'root2', undefined)) },
|
||||
], [
|
||||
TestDiffOpType.Add,
|
||||
{ controllerId: 'ctrl2', parent: 'c', expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'c', undefined, undefined, undefined)) },
|
||||
{ controllerId: 'ctrl2', parent: 'c', expand: TestItemExpandState.NotExpandable, item: Convert.TestItem.from(new TestItemImpl('c-a', 'c', undefined)) },
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(harness.flush(), [
|
||||
@@ -57,7 +57,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => {
|
||||
test('updates nodes if they add children', async () => {
|
||||
harness.flush();
|
||||
|
||||
new TestItemImpl('ac', 'ac', undefined, undefined, harness.c.root.children.get('id-a')!);
|
||||
harness.c.root.children.get('id-a')!.children.add(new TestItemImpl('ac', 'ac', undefined));
|
||||
|
||||
assert.deepStrictEqual(harness.flush(), [
|
||||
{ e: 'aa' },
|
||||
@@ -69,7 +69,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => {
|
||||
|
||||
test('updates nodes if they remove children', async () => {
|
||||
harness.flush();
|
||||
harness.c.root.children.get('id-a')!.children.get('id-ab')!.dispose();
|
||||
harness.c.root.children.get('id-a')!.children.remove('id-ab');
|
||||
|
||||
assert.deepStrictEqual(harness.flush(), [
|
||||
{ e: 'aa' },
|
||||
@@ -79,7 +79,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => {
|
||||
|
||||
test('swaps when node is no longer leaf', async () => {
|
||||
harness.flush();
|
||||
new TestItemImpl('ba', 'ba', undefined, undefined, harness.c.root.children.get('id-b')!);
|
||||
harness.c.root.children.get('id-b')!.children.add(new TestItemImpl('ba', 'ba', undefined));
|
||||
|
||||
assert.deepStrictEqual(harness.flush(), [
|
||||
{ e: 'aa' },
|
||||
|
||||
@@ -7,10 +7,6 @@ import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/own
|
||||
import { TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
|
||||
export class TestSingleUseCollection extends SingleUseTestCollection {
|
||||
public get itemToInternal() {
|
||||
return this.testItemToInternal;
|
||||
}
|
||||
|
||||
public get currentDiff() {
|
||||
return this.diff;
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
|
||||
import { ITestTaskState, ResolvedTestRunRequest, TestResultItem, TestRunConfigurationBitset } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { TestConfigurationService } from 'vs/workbench/contrib/testing/common/testConfigurationService';
|
||||
import { TestResultState } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||
import { getPathForTestInResult, HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, resultItemParents, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
|
||||
import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||
import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage';
|
||||
import { Convert, getInitializedMainTestCollection, ReExportedTestRunState as TestRunState, TestResultState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs';
|
||||
import { Convert, getInitializedMainTestCollection, testStubs } from 'vs/workbench/contrib/testing/common/testStubs';
|
||||
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
|
||||
|
||||
export const emptyOutputController = () => new LiveOutputController(
|
||||
@@ -100,29 +101,29 @@ suite('Workbench - Test Results Service', () => {
|
||||
test('initializes with valid counts', () => {
|
||||
assert.deepStrictEqual(r.counts, {
|
||||
...makeEmptyCounts(),
|
||||
[TestRunState.Queued]: 2,
|
||||
[TestRunState.Unset]: 2,
|
||||
[TestResultState.Queued]: 2,
|
||||
[TestResultState.Unset]: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('setAllToState', () => {
|
||||
changed.clear();
|
||||
r.setAllToState(TestRunState.Queued, 't', (_, t) => t.item.label !== 'root');
|
||||
r.setAllToState(TestResultState.Queued, 't', (_, t) => t.item.label !== 'root');
|
||||
assert.deepStrictEqual(r.counts, {
|
||||
...makeEmptyCounts(),
|
||||
[TestRunState.Unset]: 1,
|
||||
[TestRunState.Queued]: 3,
|
||||
[TestResultState.Unset]: 1,
|
||||
[TestResultState.Queued]: 3,
|
||||
});
|
||||
|
||||
r.setAllToState(TestRunState.Failed, 't', (_, t) => t.item.label !== 'root');
|
||||
r.setAllToState(TestResultState.Failed, 't', (_, t) => t.item.label !== 'root');
|
||||
assert.deepStrictEqual(r.counts, {
|
||||
...makeEmptyCounts(),
|
||||
[TestRunState.Unset]: 1,
|
||||
[TestRunState.Failed]: 3,
|
||||
[TestResultState.Unset]: 1,
|
||||
[TestResultState.Failed]: 3,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(r.getStateById('id-a')?.ownComputedState, TestRunState.Failed);
|
||||
assert.deepStrictEqual(r.getStateById('id-a')?.tasks[0].state, TestRunState.Failed);
|
||||
assert.deepStrictEqual(r.getStateById('id-a')?.ownComputedState, TestResultState.Failed);
|
||||
assert.deepStrictEqual(r.getStateById('id-a')?.tasks[0].state, TestResultState.Failed);
|
||||
assert.deepStrictEqual(getChangeSummary(), [
|
||||
{ label: 'a', reason: TestResultItemChangeReason.OwnStateChange },
|
||||
{ label: 'aa', reason: TestResultItemChangeReason.OwnStateChange },
|
||||
@@ -133,16 +134,16 @@ suite('Workbench - Test Results Service', () => {
|
||||
|
||||
test('updateState', () => {
|
||||
changed.clear();
|
||||
r.updateState('id-aa', 't', TestRunState.Running);
|
||||
r.updateState('id-aa', 't', TestResultState.Running);
|
||||
assert.deepStrictEqual(r.counts, {
|
||||
...makeEmptyCounts(),
|
||||
[TestRunState.Unset]: 2,
|
||||
[TestRunState.Running]: 1,
|
||||
[TestRunState.Queued]: 1,
|
||||
[TestResultState.Unset]: 2,
|
||||
[TestResultState.Running]: 1,
|
||||
[TestResultState.Queued]: 1,
|
||||
});
|
||||
assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Running);
|
||||
assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestResultState.Running);
|
||||
// update computed state:
|
||||
assert.deepStrictEqual(r.getStateById(tests.root.id)?.computedState, TestRunState.Running);
|
||||
assert.deepStrictEqual(r.getStateById(tests.root.id)?.computedState, TestResultState.Running);
|
||||
assert.deepStrictEqual(getChangeSummary(), [
|
||||
{ label: 'a', reason: TestResultItemChangeReason.ComputedStateChange },
|
||||
{ label: 'aa', reason: TestResultItemChangeReason.OwnStateChange },
|
||||
@@ -166,30 +167,30 @@ suite('Workbench - Test Results Service', () => {
|
||||
|
||||
test('ignores outside run', () => {
|
||||
changed.clear();
|
||||
r.updateState('id-b', 't', TestRunState.Running);
|
||||
r.updateState('id-b', 't', TestResultState.Running);
|
||||
assert.deepStrictEqual(r.counts, {
|
||||
...makeEmptyCounts(),
|
||||
[TestRunState.Queued]: 2,
|
||||
[TestRunState.Unset]: 2,
|
||||
[TestResultState.Queued]: 2,
|
||||
[TestResultState.Unset]: 2,
|
||||
});
|
||||
assert.deepStrictEqual(r.getStateById('id-b'), undefined);
|
||||
});
|
||||
|
||||
test('markComplete', () => {
|
||||
r.setAllToState(TestRunState.Queued, 't', () => true);
|
||||
r.updateState('id-aa', 't', TestRunState.Passed);
|
||||
r.setAllToState(TestResultState.Queued, 't', () => true);
|
||||
r.updateState('id-aa', 't', TestResultState.Passed);
|
||||
changed.clear();
|
||||
|
||||
r.markComplete();
|
||||
|
||||
assert.deepStrictEqual(r.counts, {
|
||||
...makeEmptyCounts(),
|
||||
[TestRunState.Passed]: 1,
|
||||
[TestRunState.Unset]: 3,
|
||||
[TestResultState.Passed]: 1,
|
||||
[TestResultState.Unset]: 3,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(r.getStateById(tests.root.id)?.ownComputedState, TestRunState.Unset);
|
||||
assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Passed);
|
||||
assert.deepStrictEqual(r.getStateById(tests.root.id)?.ownComputedState, TestResultState.Unset);
|
||||
assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestResultState.Passed);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -213,7 +214,7 @@ suite('Workbench - Test Results Service', () => {
|
||||
|
||||
test('serializes and re-hydrates', async () => {
|
||||
results.push(r);
|
||||
r.updateState('id-aa', 't', TestRunState.Passed);
|
||||
r.updateState('id-aa', 't', TestResultState.Passed);
|
||||
r.markComplete();
|
||||
await timeout(10); // allow persistImmediately async to happen
|
||||
|
||||
@@ -270,7 +271,7 @@ suite('Workbench - Test Results Service', () => {
|
||||
assert.deepStrictEqual(results.results, [r, r2]);
|
||||
});
|
||||
|
||||
const makeHydrated = async (completedAt = 42, state = TestRunState.Passed) => new HydratedTestResult({
|
||||
const makeHydrated = async (completedAt = 42, state = TestResultState.Passed) => new HydratedTestResult({
|
||||
completedAt,
|
||||
id: 'some-id',
|
||||
tasks: [{ id: 't', running: false, name: undefined }],
|
||||
|
||||
@@ -11,9 +11,9 @@ import { mockObject, MockObject } from 'vs/base/test/common/mock';
|
||||
import { MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { TestRunConfigurationImpl, TestRunCoordinator, TestRunDto } from 'vs/workbench/api/common/extHostTesting';
|
||||
import * as convert from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { TestMessage, TestRunConfigurationGroup } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { TestMessage, TestResultState, TestRunConfigurationGroup } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { TestDiffOpType, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||
import { TestItemImpl, TestResultState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs';
|
||||
import { TestItemImpl, testStubs } from 'vs/workbench/contrib/testing/common/testStubs';
|
||||
import { TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection';
|
||||
import type { TestItem, TestRunRequest } from 'vscode';
|
||||
|
||||
@@ -35,8 +35,8 @@ const assertTreesEqual = (a: TestItem | undefined, b: TestItem | undefined) => {
|
||||
|
||||
assert.deepStrictEqual(simplify(a), simplify(b));
|
||||
|
||||
const aChildren = [...a.children.keys()].slice().sort();
|
||||
const bChildren = [...b.children.keys()].slice().sort();
|
||||
const aChildren = a.children.all.map(c => c.id).sort();
|
||||
const bChildren = b.children.all.map(c => c.id).sort();
|
||||
assert.strictEqual(aChildren.length, bChildren.length, `expected ${a.label}.children.length == ${b.label}.children.length`);
|
||||
aChildren.forEach(key => assertTreesEqual(a.children.get(key), b.children.get(key)));
|
||||
};
|
||||
@@ -109,6 +109,16 @@ suite('ExtHost Testing', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('parents are set correctly', () => {
|
||||
single.expand(single.root.id, Infinity);
|
||||
single.collectDiff();
|
||||
|
||||
const a = single.root.children.get('id-a')!;
|
||||
const ab = a.children.get('id-ab')!;
|
||||
assert.strictEqual(a.parent, undefined);
|
||||
assert.strictEqual(ab.parent, a);
|
||||
});
|
||||
|
||||
test('no-ops if items not changed', () => {
|
||||
single.collectDiff();
|
||||
assert.deepStrictEqual(single.collectDiff(), []);
|
||||
@@ -129,19 +139,20 @@ suite('ExtHost Testing', () => {
|
||||
test('removes children', () => {
|
||||
single.expand(single.root.id, Infinity);
|
||||
single.collectDiff();
|
||||
single.root.children.get('id-a')!.dispose();
|
||||
single.root.children.remove('id-a');
|
||||
|
||||
assert.deepStrictEqual(single.collectDiff(), [
|
||||
[TestDiffOpType.Remove, 'id-a'],
|
||||
]);
|
||||
assert.deepStrictEqual([...single.tree].map(n => n.item.extId).sort(), [single.root.id, 'id-b']);
|
||||
assert.strictEqual(single.itemToInternal.size, 2);
|
||||
assert.deepStrictEqual([...single.tree].map(n => n.actual.id).sort(), [single.root.id, 'id-b']);
|
||||
assert.strictEqual(single.tree.size, 2);
|
||||
});
|
||||
|
||||
test('adds new children', () => {
|
||||
single.expand(single.root.id, Infinity);
|
||||
single.collectDiff();
|
||||
const child = new TestItemImpl('id-ac', 'c', undefined, undefined, single.root.children.get('id-a'));
|
||||
const child = new TestItemImpl('id-ac', 'c', undefined);
|
||||
single.root.children.get('id-a')!.children.add(child);
|
||||
|
||||
assert.deepStrictEqual(single.collectDiff(), [
|
||||
[TestDiffOpType.Add, {
|
||||
@@ -152,10 +163,111 @@ suite('ExtHost Testing', () => {
|
||||
}],
|
||||
]);
|
||||
assert.deepStrictEqual(
|
||||
[...single.tree].map(n => n.item.extId).sort(),
|
||||
[...single.tree].map(n => n.actual.id).sort(),
|
||||
[single.root.id, 'id-a', 'id-aa', 'id-ab', 'id-ac', 'id-b'],
|
||||
);
|
||||
assert.strictEqual(single.itemToInternal.size, 6);
|
||||
assert.strictEqual(single.tree.size, 6);
|
||||
});
|
||||
|
||||
test('treats in-place replacement as mutation', () => {
|
||||
single.expand(single.root.id, Infinity);
|
||||
single.collectDiff();
|
||||
|
||||
const oldA = single.root.children.get('id-a')!;
|
||||
const newA = new TestItemImpl('id-a', 'Hello world', undefined);
|
||||
newA.children.all = oldA.children.all;
|
||||
single.root.children.all = [
|
||||
newA,
|
||||
new TestItemImpl('id-b', single.root.children.get('id-b')!.label, undefined),
|
||||
];
|
||||
|
||||
assert.deepStrictEqual(single.collectDiff(), [
|
||||
[
|
||||
TestDiffOpType.Update,
|
||||
{ extId: 'id-a', expand: TestItemExpandState.Expanded, item: { label: 'Hello world' } },
|
||||
],
|
||||
]);
|
||||
|
||||
newA.label = 'still connected';
|
||||
assert.deepStrictEqual(single.collectDiff(), [
|
||||
[
|
||||
TestDiffOpType.Update,
|
||||
{ extId: 'id-a', item: { label: 'still connected' } }
|
||||
],
|
||||
]);
|
||||
|
||||
oldA.label = 'no longer connected';
|
||||
assert.deepStrictEqual(single.collectDiff(), []);
|
||||
});
|
||||
|
||||
test('treats in-place replacement as mutation deeply', () => {
|
||||
single.expand(single.root.id, Infinity);
|
||||
single.collectDiff();
|
||||
|
||||
const oldA = single.root.children.get('id-a')!;
|
||||
const newA = new TestItemImpl('id-a', single.root.children.get('id-a')!.label, undefined);
|
||||
const oldAA = oldA.children.get('id-aa')!;
|
||||
const oldAB = oldA.children.get('id-ab')!;
|
||||
const newAB = new TestItemImpl('id-ab', 'Hello world', undefined);
|
||||
newA.children.all = [oldAA, newAB];
|
||||
single.root.children.all = [newA, single.root.children.get('id-b')!];
|
||||
|
||||
assert.deepStrictEqual(single.collectDiff(), [
|
||||
[
|
||||
TestDiffOpType.Update,
|
||||
{ extId: 'id-a', expand: TestItemExpandState.Expanded },
|
||||
],
|
||||
[
|
||||
TestDiffOpType.Update,
|
||||
{ extId: 'id-ab', item: { label: 'Hello world' } },
|
||||
],
|
||||
]);
|
||||
|
||||
oldAA.label = 'still connected1';
|
||||
newAB.label = 'still connected2';
|
||||
oldAB.label = 'not connected3';
|
||||
assert.deepStrictEqual(single.collectDiff(), [
|
||||
[
|
||||
TestDiffOpType.Update,
|
||||
{ extId: 'id-aa', item: { label: 'still connected1' } }
|
||||
],
|
||||
[
|
||||
TestDiffOpType.Update,
|
||||
{ extId: 'id-ab', item: { label: 'still connected2' } }
|
||||
],
|
||||
]);
|
||||
|
||||
assert.strictEqual(newAB.parent, newA);
|
||||
assert.strictEqual(oldAA.parent, newA);
|
||||
assert.deepStrictEqual(newA.parent, undefined);
|
||||
});
|
||||
|
||||
test('moves an item to be a new child', () => {
|
||||
single.collectDiff();
|
||||
const b = single.root.children.get('id-b')!;
|
||||
const a = single.root.children.get('id-a')!;
|
||||
a.children.add(b);
|
||||
assert.deepStrictEqual(single.collectDiff(), [
|
||||
[
|
||||
TestDiffOpType.Remove,
|
||||
'id-b',
|
||||
],
|
||||
[
|
||||
TestDiffOpType.Add,
|
||||
{ controllerId: 'ctrlId', parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(single.tree.get('id-b')!.actual) }
|
||||
],
|
||||
]);
|
||||
|
||||
b.label = 'still connected';
|
||||
assert.deepStrictEqual(single.collectDiff(), [
|
||||
[
|
||||
TestDiffOpType.Update,
|
||||
{ extId: 'id-b', item: { label: 'still connected' } }
|
||||
],
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual(single.root.children.all, [single.root.children.get('id-a')]);
|
||||
assert.deepStrictEqual(b.parent, a);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -314,7 +426,7 @@ suite('ExtHost Testing', () => {
|
||||
configuration = new TestRunConfigurationImpl(mockObject<MainThreadTestingShape, {}>(), 'ctrlId', 42, 'Do Run', TestRunConfigurationGroup.Run, () => { }, false);
|
||||
|
||||
req = {
|
||||
tests: [single.root],
|
||||
include: undefined,
|
||||
exclude: [single.root.children.get('id-b')!],
|
||||
configuration,
|
||||
};
|
||||
@@ -325,15 +437,15 @@ suite('ExtHost Testing', () => {
|
||||
excludeExtIds: ['id-b'],
|
||||
runId: 'run-id',
|
||||
testIds: [single.root.id],
|
||||
});
|
||||
}, single);
|
||||
});
|
||||
|
||||
test('tracks a run started from a main thread request', () => {
|
||||
const tracker = c.prepareForMainThreadTestRun(req, dto, cts.token);
|
||||
assert.strictEqual(tracker.isRunning, false);
|
||||
|
||||
const task1 = c.createTestRun('ctrl', req, 'run1', true);
|
||||
const task2 = c.createTestRun('ctrl', req, 'run2', true);
|
||||
const task1 = c.createTestRun('ctrl', single, req, 'run1', true);
|
||||
const task2 = c.createTestRun('ctrl', single, req, 'run2', true);
|
||||
assert.strictEqual(proxy.$startedExtensionTestRun.called, false);
|
||||
assert.strictEqual(tracker.isRunning, true);
|
||||
|
||||
@@ -351,7 +463,7 @@ suite('ExtHost Testing', () => {
|
||||
});
|
||||
|
||||
test('tracks a run started from an extension request', () => {
|
||||
const task1 = c.createTestRun('ctrl', req, 'hello world', false);
|
||||
const task1 = c.createTestRun('ctrl', single, req, 'hello world', false);
|
||||
|
||||
const tracker = Iterable.first(c.trackers)!;
|
||||
assert.strictEqual(tracker.isRunning, true);
|
||||
@@ -360,14 +472,14 @@ suite('ExtHost Testing', () => {
|
||||
config: { group: 2, id: 42 },
|
||||
controllerId: 'ctrl',
|
||||
id: tracker.id,
|
||||
tests: [single.root.id],
|
||||
include: [single.root.id],
|
||||
exclude: ['id-b'],
|
||||
persist: false,
|
||||
}]
|
||||
]);
|
||||
|
||||
const task2 = c.createTestRun('ctrl', req, 'run2', true);
|
||||
const task3Detached = c.createTestRun('ctrl', { ...req }, 'task3Detached', true);
|
||||
const task2 = c.createTestRun('ctrl', single, req, 'run2', true);
|
||||
const task3Detached = c.createTestRun('ctrl', single, { ...req }, 'task3Detached', true);
|
||||
|
||||
task1.end();
|
||||
assert.strictEqual(proxy.$finishedExtensionTestRun.called, false);
|
||||
@@ -381,7 +493,7 @@ suite('ExtHost Testing', () => {
|
||||
});
|
||||
|
||||
test('adds tests to run smartly', () => {
|
||||
const task1 = c.createTestRun('ctrl', req, 'hello world', false);
|
||||
const task1 = c.createTestRun('ctrl', single, req, 'hello world', false);
|
||||
const tracker = Iterable.first(c.trackers)!;
|
||||
const expectedArgs: unknown[][] = [];
|
||||
assert.deepStrictEqual(proxy.$addTestsToRun.args, expectedArgs);
|
||||
@@ -416,7 +528,7 @@ suite('ExtHost Testing', () => {
|
||||
});
|
||||
|
||||
test('guards calls after runs are ended', () => {
|
||||
const task = c.createTestRun('ctrl', req, 'hello world', false);
|
||||
const task = c.createTestRun('ctrl', single, req, 'hello world', false);
|
||||
task.end();
|
||||
|
||||
task.setState(single.root, TestResultState.Passed);
|
||||
@@ -431,9 +543,9 @@ suite('ExtHost Testing', () => {
|
||||
test('excludes tests outside tree or explicitly excluded', () => {
|
||||
single.expand(single.root.id, Infinity);
|
||||
|
||||
const task = c.createTestRun('ctrl', {
|
||||
const task = c.createTestRun('ctrl', single, {
|
||||
configuration,
|
||||
tests: [single.root.children.get('id-a')!],
|
||||
include: [single.root.children.get('id-a')!],
|
||||
exclude: [single.root.children.get('id-a')!.children.get('id-aa')!],
|
||||
}, 'hello world', false);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user