mirror of
https://github.com/Microsoft/vscode
synced 2024-08-27 21:09:43 +00:00
testing: rework running side to new apis
This commit is contained in:
parent
bec017d389
commit
bb3ea733de
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -86,5 +86,5 @@
|
|||
},
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"notebook.experimental.useMarkdownRenderer": true,
|
||||
"testing.autoRun.mode": "onlyPreviouslyRun",
|
||||
"testing.autoRun.mode": "rerun",
|
||||
}
|
||||
|
|
33
src/vs/vscode.proposed.d.ts
vendored
33
src/vs/vscode.proposed.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) { }
|
||||
|
||||
|
|
|
@ -112,7 +112,6 @@ export interface ITestTreeElement {
|
|||
*/
|
||||
readonly retired: boolean;
|
||||
|
||||
readonly ownState: TestResultState;
|
||||
readonly label: string;
|
||||
readonly parentItem: ITestTreeElement | null;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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})`);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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, [
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue