未验证 提交 581ff12c 编写于 作者: C Connor Peet

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
上级 039582c0
......@@ -198,6 +198,7 @@
"type": "pwa-chrome",
"request": "launch",
"name": "Launch VS Code Internal",
"trace": true,
"windows": {
"runtimeExecutable": "${workspaceFolder}/scripts/code.bat"
},
......
......@@ -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;
}
// 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;
}
// 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
}
if (this.tree.has(actual.id)) {
throw new Error(`Attempted to insert a duplicate test item ID ${actual.id}`);
// 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 });
}
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,
};
this.connectItemAndChildren(actual, internal, parent);
this.tree.add(internal);
this.testItemToInternal.set(actual, internal);
this.pushDiff([
TestDiffOpType.Add,
{ parent: parentId, controllerId: this.controllerId, expand: internal.expand, item: internal.item },
]);
// 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');
}
this.pushDiff([TestDiffOpType.Remove, childId]);
const queue: (OwnedCollectionTestItem | undefined)[] = [internal];
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,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,
......
......@@ -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);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册