testing: rework running side to new apis

This commit is contained in:
Connor Peet 2021-04-15 15:26:51 -07:00
parent bec017d389
commit bb3ea733de
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
31 changed files with 873 additions and 541 deletions

View file

@ -86,5 +86,5 @@
},
"typescript.tsc.autoDetect": "off",
"notebook.experimental.useMarkdownRenderer": true,
"testing.autoRun.mode": "onlyPreviouslyRun",
"testing.autoRun.mode": "rerun",
}

View file

@ -2142,10 +2142,9 @@ declare module 'vscode' {
export function registerTestController<T>(testController: TestController<T>): Disposable;
/**
* Runs tests. The "run" contains the list of tests to run as well as a
* method that can be used to update their state. At the point in time
* that "run" is called, all tests given in the run have their state
* automatically set to {@link TestRunState.Queued}.
* Requests that tests be run by their controller.
* @param run Run options to use
* @param token Cancellation token for the test run
*/
export function runTests<T>(run: TestRunRequest<T>, token?: CancellationToken): Thenable<void>;
@ -2344,11 +2343,9 @@ declare module 'vscode' {
readonly name?: string;
/**
* Updates the state of the test in the run. By default, all tests involved
* in the run will have a "queued" state until they are updated by this method.
*
* Calling with method with nodes outside the {@link TestRunRequest.tests}
* or in the {@link TestRunRequest.exclude} array will no-op.
* Updates the state of the test in the run. Calling with method with nodes
* outside the {@link TestRunRequest.tests} or in the
* {@link TestRunRequest.exclude} array will no-op.
*
* @param test The test to update
* @param state The state to assign to the test
@ -2687,6 +2684,19 @@ declare module 'vscode' {
*/
readonly range?: Range;
/**
* State of the test in each task. In the common case, a test will only
* be executed in a single task and the length of this array will be 1.
*/
readonly taskStates: ReadonlyArray<TestSnapshoptTaskState>;
/**
* Optional list of nested tests for this item.
*/
readonly children: Readonly<TestResultSnapshot>[];
}
export interface TestSnapshoptTaskState {
/**
* Current result of the test.
*/
@ -2703,11 +2713,6 @@ declare module 'vscode' {
* failure information if the test fails.
*/
readonly messages: ReadonlyArray<TestMessage>;
/**
* Optional list of nested tests for this item.
*/
readonly children: Readonly<TestResultSnapshot>[];
}
//#endregion

View file

@ -3,17 +3,16 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { emptyStream } from 'vs/base/common/stream';
import { isDefined } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { getTestSubscriptionKey, ISerializedTestResults, ITestMessage, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { HydratedTestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ExtensionRunTestsRequest, getTestSubscriptionKey, ITestItem, ITestMessage, ITestRunTask, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
@ -48,7 +47,6 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri)));
this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri)));
const prevResults = resultService.results.map(r => r.toJSON()).filter(isDefined);
if (prevResults.length) {
this.proxy.$publishTestResults(prevResults);
@ -72,43 +70,64 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
/**
* @inheritdoc
*/
public $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void {
this.resultService.push(new HydratedTestResult(
results,
() => Promise.resolve(
results.output
? bufferToStream(VSBuffer.fromString(results.output))
: emptyStream(),
),
persist,
));
}
/**
* @inheritdoc
*/
public $updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void {
const r = this.resultService.getResult(runId);
if (r && r instanceof LiveTestResult) {
r.updateState(testId, state, duration);
$addTestsToRun(runId: string, tests: ITestItem[]): void {
for (const test of tests) {
test.uri = URI.revive(test.uri);
if (test.range) {
test.range = Range.lift(test.range);
}
}
this.withLiveRun(runId, r => r.addTestChainToRun(tests));
}
/**
* @inheritdoc
*/
public $appendOutputToRun(runId: string, output: VSBuffer): void {
const r = this.resultService.getResult(runId);
if (r && r instanceof LiveTestResult) {
r.output.append(output);
}
$startedExtensionTestRun(req: ExtensionRunTestsRequest): void {
this.resultService.createLiveResult(req);
}
/**
* @inheritdoc
*/
$startedTestRunTask(runId: string, task: ITestRunTask): void {
this.withLiveRun(runId, r => r.addTask(task));
}
/**
* @inheritdoc
*/
$finishedTestRunTask(runId: string, taskId: string): void {
this.withLiveRun(runId, r => r.markTaskComplete(taskId));
}
/**
* @inheritdoc
*/
$finishedExtensionTestRun(runId: string): void {
this.withLiveRun(runId, r => r.markComplete());
}
/**
* @inheritdoc
*/
public $updateTestStateInRun(runId: string, taskId: string, testId: string, state: TestResultState, duration?: number): void {
this.withLiveRun(runId, r => r.updateState(testId, taskId, state, duration));
}
/**
* @inheritdoc
*/
public $appendOutputToRun(runId: string, _taskId: string, output: VSBuffer): void {
this.withLiveRun(runId, r => r.output.append(output));
}
/**
* @inheritdoc
*/
public $appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void {
public $appendTestMessageInRun(runId: string, taskId: string, testId: string, message: ITestMessage): void {
const r = this.resultService.getResult(runId);
if (r && r instanceof LiveTestResult) {
if (message.location) {
@ -116,7 +135,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
message.location.range = Range.lift(message.location.range);
}
r.appendMessage(testId, message);
r.appendMessage(testId, taskId, message);
}
}
@ -180,4 +199,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
}
this.testSubscriptions.clear();
}
private withLiveRun<T>(runId: string, fn: (run: LiveTestResult) => T): T | undefined {
const r = this.resultService.getResult(runId);
return r && r instanceof LiveTestResult ? fn(r) : undefined;
}
}

View file

@ -337,7 +337,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const test: typeof vscode.test = {
registerTestController(provider) {
checkProposedApiEnabled(extension);
return extHostTesting.registerTestController(provider);
return extHostTesting.registerTestController(extension.identifier.value, provider);
},
createDocumentTestObserver(document) {
checkProposedApiEnabled(extension);
@ -354,9 +354,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
createTestItem<T>(options: vscode.TestItemOptions, data?: T) {
return new extHostTypes.TestItemImpl(options.id, options.label, options.uri, data);
},
createTestRunTask() {
createTestRunTask(request, name, persist) {
checkProposedApiEnabled(extension);
throw new Error('todo');
return extHostTesting.createTestRunTask(extension.identifier.value, request, name, persist);
},
get onDidChangeTestResults() {
checkProposedApiEnabled(extension);

View file

@ -56,7 +56,7 @@ import { Dto } from 'vs/base/common/types';
import { DebugConfigurationProviderTriggerKind, TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync';
import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff, ISerializedTestResults, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff, ISerializedTestResults, ITestMessage, ITestItem, ITestRunTask, ExtensionRunTestsRequest } from 'vs/workbench/contrib/testing/common/testCollection';
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
@ -1997,16 +1997,39 @@ export interface ExtHostTestingShape {
}
export interface MainThreadTestingShape {
/** Registeres that there's a test controller with the given ID */
$registerTestController(id: string): void;
/** Diposes of the test controller with the given ID */
$unregisterTestController(id: string): void;
/** Requests tests from the given resource/uri, from the observer API. */
$subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
/** Stops requesting tests from the given resource/uri, from the observer API. */
$unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
/** Publishes that new tests were available on the given source. */
$publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void;
$updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void;
$appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void;
$appendOutputToRun(runId: string, output: VSBuffer): void;
/** Request by an extension to run tests. */
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<string>;
$publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void;
// --- test run handling:
/**
* Adds tests to the run. The tests are given in descending depth. The first
* item will be a previously-known test, or a test root.
*/
$addTestsToRun(runId: string, tests: ITestItem[]): void;
/** Updates the state of a test run in the given run. */
$updateTestStateInRun(runId: string, taskId: string, testId: string, state: TestResultState, duration?: number): void;
/** Appends a message to a test in the run. */
$appendTestMessageInRun(runId: string, taskId: string, testId: string, message: ITestMessage): void;
/** Appends raw output to the test run.. */
$appendOutputToRun(runId: string, taskId: string, output: VSBuffer): void;
/** Signals a task in a test run started. */
$startedTestRunTask(runId: string, task: ITestRunTask): void;
/** Signals a task in a test run ended. */
$finishedTestRunTask(runId: string, taskId: string): void;
/** Start a new extension-provided test run. */
$startedExtensionTestRun(req: ExtensionRunTestsRequest): void;
/** Signals that an extension-provided test run finished. */
$finishedExtensionTestRun(runId: string): void;
}
// --- proxy identifiers

View file

@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import { mapFind } from 'vs/base/common/arrays';
import { disposableTimeout } from 'vs/base/common/async';
import { Barrier, DeferredPromise, disposableTimeout, isThenable } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
import { Iterable } from 'vs/base/common/iterator';
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { deepFreeze } from 'vs/base/common/objects';
import { isDefined } from 'vs/base/common/types';
@ -23,16 +24,20 @@ import * as Convert from 'vs/workbench/api/common/extHostTypeConverters';
import { Disposable, TestItemImpl } from 'vs/workbench/api/common/extHostTypes';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { OwnedTestCollection, SingleUseTestCollection, TestPosition } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, RunTestForProviderRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForProviderRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import type * as vscode from 'vscode';
const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`;
export class ExtHostTesting implements ExtHostTestingShape {
private readonly resultsChangedEmitter = new Emitter<void>();
private readonly controllers = new Map<string, vscode.TestController<unknown>>();
private readonly controllers = new Map<string, {
extensionId: string,
instance: vscode.TestController<unknown>
}>();
private readonly proxy: MainThreadTestingShape;
private readonly ownedTests = new OwnedTestCollection();
private readonly runQueue: TestRunQueue;
private readonly testControllers = new Map<string, {
collection: SingleUseTestCollection;
store: IDisposable;
@ -47,6 +52,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
constructor(@IExtHostRpcService rpc: IExtHostRpcService, @IExtHostDocumentsAndEditors private readonly documents: IExtHostDocumentsAndEditors, @IExtHostWorkspace private readonly workspace: IExtHostWorkspace) {
this.proxy = rpc.getProxy(MainContext.MainThreadTesting);
this.runQueue = new TestRunQueue(this.proxy);
this.workspaceObservers = new WorkspaceFolderTestObserverFactory(this.proxy);
this.textDocumentObservers = new TextDocumentTestObserverFactory(this.proxy, documents);
}
@ -54,9 +60,9 @@ export class ExtHostTesting implements ExtHostTestingShape {
/**
* Implements vscode.test.registerTestProvider
*/
public registerTestController<T>(controller: vscode.TestController<T>): vscode.Disposable {
public registerTestController<T>(extensionId: string, controller: vscode.TestController<T>): vscode.Disposable {
const controllerId = generateUuid();
this.controllers.set(controllerId, controller);
this.controllers.set(controllerId, { instance: controller, extensionId });
this.proxy.$registerTestController(controllerId);
// give the ext a moment to register things rather than synchronously invoking within activate()
@ -105,10 +111,10 @@ export class ExtHostTesting implements ExtHostTestingShape {
}
/**
* Implements vscode.test.publishTestResults
* Implements vscode.test.createTestRunTask
*/
public publishExtensionProvidedResults(results: vscode.TestRunResult, persist: boolean): void {
this.proxy.$publishExtensionProvidedResults(Convert.TestResults.from(generateUuid(), results), persist);
public createTestRunTask<T>(extensionId: string, request: vscode.TestRunRequest<T>, name: string | undefined, persist = true): vscode.TestRunTask<T> {
return this.runQueue.createTestRunTask(extensionId, request, name, persist);
}
/**
@ -188,8 +194,8 @@ export class ExtHostTesting implements ExtHostTestingShape {
const collection = disposable.add(this.ownedTests.createForHierarchy(
diff => this.proxy.$publishDiff(resource, uriComponents, diff)));
disposable.add(toDisposable(() => cancellation.dispose(true)));
for (const [id, provider] of this.controllers) {
subscribeFn(id, provider);
for (const [id, controller] of this.controllers) {
subscribeFn(id, controller.instance);
}
// note: we don't increment the root count initially -- this is done by the
@ -239,14 +245,14 @@ export class ExtHostTesting implements ExtHostTestingShape {
* providers to be run.
* @override
*/
public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise<void> {
const provider = this.controllers.get(req.tests[0].src.provider);
if (!provider) {
public async $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise<void> {
const controller = this.controllers.get(req.tests[0].src.controller);
if (!controller) {
return;
}
const includeTests = req.tests
.map(({ testId, src }) => this.ownedTests.getTestById(testId, src.tree))
.map(({ testId, src }) => this.ownedTests.getTestById(testId, src?.tree))
.filter(isDefined)
.map(([_tree, test]) => test);
@ -261,50 +267,19 @@ export class ExtHostTesting implements ExtHostTestingShape {
return;
}
const isExcluded = (test: vscode.TestItem<unknown>) => {
// for test providers that don't support excluding natively,
// make sure not to report excluded result otherwise summaries will be off.
for (const [tree, exclude] of excludeTests) {
const e = tree.comparePositions(exclude, test.id);
if (e === TestPosition.IsChild || e === TestPosition.IsSame) {
return true;
}
}
return false;
const publicReq: vscode.TestRunRequest<unknown> = {
tests: includeTests.map(t => TestItemFilteredWrapper.unwrap(t.actual)),
exclude: excludeTests.map(([, t]) => TestItemFilteredWrapper.unwrap(t.actual)),
debug: req.debug,
};
try {
await provider.runTests({
appendOutput: message => {
this.proxy.$appendOutputToRun(req.runId, VSBuffer.fromString(message));
},
appendMessage: (test, message) => {
if (!isExcluded(test)) {
this.flushCollectionDiffs();
this.proxy.$appendTestMessageInRun(req.runId, test.id, Convert.TestMessage.from(message));
}
},
setState: (test, state, duration) => {
if (!isExcluded(test)) {
this.flushCollectionDiffs();
this.proxy.$updateTestStateInRun(req.runId, test.id, state, duration);
}
},
tests: includeTests.map(t => TestItemFilteredWrapper.unwrap(t.actual)),
exclude: excludeTests.map(([, t]) => TestItemFilteredWrapper.unwrap(t.actual)),
debug: req.debug,
}, cancellation);
for (const { collection } of this.testControllers.values()) {
collection.flushDiff(); // ensure all states are updated
}
return;
} catch (e) {
console.error(e); // so it appears to attached debuggers
throw e;
}
await this.runQueue.enqueueRun({
dto: TestRunDto.fromInternal(req),
token,
extensionId: controller.extensionId,
req: publicReq,
doRun: () => controller!.instance.runTests(publicReq, token)
});
}
public $lookupTest(req: TestIdWithSrc): Promise<InternalTestItem | undefined> {
@ -340,6 +315,224 @@ export class ExtHostTesting implements ExtHostTestingShape {
}
}
/**
* Queues runs for a single extension and provides the currently-executing
* run so that `createTestRunTask` can be properly correlated.
*/
class TestRunQueue {
private readonly state = new Map</* extensionId */ string, {
current: {
publicReq: vscode.TestRunRequest<unknown>,
factory: (name: string | undefined) => TestRunTask<unknown>,
},
queue: (() => (Promise<void> | void))[];
}>();
constructor(private readonly proxy: MainThreadTestingShape) { }
/**
* Registers and enqueues a test run. `doRun` will be called when an
* invokation to {@link TestController.runTests} should be called.
*/
public enqueueRun(opts: {
extensionId: string,
req: vscode.TestRunRequest<unknown>,
dto: TestRunDto,
token: CancellationToken,
doRun: () => Thenable<void> | void,
},
) {
let record = this.state.get(opts.extensionId);
if (!record) {
record = { queue: [], current: undefined as any };
this.state.set(opts.extensionId, record);
}
const deferred = new DeferredPromise<void>();
const runner = () => {
const tasks: TestRunTask<unknown>[] = [];
const shared = new Set<string>();
record!.current = {
publicReq: opts.req,
factory: name => {
const task = new TestRunTask(name, opts.dto, shared, this.proxy);
tasks.push(task);
opts.token.onCancellationRequested(() => task.end());
return task;
},
};
this.invokeRunner(opts.extensionId, opts.dto.id, opts.doRun, tasks).finally(() => deferred.complete());
};
record.queue.push(runner);
if (record.queue.length === 1) {
runner();
}
return deferred.p;
}
/**
* Implements the public `createTestRunTask` API.
*/
public createTestRunTask<T>(extensionId: string, request: vscode.TestRunRequest<T>, name: string | undefined, persist: boolean): vscode.TestRunTask<T> {
const state = this.state.get(extensionId);
// If the request is for the currently-executing `runTests`, then correlate
// it to that existing run. Otherwise return a new, detached run.
if (state?.current.publicReq === request) {
return state.current.factory(name);
}
const dto = TestRunDto.fromPublic(request);
const task = new TestRunTask(name, dto, new Set(), this.proxy);
this.proxy.$startedExtensionTestRun({
debug: request.debug,
exclude: request.exclude?.map(t => t.id) ?? [],
id: dto.id,
tests: request.tests.map(t => t.id),
persist: persist
});
task.onEnd.wait().then(() => this.proxy.$finishedExtensionTestRun(dto.id));
return task;
}
private invokeRunner<T>(extensionId: string, runId: string, fn: () => Thenable<void> | void, tasks: TestRunTask<T>[]): Promise<void> {
try {
const res = fn();
if (isThenable(res)) {
return res
.then(() => this.handleInvokeResult(extensionId, runId, tasks, undefined))
.catch(err => this.handleInvokeResult(extensionId, runId, tasks, err));
} else {
return this.handleInvokeResult(extensionId, runId, tasks, undefined);
}
} catch (e) {
return this.handleInvokeResult(extensionId, runId, tasks, e);
}
}
private async handleInvokeResult<T>(extensionId: string, runId: string, tasks: TestRunTask<T>[], error?: Error) {
const record = this.state.get(extensionId);
if (!record) {
return;
}
record.queue.shift();
if (record.queue.length > 0) {
record.queue[0]();
} else {
this.state.delete(extensionId);
}
await Promise.all(tasks.map(t => t.onEnd.wait()));
}
}
class TestRunDto {
public static fromPublic(request: vscode.TestRunRequest<unknown>) {
return new TestRunDto(
generateUuid(),
new Set(request.tests.map(t => t.id)),
new Set(request.exclude?.map(t => t.id) ?? Iterable.empty()),
);
}
public static fromInternal(request: RunTestForProviderRequest) {
return new TestRunDto(
request.runId,
new Set(request.tests.map(t => t.testId)),
new Set(request.excludeExtIds),
);
}
constructor(
public readonly id: string,
private readonly include: ReadonlySet<string>,
private readonly exclude: ReadonlySet<string>,
) { }
public isIncluded(test: vscode.TestItem<unknown>) {
for (let t: vscode.TestItem<unknown> | undefined = test; t; t = t.parent) {
if (this.include.has(t.id)) {
return true;
} else if (this.exclude.has(t.id)) {
return false;
}
}
return true;
}
}
class TestRunTask<T> implements vscode.TestRunTask<T> {
readonly #proxy: MainThreadTestingShape;
readonly #req: TestRunDto;
readonly #taskId = generateUuid();
readonly #sharedIds: Set<string>;
public readonly onEnd = new Barrier();
constructor(
public readonly name: string | undefined,
dto: TestRunDto,
sharedTestIds: Set<string>,
proxy: MainThreadTestingShape,
) {
this.#proxy = proxy;
this.#req = dto;
this.#sharedIds = sharedTestIds;
proxy.$startedTestRunTask(dto.id, { id: this.#taskId, name, running: true });
}
setState(test: vscode.TestItem<T>, state: vscode.TestResultState, duration?: number): void {
if (this.#req.isIncluded(test)) {
this.ensureTestIsKnown(test);
this.#proxy.$updateTestStateInRun(this.#req.id, this.#taskId, test.id, state, duration);
}
}
appendMessage(test: vscode.TestItem<T>, message: vscode.TestMessage): void {
if (this.#req.isIncluded(test)) {
this.ensureTestIsKnown(test);
this.#proxy.$appendTestMessageInRun(this.#req.id, this.#taskId, test.id, Convert.TestMessage.from(message));
}
}
appendOutput(output: string): void {
this.#proxy.$appendOutputToRun(this.#req.id, this.#taskId, VSBuffer.fromString(output));
}
end(): void {
this.#proxy.$finishedTestRunTask(this.#req.id, this.#taskId);
this.onEnd.open();
}
private ensureTestIsKnown(test: vscode.TestItem<T>) {
const sent = this.#sharedIds;
if (sent.has(test.id)) {
return;
}
const chain: ITestItem[] = [];
while (true) {
chain.unshift(Convert.TestItem.from(test));
if (sent.has(test.id)) {
break;
}
sent.add(test.id);
if (!test.parent) {
break;
}
test = test.parent;
}
this.#proxy.$addTestsToRun(this.#req.id, chain);
}
}
export const createDefaultDocumentTestRoot = async <T>(
provider: vscode.TestController<T>,
document: vscode.TextDocument,
@ -439,7 +632,9 @@ export class TestItemFilteredWrapper extends TestItemImpl {
(this as Record<string, unknown>)[evt[1]] = evt[2];
break;
case ExtHostTestItemEventType.NewChild:
TestItemFilteredWrapper.getWrapperForTestItem(evt[1], this.filterDocument, this).refreshMatch();
const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(evt[1], this.filterDocument, this);
getPrivateApiFor(wrapper).parent = actual;
wrapper.refreshMatch();
break;
default:
wrapperApi.bus.fire(evt);

View file

@ -28,7 +28,7 @@ import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebo
import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor';
import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon';
import * as search from 'vs/workbench/contrib/search/common/search';
import { ISerializedTestResults, ITestItem, ITestMessage, SerializedTestResultItem, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection';
import { ISerializedTestResults, ITestItem, ITestMessage, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import type * as vscode from 'vscode';
import * as types from './extHostTypes';
@ -1702,52 +1702,13 @@ export namespace TestItem {
}
export namespace TestResults {
export function from(id: string, results: vscode.TestRunResult): ISerializedTestResults {
const serialized: ISerializedTestResults = {
completedAt: results.completedAt,
id,
output: results.output,
items: [],
};
const queue: [parent: SerializedTestResultItem | null, children: Iterable<vscode.TestResultSnapshot>][] = [
[null, results.results],
];
while (queue.length) {
const [parent, children] = queue.pop()!;
for (const item of children) {
const serializedItem: SerializedTestResultItem = {
children: item.children?.map(c => c.id) ?? [],
computedState: item.state,
item: TestItem.fromResultSnapshot(item),
state: {
state: item.state,
duration: item.duration,
messages: item.messages.map(TestMessage.from),
},
retired: undefined,
expand: TestItemExpandState.Expanded,
parent: parent?.item.extId ?? null,
src: { provider: '', tree: -1 },
direct: !parent,
};
serialized.items.push(serializedItem);
if (item.children) {
queue.push([serializedItem, item.children]);
}
}
}
return serialized;
}
const convertTestResultItem = (item: SerializedTestResultItem, byInternalId: Map<string, SerializedTestResultItem>): vscode.TestResultSnapshot => ({
...TestItem.toPlain(item.item),
state: item.state.state,
duration: item.state.duration,
messages: item.state.messages.map(TestMessage.to),
taskStates: item.tasks.map(t => ({
state: t.state,
duration: t.duration,
messages: t.messages.map(TestMessage.to),
})),
children: item.children
.map(c => byInternalId.get(c))
.filter(isDefined)

View file

@ -3248,6 +3248,7 @@ const testItemPropAccessor = <K extends keyof vscode.TestItem<never>>(
api: IExtHostTestItemApi,
key: K,
defaultValue: vscode.TestItem<never>[K],
equals: (a: vscode.TestItem<never>[K], b: vscode.TestItem<never>[K]) => boolean
) => {
let value = defaultValue;
return {
@ -3257,7 +3258,7 @@ const testItemPropAccessor = <K extends keyof vscode.TestItem<never>>(
return value;
},
set(newValue: vscode.TestItem<never>[K]) {
if (newValue !== value) {
if (!equals(value, newValue)) {
value = newValue;
api.bus.fire([ExtHostTestItemEventType.SetProp, key, newValue]);
}
@ -3265,6 +3266,12 @@ const testItemPropAccessor = <K extends keyof vscode.TestItem<never>>(
};
};
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 class TestItemImpl implements vscode.TestItem<unknown> {
public readonly id!: string;
@ -3305,11 +3312,11 @@ export class TestItemImpl implements vscode.TestItem<unknown> {
enumerable: true,
writable: false,
},
range: testItemPropAccessor(api, 'range', undefined),
description: testItemPropAccessor(api, 'description', undefined),
runnable: testItemPropAccessor(api, 'runnable', true),
debuggable: testItemPropAccessor(api, 'debuggable', true),
status: testItemPropAccessor(api, 'status', TestItemStatus.Resolved),
range: testItemPropAccessor(api, 'range', undefined, rangeComparator),
description: testItemPropAccessor(api, 'description', undefined, strictEqualComparator),
runnable: testItemPropAccessor(api, 'runnable', true, strictEqualComparator),
debuggable: testItemPropAccessor(api, 'debuggable', true, strictEqualComparator),
status: testItemPropAccessor(api, 'status', TestItemStatus.Resolved, strictEqualComparator),
});
}

View file

@ -68,7 +68,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
for (const inTree of [...this.items.values()].sort((a, b) => b.depth - a.depth)) {
const lookup = this.results.getStateById(inTree.test.item.extId)?.[1];
inTree.ownState = lookup?.state.state ?? TestResultState.Unset;
const computed = lookup?.computedState ?? TestResultState.Unset;
if (computed !== inTree.state) {
inTree.state = computed;
@ -84,7 +83,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
this._register(results.onTestChanged(({ item: result }) => {
const item = this.items.get(result.item.extId);
if (item) {
item.ownState = result.state.state;
item.retired = result.retired;
refreshComputedState(computedStateAccessor, item, this.addUpdated, result.computedState);
this.addUpdated(item);
@ -270,7 +268,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
const prevState = this.results.getStateById(treeElement.test.item.extId)?.[1];
if (prevState) {
treeElement.ownState = prevState.state.state;
treeElement.retired = prevState.retired;
refreshComputedState(computedStateAccessor, treeElement, this.addUpdated, prevState.computedState);
}

View file

@ -140,7 +140,7 @@ export class HierarchicalByNameProjection extends HierarchicalByLocationProjecti
const parent = this.getOrCreateFolderElement(folder);
const actualParent = item.parent ? this.items.get(item.parent) as HierarchicalByNameElement : undefined;
for (const testRoot of parent.children) {
if (testRoot.test.src.provider === item.src.provider) {
if (testRoot.test.src.controller === item.src.controller) {
return new HierarchicalByNameElement(item, testRoot, r => this.changes.addedOrRemoved(r), actualParent);
}
}

View file

@ -55,7 +55,6 @@ export class HierarchicalElement implements ITestTreeElement {
public state = TestResultState.Unset;
public retired = false;
public ownState = TestResultState.Unset;
constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) {
this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese
@ -97,7 +96,6 @@ export class HierarchicalFolder implements ITestTreeElement {
public retired = false;
public state = TestResultState.Unset;
public ownState = TestResultState.Unset;
constructor(public readonly folder: IWorkspaceFolder) { }

View file

@ -112,7 +112,6 @@ export interface ITestTreeElement {
*/
readonly retired: boolean;
readonly ownState: TestResultState;
readonly label: string;
readonly parentItem: ITestTreeElement | null;
}

View file

@ -72,10 +72,8 @@
.monaco-action-bar
.action-item
> .action-label {
width: 16px;
height: 100%;
line-height: 22px;
margin-right: 8px;
padding: 2px;
margin-right: 2px;
}
.monaco-workbench .part > .title > .title-actions .action-label.codicon-testing-autorun::after {

View file

@ -440,8 +440,9 @@ export class ShowMostRecentOutputAction extends Action2 {
constructor() {
super({
id: 'testing.showMostRecentOutput',
title: localize('testing.showMostRecentOutput', "Show Most Recent Output"),
f1: false,
title: localize('testing.showMostRecentOutput', "Show Output"),
f1: true,
category,
icon: Codicon.terminal,
menu: {
id: MenuId.ViewTitle,
@ -878,7 +879,7 @@ abstract class RunOrDebugFailedTests extends RunOrDebugExtsById {
const resultSet = results[i];
for (const test of resultSet.tests) {
const path = this.getPathForTest(test, resultSet).join(sep);
if (isFailedState(test.state.state)) {
if (isFailedState(test.ownComputedState)) {
paths.add(path);
} else {
paths.delete(path);

View file

@ -185,17 +185,21 @@ export class TestingDecorations extends Disposable implements IEditorContributio
continue; // do not show decorations for outdated tests
}
for (let i = 0; i < stateItem.state.messages.length; i++) {
const m = stateItem.state.messages[i];
if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) {
const uri = buildTestUri({
type: TestUriType.ResultActualOutput,
messageIndex: i,
resultId: result.id,
testExtId: stateItem.item.extId,
});
for (let taskId = 0; taskId < stateItem.tasks.length; taskId++) {
const state = stateItem.tasks[taskId];
for (let i = 0; i < state.messages.length; i++) {
const m = state.messages[i];
if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) {
const uri = buildTestUri({
type: TestUriType.ResultActualOutput,
messageIndex: i,
taskIndex: taskId,
resultId: result.id,
testExtId: stateItem.item.extId,
});
newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor));
newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor));
}
}
}
}
@ -275,7 +279,7 @@ class RunTestDecoration extends Disposable implements ITestDecoration {
: test.children.size > 0 ? testingRunAllIcon : testingRunIcon;
const hoverMessage = new MarkdownString('', true).appendText(localize('failedHoverMessage', '{0} has failed. ', test.item.label));
if (stateItem?.state.messages.length) {
if (stateItem?.tasks.some(s => s.messages.length > 0)) {
const args = encodeURIComponent(JSON.stringify([test.item.extId]));
hoverMessage.appendMarkdown(`[${localize('failedPeekAction', 'Peek Error')}](command:vscode.peekTestError?${args})`);
}

View file

@ -475,7 +475,7 @@ export class TestingExplorerViewModel extends Disposable {
*/
private async tryPeekError(item: ITestTreeElement) {
const lookup = item.test && this.testResults.getStateById(item.test.item.extId);
return lookup && isFailedState(lookup[1].state.state)
return lookup && lookup[1].tasks.some(s => isFailedState(s.state))
? this.peekOpener.tryPeekFirstError(lookup[0], lookup[1], { preserveFocus: true })
: false;
}
@ -638,9 +638,9 @@ class TestsFilter implements ITreeFilter<ITestTreeElement> {
case TestExplorerStateFilter.All:
return FilterResult.Include;
case TestExplorerStateFilter.OnlyExecuted:
return element.ownState !== TestResultState.Unset ? FilterResult.Include : FilterResult.Inherit;
return element.state !== TestResultState.Unset ? FilterResult.Include : FilterResult.Inherit;
case TestExplorerStateFilter.OnlyFailed:
return isFailedState(element.ownState) ? FilterResult.Include : FilterResult.Inherit;
return isFailedState(element.state) ? FilterResult.Include : FilterResult.Inherit;
}
}

View file

@ -31,7 +31,7 @@ import { EditorModel } from 'vs/workbench/common/editor';
import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { ITestItem, ITestMessage, ITestState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestItem, ITestMessage, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
@ -42,7 +42,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
interface ITestDto {
test: ITestItem,
messageIndex: number;
state: ITestState;
messages: ITestMessage[];
expectedUri: URI;
actualUri: URI;
messageUri: URI;
@ -78,12 +78,12 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
* @returns a boolean if a peek was opened
*/
public async tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial<ITextEditorOptions>) {
const index = test.state.messages.findIndex(m => !!m.location);
if (index === -1) {
const candidate = this.getCandidateMessage(test);
if (!candidate) {
return false;
}
const message = test.state.messages[index];
const message = candidate.message;
const pane = await this.editorService.openEditor({
resource: message.location!.uri,
options: { selection: message.location!.range, revealIfOpened: true, ...options }
@ -96,7 +96,8 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
TestingOutputPeekController.get(control).show(buildTestUri({
type: TestUriType.ResultMessage,
messageIndex: index,
taskIndex: candidate.taskId,
messageIndex: candidate.index,
resultId: result.id,
testExtId: test.item.extId,
}));
@ -112,7 +113,8 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
return;
}
if (!isFailedState(evt.item.state.state) || !evt.item.state.messages.length) {
const candidate = this.getCandidateMessage(evt.item);
if (!candidate) {
return;
}
@ -137,6 +139,24 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener
this.tryPeekFirstError(evt.result, evt.item);
}
private getCandidateMessage(test: TestResultItem) {
for (let taskId = 0; taskId < test.tasks.length; taskId++) {
const { messages, state } = test.tasks[taskId];
if (!isFailedState(state)) {
continue;
}
const index = messages.findIndex(m => !!m.location);
if (index === -1) {
continue;
}
return { taskId, index, message: messages[index] };
}
return undefined;
}
}
/**
@ -205,7 +225,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
return;
}
const message = dto.state.messages[dto.messageIndex];
const message = dto.messages[dto.messageIndex];
if (!message?.location) {
return;
}
@ -253,7 +273,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
* else, then clear the peek.
*/
private closePeekOnTestChange(evt: TestResultItemChange) {
if (evt.reason !== TestResultItemChangeReason.OwnStateChange || evt.previous === evt.item.state.state) {
if (evt.reason !== TestResultItemChangeReason.OwnStateChange || evt.previous === evt.item.ownComputedState) {
return;
}
@ -273,9 +293,13 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
}
const test = this.testResults.getResult(parts.resultId)?.getStateById(parts.testExtId);
if (!test || !test.tasks[parts.taskIndex]) {
return;
}
return test && {
test: test.item,
state: test.state,
messages: test.tasks[parts.taskIndex].messages,
messageIndex: parts.messageIndex,
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
@ -382,8 +406,8 @@ class TestingDiffOutputPeek extends TestingOutputPeek {
/**
* @override
*/
public async setModel({ test, state, messageIndex, expectedUri, actualUri }: ITestDto) {
const message = state.messages[messageIndex];
public async setModel({ test, messages, messageIndex, expectedUri, actualUri }: ITestDto) {
const message = messages[messageIndex];
if (!message?.location) {
return;
}
@ -440,8 +464,8 @@ class TestingMessageOutputPeek extends TestingOutputPeek {
/**
* @override
*/
public async setModel({ state, test, messageIndex, messageUri }: ITestDto) {
const message = state.messages[messageIndex];
public async setModel({ messages, test, messageIndex, messageUri }: ITestDto) {
const message = messages[messageIndex];
if (!message?.location) {
return;
}

View file

@ -211,8 +211,8 @@ export class SingleUseTestCollection implements IDisposable {
/**
* Adds a new root node to the collection.
*/
public addRoot(item: TestItemRaw, providerId: string) {
this.addItem(item, providerId, null);
public addRoot(item: TestItemRaw, controllerId: string) {
this.addItem(item, controllerId, null);
}
/**
@ -313,7 +313,7 @@ export class SingleUseTestCollection implements IDisposable {
break;
case ExtHostTestItemEventType.NewChild:
this.addItem(evt[1], internal.src.provider, internal);
this.addItem(evt[1], internal.src.controller, internal);
break;
case ExtHostTestItemEventType.SetProp:
@ -335,7 +335,7 @@ export class SingleUseTestCollection implements IDisposable {
}
}
private addItem(actual: TestItemRaw, providerId: string, parent: OwnedCollectionTestItem | null) {
private addItem(actual: TestItemRaw, controllerId: string, 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`);
}
@ -351,7 +351,7 @@ export class SingleUseTestCollection implements IDisposable {
const parentId = parent ? parent.item.extId : null;
const expand = actual.resolveHandler ? TestItemExpandState.Expandable : TestItemExpandState.NotExpandable;
const pExpandLvls = parent?.expandLevels;
const src = { provider: providerId, tree: this.testIdToInternal.object.id };
const src = { controller: controllerId, tree: this.testIdToInternal.object.id };
const internal: OwnedCollectionTestItem = {
actual,
parent: parentId,
@ -366,6 +366,7 @@ export class SingleUseTestCollection implements IDisposable {
this.pushDiff([TestDiffOpType.Add, { parent: parentId, src, expand, item: internal.item }]);
const api = getPrivateApiFor(actual);
api.parent = parent?.actual;
api.bus.event(this.onTestItemEvent.bind(this, internal));
// important that this comes after binding the event bus otherwise we
@ -374,7 +375,7 @@ export class SingleUseTestCollection implements IDisposable {
// Discover any existing children that might have already been added
for (const child of api.children.values()) {
this.addItem(child, providerId, internal);
this.addItem(child, controllerId, internal);
}
}

View file

@ -9,9 +9,11 @@ import { IRange, Range } from 'vs/editor/common/core/range';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { TestMessageSeverity, TestResultState } from 'vs/workbench/api/common/extHostTypes';
export interface TestIdWithSrc {
export type TestIdWithSrc = Required<TestIdWithMaybeSrc>;
export interface TestIdWithMaybeSrc {
testId: string;
src: { provider: string; tree: number };
src?: { controller: string; tree: number };
}
/**
@ -24,14 +26,25 @@ export type TestIdPath = string[];
* Request to the main thread to run a set of tests.
*/
export interface RunTestsRequest {
tests: TestIdWithSrc[];
tests: TestIdWithMaybeSrc[];
exclude?: string[];
debug: boolean;
isAutoRun?: boolean;
}
/**
* Request from the main thread to run tests for a single provider.
* Request to the main thread to run a set of tests.
*/
export interface ExtensionRunTestsRequest {
id: string;
tests: string[];
exclude: string[];
debug: boolean;
persist: boolean;
}
/**
* Request from the main thread to run tests for a single controller.
*/
export interface RunTestForProviderRequest {
runId: string;
@ -56,17 +69,23 @@ export interface ITestMessage {
location: IRichLocation | undefined;
}
export interface ITestState {
export interface ITestTaskState {
state: TestResultState;
duration: number | undefined;
messages: ITestMessage[];
}
export interface ITestRunTask {
id: string;
name: string | undefined;
running: boolean;
}
/**
* The TestItem from .d.ts, as a plain object without children.
*/
export interface ITestItem {
/** ID of the test given by the test provider */
/** ID of the test given by the test controller */
extId: string;
label: string;
children?: never;
@ -88,7 +107,7 @@ export const enum TestItemExpandState {
* TestItem-like shape, butm with an ID and children as strings.
*/
export interface InternalTestItem {
src: { provider: string; tree: number };
src: { controller: string; tree: number };
expand: TestItemExpandState;
parent: string | null;
item: ITestItem;
@ -115,9 +134,15 @@ export const applyTestItemUpdate = (internal: InternalTestItem | ITestItemUpdate
/**
* Test result item used in the main thread.
*/
export interface TestResultItem extends IncrementalTestCollectionItem {
/** Current state of this test */
state: ITestState;
export interface TestResultItem {
/** Parent ID, if any */
parent: string | null;
/** Raw test item properties */
item: ITestItem;
/** State of this test in various tasks */
tasks: ITestTaskState[];
/** State of this test as a computation of its tasks */
ownComputedState: TestResultState;
/** Computed state based on children */
computedState: TestResultState;
/** True if the test is outdated */
@ -141,6 +166,8 @@ export interface ISerializedTestResults {
output?: string;
/** Subset of test result items */
items: SerializedTestResultItem[];
/** Tasks involved in the run. */
tasks: ITestRunTask[];
}
export const enum TestDiffOpType {
@ -150,7 +177,7 @@ export const enum TestDiffOpType {
Update,
/** Removes a test (and all its children) */
Remove,
/** Changes the number of providers who are yet to publish their collection roots. */
/** Changes the number of controllers who are yet to publish their collection roots. */
DeltaRootsComplete,
/** Retires a test/result */
Retire,
@ -226,9 +253,9 @@ export abstract class AbstractIncrementalTestCollection<T extends IncrementalTes
protected readonly roots = new Set<string>();
/**
* Number of 'busy' providers.
* Number of 'busy' controllers.
*/
protected busyProviderCount = 0;
protected busyControllerCount = 0;
/**
* Number of pending roots.
@ -259,7 +286,7 @@ export abstract class AbstractIncrementalTestCollection<T extends IncrementalTes
}
if (internalTest.expand === TestItemExpandState.BusyExpanding) {
this.updateBusyProviders(1);
this.updateBusyControllers(1);
}
break;
}
@ -274,7 +301,7 @@ export abstract class AbstractIncrementalTestCollection<T extends IncrementalTes
applyTestItemUpdate(existing, patch);
changes.update(existing);
if (patch.expand !== undefined && existing.expand === TestItemExpandState.BusyExpanding && patch.expand !== TestItemExpandState.BusyExpanding) {
this.updateBusyProviders(-1);
this.updateBusyControllers(-1);
}
break;
}
@ -302,7 +329,7 @@ export abstract class AbstractIncrementalTestCollection<T extends IncrementalTes
changes.remove(existing, existing !== toRemove);
if (existing.expand === TestItemExpandState.BusyExpanding) {
this.updateBusyProviders(-1);
this.updateBusyControllers(-1);
}
}
}
@ -331,15 +358,15 @@ export abstract class AbstractIncrementalTestCollection<T extends IncrementalTes
}
/**
* Updates the number of providers who are still discovering items.
* Updates the number of controllers who are still discovering items.
*/
protected updateBusyProviders(delta: number) {
this.busyProviderCount += delta;
protected updateBusyControllers(delta: number) {
this.busyControllerCount += delta;
}
/**
* Updates the number of test root sources who are yet to report. When
* the total pending test roots reaches 0, the roots for all providers
* the total pending test roots reaches 0, the roots for all controllers
* will exist in the collection.
*/
public updatePendingRoots(delta: number) {

View file

@ -11,9 +11,8 @@ import { URI } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
import { IncrementalTestCollectionItem, ISerializedTestResults, ITestMessage, RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService';
import { ExtensionRunTestsRequest, ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { maxPriority, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
export interface ITestResult {
/**
@ -42,6 +41,11 @@ export interface ITestResult {
*/
tests: IterableIterator<TestResultItem>;
/**
* List of this result's subtasks.
*/
tasks: ReadonlyArray<ITestRunTask>;
/**
* Gets the state of the test by its extension-assigned ID.
*/
@ -166,77 +170,20 @@ export class LiveOutputController {
}
}
interface TestResultItemWithChildren extends TestResultItem {
/** Children in the run */
children: TestResultItemWithChildren[];
}
const itemToNode = (
item: IncrementalTestCollectionItem,
byExtId: Map<string, TestResultItem>,
): TestResultItem => {
const n: TestResultItem = {
...item,
// shallow-clone the test to take a 'snapshot' of it at the point in time where tests run
item: { ...item.item },
children: new Set(item.children),
state: {
duration: undefined,
messages: [],
state: TestResultState.Unset
},
computedState: TestResultState.Unset,
retired: false,
};
byExtId.set(n.item.extId, n);
return n;
};
const makeParents = (
collection: IMainThreadTestCollection,
child: IncrementalTestCollectionItem,
byExtId: Map<string, TestResultItem>,
) => {
const parent = child.parent && collection.getNodeById(child.parent);
if (!parent) {
return;
}
let parentResultItem = byExtId.get(parent.item.extId);
if (parentResultItem) {
parentResultItem.children.add(child.item.extId);
return; // no need to recurse, all parents already in result
}
parentResultItem = itemToNode(parent, byExtId);
parentResultItem.children = new Set([child.item.extId]);
makeParents(collection, parent, byExtId);
};
const makeNodeAndChildren = (
collection: IMainThreadTestCollection,
test: IncrementalTestCollectionItem,
excluded: ReadonlySet<string>,
byExtId: Map<string, TestResultItem>,
isExecutedDirectly = true,
): TestResultItem => {
const existing = byExtId.get(test.item.extId);
if (existing) {
return existing;
}
const mapped = itemToNode(test, byExtId);
if (isExecutedDirectly) {
mapped.direct = true;
}
for (const childId of test.children) {
const child = collection.getNodeById(childId);
if (child && !excluded.has(childId)) {
makeNodeAndChildren(collection, child, excluded, byExtId, false);
}
}
return mapped;
};
const itemToNode = (item: ITestItem, parent: string | null): TestResultItemWithChildren => ({
parent,
item: { ...item },
children: [],
tasks: [],
ownComputedState: TestResultState.Unset,
computedState: TestResultState.Unset,
retired: false,
});
export const enum TestResultItemChangeReason {
Retired,
@ -255,39 +202,29 @@ export type TestResultItemChange = { item: TestResultItem; result: ITestResult }
* and marked as "complete" when the run finishes.
*/
export class LiveTestResult implements ITestResult {
/**
* Creates a new TestResult, pulling tests from the associated list
* of collections.
*/
public static from(
resultId: string,
collections: ReadonlyArray<IMainThreadTestCollection>,
output: LiveOutputController,
req: RunTestsRequest,
) {
const testByExtId = new Map<string, TestResultItem>();
const excludeSet = new Set<string>(req.exclude);
for (const test of req.tests) {
for (const collection of collections) {
const node = collection.getNodeById(test.testId);
if (!node) {
continue;
}
makeNodeAndChildren(collection, node, excludeSet, testByExtId);
makeParents(collection, node, testByExtId);
}
}
return new LiveTestResult(resultId, collections, testByExtId, excludeSet, output, !!req.isAutoRun);
}
private readonly completeEmitter = new Emitter<void>();
private readonly changeEmitter = new Emitter<TestResultItemChange>();
private readonly testById = new Map<string, TestResultItemWithChildren>();
private _completedAt?: number;
public readonly onChange = this.changeEmitter.event;
public readonly onComplete = this.completeEmitter.event;
public readonly tasks: ITestRunTask[] = [];
/**
* Test IDs directly included in this run.
*/
public readonly includedIds: ReadonlySet<string>;
/**
* Test IDs excluded from this run.
*/
public readonly excludedIds: ReadonlySet<string>;
/**
* Gets whether this test is from an auto-run.
*/
public readonly isAutoRun: boolean;
/**
* @inheritdoc
@ -308,21 +245,11 @@ export class LiveTestResult implements ITestResult {
return this.testById.values();
}
private readonly computedStateAccessor: IComputedStateAccessor<TestResultItem> = {
getOwnState: i => i.state.state,
private readonly computedStateAccessor: IComputedStateAccessor<TestResultItemWithChildren> = {
getOwnState: i => i.ownComputedState,
getCurrentComputedState: i => i.computedState,
setComputedState: (i, s) => i.computedState = s,
getChildren: i => {
const { testById: testByExtId } = this;
return (function* () {
for (const childId of i.children) {
const child = testByExtId.get(childId);
if (child) {
yield child;
}
}
})();
},
getChildren: i => i.children[Symbol.iterator](),
getParents: i => {
const { testById: testByExtId } = this;
return (function* () {
@ -341,13 +268,12 @@ export class LiveTestResult implements ITestResult {
constructor(
public readonly id: string,
private readonly collections: ReadonlyArray<IMainThreadTestCollection>,
private readonly testById: Map<string, TestResultItem>,
private readonly excluded: ReadonlySet<string>,
public readonly output: LiveOutputController,
public readonly isAutoRun: boolean,
private readonly req: ExtensionRunTestsRequest | RunTestsRequest,
) {
this.counts[TestResultState.Unset] = testById.size;
this.isAutoRun = 'isAutoRun' in this.req && !!this.req.isAutoRun;
this.includedIds = new Set(req.tests.map(t => typeof t === 'string' ? t : t.testId));
this.excludedIds = new Set(req.exclude);
}
/**
@ -358,47 +284,71 @@ export class LiveTestResult implements ITestResult {
}
/**
* Updates all tests in the collection to the given state.
* Adds a new run task to the results.
*/
public setAllToState(state: TestResultState, when: (_t: TestResultItem) => boolean) {
for (const test of this.testById.values()) {
if (when(test)) {
this.fireUpdateAndRefresh(test, state);
}
public addTask(task: ITestRunTask) {
const index = this.tasks.length;
this.tasks.push(task);
for (const test of this.tests) {
test.tasks.push({ duration: undefined, messages: [], state: TestResultState.Unset });
this.fireUpdateAndRefresh(test, index, TestResultState.Queued);
}
}
/**
* Add the chain of tests to the run. The first test in the chain should
* be either a test root, or a previously-known test.
*/
public addTestChainToRun(chain: ReadonlyArray<ITestItem>) {
let parent = this.testById.get(chain[0].extId);
if (!parent) { // must be a test root
parent = this.addTestToRun(chain[0], null);
}
for (let i = 1; i < chain.length; i++) {
parent = this.addTestToRun(chain[i], parent.item.extId);
}
for (let i = 0; i < this.tasks.length; i++) {
this.fireUpdateAndRefresh(parent, i, TestResultState.Queued);
}
return undefined;
}
/**
* Updates the state of the test by its internal ID.
*/
public updateState(testId: string, state: TestResultState, duration?: number) {
const entry = this.testById.get(testId) ?? this.addTestToRun(testId);
public updateState(testId: string, taskId: string, state: TestResultState, duration?: number) {
const entry = this.testById.get(testId);
if (!entry) {
return;
}
const index = this.mustGetTaskIndex(taskId);
if (duration !== undefined) {
entry.state.duration = duration;
entry.tasks[index].duration = duration;
}
this.fireUpdateAndRefresh(entry, state);
this.fireUpdateAndRefresh(entry, index, state);
}
/**
* Appends a message for the test in the run.
*/
public appendMessage(testId: string, message: ITestMessage) {
const entry = this.testById.get(testId) ?? this.addTestToRun(testId);
public appendMessage(testId: string, taskId: string, message: ITestMessage) {
const entry = this.testById.get(testId);
if (!entry) {
return;
}
entry.state.messages.push(message);
entry.tasks[this.mustGetTaskIndex(taskId)].messages.push(message);
this.changeEmitter.fire({
item: entry,
result: this,
reason: TestResultItemChangeReason.OwnStateChange,
previous: entry.state.state,
previous: entry.ownComputedState,
});
}
@ -409,24 +359,6 @@ export class LiveTestResult implements ITestResult {
return this.output.read();
}
private fireUpdateAndRefresh(entry: TestResultItem, newState: TestResultState) {
const previous = entry.state.state;
if (newState === previous) {
return;
}
entry.state.state = newState;
this.counts[previous]--;
this.counts[newState]++;
refreshComputedState(this.computedStateAccessor, entry, t =>
this.changeEmitter.fire(
t === entry
? { item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, previous }
: { item: t, result: this, reason: TestResultItemChangeReason.ComputedStateChange }
),
);
}
/**
* Marks a test as retired. This can trigger it to be rerun in live mode.
*/
@ -436,11 +368,10 @@ export class LiveTestResult implements ITestResult {
return;
}
const queue: Iterable<string>[] = [[root.item.extId]];
const queue = [[root]];
while (queue.length) {
for (const id of queue.pop()!) {
const entry = this.testById.get(id);
if (entry && !entry.retired) {
for (const entry of queue.pop()!) {
if (!entry.retired) {
entry.retired = true;
queue.push(entry.children);
this.changeEmitter.fire({
@ -456,23 +387,15 @@ export class LiveTestResult implements ITestResult {
}
/**
* Adds a test, by its ID, to the test run. This can end up being called
* if tests were started while discovery was still happening, so initially
* we didn't serialize/capture the test.
* Marks the task in the test run complete.
*/
private addTestToRun(testId: string) {
for (const collection of this.collections) {
let test = collection.getNodeById(testId);
if (test) {
const originalSize = this.testById.size;
makeParents(collection, test, this.testById);
const node = makeNodeAndChildren(collection, test, this.excluded, this.testById, false);
this.counts[TestResultState.Unset] += this.testById.size - originalSize;
return node;
}
}
return undefined;
public markTaskComplete(taskId: string) {
this.tasks[this.mustGetTaskIndex(taskId)].running = false;
this.setAllToState(
TestResultState.Unset,
taskId,
t => t.state === TestResultState.Queued || t.state === TestResultState.Running,
);
}
/**
@ -483,11 +406,11 @@ export class LiveTestResult implements ITestResult {
throw new Error('cannot complete a test result multiple times');
}
// un-queue any tests that weren't explicitly updated
this.setAllToState(
TestResultState.Unset,
t => t.state.state === TestResultState.Queued || t.state.state === TestResultState.Running,
);
for (const task of this.tasks) {
if (task.running) {
this.markTaskComplete(task.id);
}
}
this._completedAt = Date.now();
this.completeEmitter.fire();
@ -497,16 +420,80 @@ export class LiveTestResult implements ITestResult {
* @inheritdoc
*/
public toJSON(): ISerializedTestResults | undefined {
return this.completedAt ? this.doSerialize.getValue() : undefined;
return this.completedAt && !('persist' in this.req && this.req.persist === false)
? this.doSerialize.getValue()
: undefined;
}
/**
* Updates all tests in the collection to the given state.
*/
protected setAllToState(state: TestResultState, taskId: string, when: (task: ITestTaskState, item: TestResultItem) => boolean) {
const index = this.mustGetTaskIndex(taskId);
for (const test of this.testById.values()) {
if (when(test.tasks[index], test)) {
this.fireUpdateAndRefresh(test, index, state);
}
}
}
private fireUpdateAndRefresh(entry: TestResultItem, taskIndex: number, newState: TestResultState) {
const previousOwnComputed = entry.ownComputedState;
entry.tasks[taskIndex].state = newState;
const newOwnComputed = maxPriority(...entry.tasks.map(t => t.state));
if (newOwnComputed === previousOwnComputed) {
return;
}
entry.ownComputedState = newOwnComputed;
this.counts[previousOwnComputed]--;
this.counts[newOwnComputed]++;
refreshComputedState(this.computedStateAccessor, entry, t =>
this.changeEmitter.fire(
t === entry
? { item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, previous: previousOwnComputed }
: { item: t, result: this, reason: TestResultItemChangeReason.ComputedStateChange }
),
);
}
private addTestToRun(item: ITestItem, parent: string | null) {
const node = itemToNode(item, parent);
node.direct = this.includedIds.has(item.extId);
this.testById.set(item.extId, node);
this.counts[TestResultState.Unset]++;
if (parent) {
this.testById.get(parent)?.children.push(node);
}
if (this.tasks.length) {
for (let i = 0; i < this.tasks.length; i++) {
node.tasks.push({ duration: undefined, messages: [], state: TestResultState.Queued });
}
}
return node;
}
private mustGetTaskIndex(taskId: string) {
const index = this.tasks.findIndex(t => t.id === taskId);
if (index === -1) {
throw new Error(`Unknown task ${taskId} in updateState`);
}
return index;
}
private readonly doSerialize = new Lazy((): ISerializedTestResults => ({
id: this.id,
completedAt: this.completedAt!,
tasks: this.tasks,
items: [...this.testById.values()].map(entry => ({
...entry,
retired: undefined,
children: [...entry.children],
src: undefined,
children: [...entry.children.map(c => c.item.extId)],
})),
}));
}
@ -530,6 +517,11 @@ export class HydratedTestResult implements ITestResult {
*/
public readonly completedAt: number;
/**
* @inheritdoc
*/
public readonly tasks: ITestRunTask[];
/**
* @inheritdoc
*/
@ -546,19 +538,22 @@ export class HydratedTestResult implements ITestResult {
) {
this.id = serialized.id;
this.completedAt = serialized.completedAt;
this.tasks = serialized.tasks;
for (const item of serialized.items) {
const cast: TestResultItem = { ...item, retired: true, children: new Set(item.children) };
const cast: TestResultItem = { ...item, retired: true };
cast.item.uri = URI.revive(cast.item.uri);
for (const message of cast.state.messages) {
if (message.location) {
message.location.uri = URI.revive(message.location.uri);
message.location.range = Range.lift(message.location.range);
for (const task of cast.tasks) {
for (const message of task.messages) {
if (message.location) {
message.location.uri = URI.revive(message.location.uri);
message.location.range = Range.lift(message.location.range);
}
}
}
this.counts[item.state.state]++;
this.counts[item.ownComputedState]++;
this.testById.set(item.item.extId, cast);
}
}

View file

@ -7,17 +7,14 @@ import { findFirstInSorted } from 'vs/base/common/arrays';
import { RunOnceScheduler } from 'vs/base/common/async';
import { Emitter, Event } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
import { Iterable } from 'vs/base/common/iterator';
import { equals } from 'vs/base/common/objects';
import { generateUuid } from 'vs/base/common/uuid';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { ExtensionRunTestsRequest, RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService';
export type ResultChangeEvent =
| { completed: LiveTestResult }
@ -50,7 +47,7 @@ export interface ITestResultService {
/**
* Creates a new, live test result.
*/
createLiveResult(collections: ReadonlyArray<IMainThreadTestCollection>, req: RunTestsRequest): LiveTestResult;
createLiveResult(req: RunTestsRequest | ExtensionRunTestsRequest): LiveTestResult;
/**
* Adds a new test result to the collection.
@ -70,14 +67,6 @@ export interface ITestResultService {
export const ITestResultService = createDecorator<ITestResultService>('testResultService');
/**
* Returns if the tests in the results are exactly equal. Check the counts
* first as a cheap check before starting to iterate.
*/
const resultsEqual = (a: ITestResult, b: ITestResult) =>
a.completedAt === b.completedAt && equals(a.counts, b.counts) && Iterable.equals(a.tests, b.tests,
(at, bt) => equals(at.state, bt.state) && equals(at.item, bt.item));
export class TestResultService implements ITestResultService {
declare _serviceBrand: undefined;
private changeResultEmitter = new Emitter<ResultChangeEvent>();
@ -135,9 +124,13 @@ export class TestResultService implements ITestResultService {
/**
* @inheritdoc
*/
public createLiveResult(collections: ReadonlyArray<IMainThreadTestCollection>, req: RunTestsRequest) {
const id = generateUuid();
return this.push(LiveTestResult.from(id, collections, this.storage.getOutputController(id), req));
public createLiveResult(req: RunTestsRequest | ExtensionRunTestsRequest) {
if ('id' in req) {
return this.push(new LiveTestResult(req.id, this.storage.getOutputController(req.id), req));
} else {
const id = generateUuid();
return this.push(new LiveTestResult(id, this.storage.getOutputController(id), req));
}
}
/**
@ -148,11 +141,6 @@ export class TestResultService implements ITestResultService {
this.results.unshift(result);
} else {
const index = findFirstInSorted(this.results, r => r.completedAt !== undefined && r.completedAt <= result.completedAt!);
const prev = this.results[index];
if (prev && resultsEqual(result, prev)) {
return result;
}
this.results.splice(index, 0, result);
this.persistScheduler.schedule();
}
@ -166,7 +154,6 @@ export class TestResultService implements ITestResultService {
result.onChange(this.testChangeEmitter.fire, this.testChangeEmitter);
this.isRunning.set(true);
this.changeResultEmitter.fire({ started: result });
result.setAllToState(TestResultState.Queued, () => true);
} else {
this.changeResultEmitter.fire({ inserted: result });
// If this is not a new result, go through each of its tests. For each

View file

@ -3,11 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { groupBy } from 'vs/base/common/arrays';
import { groupBy, mapFind } from 'vs/base/common/arrays';
import { disposableTimeout } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle';
import { isDefined } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@ -74,7 +75,7 @@ export class TestService extends Disposable implements ITestService {
* @inheritdoc
*/
public async expandTest(test: TestIdWithSrc, levels: number) {
await this.testControllers.get(test.src.provider)?.expandTest(test, levels);
await this.testControllers.get(test.src.controller)?.expandTest(test, levels);
}
/**
@ -159,7 +160,7 @@ export class TestService extends Disposable implements ITestService {
}
}
return this.testControllers.get(test.src.provider)?.lookupTest(test);
return this.testControllers.get(test.src.controller)?.lookupTest(test);
}
/**
@ -193,18 +194,27 @@ export class TestService extends Disposable implements ITestService {
req.exclude = [...this.excludeTests.value];
}
const subscriptions = [...this.testSubscriptions.values()]
.filter(v => req.tests.some(t => v.collection.getNodeById(t.testId)))
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri));
const result = this.testResults.createLiveResult(subscriptions.map(s => s.object), req);
const result = this.testResults.createLiveResult(req);
const testsWithIds = req.tests.map(test => {
if (test.src) {
return test as TestIdWithSrc;
}
const subscribed = mapFind(this.testSubscriptions.values(), s => s.collection.getNodeById(test.testId));
if (!subscribed) {
return undefined;
}
return { testId: test.testId, src: subscribed.src };
}).filter(isDefined);
try {
const tests = groupBy(req.tests, (a, b) => a.src.provider === b.src.provider ? 0 : 1);
const tests = groupBy(testsWithIds, (a, b) => a.src.controller === b.src.controller ? 0 : 1);
const cancelSource = new CancellationTokenSource(token);
this.runningTests.set(req, cancelSource);
const requests = tests.map(
group => this.testControllers.get(group[0].src.provider)?.runTests(
group => this.testControllers.get(group[0].src.controller)?.runTests(
{
runId: result.id,
debug: req.debug,
@ -221,7 +231,6 @@ export class TestService extends Disposable implements ITestService {
return result;
} finally {
this.runningTests.delete(req);
subscriptions.forEach(s => s.dispose());
result.markComplete();
}
}
@ -372,7 +381,7 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
* @inheritdoc
*/
public get busyProviders() {
return this.busyProviderCount;
return this.busyControllerCount;
}
/**
@ -457,12 +466,12 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
* Applies the diff to the collection.
*/
public override apply(diff: TestsDiff) {
let prevBusy = this.busyProviderCount;
let prevBusy = this.busyControllerCount;
let prevPendingRoots = this.pendingRootCount;
super.apply(diff);
if (prevBusy !== this.busyProviderCount) {
this.busyProvidersChangeEmitter.fire(this.busyProviderCount);
if (prevBusy !== this.busyControllerCount) {
this.busyProvidersChangeEmitter.fire(this.busyControllerCount);
}
if (prevPendingRoots !== this.pendingRootCount) {
this.pendingRootChangeEmitter.fire(this.pendingRootCount);

View file

@ -3,9 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { URI } from 'vs/base/common/uri';
import { TestItemImpl, TestItemStatus, TestResultState } from 'vs/workbench/api/common/extHostTypes';
export { TestItemImpl, TestResultState } from 'vs/workbench/api/common/extHostTypes';
export * as Convert from 'vs/workbench/api/common/extHostTypeConverters';
export const stubTest = (label: string, idPrefix = 'id-', children: TestItemImpl[] = []): TestItemImpl => {
const item = new TestItemImpl(idPrefix + label, label, URI.file('/'), undefined);
if (children.length) {
@ -22,6 +26,24 @@ export const stubTest = (label: string, idPrefix = 'id-', children: TestItemImpl
return item;
};
export const testStubsChain = (stub: TestItemImpl, path: string[], slice = 0) => {
const tests = [stub];
for (const segment of path) {
if (stub.status !== TestItemStatus.Resolved) {
stub.resolveHandler!(CancellationToken.None);
}
stub = stub.children.get(segment)!;
if (!stub) {
throw new Error(`missing child ${segment}`);
}
tests.push(stub);
}
return tests.slice(slice);
};
export const testStubs = {
test: stubTest,
nested: (idPrefix = 'id-') => stubTest('root', idPrefix, [

View file

@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { AutoRunMode, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { InternalTestItem, TestDiffOpType, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestDiffOpType, TestIdWithMaybeSrc } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
@ -67,7 +67,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
*/
private makeRunner() {
let isRunning = false;
const rerunIds = new Map<string, TestIdWithSrc>();
const rerunIds = new Map<string, TestIdWithMaybeSrc>();
const store = new DisposableStore();
const cts = new CancellationTokenSource();
store.add(toDisposable(() => cts.dispose(true)));
@ -91,8 +91,8 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
}
}, delay));
const addToRerun = (test: InternalTestItem) => {
rerunIds.set(`${test.item.extId}/${test.src.provider}`, ({ testId: test.item.extId, src: test.src }));
const addToRerun = (test: TestIdWithMaybeSrc) => {
rerunIds.set(`${test.testId}/${test.src?.controller}`, test);
if (!isRunning) {
scheduler.schedule(delay);
}
@ -100,7 +100,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
store.add(this.results.onTestChanged(evt => {
if (evt.reason === TestResultItemChangeReason.Retired) {
addToRerun(evt.item);
addToRerun({ testId: evt.item.item.extId });
}
}));
@ -113,7 +113,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
for (const [, collection] of sub.workspaceFolderCollections) {
for (const rootId of collection.rootIds) {
const root = collection.getNodeById(rootId);
if (root) { addToRerun(root); }
if (root) { addToRerun({ testId: root.item.extId, src: root.src }); }
}
}
}
@ -122,7 +122,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun {
store.add(sub.onDiff(([, diff]) => {
for (const entry of diff) {
if (entry[0] === TestDiffOpType.Add) {
addToRerun(entry[1]);
addToRerun({ testId: entry[1].item.extId, src: entry[1].src });
}
}
}));

View file

@ -47,13 +47,13 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
let text: string | undefined;
switch (parsed.type) {
case TestUriType.ResultActualOutput:
text = test.state.messages[parsed.messageIndex]?.actualOutput;
text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.actualOutput;
break;
case TestUriType.ResultExpectedOutput:
text = test.state.messages[parsed.messageIndex]?.expectedOutput;
text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.expectedOutput;
break;
case TestUriType.ResultMessage:
text = test.state.messages[parsed.messageIndex]?.message.toString();
text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.message.toString();
break;
}

View file

@ -34,7 +34,25 @@ export const stateNodes = Object.entries(statePriority).reduce(
export const cmpPriority = (a: TestResultState, b: TestResultState) => statePriority[b] - statePriority[a];
export const maxPriority = (a: TestResultState, b: TestResultState) => statePriority[a] > statePriority[b] ? a : b;
export const maxPriority = (...states: TestResultState[]) => {
switch (states.length) {
case 0:
return TestResultState.Unset;
case 1:
return states[0];
case 2:
return statePriority[states[0]] > statePriority[states[1]] ? states[0] : states[1];
default:
let max = states[0];
for (let i = 1; i < states.length; i++) {
if (statePriority[max] < statePriority[states[i]]) {
max = states[i];
}
}
return max;
}
};
export const statesInOrder = Object.keys(statePriority).map(s => Number(s) as TestResultState).sort(cmpPriority);

View file

@ -15,6 +15,7 @@ export const enum TestUriType {
interface IResultTestUri {
resultId: string;
taskIndex: number;
testExtId: string;
}
@ -46,17 +47,18 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
const [locationId, ...request] = uri.path.slice(1).split('/');
if (request[0] === TestUriParts.Messages) {
const index = Number(request[1]);
const part = request[2];
const taskIndex = Number(request[1]);
const index = Number(request[2]);
const part = request[3];
const testExtId = uri.query;
if (type === TestUriParts.Results) {
switch (part) {
case TestUriParts.Text:
return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultMessage };
return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultMessage };
case TestUriParts.ActualOutput:
return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultActualOutput };
return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultActualOutput };
case TestUriParts.ExpectedOutput:
return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultExpectedOutput };
return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultExpectedOutput };
}
}
}
@ -69,20 +71,20 @@ export const buildTestUri = (parsed: ParsedTestUri): URI => {
scheme: TEST_DATA_SCHEME,
authority: TestUriParts.Results
};
const msgRef = (locationId: string, index: number, ...remaining: string[]) =>
const msgRef = (locationId: string, ...remaining: (string | number)[]) =>
URI.from({
...uriParts,
query: parsed.testExtId,
path: ['', locationId, TestUriParts.Messages, index, ...remaining].join('/'),
path: ['', locationId, TestUriParts.Messages, ...remaining].join('/'),
});
switch (parsed.type) {
case TestUriType.ResultActualOutput:
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ActualOutput);
return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.ActualOutput);
case TestUriType.ResultExpectedOutput:
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput);
return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.ExpectedOutput);
case TestUriType.ResultMessage:
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.Text);
return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.Text);
default:
throw new Error('Invalid test uri');
}

View file

@ -9,11 +9,11 @@ import { bufferToStream, newWriteableBufferStream, VSBuffer } from 'vs/base/comm
import { Lazy } from 'vs/base/common/lazy';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { NullLogService } from 'vs/platform/log/common/log';
import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestTaskState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, 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 { ReExportedTestRunState as TestRunState } from 'vs/workbench/contrib/testing/common/testStubs';
import { Convert, ReExportedTestRunState as TestRunState, TestItemImpl, TestResultState, testStubs, testStubsChain } from 'vs/workbench/contrib/testing/common/testStubs';
import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
@ -23,34 +23,60 @@ export const emptyOutputController = () => new LiveOutputController(
);
suite('Workbench - Test Results Service', () => {
const getLabelsIn = (it: Iterable<InternalTestItem>) => [...it].map(t => t.item.label).sort();
const getLabelsIn = (it: Iterable<TestResultItem>) => [...it].map(t => t.item.label).sort();
const getChangeSummary = () => [...changed]
.map(c => ({ reason: c.reason, label: c.item.item.label }))
.sort((a, b) => a.label.localeCompare(b.label));
let r: LiveTestResult;
let r: TestLiveTestResult;
let changed = new Set<TestResultItemChange>();
let tests: TestItemImpl;
const defaultOpts = {
exclude: [],
debug: false,
id: 'x',
persist: true,
};
class TestLiveTestResult extends LiveTestResult {
public setAllToState(state: TestResultState, taskId: string, when: (task: ITestTaskState, item: TestResultItem) => boolean) {
super.setAllToState(state, taskId, when);
}
}
setup(async () => {
changed = new Set();
r = LiveTestResult.from(
r = new TestLiveTestResult(
'foo',
[await getInitializedMainTestCollection()],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false },
{ ...defaultOpts, tests: ['id-a'] },
);
r.onChange(e => changed.add(e));
r.addTask({ id: 't', name: undefined, running: true });
tests = testStubs.nested();
r.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-aa']).map(Convert.TestItem.from));
r.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-ab'], 1).map(Convert.TestItem.from));
});
suite('LiveTestResult', () => {
test('is empty if no tests are requesteed', async () => {
const r = LiveTestResult.from('', [await getInitializedMainTestCollection()], emptyOutputController(), { tests: [], debug: false });
assert.deepStrictEqual(getLabelsIn(r.tests), []);
test('is empty if no tests are yet present', async () => {
assert.deepStrictEqual(getLabelsIn(new TestLiveTestResult(
'foo',
emptyOutputController(),
{ ...defaultOpts, tests: ['id-a'] },
).tests), []);
});
test('does not change or retire initially', () => {
assert.deepStrictEqual(0, changed.size);
test('initially queues with update', () => {
assert.deepStrictEqual(getChangeSummary(), [
{ label: 'a', reason: TestResultItemChangeReason.ComputedStateChange },
{ label: 'aa', reason: TestResultItemChangeReason.OwnStateChange },
{ label: 'ab', reason: TestResultItemChangeReason.OwnStateChange },
{ label: 'root', reason: TestResultItemChangeReason.ComputedStateChange },
]);
});
test('initializes with the subtree of requested tests', () => {
@ -60,19 +86,29 @@ suite('Workbench - Test Results Service', () => {
test('initializes with valid counts', () => {
assert.deepStrictEqual(r.counts, {
...makeEmptyCounts(),
[TestRunState.Unset]: 4
[TestRunState.Queued]: 2,
[TestRunState.Unset]: 2,
});
});
test('setAllToState', () => {
r.setAllToState(TestRunState.Queued, t => t.item.label !== 'root');
changed.clear();
r.setAllToState(TestRunState.Queued, 't', (_, t) => t.item.label !== 'root');
assert.deepStrictEqual(r.counts, {
...makeEmptyCounts(),
[TestRunState.Unset]: 1,
[TestRunState.Queued]: 3,
});
assert.deepStrictEqual(r.getStateById('id-a')?.state.state, TestRunState.Queued);
r.setAllToState(TestRunState.Passed, 't', (_, t) => t.item.label !== 'root');
assert.deepStrictEqual(r.counts, {
...makeEmptyCounts(),
[TestRunState.Unset]: 1,
[TestRunState.Passed]: 3,
});
assert.deepStrictEqual(r.getStateById('id-a')?.ownComputedState, TestRunState.Passed);
assert.deepStrictEqual(r.getStateById('id-a')?.tasks[0].state, TestRunState.Passed);
assert.deepStrictEqual(getChangeSummary(), [
{ label: 'a', reason: TestResultItemChangeReason.OwnStateChange },
{ label: 'aa', reason: TestResultItemChangeReason.OwnStateChange },
@ -82,22 +118,26 @@ suite('Workbench - Test Results Service', () => {
});
test('updateState', () => {
r.updateState('id-a', TestRunState.Running);
changed.clear();
r.updateState('id-aa', 't', TestRunState.Running);
assert.deepStrictEqual(r.counts, {
...makeEmptyCounts(),
[TestRunState.Unset]: 2,
[TestRunState.Running]: 1,
[TestRunState.Unset]: 3,
[TestRunState.Queued]: 1,
});
assert.deepStrictEqual(r.getStateById('id-a')?.state.state, TestRunState.Running);
assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Running);
// update computed state:
assert.deepStrictEqual(r.getStateById('id-root')?.computedState, TestRunState.Running);
assert.deepStrictEqual(getChangeSummary(), [
{ label: 'a', reason: TestResultItemChangeReason.OwnStateChange },
{ label: 'a', reason: TestResultItemChangeReason.ComputedStateChange },
{ label: 'aa', reason: TestResultItemChangeReason.OwnStateChange },
{ label: 'root', reason: TestResultItemChangeReason.ComputedStateChange },
]);
});
test('retire', () => {
changed.clear();
r.retire('id-a');
assert.deepStrictEqual(getChangeSummary(), [
{ label: 'a', reason: TestResultItemChangeReason.Retired },
@ -110,21 +150,20 @@ suite('Workbench - Test Results Service', () => {
assert.strictEqual(changed.size, 0);
});
test('addTestToRun', () => {
r.updateState('id-b', TestRunState.Running);
test('ignores outside run', () => {
changed.clear();
r.updateState('id-b', 't', TestRunState.Running);
assert.deepStrictEqual(r.counts, {
...makeEmptyCounts(),
[TestRunState.Running]: 1,
[TestRunState.Unset]: 4,
[TestRunState.Queued]: 2,
[TestRunState.Unset]: 2,
});
assert.deepStrictEqual(r.getStateById('id-b')?.state.state, TestRunState.Running);
// update computed state:
assert.deepStrictEqual(r.getStateById('id-root')?.computedState, TestRunState.Running);
assert.deepStrictEqual(r.getStateById('id-b'), undefined);
});
test('markComplete', () => {
r.setAllToState(TestRunState.Queued, () => true);
r.updateState('id-aa', TestRunState.Passed);
r.setAllToState(TestRunState.Queued, 't', () => true);
r.updateState('id-aa', 't', TestRunState.Passed);
changed.clear();
r.markComplete();
@ -135,8 +174,8 @@ suite('Workbench - Test Results Service', () => {
[TestRunState.Unset]: 3,
});
assert.deepStrictEqual(r.getStateById('id-root')?.state.state, TestRunState.Unset);
assert.deepStrictEqual(r.getStateById('id-aa')?.state.state, TestRunState.Passed);
assert.deepStrictEqual(r.getStateById('id-root')?.ownComputedState, TestRunState.Unset);
assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Passed);
});
});
@ -160,7 +199,7 @@ suite('Workbench - Test Results Service', () => {
test('serializes and re-hydrates', async () => {
results.push(r);
r.updateState('id-aa', TestRunState.Passed);
r.updateState('id-aa', 't', TestRunState.Passed);
r.markComplete();
await timeout(0); // allow persistImmediately async to happen
@ -175,12 +214,12 @@ suite('Workbench - Test Results Service', () => {
const [rehydrated, actual] = results.getStateById('id-root')!;
const expected: any = { ...r.getStateById('id-root')! };
delete expected.state.duration; // delete undefined props that don't survive serialization
delete expected.tasks[0].duration; // delete undefined props that don't survive serialization
delete expected.item.range;
delete expected.item.description;
expected.item.uri = actual.item.uri;
assert.deepStrictEqual(actual, { ...expected, retired: true });
assert.deepStrictEqual(actual, { ...expected, src: undefined, retired: true, children: ['id-a'] });
assert.deepStrictEqual(rehydrated.counts, r.counts);
assert.strictEqual(typeof rehydrated.completedAt, 'number');
});
@ -189,11 +228,10 @@ suite('Workbench - Test Results Service', () => {
results.push(r);
r.markComplete();
const r2 = results.push(LiveTestResult.from(
const r2 = results.push(new LiveTestResult(
'',
[await getInitializedMainTestCollection()],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false }
{ ...defaultOpts, tests: [] }
));
results.clear();
@ -202,11 +240,10 @@ suite('Workbench - Test Results Service', () => {
test('keeps ongoing tests on top', async () => {
results.push(r);
const r2 = results.push(LiveTestResult.from(
const r2 = results.push(new LiveTestResult(
'',
[await getInitializedMainTestCollection()],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false }
{ ...defaultOpts, tests: [] }
));
assert.deepStrictEqual(results.results, [r2, r]);
@ -219,10 +256,12 @@ suite('Workbench - Test Results Service', () => {
const makeHydrated = async (completedAt = 42, state = TestRunState.Passed) => new HydratedTestResult({
completedAt,
id: 'some-id',
tasks: [{ id: 't', running: false, name: undefined }],
items: [{
...(await getInitializedMainTestCollection()).getNodeById('id-a')!,
state: { state, duration: 0, messages: [] },
tasks: [{ state, duration: 0, messages: [] }],
computedState: state,
ownComputedState: state,
retired: undefined,
children: [],
}]
@ -235,16 +274,14 @@ suite('Workbench - Test Results Service', () => {
assert.deepStrictEqual(results.results, [r, hydrated]);
});
test('deduplicates identical results', async () => {
test('inserts in correct order', async () => {
results.push(r);
const hydrated1 = await makeHydrated();
results.push(hydrated1);
const hydrated2 = await makeHydrated();
results.push(hydrated2);
assert.deepStrictEqual(results.results, [r, hydrated1]);
});
test('does not deduplicate if different completedAt', async () => {
test('inserts in correct order 2', async () => {
results.push(r);
const hydrated1 = await makeHydrated();
results.push(hydrated1);
@ -252,14 +289,5 @@ suite('Workbench - Test Results Service', () => {
results.push(hydrated2);
assert.deepStrictEqual(results.results, [r, hydrated1, hydrated2]);
});
test('does not deduplicate if different tests', async () => {
results.push(r);
const hydrated1 = await makeHydrated();
results.push(hydrated1);
const hydrated2 = await makeHydrated(undefined, TestRunState.Failed);
results.push(hydrated2);
assert.deepStrictEqual(results.results, [r, hydrated2, hydrated1]);
});
});
});

View file

@ -8,24 +8,32 @@ import { range } from 'vs/base/common/arrays';
import { NullLogService } from 'vs/platform/log/common/log';
import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testServiceImpl';
import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection';
import { Convert, testStubs, testStubsChain } from 'vs/workbench/contrib/testing/common/testStubs';
import { emptyOutputController } from 'vs/workbench/contrib/testing/test/common/testResultService.test';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
suite('Workbench - Test Result Storage', () => {
let storage: InMemoryResultStorage;
let collection: MainThreadTestCollection;
const makeResult = (addMessage?: string) => {
const t = LiveTestResult.from(
const t = new LiveTestResult(
'',
[collection],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false }
{
tests: [],
exclude: [],
debug: false,
id: 'x',
persist: true,
}
);
t.addTask({ id: 't', name: undefined, running: true });
const tests = testStubs.nested();
t.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-aa']).map(Convert.TestItem.from));
if (addMessage) {
t.appendMessage('id-a', {
t.appendMessage('id-a', 't', {
message: addMessage,
actualOutput: undefined,
expectedOutput: undefined,
@ -41,7 +49,6 @@ suite('Workbench - Test Result Storage', () => {
assert.deepStrictEqual((await storage.read()).map(r => r.id), stored.map(s => s.id));
setup(async () => {
collection = await getInitializedMainTestCollection();
storage = new InMemoryResultStorage(new TestStorageService(), new NullLogService());
});
@ -68,7 +75,7 @@ suite('Workbench - Test Result Storage', () => {
test('limits stored result by budget', async () => {
const r = range(100).map(() => makeResult('a'.repeat(2048)));
await storage.persist(r);
await assertStored(r.slice(0, 41));
await assertStored(r.slice(0, 46));
});
test('always stores the min number of results', async () => {

View file

@ -9,9 +9,9 @@ import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workb
suite('Workbench - Testing URIs', () => {
test('round trip', () => {
const uris: ParsedTestUri[] = [
{ type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testExtId: 't' },
{ type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testExtId: 't' },
{ type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testExtId: 't' },
{ type: TestUriType.ResultActualOutput, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' },
{ type: TestUriType.ResultExpectedOutput, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' },
{ type: TestUriType.ResultMessage, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' },
];
for (const uri of uris) {

View file

@ -75,19 +75,19 @@ suite('ExtHost Testing', () => {
assert.deepStrictEqual(single.collectDiff(), [
[
TestDiffOpType.Add,
{ src: { tree: 0, provider: 'pid' }, parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('root')) } }
{ src: { tree: 0, controller: 'pid' }, parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('root')) } }
],
[
TestDiffOpType.Add,
{ src: { tree: 0, provider: 'pid' }, parent: 'id-root', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('a')) } }
{ src: { tree: 0, controller: 'pid' }, parent: 'id-root', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('a')) } }
],
[
TestDiffOpType.Add,
{ src: { tree: 0, provider: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('aa')) }
{ src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('aa')) }
],
[
TestDiffOpType.Add,
{ src: { tree: 0, provider: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('ab')) }
{ src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('ab')) }
],
[
TestDiffOpType.Update,
@ -95,7 +95,7 @@ suite('ExtHost Testing', () => {
],
[
TestDiffOpType.Add,
{ src: { tree: 0, provider: 'pid' }, parent: 'id-root', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('b')) }
{ src: { tree: 0, controller: 'pid' }, parent: 'id-root', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('b')) }
],
[
TestDiffOpType.Update,
@ -149,7 +149,7 @@ suite('ExtHost Testing', () => {
assert.deepStrictEqual(single.collectDiff(), [
[TestDiffOpType.Add, {
src: { tree: 0, provider: 'pid' },
src: { tree: 0, controller: 'pid' },
parent: 'id-a',
expand: TestItemExpandState.NotExpandable,
item: convert.TestItem.from(child),