testing: refactor to new runState API

Fixes: https://github.com/microsoft/vscode/issues/115101.
See issue for details.

- Adopts the new API
- Test results now persist across reloads (last 64 runs).
- Removed state grouping in favor of sorting option.
- Code lenses are disabled for now
This commit is contained in:
Connor Peet 2021-02-08 17:13:37 -08:00
parent 783bb42362
commit 3d4cabb608
No known key found for this signature in database
GPG Key ID: CF8FD2EA0DBC61BD
40 changed files with 819 additions and 1811 deletions

View File

@ -1009,6 +1009,7 @@
"end",
"expand",
"hide",
"invalidate",
"open",
"override",
"receive",

View File

@ -3,410 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
interface IDisposable {
dispose(): void;
export function activate() {
// no-op. This extension may be removed in the future
}
const enum Constants {
ConfigSection = 'testing',
EnableCodeLensConfig = 'enableCodeLens',
EnableDiagnosticsConfig = 'enableProblemDiagnostics',
}
export function activate(context: vscode.ExtensionContext) {
const diagnostics = vscode.languages.createDiagnosticCollection();
const services = new TestingEditorServices(diagnostics);
context.subscriptions.push(
services,
diagnostics,
vscode.languages.registerCodeLensProvider({ scheme: 'file' }, services),
);
}
class TestingConfig implements IDisposable {
private section = vscode.workspace.getConfiguration(Constants.ConfigSection);
private readonly changeEmitter = new vscode.EventEmitter<void>();
private readonly listener = vscode.workspace.onDidChangeConfiguration(evt => {
if (evt.affectsConfiguration(Constants.ConfigSection)) {
this.section = vscode.workspace.getConfiguration(Constants.ConfigSection);
this.changeEmitter.fire();
}
});
public readonly onChange = this.changeEmitter.event;
public get codeLens() {
return this.section.get(Constants.EnableCodeLensConfig, true);
}
public get diagnostics() {
return this.section.get(Constants.EnableDiagnosticsConfig, false);
}
public get isEnabled() {
return this.codeLens || this.diagnostics;
}
public dispose() {
this.listener.dispose();
}
}
export class TestingEditorServices implements IDisposable, vscode.CodeLensProvider {
private readonly codeLensChangeEmitter = new vscode.EventEmitter<void>();
private readonly documents = new Map<string, DocumentTestObserver>();
private readonly config = new TestingConfig();
private disposables: IDisposable[];
private wasEnabled = this.config.isEnabled;
/**
* @inheritdoc
*/
public readonly onDidChangeCodeLenses = this.codeLensChangeEmitter.event;
constructor(private readonly diagnostics: vscode.DiagnosticCollection) {
this.disposables = [
new vscode.Disposable(() => this.expireAll()),
this.config,
vscode.window.onDidChangeVisibleTextEditors((editors) => {
if (!this.config.isEnabled) {
return;
}
const expiredEditors = new Set(this.documents.keys());
for (const editor of editors) {
const key = editor.document.uri.toString();
this.ensure(key, editor.document);
expiredEditors.delete(key);
}
for (const expired of expiredEditors) {
this.expire(expired);
}
}),
vscode.workspace.onDidCloseTextDocument((document) => {
this.expire(document.uri.toString());
}),
this.config.onChange(() => {
if (!this.wasEnabled || this.config.isEnabled) {
this.attachToAllVisible();
} else if (this.wasEnabled || !this.config.isEnabled) {
this.expireAll();
}
this.wasEnabled = this.config.isEnabled;
this.codeLensChangeEmitter.fire();
}),
];
if (this.config.isEnabled) {
this.attachToAllVisible();
}
}
/**
* @inheritdoc
*/
public provideCodeLenses(document: vscode.TextDocument) {
if (!this.config.codeLens) {
return [];
}
return this.documents.get(document.uri.toString())?.provideCodeLenses() ?? [];
}
/**
* Attach to all currently visible editors.
*/
private attachToAllVisible() {
for (const editor of vscode.window.visibleTextEditors) {
this.ensure(editor.document.uri.toString(), editor.document);
}
}
/**
* Unattaches to all tests.
*/
private expireAll() {
for (const observer of this.documents.values()) {
observer.dispose();
}
this.documents.clear();
}
/**
* Subscribes to tests for the document URI.
*/
private ensure(key: string, document: vscode.TextDocument) {
const state = this.documents.get(key);
if (!state) {
const observer = new DocumentTestObserver(document, this.diagnostics, this.config);
this.documents.set(key, observer);
observer.onDidChangeCodeLenses(() => this.config.codeLens && this.codeLensChangeEmitter.fire());
}
}
/**
* Expires and removes the watcher for the document.
*/
private expire(key: string) {
const observer = this.documents.get(key);
if (!observer) {
return;
}
observer.dispose();
this.documents.delete(key);
}
/**
* @override
*/
public dispose() {
this.disposables.forEach((d) => d.dispose());
}
}
class DocumentTestObserver implements IDisposable {
private readonly codeLensChangeEmitter = new vscode.EventEmitter<void>();
private readonly observer = vscode.test.createDocumentTestObserver(this.document);
private readonly disposables: IDisposable[];
public readonly onDidChangeCodeLenses = this.codeLensChangeEmitter.event;
private didHaveDiagnostics = this.config.diagnostics;
constructor(
private readonly document: vscode.TextDocument,
private readonly diagnostics: vscode.DiagnosticCollection,
private readonly config: TestingConfig,
) {
this.disposables = [
this.observer,
this.codeLensChangeEmitter,
config.onChange(() => {
if (this.didHaveDiagnostics && !config.diagnostics) {
this.diagnostics.set(document.uri, []);
} else if (!this.didHaveDiagnostics && config.diagnostics) {
this.updateDiagnostics();
}
this.didHaveDiagnostics = config.diagnostics;
}),
this.observer.onDidChangeTest(() => {
this.updateDiagnostics();
this.codeLensChangeEmitter.fire();
}),
];
}
private updateDiagnostics() {
if (!this.config.diagnostics) {
return;
}
const uriString = this.document.uri.toString();
const diagnostics: vscode.Diagnostic[] = [];
for (const test of iterateOverTests(this.observer.tests)) {
for (const message of test.state.messages) {
if (message.location?.uri.toString() === uriString) {
diagnostics.push({
range: message.location.range,
message: message.message.toString(),
severity: testToDiagnosticSeverity(message.severity),
});
}
}
}
this.diagnostics.set(this.document.uri, diagnostics);
}
public provideCodeLenses(): vscode.CodeLens[] {
const lenses: vscode.CodeLens[] = [];
for (const test of iterateOverTests(this.observer.tests)) {
const { debuggable = false, runnable = true } = test;
if (!test.location || !(debuggable || runnable)) {
continue;
}
const summary = summarize(test);
lenses.push({
isResolved: true,
range: test.location.range,
command: {
title: `$(${testStateToIcon[summary.computedState]}) ${getLabelFor(test, summary)}`,
command: 'vscode.runTests',
arguments: [[test]],
tooltip: localize('tooltip.debug', 'Debug {0}', test.label),
},
});
if (debuggable) {
lenses.push({
isResolved: true,
range: test.location.range,
command: {
title: localize('action.debug', 'Debug'),
command: 'vscode.debugTests',
arguments: [[test]],
tooltip: localize('tooltip.debug', 'Debug {0}', test.label),
},
});
}
}
return lenses;
}
/**
* @override
*/
public dispose() {
this.diagnostics.set(this.document.uri, []);
this.disposables.forEach(d => d.dispose());
}
}
function getLabelFor(test: vscode.TestItem, summary: ITestSummary) {
if (summary.duration !== undefined) {
return localize(
'tooltip.runStateWithDuration',
'{0}/{1} Tests Passed in {2}',
summary.passed,
summary.passed + summary.failed,
formatDuration(summary.duration),
);
}
if (summary.passed > 0 || summary.failed > 0) {
return localize('tooltip.runState', '{0}/{1} Tests Passed', summary.passed, summary.failed);
}
if (test.state.runState === vscode.TestRunState.Passed) {
return test.state.duration !== undefined
? localize('state.passedWithDuration', 'Passed in {0}', formatDuration(test.state.duration))
: localize('state.passed', 'Passed');
}
if (isFailedState(test.state.runState)) {
return localize('state.failed', 'Failed');
}
return localize('action.run', 'Run Tests');
}
function formatDuration(duration: number) {
if (duration < 1_000) {
return `${Math.round(duration)}ms`;
}
if (duration < 100_000) {
return `${(duration / 1000).toPrecision(3)}s`;
}
return `${(duration / 1000 / 60).toPrecision(3)}m`;
}
const statePriority: { [K in vscode.TestRunState]: number } = {
[vscode.TestRunState.Running]: 6,
[vscode.TestRunState.Queued]: 5,
[vscode.TestRunState.Errored]: 4,
[vscode.TestRunState.Failed]: 3,
[vscode.TestRunState.Passed]: 2,
[vscode.TestRunState.Skipped]: 1,
[vscode.TestRunState.Unset]: 0,
};
const maxPriority = (a: vscode.TestRunState, b: vscode.TestRunState) =>
statePriority[a] > statePriority[b] ? a : b;
const isFailedState = (s: vscode.TestRunState) =>
s === vscode.TestRunState.Failed || s === vscode.TestRunState.Errored;
interface ITestSummary {
passed: number;
failed: number;
duration: number | undefined;
computedState: vscode.TestRunState;
}
function summarize(test: vscode.TestItem) {
let passed = 0;
let failed = 0;
let duration: number | undefined;
let computedState = test.state.runState;
const queue = test.children ? [test.children] : [];
while (queue.length) {
for (const test of queue.pop()!) {
computedState = maxPriority(computedState, test.state.runState);
if (test.state.runState === vscode.TestRunState.Passed) {
passed++;
if (test.state.duration !== undefined) {
duration = test.state.duration + (duration ?? 0);
}
} else if (isFailedState(test.state.runState)) {
failed++;
if (test.state.duration !== undefined) {
duration = test.state.duration + (duration ?? 0);
}
}
if (test.children) {
queue.push(test.children);
}
}
}
return { passed, failed, duration, computedState };
}
function* iterateOverTests(tests: ReadonlyArray<vscode.TestItem>) {
const queue = [tests];
while (queue.length) {
for (const test of queue.pop()!) {
yield test;
if (test.children) {
queue.push(test.children);
}
}
}
}
const testStateToIcon: { [K in vscode.TestRunState]: string } = {
[vscode.TestRunState.Errored]: 'testing-error-icon',
[vscode.TestRunState.Failed]: 'testing-failed-icon',
[vscode.TestRunState.Passed]: 'testing-passed-icon',
[vscode.TestRunState.Queued]: 'testing-queued-icon',
[vscode.TestRunState.Skipped]: 'testing-skipped-icon',
[vscode.TestRunState.Unset]: 'beaker',
[vscode.TestRunState.Running]: 'loading~spin',
};
const testToDiagnosticSeverity = (severity: vscode.TestMessageSeverity | undefined) => {
switch (severity) {
case vscode.TestMessageSeverity.Hint:
return vscode.DiagnosticSeverity.Hint;
case vscode.TestMessageSeverity.Information:
return vscode.DiagnosticSeverity.Information;
case vscode.TestMessageSeverity.Warning:
return vscode.DiagnosticSeverity.Warning;
case vscode.TestMessageSeverity.Error:
default:
return vscode.DiagnosticSeverity.Error;
}
};

View File

@ -69,7 +69,7 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
this.model.updateElementHeight(element, height);
}
resort(element: T, recursive = true): void {
resort(element: T | null, recursive = true): void {
this.model.resort(element, recursive);
}

View File

@ -2109,10 +2109,12 @@ declare module 'vscode' {
export function registerTestProvider<T extends TestItem>(testProvider: TestProvider<T>): Disposable;
/**
* Runs tests with the given options. If no options are given, then
* all tests are run. Returns the resulting test run.
* 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}.
*/
export function runTests<T extends TestItem>(options: TestRunOptions<T>, cancellationToken?: CancellationToken): Thenable<void>;
export function runTests<T extends TestItem>(run: TestRunOptions<T>, cancellationToken?: CancellationToken): Thenable<void>;
/**
* Returns an observer that retrieves tests in the given workspace folder.
@ -2226,6 +2228,14 @@ declare module 'vscode' {
*/
readonly discoveredInitialTests?: Thenable<unknown>;
/**
* An event that fires when a test becomes outdated, as a result of
* file changes, for example. In "watch" mode, tests that are outdated
* will be automatically re-run after a short delay. Firing a test
* with children will mark the entire subtree as outdated.
*/
readonly onDidInvalidateTest?: Event<T>;
/**
* Dispose will be called when there are no longer observers interested
* in the hierarchy.
@ -2270,11 +2280,11 @@ declare module 'vscode' {
* @todo this will eventually need to be able to return a summary report, coverage for example.
*/
// eslint-disable-next-line vscode-dts-provider-naming
runTests?(options: TestRunOptions<T>, cancellationToken: CancellationToken): ProviderResult<void>;
runTests?(options: TestRun<T>, cancellationToken: CancellationToken): ProviderResult<void>;
}
/**
* Options given to `TestProvider.runTests`
* Options given to {@link test.runTests}
*/
export interface TestRunOptions<T extends TestItem = TestItem> {
/**
@ -2289,6 +2299,17 @@ declare module 'vscode' {
debug: boolean;
}
/**
* Options given to `TestProvider.runTests`
*/
export interface TestRun<T extends TestItem = TestItem> extends TestRunOptions<T> {
/**
* 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.
*/
setState(test: T, state: TestState): void;
}
/**
* A test item is an item shown in the "test explorer" view. It encompasses
* both a suite and a test, since they have almost or identical capabilities.
@ -2337,12 +2358,6 @@ declare module 'vscode' {
* Optional list of nested tests for this item.
*/
children?: TestItem[];
/**
* Test run state. Will generally be {@link TestRunState.Unset} by
* default.
*/
state: TestState;
}
/**
@ -2377,11 +2392,11 @@ declare module 'vscode' {
* in order to update it. This allows consumers to quickly and easily check
* for changes via object identity.
*/
export class TestState {
export interface TestState {
/**
* Current state of the test.
*/
readonly runState: TestRunState;
readonly state: TestRunState;
/**
* Optional duration of the test run, in milliseconds.
@ -2392,14 +2407,7 @@ declare module 'vscode' {
* Associated test run message. Can, for example, contain assertion
* failure information if the test fails.
*/
readonly messages: ReadonlyArray<Readonly<TestMessage>>;
/**
* @param state Run state to hold in the test state
* @param messages List of associated messages for the test
* @param duration Length of time the test run took, if appropriate.
*/
constructor(runState: TestRunState, messages?: TestMessage[], duration?: number);
readonly messages?: ReadonlyArray<Readonly<TestMessage>>;
}
/**

View File

@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { getTestSubscriptionKey, ITestState, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
@ -19,12 +19,6 @@ const reviveDiff = (diff: TestsDiff) => {
if (item.item.location) {
item.item.location.uri = URI.revive(item.item.location.uri);
}
for (const message of item.item.state.messages) {
if (message.location) {
message.location.uri = URI.revive(message.location.uri);
}
}
}
}
};
@ -37,30 +31,42 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
constructor(
extHostContext: IExtHostContext,
@ITestService private readonly testService: ITestService,
@ITestResultService resultService: ITestResultService,
@ITestResultService private readonly resultService: ITestResultService,
) {
super();
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting);
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 testCompleteListener = this._register(new MutableDisposable());
this._register(resultService.onNewTestResult(results => {
testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests }));
}));
// const testCompleteListener = this._register(new MutableDisposable());
// todo(@connor4312): reimplement, maybe
// this._register(resultService.onResultsChanged(results => {
// testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: [] }));
// }));
testService.updateRootProviderCount(1);
const lastCompleted = resultService.results.find(r => !r.isComplete);
if (lastCompleted) {
this.proxy.$publishTestResults({ tests: lastCompleted.tests });
}
for (const { resource, uri } of this.testService.subscriptions) {
this.proxy.$subscribeToTests(resource, uri);
}
}
/**
* @inheritdoc
*/
$updateTestStateInRun(runId: string, testId: string, state: ITestState): void {
const r = this.resultService.getResult(runId);
if (r && r instanceof LiveTestResult) {
for (const message of state.messages) {
if (message.location) {
message.location.uri = URI.revive(message.location.uri);
}
}
r.updateState(testId, state);
}
}
/**
* @inheritdoc
*/
@ -105,8 +111,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
this.testService.publishDiff(resource, URI.revive(uri), diff);
}
public $runTests(req: RunTestsRequest, token: CancellationToken): Promise<RunTestsResult> {
return this.testService.runTests(req, token);
public async $runTests(req: RunTestsRequest, token: CancellationToken): Promise<string> {
const result = await this.testService.runTests(req, token);
return result.id;
}
public dispose() {

View File

@ -1292,10 +1292,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
// checkProposedApiEnabled(extension);
return extHostTypes.TestMessageSeverity;
},
get TestState() {
// checkProposedApiEnabled(extension);
return extHostTypes.TestState;
},
get WorkspaceTrustState() {
// checkProposedApiEnabled(extension);
return extHostTypes.WorkspaceTrustState;

View File

@ -58,7 +58,7 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib
import { DebugConfigurationProviderTriggerKind, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes';
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync';
import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem, InternalTestResults, ITestState, RunTestForProviderRequest, RunTestsRequest, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { WorkspaceTrustStateChangeEvent } from 'vs/platform/workspace/common/workspaceTrust';
@ -1826,7 +1826,7 @@ export const enum ExtHostTestingResource {
}
export interface ExtHostTestingShape {
$runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise<RunTestsResult>;
$runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise<void>;
$subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void;
$unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void;
$lookupTest(test: TestIdWithProvider): Promise<InternalTestItem | undefined>;
@ -1840,7 +1840,8 @@ export interface MainThreadTestingShape {
$subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
$unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
$publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void;
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<RunTestsResult>;
$updateTestStateInRun(runId: string, testId: string, state: ITestState): void;
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<string>;
}
// --- proxy identifiers

View File

@ -16,11 +16,11 @@ import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTes
import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData';
import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters';
import { TestItem, TestState } from 'vs/workbench/api/common/extHostTypeConverters';
import { Disposable } from 'vs/workbench/api/common/extHostTypes';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import type * as vscode from 'vscode';
const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`;
@ -93,9 +93,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
// Find workspace items first, then owned tests, then document tests.
// If a test instance exists in both the workspace and document, prefer
// the workspace because it's less ephemeral.
.map(test => this.workspaceObservers.getMirroredTestDataByReference(test)
?? mapFind(this.testSubscriptions.values(), c => c.collection.getTestByReference(test))
?? this.textDocumentObservers.getMirroredTestDataByReference(test))
.map(this.getInternalTestForReference, this)
.filter(isDefined)
.map(item => ({ providerId: item.providerId, testId: item.id })),
debug: req.debug
@ -219,10 +217,10 @@ export class ExtHostTesting implements ExtHostTestingShape {
* providers to be run.
* @override
*/
public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise<RunTestsResult> {
public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise<void> {
const provider = this.providers.get(req.providerId);
if (!provider || !provider.runTests) {
return EMPTY_TEST_RESULT;
return;
}
const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual)
@ -230,16 +228,25 @@ export class ExtHostTesting implements ExtHostTestingShape {
// Only send the actual TestItem's to the user to run.
.map(t => t instanceof TestItemFilteredWrapper ? t.actual : t);
if (!tests.length) {
return EMPTY_TEST_RESULT;
return;
}
try {
await provider.runTests({ tests, debug: req.debug }, cancellation);
await provider.runTests({
setState: (test, state) => {
const internal = this.getInternalTestForReference(test);
if (internal) {
this.flushCollectionDiffs();
this.proxy.$updateTestStateInRun(req.runId, internal.id, TestState.from(state));
}
}, tests, debug: req.debug
}, cancellation);
for (const { collection } of this.testSubscriptions.values()) {
collection.flushDiff(); // ensure all states are updated
}
return EMPTY_TEST_RESULT;
return;
} catch (e) {
console.error(e); // so it appears to attached debuggers
throw e;
@ -256,6 +263,28 @@ export class ExtHostTesting implements ExtHostTestingShape {
return Promise.resolve(item);
}
/**
* Flushes diff information for all collections to ensure state in the
* main thread is updated.
*/
private flushCollectionDiffs() {
for (const { collection } of this.testSubscriptions.values()) {
collection.flushDiff();
}
}
/**
* Gets the internal test item associated with the reference from the extension.
*/
private getInternalTestForReference(test: vscode.TestItem) {
// Find workspace items first, then owned tests, then document tests.
// If a test instance exists in both the workspace and document, prefer
// the workspace because it's less ephemeral.
return this.workspaceObservers.getMirroredTestDataByReference(test)
?? mapFind(this.testSubscriptions.values(), c => c.collection.getTestByReference(test))
?? this.textDocumentObservers.getMirroredTestDataByReference(test);
}
private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy<vscode.TestItem> | undefined {
if (!folder) {
return;
@ -361,10 +390,6 @@ export class TestItemFilteredWrapper implements vscode.TestItem {
return this.actual.runnable;
}
public get state() {
return this.actual.state;
}
public get children() {
// We only want children that match the filter.
return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter);
@ -645,7 +670,6 @@ class TestItemFromMirror implements vscode.RequiredTestItem {
public get id() { return this.#internal.revived.id!; }
public get label() { return this.#internal.revived.label; }
public get description() { return this.#internal.revived.description; }
public get state() { return this.#internal.revived.state; }
public get location() { return this.#internal.revived.location; }
public get runnable() { return this.#internal.revived.runnable ?? true; }
public get debuggable() { return this.#internal.revived.debuggable ?? false; }
@ -665,7 +689,6 @@ class TestItemFromMirror implements vscode.RequiredTestItem {
id: this.id,
label: this.label,
description: this.description,
state: this.state,
location: this.location,
runnable: this.runnable,
debuggable: this.debuggable,

View File

@ -1380,22 +1380,22 @@ export namespace NotebookDecorationRenderOptions {
export namespace TestState {
export function from(item: vscode.TestState): ITestState {
return {
runState: item.runState,
state: item.state,
duration: item.duration,
messages: item.messages.map(message => ({
messages: item.messages?.map(message => ({
message: MarkdownString.fromStrict(message.message) || '',
severity: message.severity,
expectedOutput: message.expectedOutput,
actualOutput: message.actualOutput,
location: message.location ? location.from(message.location) : undefined,
})),
})) ?? [],
};
}
export function to(item: ITestState): vscode.TestState {
return new types.TestState(
item.runState,
item.messages.map(message => ({
return {
state: item.state,
messages: item.messages.map(message => ({
message: typeof message.message === 'string' ? message.message : MarkdownString.to(message.message),
severity: message.severity,
expectedOutput: message.expectedOutput,
@ -1405,8 +1405,8 @@ export namespace TestState {
uri: URI.revive(message.location.uri)
}),
})),
item.duration,
);
duration: item.duration,
};
}
}
@ -1420,7 +1420,6 @@ export namespace TestItem {
debuggable: item.debuggable ?? false,
description: item.description,
runnable: item.runnable ?? true,
state: TestState.from(item.state),
};
}
@ -1435,7 +1434,6 @@ export namespace TestItem {
debuggable: item.debuggable,
description: item.description,
runnable: item.runnable,
state: TestState.to(item.state),
};
}
}

View File

@ -2964,31 +2964,6 @@ export enum TestMessageSeverity {
Hint = 3
}
@es5ClassCompat
export class TestState {
#runState: TestRunState;
#duration?: number;
#messages: ReadonlyArray<Readonly<vscode.TestMessage>>;
public get runState() {
return this.#runState;
}
public get duration() {
return this.#duration;
}
public get messages() {
return this.#messages;
}
constructor(runState: TestRunState, messages: vscode.TestMessage[] = [], duration?: number) {
this.#runState = runState;
this.#messages = Object.freeze(messages.map(m => Object.freeze(m)));
this.#duration = duration;
}
}
export type RequiredTestItem = vscode.RequiredTestItem;
export type TestItem = vscode.TestItem;

View File

@ -10,13 +10,28 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
import { InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
const computedStateAccessor: IComputedStateAccessor<ITestTreeElement> = {
getOwnState: i => i.state,
getCurrentComputedState: i => i.state,
setComputedState: (i, s) => i.state = s,
getChildren: i => i.children.values(),
*getParents(i) {
for (let parent = i.parentItem; parent; parent = parent.parentItem) {
yield parent;
}
},
};
/**
* Projection that lists tests in their traditional tree view.
*/
@ -40,11 +55,42 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
*/
public readonly onUpdate = this.updateEmitter.event;
constructor(listener: TestSubscriptionListener) {
constructor(listener: TestSubscriptionListener, @ITestResultService private readonly results: ITestResultService) {
super();
this._register(listener.onDiff(([folder, diff]) => this.applyDiff(folder, diff)));
this._register(listener.onFolderChange(this.applyFolderChange, this));
// when test results are cleared, recalculate all state
this._register(results.onResultsChanged((evt) => {
if (!('removed' in evt)) {
return;
}
for (const inTree of [...this.items.values()].sort((a, b) => b.depth - a.depth)) {
const lookup = this.results.getStateByExtId(inTree.test.item.extId)?.[1];
inTree.ownState = lookup?.state.state ?? TestRunState.Unset;
const computed = lookup?.computedState ?? TestRunState.Unset;
if (computed !== inTree.state) {
inTree.state = computed;
this.addUpdated(inTree);
}
}
this.updateEmitter.fire();
}));
// when test states change, reflect in the tree
this._register(results.onTestChanged(([, { item, state, computedState }]) => {
for (const i of this.items.values()) {
if (i.test.item.extId === item.extId) {
i.ownState = state.state;
refreshComputedState(computedStateAccessor, i, this.addUpdated, computedState);
this.updateEmitter.fire();
return;
}
}
}));
for (const [folder, collection] of listener.workspaceFolderCollections) {
for (const node of collection.all) {
this.storeItem(this.createItem(node, folder.folder));
@ -96,7 +142,7 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
const locationChanged = !locationsEqual(existing.location, item.item.location);
if (locationChanged) { this.locations.remove(existing); }
existing.update(item, this.addUpdated);
existing.update(item);
if (locationChanged) { this.locations.add(existing); }
this.addUpdated(existing);
break;
@ -172,5 +218,11 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
item.parentItem.children.add(item);
this.items.set(item.test.id, item);
this.locations.add(item);
const prevState = this.results.getStateByExtId(item.test.item.extId)?.[1];
if (prevState) {
item.ownState = prevState.state.state;
refreshComputedState(computedStateAccessor, item, this.addUpdated, prevState.computedState);
}
}
}

View File

@ -10,6 +10,7 @@ import { HierarchicalByLocationProjection as HierarchicalByLocationProjection }
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
/**
@ -64,9 +65,9 @@ export class HierarchicalByNameElement extends HierarchicalElement {
/**
* @override
*/
public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) {
public update(actual: InternalTestItem) {
const wasRunnable = this.test.item.runnable;
super.update(actual, addUpdated);
super.update(actual);
if (this.test.item.runnable !== wasRunnable) {
this.updateLeafTestState();
@ -117,8 +118,8 @@ export class HierarchicalByNameElement extends HierarchicalElement {
* test root rather than the heirarchal parent.
*/
export class HierarchicalByNameProjection extends HierarchicalByLocationProjection {
constructor(listener: TestSubscriptionListener) {
super(listener);
constructor(listener: TestSubscriptionListener, @ITestResultService results: ITestResultService) {
super(listener, results);
const originalRenderNode = this.renderNode.bind(this);
this.renderNode = (node, recurse) => {

View File

@ -7,7 +7,6 @@ import { Iterable } from 'vs/base/common/iterator';
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates';
import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
/**
@ -15,7 +14,6 @@ import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testi
*/
export class HierarchicalElement implements ITestTreeElement {
public readonly children = new Set<HierarchicalElement>();
public computedState: TestRunState | undefined;
public readonly depth: number = this.parentItem.depth + 1;
public get treeId() {
@ -26,10 +24,6 @@ export class HierarchicalElement implements ITestTreeElement {
return this.test.item.label;
}
public get state() {
return this.test.item.state.runState;
}
public get location() {
return this.test.item.location;
}
@ -46,16 +40,15 @@ export class HierarchicalElement implements ITestTreeElement {
: Iterable.empty();
}
public state = TestRunState.Unset;
public ownState = TestRunState.Unset;
constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) {
this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese
}
public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) {
const stateChange = actual.item.state.runState !== this.state;
public update(actual: InternalTestItem) {
Object.assign(this.test, actual);
if (stateChange) {
refreshComputedState(this, addUpdated);
}
}
}
@ -80,67 +73,12 @@ export class HierarchicalFolder implements ITestTreeElement {
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
}
public state = TestRunState.Unset;
public ownState = TestRunState.Unset;
constructor(private readonly folder: IWorkspaceFolder) { }
public get label() {
return this.folder.name;
}
}
/**
* Gets the computed state for the node.
*/
export const getComputedState = (node: ITestTreeElement) => {
if (node.computedState === undefined) {
node.computedState = node.state ?? TestRunState.Unset;
for (const child of node.children) {
node.computedState = maxPriority(node.computedState, getComputedState(child));
}
}
return node.computedState;
};
/**
* Refreshes the computed state for the node and its parents. Any changes
* elements cause `addUpdated` to be called.
*/
export const refreshComputedState = (node: ITestTreeElement, addUpdated: (n: ITestTreeElement) => void) => {
if (node.computedState === undefined) {
return;
}
const oldPriority = statePriority[node.computedState];
node.computedState = undefined;
const newState = getComputedState(node);
const newPriority = statePriority[getComputedState(node)];
if (newPriority === oldPriority) {
return;
}
addUpdated(node);
if (newPriority > oldPriority) {
// Update all parents to ensure they're at least this priority.
for (let parent = node.parentItem; parent; parent = parent.parentItem) {
const prev = parent.computedState;
if (prev !== undefined && statePriority[prev] >= newPriority) {
break;
}
parent.computedState = newState;
addUpdated(parent);
}
} else if (newPriority < oldPriority) {
// Re-render all parents of this node whose computed priority might have come from this node
for (let parent = node.parentItem; parent; parent = parent.parentItem) {
const prev = parent.computedState;
if (prev === undefined || statePriority[prev] > oldPriority) {
break;
}
parent.computedState = undefined;
parent.computedState = getComputedState(parent);
addUpdated(parent);
}
}
};

View File

@ -40,13 +40,6 @@ export interface ITestTreeProjection extends IDisposable {
export interface ITestTreeElement {
/**
* Computed element state. Will be set automatically if not initially provided.
* The projection is responsible for clearing (or updating) this if it
* becomes invalid.
*/
computedState: TestRunState | undefined;
readonly children: Set<ITestTreeElement>;
/**
@ -85,9 +78,11 @@ export interface ITestTreeElement {
readonly debuggable: Iterable<TestIdWithProvider>;
/**
* State of of the tree item. Mostly used for deriving the computed state.
* Element state to display.
*/
readonly state?: TestRunState;
state: TestRunState;
readonly ownState: TestRunState;
readonly label: string;
readonly parentItem: ITestTreeElement | null;
}

View File

@ -1,321 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
import { Emitter } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
import { Iterable } from 'vs/base/common/iterator';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
import { Location as ModeLocation } from 'vs/editor/common/modes';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
import { NodeChangeList, NodeRenderDirective, NodeRenderFn, peersHaveChildren } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { isRunningState, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
interface IStatusTestItem extends IncrementalTestCollectionItem {
treeElements: Map<TestRunState, TestStateElement>;
previousState: TestRunState;
depth: number;
parentItem?: IStatusTestItem;
location?: ModeLocation;
}
type TreeElement = StateElement<TestStateElement> | TestStateElement;
class TestStateElement implements ITestTreeElement {
public computedState = this.state;
public get treeId() {
return `sltest:${this.test.id}`;
}
public get label() {
return this.test.item.label;
}
public get location() {
return this.test.item.location;
}
public get runnable(): Iterable<TestIdWithProvider> {
// if this item is runnable and all its children are in the same state,
// we can run all of them in one go. This will eventually be true
// for leaf nodes, whose treeElements contain only their own state.
if (this.test.item.runnable && this.test.treeElements.size === 1) {
return [{ testId: this.test.id, providerId: this.test.providerId }];
}
return Iterable.concatNested(Iterable.map(this.children, c => c.runnable));
}
public get debuggable(): Iterable<TestIdWithProvider> {
// same logic as runnable above
if (this.test.item.debuggable && this.test.treeElements.size === 1) {
return [{ testId: this.test.id, providerId: this.test.providerId }];
}
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
}
public readonly depth = this.test.depth;
public readonly children = new Set<TestStateElement>();
constructor(
public readonly state: TestRunState,
public readonly test: IStatusTestItem,
public readonly parentItem: TestStateElement | StateElement<TestStateElement>,
) {
parentItem.children.add(this);
}
public remove() {
this.parentItem.children.delete(this);
}
}
/**
* Shows tests in a hierarchical way, but grouped by status. This is more
* complex than it may look at first glance, because nodes can appear in
* multiple places if they have children with different statuses.
*/
export class StateByLocationProjection extends AbstractIncrementalTestCollection<IStatusTestItem> implements ITestTreeProjection {
private readonly updateEmitter = new Emitter<void>();
private readonly changes = new NodeChangeList<TreeElement>();
private readonly locations = new TestLocationStore<IStatusTestItem>();
private readonly disposable = new DisposableStore();
/**
* @inheritdoc
*/
public readonly onUpdate = this.updateEmitter.event;
/**
* Root elements for states in the tree.
*/
protected readonly stateRoots = new Map<TestRunState, StateElement<TestStateElement>>();
constructor(listener: TestSubscriptionListener) {
super();
this.disposable.add(listener.onDiff(([, diff]) => this.apply(diff)));
const firstDiff: TestsDiff = [];
for (const [, collection] of listener.workspaceFolderCollections) {
firstDiff.push(...collection.getReviverDiff());
}
this.apply(firstDiff);
}
/**
* Frees listeners associated with the projection.
*/
public dispose() {
this.disposable.dispose();
}
/**
* @inheritdoc
*/
public getTestAtPosition(uri: URI, position: Position) {
const item = this.locations.getTestAtPosition(uri, position);
if (!item) {
return undefined;
}
for (const state of statesInOrder) {
const element = item.treeElements.get(state);
if (element) {
return element;
}
}
return undefined;
}
/**
* @inheritdoc
*/
public applyTo(tree: ObjectTree<ITestTreeElement, FuzzyScore>) {
this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values());
}
private readonly renderNode: NodeRenderFn<TreeElement> = (node, recurse) => {
if (node.depth === 1 /* test provider */) {
if (node.children.size === 0) {
return NodeRenderDirective.Omit;
} else if (!peersHaveChildren(node, () => this.stateRoots.values())) {
return NodeRenderDirective.Concat;
}
}
return {
element: node,
children: recurse(node.children),
};
};
/**
* @override
*/
protected createChangeCollector(): IncrementalChangeCollector<IStatusTestItem> {
return {
add: node => {
this.resolveNodesRecursive(node);
this.locations.add(node);
},
remove: (node, isNested) => {
this.locations.remove(node);
if (!isNested) {
for (const state of node.treeElements.keys()) {
this.pruneStateElements(node, state, true);
}
}
},
update: node => {
const isRunning = isRunningState(node.item.state.runState);
if (node.item.state.runState !== node.previousState) {
if (isRunning && node.treeElements.has(node.previousState)) {
node.treeElements.get(node.previousState)!.computedState = TestRunState.Running;
} else {
this.pruneStateElements(node, node.previousState);
this.resolveNodesRecursive(node);
}
} else if (!isRunning) {
const previous = node.treeElements.get(node.item.state.runState);
if (previous) {
previous.computedState = node.item.state.runState;
}
}
const locationChanged = !locationsEqual(node.location, node.item.location);
if (locationChanged) {
this.locations.remove(node);
node.location = node.item.location;
this.locations.add(node);
}
const treeNode = node.treeElements.get(node.previousState)!;
this.changes.updated(treeNode);
},
complete: () => {
this.updateEmitter.fire();
}
};
}
/**
* Ensures tree nodes for the item state are present in the tree.
*/
protected resolveNodesRecursive(item: IStatusTestItem) {
const state = item.item.state.runState;
item.previousState = item.item.state.runState;
// Create a list of items until the current item who don't have a tree node for the status yet
let chain: IStatusTestItem[] = [];
for (let i: IStatusTestItem | undefined = item; i && !i.treeElements.has(state); i = i.parentItem) {
chain.push(i);
}
for (let i = chain.length - 1; i >= 0; i--) {
const item2 = chain[i];
// the loop would have stopped pushing parents when either it reaches
// the root, or it reaches a parent who already has a node for this state.
const parent = item2.parentItem?.treeElements.get(state) ?? this.getOrCreateStateElement(state);
const node = this.createElement(state, item2, parent);
item2.treeElements.set(state, node);
parent.children.add(node);
if (i === chain.length - 1) {
this.changes.added(node);
}
}
}
protected createElement(state: TestRunState, item: IStatusTestItem, parent: TreeElement) {
return new TestStateElement(state, item, parent);
}
/**
* Recursively (from the leaf to the root) removes tree elements if there's
* no children who have the given state left.
*
* Returns true if it resulted in a node being removed.
*/
protected pruneStateElements(item: IStatusTestItem | undefined, state: TestRunState, force = false) {
if (!item) {
const stateRoot = this.stateRoots.get(state);
if (stateRoot?.children.size === 0) {
this.changes.removed(stateRoot);
this.stateRoots.delete(state);
return true;
}
return false;
}
const node = item.treeElements.get(state);
if (!node) {
return false;
}
// Check to make sure we aren't in the state, and there's no child with the
// state. For the unset state, only show the node if it's a leaf or it
// has children in the unset state.
if (!force) {
if (item.item.state.runState === state && !(state === TestRunState.Unset && item.children.size > 0)) {
return false;
}
for (const childId of item.children) {
if (this.items.get(childId)?.treeElements.has(state)) {
return false;
}
}
}
// If so, proceed to deletion and recurse upwards.
item.treeElements.delete(state);
node.remove();
if (!this.pruneStateElements(item.parentItem, state)) {
this.changes.removed(node);
}
return true;
}
protected getOrCreateStateElement(state: TestRunState) {
let s = this.stateRoots.get(state);
if (!s) {
s = new StateElement(state);
this.changes.added(s);
this.stateRoots.set(state, s);
}
return s;
}
protected createItem(item: InternalTestItem, parentItem?: IStatusTestItem): IStatusTestItem {
return {
...item,
depth: parentItem ? parentItem.depth + 1 : 1,
parentItem: parentItem,
previousState: item.item.state.runState,
location: item.item.location,
children: new Set(),
treeElements: new Map(),
};
}
}

View File

@ -1,289 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
import { Emitter } from 'vs/base/common/event';
import { FuzzyScore } from 'vs/base/common/filters';
import { Iterable } from 'vs/base/common/iterator';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Position } from 'vs/editor/common/core/position';
import { Location as ModeLocation } from 'vs/editor/common/modes';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
import { ListElementType } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName';
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
import { NodeChangeList, NodeRenderFn } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { isRunningState } from 'vs/workbench/contrib/testing/common/testingStates';
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
class ListTestStateElement implements ITestTreeElement {
public computedState = this.test.item.state.runState;
public get treeId() {
return `sntest:${this.test.id}`;
}
public get label() {
return this.test.item.label;
}
public get location() {
return this.test.item.location;
}
public get runnable(): Iterable<TestIdWithProvider> {
return this.test.item.runnable
? [{ testId: this.test.id, providerId: this.test.providerId }]
: Iterable.empty();
}
public get debuggable(): Iterable<TestIdWithProvider> {
return this.test.item.debuggable
? [{ testId: this.test.id, providerId: this.test.providerId }]
: Iterable.empty();
}
public get description() {
let description: string | undefined;
for (let parent = this.test.parentItem; parent && parent.depth > 0; parent = parent.parentItem) {
description = description ? `${parent.item.label} ${description}` : parent.item.label;
}
return description;
}
public readonly depth = 1;
public readonly children = new Set<never>();
constructor(
public readonly test: IStatusListTestItem,
public readonly parentItem: StateElement<ListTestStateElement>,
) {
parentItem.children.add(this);
}
public remove() {
this.parentItem.children.delete(this);
}
}
interface IStatusListTestItem extends IncrementalTestCollectionItem {
node?: ListTestStateElement;
type: ListElementType;
previousState: TestRunState;
depth: number;
parentItem?: IStatusListTestItem;
location?: ModeLocation;
}
type TreeElement = StateElement<ListTestStateElement> | ListTestStateElement;
/**
* Projection that shows tests in a flat list (grouped by status).
*/
export class StateByNameProjection extends AbstractIncrementalTestCollection<IStatusListTestItem> implements ITestTreeProjection {
private readonly updateEmitter = new Emitter<void>();
private readonly changes = new NodeChangeList<TreeElement>();
private readonly locations = new TestLocationStore<IStatusListTestItem>();
private readonly disposable = new DisposableStore();
/**
* @inheritdoc
*/
public readonly onUpdate = this.updateEmitter.event;
/**
* Root elements for states in the tree.
*/
protected readonly stateRoots = new Map<TestRunState, StateElement<ListTestStateElement>>();
constructor(listener: TestSubscriptionListener) {
super();
this.disposable.add(listener.onDiff(([, diff]) => this.apply(diff)));
const firstDiff: TestsDiff = [];
for (const [, collection] of listener.workspaceFolderCollections) {
firstDiff.push(...collection.getReviverDiff());
}
this.apply(firstDiff);
}
/**
* Frees listeners associated with the projection.
*/
public dispose() {
this.disposable.dispose();
}
/**
* @inheritdoc
*/
public getTestAtPosition(uri: URI, position: Position) {
return this.locations.getTestAtPosition(uri, position)?.node;
}
/**
* @inheritdoc
*/
public applyTo(tree: ObjectTree<ITestTreeElement, FuzzyScore>) {
this.changes.applyTo(tree, this.renderNode, () => this.stateRoots.values());
}
private readonly renderNode: NodeRenderFn<TreeElement> = (node, recurse) => {
return {
element: node,
children: node instanceof StateElement ? recurse(node.children) : undefined,
};
};
/**
* @override
*/
protected createChangeCollector(): IncrementalChangeCollector<IStatusListTestItem> {
return {
add: node => {
this.resolveNodesRecursive(node);
this.locations.add(node);
},
remove: (node, isNested) => {
if (node.node) {
this.locations.remove(node);
}
// for the top node being deleted, we need to update parents. For
// others we only need to remove them from the locations cache.
if (isNested) {
this.removeNodeSingle(node);
} else {
this.removeNode(node);
}
},
update: node => {
if (node.item.state.runState !== node.previousState && node.node) {
if (isRunningState(node.item.state.runState)) {
node.node.computedState = node.item.state.runState;
} else {
this.removeNode(node);
}
}
node.previousState = node.item.state.runState;
this.resolveNodesRecursive(node);
const locationChanged = !locationsEqual(node.location, node.item.location);
if (locationChanged) {
this.locations.remove(node);
node.location = node.item.location;
this.locations.add(node);
}
if (node.node) {
this.changes.updated(node.node);
}
},
complete: () => {
this.updateEmitter.fire();
}
};
}
/**
* Ensures tree nodes for the item state are present in the tree.
*/
protected resolveNodesRecursive(item: IStatusListTestItem) {
const newType = Iterable.some(item.children, c => this.items.get(c)?.type !== ListElementType.BranchWithoutLeaf)
? ListElementType.BranchWithLeaf
: item.item.runnable
? ListElementType.TestLeaf
: ListElementType.BranchWithoutLeaf;
if (newType === item.type) {
return;
}
const isVisible = newType === ListElementType.TestLeaf;
const wasVisible = item.type === ListElementType.TestLeaf;
item.type = newType;
if (!isVisible && wasVisible && item.node) {
this.removeNodeSingle(item);
} else if (isVisible && !wasVisible) {
const state = item.item.state.runState;
item.node = item.node || new ListTestStateElement(item, this.getOrCreateStateElement(state));
this.changes.added(item.node);
}
if (item.parentItem) {
this.resolveNodesRecursive(item.parentItem);
}
}
/**
* Recursively (from the leaf to the root) removes tree elements if there's
* no children who have the given state left.
*
* Returns true if it resulted in a node being removed.
*/
private removeNode(item: IStatusListTestItem) {
if (!item.node) {
return;
}
this.removeNodeSingle(item);
if (item.parentItem) {
this.resolveNodesRecursive(item.parentItem);
}
}
private removeNodeSingle(item: IStatusListTestItem) {
if (!item.node) {
return;
}
item.node.remove();
this.changes.removed(item.node);
const parent = item.node.parentItem;
item.node = undefined;
item.type = ListElementType.Unset;
if (parent.children.size === 0) {
this.changes.removed(parent);
this.stateRoots.delete(parent.state);
}
}
private getOrCreateStateElement(state: TestRunState) {
let s = this.stateRoots.get(state);
if (!s) {
s = new StateElement(state);
this.changes.added(s);
this.stateRoots.set(state, s);
}
return s;
}
/**
* @override
*/
protected createItem(item: InternalTestItem, parentItem?: IStatusListTestItem): IStatusListTestItem {
return {
...item,
type: ListElementType.Unset,
depth: parentItem ? parentItem.depth + 1 : 0,
parentItem: parentItem,
previousState: item.item.state.runState,
location: item.item.location,
children: new Set(),
};
}
}

View File

@ -1,35 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Iterable } from 'vs/base/common/iterator';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
import { testStateNames } from 'vs/workbench/contrib/testing/common/constants';
/**
* Base state node element, used in both name and location grouping.
*/
export class StateElement<T extends ITestTreeElement> implements ITestTreeElement {
public computedState = this.state;
public get treeId() {
return `sestate:${this.state}`;
}
public readonly depth = 0;
public readonly label = testStateNames[this.state];
public readonly parentItem = null;
public readonly children = new Set<T>();
public get runnable() {
return Iterable.concatNested(Iterable.map(this.children, c => c.runnable));
}
public get debuggable() {
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
}
constructor(public readonly state: TestRunState) { }
}

View File

@ -21,9 +21,10 @@ import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
import { FocusedViewContext } from 'vs/workbench/common/views';
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestExplorerViewSorting, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestResult, ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService';
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
@ -101,10 +102,10 @@ abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
/**
* @override
*/
public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise<RunTestsResult> {
public runInView(accessor: ServicesAccessor, view: TestingExplorerView): Promise<ITestResult | undefined> {
const tests = this.getActionableTests(accessor.get(IWorkspaceTestCollectionService), view.viewModel);
if (!tests.length) {
return Promise.resolve(EMPTY_TEST_RESULT);
return Promise.resolve(undefined);
}
return accessor.get(ITestService).runTests({ tests, debug: this.debug });
@ -327,18 +328,18 @@ export class TestingViewAsTreeAction extends ViewAction<TestingExplorerView> {
}
export class TestingGroupByLocationAction extends ViewAction<TestingExplorerView> {
export class TestingSortByNameAction extends ViewAction<TestingExplorerView> {
constructor() {
super({
id: 'testing.groupByLocation',
id: 'testing.sortByName',
viewId: Testing.ExplorerViewId,
title: localize('testing.groupByLocation', "Sort by Name"),
title: localize('testing.sortByName', "Sort by Name"),
f1: false,
toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByLocation),
toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByName),
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'groupBy',
group: 'sortBy',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
@ -348,22 +349,22 @@ export class TestingGroupByLocationAction extends ViewAction<TestingExplorerView
* @override
*/
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
view.viewModel.viewGrouping = TestExplorerViewGrouping.ByLocation;
view.viewModel.viewSorting = TestExplorerViewSorting.ByName;
}
}
export class TestingGroupByStatusAction extends ViewAction<TestingExplorerView> {
export class TestingSortByLocationAction extends ViewAction<TestingExplorerView> {
constructor() {
super({
id: 'testing.groupByStatus',
id: 'testing.sortByLocation',
viewId: Testing.ExplorerViewId,
title: localize('testing.groupByStatus', "Sort by Status"),
title: localize('testing.sortByLocation', "Sort by Location"),
f1: false,
toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByStatus),
toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByLocation),
menu: {
id: MenuId.ViewTitle,
order: 10,
group: 'groupBy',
group: 'sortBy',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
@ -373,7 +374,7 @@ export class TestingGroupByStatusAction extends ViewAction<TestingExplorerView>
* @override
*/
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
view.viewModel.viewGrouping = TestExplorerViewGrouping.ByStatus;
view.viewModel.viewSorting = TestExplorerViewSorting.ByLocation;
}
}
@ -427,6 +428,24 @@ export class RefreshTestsAction extends Action2 {
}
}
export class ClearTestResultsAction extends Action2 {
constructor() {
super({
id: 'testing.clearTestResults',
title: localize('testing.clearResults', "Clear All Results"),
category,
f1: true
});
}
/**
* @override
*/
public run(accessor: ServicesAccessor) {
accessor.get(ITestResultService).clear();
}
}
export class EditFocusedTest extends ViewAction<TestingExplorerView> {
constructor() {
super({

View File

@ -83,13 +83,14 @@ registerAction2(Action.TestingViewAsTreeAction);
registerAction2(Action.CancelTestRunAction);
registerAction2(Action.RunSelectedAction);
registerAction2(Action.DebugSelectedAction);
registerAction2(Action.TestingGroupByLocationAction);
registerAction2(Action.TestingGroupByStatusAction);
registerAction2(Action.TestingSortByNameAction);
registerAction2(Action.TestingSortByLocationAction);
registerAction2(Action.RefreshTestsAction);
registerAction2(Action.CollapseAllAction);
registerAction2(Action.RunAllAction);
registerAction2(Action.DebugAllAction);
registerAction2(Action.EditFocusedTest);
registerAction2(Action.ClearTestResultsAction);
registerAction2(CloseTestPeek);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);

View File

@ -28,8 +28,8 @@ import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/work
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
import { IncrementalTestCollectionItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
import { maxPriority } from 'vs/workbench/contrib/testing/common/testingStates';
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService';
export class TestingDecorations extends Disposable implements IEditorContribution {
@ -39,6 +39,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio
constructor(
private readonly editor: ICodeEditor,
@ITestService private readonly testService: ITestService,
@ITestResultService private readonly results: ITestResultService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
@ -62,6 +63,15 @@ export class TestingDecorations extends Disposable implements IEditorContributio
}
this.collection.value = this.testService.subscribeToDiffs(ExtHostTestingResource.TextDocument, uri, () => this.setDecorations(uri));
this._register(this.results.onTestChanged(([, changed]) => {
if (changed.item.location?.uri.toString() === uri.toString()) {
this.setDecorations(uri);
}
}));
this._register(this.results.onResultsChanged(() => {
this.setDecorations(uri);
}));
this.setDecorations(uri);
}
@ -74,19 +84,25 @@ export class TestingDecorations extends Disposable implements IEditorContributio
this.editor.changeDecorations(accessor => {
const newDecorations: ITestDecoration[] = [];
for (const test of ref.object.all) {
const stateLookup = this.results.getStateByExtId(test.item.extId);
if (hasValidLocation(uri, test.item)) {
newDecorations.push(this.instantiationService.createInstance(
RunTestDecoration, test, ref.object, test.item.location, this.editor));
RunTestDecoration, test, ref.object, test.item.location, this.editor, stateLookup?.[1].computedState));
}
for (let i = 0; i < test.item.state.messages.length; i++) {
const m = test.item.state.messages[i];
if (!stateLookup) {
continue;
}
const [result, stateItem] = stateLookup;
for (let i = 0; i < stateItem.state.messages.length; i++) {
const m = stateItem.state.messages[i];
if (hasValidLocation(uri, m)) {
const uri = buildTestUri({
type: TestUriType.LiveMessage,
type: TestUriType.ResultActualOutput,
messageIndex: i,
providerId: test.providerId,
testId: test.id,
resultId: result.id,
testId: stateItem.item.extId,
});
newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor));
@ -138,7 +154,7 @@ const firstLineRange = (originalRange: IRange) => ({
endColumn: 1,
});
class RunTestDecoration implements ITestDecoration {
class RunTestDecoration extends Disposable implements ITestDecoration {
/**
* @inheritdoc
*/
@ -156,25 +172,16 @@ class RunTestDecoration implements ITestDecoration {
private readonly collection: IMainThreadTestCollection,
private readonly location: ModeLocation,
private readonly editor: ICodeEditor,
computedState: TestRunState | undefined,
@ITestService private readonly testService: ITestService,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@ICommandService private readonly commandService: ICommandService,
) {
super();
this.line = location.range.startLineNumber;
const queue = [test.children];
let state = this.test.item.state.runState;
while (queue.length) {
for (const child of queue.pop()!) {
const node = collection.getNodeById(child);
if (node) {
state = maxPriority(node.item.state.runState, state);
}
}
}
const icon = state !== TestRunState.Unset
? testingStatesToIcons.get(state)!
const icon = computedState !== undefined && computedState !== TestRunState.Unset
? testingStatesToIcons.get(computedState)!
: test.children.size > 0 ? testingRunAllIcon : testingRunIcon;
this.editorDecoration = {

View File

@ -5,12 +5,12 @@
import * as dom from 'vs/base/browser/dom';
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
import { ITreeEvent, ITreeFilter, ITreeNode, ITreeRenderer, ITreeSorter, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree';
import * as aria from 'vs/base/browser/ui/aria/aria';
import { Action, IAction, IActionViewItem } from 'vs/base/common/actions';
import { DeferredPromise } from 'vs/base/common/async';
import { Color, RGBA } from 'vs/base/common/color';
@ -47,14 +47,10 @@ import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/comm
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation';
import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName';
import { getComputedState } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
import { StateByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation';
import { StateByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByName';
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
import { testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants';
import { TestExplorerViewMode, TestExplorerViewSorting, Testing, testStateNames } from 'vs/workbench/contrib/testing/common/constants';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
@ -188,7 +184,7 @@ export class TestingExplorerViewModel extends Disposable {
public projection!: ITestTreeProjection;
private readonly _viewMode = TestingContextKeys.viewMode.bindTo(this.contextKeyService);
private readonly _viewGrouping = TestingContextKeys.viewGrouping.bindTo(this.contextKeyService);
private readonly _viewSorting = TestingContextKeys.viewSorting.bindTo(this.contextKeyService);
/**
* Fires when the selected tests change.
@ -210,18 +206,18 @@ export class TestingExplorerViewModel extends Disposable {
}
public get viewGrouping() {
return this._viewGrouping.get() ?? TestExplorerViewGrouping.ByLocation;
public get viewSorting() {
return this._viewSorting.get() ?? TestExplorerViewSorting.ByLocation;
}
public set viewGrouping(newGrouping: TestExplorerViewGrouping) {
if (newGrouping === this._viewGrouping.get()) {
public set viewSorting(newSorting: TestExplorerViewSorting) {
if (newSorting === this._viewSorting.get()) {
return;
}
this._viewGrouping.set(newGrouping);
this.updatePreferredProjection();
this.storageService.store('testing.viewGrouping', newGrouping, StorageScope.WORKSPACE, StorageTarget.USER);
this._viewSorting.set(newSorting);
this.tree.resort(null);
this.storageService.store('testing.viewSorting', newSorting, StorageScope.WORKSPACE, StorageTarget.USER);
}
constructor(
@ -229,16 +225,17 @@ export class TestingExplorerViewModel extends Disposable {
onDidChangeVisibility: Event<boolean>,
private listener: TestSubscriptionListener | undefined,
@ITestExplorerFilterState filterState: TestExplorerFilterState,
@IInstantiationService instantiationService: IInstantiationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IEditorService private readonly editorService: IEditorService,
@ICodeEditorService codeEditorService: ICodeEditorService,
@IStorageService private readonly storageService: IStorageService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@ITestResultService private readonly testResults: ITestResultService,
) {
super();
this._viewMode.set(this.storageService.get('testing.viewMode', StorageScope.WORKSPACE, TestExplorerViewMode.Tree) as TestExplorerViewMode);
this._viewGrouping.set(this.storageService.get('testing.viewGrouping', StorageScope.WORKSPACE, TestExplorerViewGrouping.ByLocation) as TestExplorerViewGrouping);
this._viewSorting.set(this.storageService.get('testing.viewSorting', StorageScope.WORKSPACE, TestExplorerViewSorting.ByLocation) as TestExplorerViewSorting);
const labels = this._register(instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: onDidChangeVisibility }));
@ -261,7 +258,7 @@ export class TestingExplorerViewModel extends Disposable {
simpleKeyboardNavigation: true,
identityProvider: instantiationService.createInstance(IdentityProvider),
hideTwistiesOfChildlessElements: true,
sorter: instantiationService.createInstance(TreeSorter),
sorter: instantiationService.createInstance(TreeSorter, this),
keyboardNavigationLabelProvider: instantiationService.createInstance(TreeKeyboardNavigationLabelProvider),
accessibilityProvider: instantiationService.createInstance(ListAccessibilityProvider),
filter: this.filter,
@ -293,6 +290,10 @@ export class TestingExplorerViewModel extends Disposable {
tracker.deactivate();
}
}));
this._register(testResults.onResultsChanged(() => {
this.tree.resort(null);
}));
}
/**
@ -380,16 +381,18 @@ export class TestingExplorerViewModel extends Disposable {
* Tries to peek the first test error, if the item is in a failed state.
*/
private async tryPeekError(item: ITestTreeElement) {
if (!item.test || !isFailedState(item.test.item.state.runState)) {
const lookup = item.test && this.testResults.getStateByExtId(item.test.item.extId);
if (!lookup || !isFailedState(lookup[1].state.state)) {
return false;
}
const index = item.test.item.state.messages.findIndex(m => !!m.location);
const [result, test] = lookup;
const index = test.state.messages.findIndex(m => !!m.location);
if (index === -1) {
return;
}
const message = item.test.item.state.messages[index];
const message = test.state.messages[index];
const pane = await this.editorService.openEditor({
resource: message.location!.uri,
options: { selection: message.location!.range, preserveFocus: true }
@ -401,10 +404,10 @@ export class TestingExplorerViewModel extends Disposable {
}
TestingOutputPeekController.get(control).show(buildTestUri({
type: TestUriType.LiveMessage,
type: TestUriType.ResultMessage,
messageIndex: index,
providerId: item.test.providerId,
testId: item.test.id,
resultId: result.id,
testId: item.test!.item.extId,
}));
return true;
@ -417,18 +420,10 @@ export class TestingExplorerViewModel extends Disposable {
return;
}
if (this._viewGrouping.get() === TestExplorerViewGrouping.ByLocation) {
if (this._viewMode.get() === TestExplorerViewMode.List) {
this.projection = new HierarchicalByNameProjection(this.listener);
} else {
this.projection = new HierarchicalByLocationProjection(this.listener);
}
if (this._viewMode.get() === TestExplorerViewMode.List) {
this.projection = this.instantiationService.createInstance(HierarchicalByNameProjection, this.listener);
} else {
if (this._viewMode.get() === TestExplorerViewMode.List) {
this.projection = new StateByNameProjection(this.listener);
} else {
this.projection = new StateByLocationProjection(this.listener);
}
this.projection = this.instantiationService.createInstance(HierarchicalByLocationProjection, this.listener);
}
this.projection.onUpdate(this.deferUpdate, this);
@ -569,9 +564,19 @@ class TestsFilter implements ITreeFilter<ITestTreeElement> {
}
class TreeSorter implements ITreeSorter<ITestTreeElement> {
constructor(private readonly viewModel: TestingExplorerViewModel) { }
public compare(a: ITestTreeElement, b: ITestTreeElement): number {
if (a instanceof StateElement && b instanceof StateElement) {
return cmpPriority(a.computedState, b.computedState);
let delta = cmpPriority(a.state, b.state);
if (delta !== 0) {
return delta;
}
if (this.viewModel.viewSorting === TestExplorerViewSorting.ByLocation && a.location && b.location && a.location.uri.toString() === b.location.uri.toString()) {
delta = a.location.range.startLineNumber - b.location.range.startLineNumber;
if (delta !== 0) {
return delta;
}
}
return a.label.localeCompare(b.label);
@ -587,7 +592,7 @@ class ListAccessibilityProvider implements IListAccessibilityProvider<ITestTreeE
return localize({
key: 'testing.treeElementLabel',
comment: ['label then the unit tests state, for example "Addition Tests (Running)"'],
}, '{0} ({1})', element.label, testStateNames[getComputedState(element)]);
}, '{0} ({1})', element.label, testStateNames[element.state]);
}
}
@ -662,8 +667,7 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
const options: IResourceLabelOptions = {};
data.actionBar.clear();
const state = getComputedState(element);
const icon = testingStatesToIcons.get(state);
const icon = testingStatesToIcons.get(element.state);
data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : '');
const test = element.test;
if (test) {
@ -683,7 +687,7 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
options.fileKind = FileKind.ROOT_FOLDER;
}
const running = state === TestRunState.Running;
const running = element.state === TestRunState.Running;
if (!Iterable.isEmpty(element.runnable)) {
data.actionBar.push(
this.instantiationService.createInstance(RunAction, element.runnable, running),
@ -740,12 +744,16 @@ const getProgressText = ({ passed, runSoFar, skipped, failed }: CountSummary) =>
class TestRunProgress {
private current?: { update: IProgress<IProgressStep>; deferred: DeferredPromise<void> };
private badge = new MutableDisposable();
private readonly resultLister = this.resultService.onNewTestResult(result => {
private readonly resultLister = this.resultService.onResultsChanged(result => {
if (!('started' in result)) {
return;
}
this.updateProgress();
this.updateBadge();
result.onChange(this.throttledProgressUpdate, this);
result.onComplete(() => {
result.started.onChange(this.throttledProgressUpdate, this);
result.started.onComplete(() => {
this.throttledProgressUpdate();
this.updateBadge();
});

View File

@ -27,15 +27,15 @@ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeServic
import { EditorModel } from 'vs/workbench/common/editor';
import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { InternalTestItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestItem, ITestMessage, ITestState } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
interface ITestDto {
test: ITestItem,
messageIndex: number;
test: InternalTestItem;
state: ITestState;
expectedUri: URI;
actualUri: URI;
messageUri: URI;
@ -66,7 +66,6 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
private readonly editor: ICodeEditor,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ITestResultService private readonly testResults: ITestResultService,
@ITestService private readonly testService: ITestService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super();
@ -83,7 +82,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
return;
}
const message = dto.test.item.state.messages[dto.messageIndex];
const message = dto.state.messages[dto.messageIndex];
if (!message?.location) {
return;
}
@ -120,28 +119,14 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
return undefined;
}
if ('resultId' in parts) {
const test = this.testResults.lookup(parts.resultId)?.tests.find(t => t.id === parts.testId);
return test && {
test,
messageIndex: parts.messageIndex,
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }),
};
}
const test = await this.testService.lookupTest({ providerId: parts.providerId, testId: parts.testId });
if (!test) {
return;
}
return {
test,
const test = this.testResults.getResult(parts.resultId)?.getStateByExtId(parts.testId);
return test && {
test: test.item,
state: test.state,
messageIndex: parts.messageIndex,
expectedUri: buildTestUri({ ...parts, type: TestUriType.LiveActualOutput }),
actualUri: buildTestUri({ ...parts, type: TestUriType.LiveExpectedOutput }),
messageUri: buildTestUri({ ...parts, type: TestUriType.LiveMessage }),
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }),
};
}
}
@ -236,14 +221,14 @@ class TestingDiffOutputPeek extends TestingOutputPeek {
/**
* @override
*/
public async setModel({ test, messageIndex, expectedUri, actualUri }: ITestDto) {
const message = test.item.state.messages[messageIndex];
public async setModel({ test, state, messageIndex, expectedUri, actualUri }: ITestDto) {
const message = state.messages[messageIndex];
if (!message?.location) {
return;
}
this.show(message.location.range, hintDiffPeekHeight(message));
this.setTitle(message.message.toString(), test.item.label);
this.setTitle(message.message.toString(), test.label);
const [original, modified] = await Promise.all([
this.modelService.createModelReference(expectedUri),
@ -285,14 +270,14 @@ class TestingMessageOutputPeek extends TestingOutputPeek {
/**
* @override
*/
public async setModel({ test, messageIndex, messageUri }: ITestDto) {
const message = test.item.state.messages[messageIndex];
public async setModel({ state, test, messageIndex, messageUri }: ITestDto) {
const message = state.messages[messageIndex];
if (!message?.location) {
return;
}
this.show(message.location.range, hintPeekStrHeight(message.message.toString()));
this.setTitle(message.message.toString(), test.item.label);
this.setTitle(message.message.toString(), test.label);
const modelRef = this.model.value = await this.modelService.createModelReference(messageUri);
if (this.preview.value) {

View File

@ -20,9 +20,9 @@ export const enum TestExplorerViewMode {
Tree = 'true'
}
export const enum TestExplorerViewGrouping {
export const enum TestExplorerViewSorting {
ByLocation = 'location',
ByStatus = 'status',
ByName = 'name',
}
export const testStateNames: { [K in TestRunState]: string } = {

View File

@ -0,0 +1,84 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { maxPriority, statePriority } from 'vs/workbench/contrib/testing/common/testingStates';
/**
* Accessor for nodes in get and refresh computed state.
*/
export interface IComputedStateAccessor<T> {
getOwnState(item: T): TestRunState | undefined;
getCurrentComputedState(item: T): TestRunState;
setComputedState(item: T, state: TestRunState): void;
getChildren(item: T): IterableIterator<T>;
getParents(item: T): IterableIterator<T>;
}
/**
* Gets the computed state for the node.
* @param force whether to refresh the computed state for this node, even
* if it was previously set.
*/
export const getComputedState = <T>(accessor: IComputedStateAccessor<T>, node: T, force = false) => {
let computed = accessor.getCurrentComputedState(node);
if (computed === undefined || force) {
computed = accessor.getOwnState(node) ?? TestRunState.Unset;
for (const child of accessor.getChildren(node)) {
computed = maxPriority(computed, getComputedState(accessor, child));
}
accessor.setComputedState(node, computed);
}
return computed;
};
/**
* Refreshes the computed state for the node and its parents. Any changes
* elements cause `addUpdated` to be called.
*/
export const refreshComputedState = <T>(
accessor: IComputedStateAccessor<T>,
node: T,
addUpdated: (node: T) => void,
explicitNewComputedState?: TestRunState,
) => {
const oldState = accessor.getCurrentComputedState(node);
const oldPriority = statePriority[oldState];
const newState = explicitNewComputedState ?? getComputedState(accessor, node, true);
const newPriority = statePriority[newState];
if (newPriority === oldPriority) {
return;
}
accessor.setComputedState(node, newState);
addUpdated(node);
if (newPriority > oldPriority) {
// Update all parents to ensure they're at least this priority.
for (const parent of accessor.getParents(node)) {
const prev = accessor.getCurrentComputedState(parent);
if (prev !== undefined && statePriority[prev] >= newPriority) {
break;
}
accessor.setComputedState(parent, newState);
addUpdated(parent);
}
} else if (newPriority < oldPriority) {
// Re-render all parents of this node whose computed priority might have come from this node
for (const parent of accessor.getParents(node)) {
const prev = accessor.getCurrentComputedState(parent);
if (prev === undefined || statePriority[prev] > oldPriority) {
break;
}
accessor.setComputedState(parent, getComputedState(accessor, parent, true));
addUpdated(parent);
}
}
};

View File

@ -204,7 +204,6 @@ const keyMap: { [K in keyof Omit<RequiredTestItem, 'children'>]: null } = {
id: null,
label: null,
location: null,
state: null,
debuggable: null,
description: null,
runnable: null

View File

@ -26,24 +26,12 @@ export interface RunTestsRequest {
* Request from the main thread to run tests for a single provider.
*/
export interface RunTestForProviderRequest {
runId: string;
providerId: string;
ids: string[];
debug: boolean;
}
/**
* Response to a {@link RunTestsRequest}
*/
export interface RunTestsResult {
// todo
}
export const EMPTY_TEST_RESULT: RunTestsResult = {};
export const collectTestResults = (results: ReadonlyArray<RunTestsResult>) => {
return results[0] || {}; // todo
};
export interface ITestMessage {
message: string | IMarkdownString;
severity: TestMessageSeverity | undefined;
@ -53,7 +41,7 @@ export interface ITestMessage {
}
export interface ITestState {
runState: TestRunState;
state: TestRunState;
duration: number | undefined;
messages: ITestMessage[];
}
@ -70,7 +58,6 @@ export interface ITestItem {
description: string | undefined;
runnable: boolean;
debuggable: boolean;
state: ITestState;
}
/**
@ -84,7 +71,7 @@ export interface InternalTestItem {
}
export interface InternalTestItemWithChildren extends InternalTestItem {
children: InternalTestItemWithChildren[];
children: this[];
}
export interface InternalTestResults {

View File

@ -4,17 +4,52 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
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 { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
import { IncrementalTestCollectionItem, InternalTestItemWithChildren, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { IncrementalTestCollectionItem, ITestState, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { isRunningState, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
import { statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService';
/**
* Count of the number of tests in each run state.
*/
export type TestStateCount = { [K in TestRunState]: number };
export interface ITestResult {
/**
* Count of the number of tests in each run state.
*/
readonly counts: Readonly<TestStateCount>;
/**
* Unique ID of this set of test results.
*/
readonly id: string;
/**
* Gets whether the test run has finished.
*/
readonly isComplete: boolean;
/**
* Gets the state of the test by its extension-assigned ID.
*/
getStateByExtId(testExtId: string): TestResultItem | undefined;
/**
* Serializes the test result. Used to save and restore results
* in the workspace.
*/
toJSON(): ISerializedResults;
}
const makeEmptyCounts = () => {
const o: Partial<TestStateCount> = {};
for (const state of statesInOrder) {
@ -24,7 +59,7 @@ const makeEmptyCounts = () => {
return o as TestStateCount;
};
export const sumCounts = (counts: TestStateCount[]) => {
export const sumCounts = (counts: Iterable<TestStateCount>) => {
const total = makeEmptyCounts();
for (const count of counts) {
for (const state of statesInOrder) {
@ -35,28 +70,97 @@ export const sumCounts = (counts: TestStateCount[]) => {
return total;
};
const makeNode = (
const queuedState: ITestState = {
duration: undefined,
messages: [],
state: TestRunState.Queued
};
const unsetState: ITestState = {
duration: undefined,
messages: [],
state: TestRunState.Unset
};
const itemToNode = (
item: IncrementalTestCollectionItem,
byExtId: Map<string, TestResultItem>,
byInternalId: 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 },
state: unsetState,
computedState: TestRunState.Unset,
};
byExtId.set(n.item.extId, n);
byInternalId.set(n.id, n);
return n;
};
const makeParents = (
collection: IMainThreadTestCollection,
child: IncrementalTestCollectionItem,
byExtId: Map<string, TestResultItem>,
byInternalId: Map<string, TestResultItem>,
) => {
const parent = child.parent && collection.getNodeById(child.parent);
if (!parent) {
return;
}
let parentResultItem = byInternalId.get(parent.id);
if (parentResultItem) {
parentResultItem.children.add(child.id);
return; // no need to recurse, all parents already in result
}
parentResultItem = itemToNode(parent, byExtId, byInternalId);
parentResultItem.children = new Set([child.id]);
makeParents(collection, parent, byExtId, byInternalId);
};
const makeNodeAndChildren = (
collection: IMainThreadTestCollection,
test: IncrementalTestCollectionItem,
byExtId: Map<string, TestResultItem>,
byInternalId: Map<string, TestResultItem>,
): TestResultItem => {
const mapped: TestResultItem = { ...test, children: [] };
const existing = byInternalId.get(test.id);
if (existing) {
return existing;
}
const mapped = itemToNode(test, byExtId, byInternalId);
for (const childId of test.children) {
const child = collection.getNodeById(childId);
if (child) {
mapped.children.push(makeNode(collection, child));
makeNodeAndChildren(collection, child, byExtId, byInternalId);
}
}
return mapped;
};
export interface TestResultItem extends InternalTestItemWithChildren { }
interface ISerializedResults {
id: string;
counts: TestStateCount;
items: Iterable<[extId: string, item: TestResultItem]>;
}
interface TestResultItem extends IncrementalTestCollectionItem {
state: ITestState;
computedState: TestRunState;
}
/**
* Results of a test. These are created when the test initially started running
* and marked as "complete" when the run finishes.
*/
export class TestResult {
export class LiveTestResult implements ITestResult {
/**
* Creates a new TestResult, pulling tests from the associated list
* of collections.
@ -65,27 +169,29 @@ export class TestResult {
collections: ReadonlyArray<IMainThreadTestCollection>,
tests: ReadonlyArray<TestIdWithProvider>,
) {
const mapped: TestResultItem[] = [];
const testByExtId = new Map<string, TestResultItem>();
const testByInternalId = new Map<string, TestResultItem>();
for (const test of tests) {
for (const collection of collections) {
const node = collection.getNodeById(test.testId);
if (node) {
mapped.push(makeNode(collection, node));
break;
if (!node) {
continue;
}
makeNodeAndChildren(collection, node, testByExtId, testByInternalId);
makeParents(collection, node, testByExtId, testByInternalId);
}
}
return new TestResult(mapped);
return new LiveTestResult(collections, testByExtId, testByInternalId);
}
private completeEmitter = new Emitter<void>();
private changeEmitter = new Emitter<void>();
private readonly completeEmitter = new Emitter<void>();
private readonly changeEmitter = new Emitter<TestResultItem>();
private _complete = false;
private _cachedCounts?: { [K in TestRunState]: number };
public onChange = this.changeEmitter.event;
public onComplete = this.completeEmitter.event;
public readonly onChange = this.changeEmitter.event;
public readonly onComplete = this.completeEmitter.event;
/**
* Unique ID for referring to this set of test results.
@ -93,34 +199,122 @@ export class TestResult {
public readonly id = generateUuid();
/**
* Gets whether the test run has finished.
* @inheritdoc
*/
public get isComplete() {
return this._complete;
}
/**
* Gets a count of tests in each state.
* @inheritdoc
*/
public get counts() {
if (this._cachedCounts) {
return this._cachedCounts;
}
public readonly counts: { [K in TestRunState]: number } = makeEmptyCounts();
const counts = makeEmptyCounts();
this.forEachTest(({ item }) => {
counts[item.state.runState]++;
});
if (this._complete) {
this._cachedCounts = counts;
}
return counts;
/**
* Gets all tests involved in the run by ID.
*/
public get tests() {
return this.testByInternalId.values();
}
constructor(public readonly tests: TestResultItem[]) { }
private readonly computedStateAccessor: IComputedStateAccessor<TestResultItem> = {
getOwnState: i => i.state.state,
getCurrentComputedState: i => i.computedState,
setComputedState: (i, s) => i.computedState = s,
getChildren: i => {
const { testByInternalId } = this;
return (function* () {
for (const childId of i.children) {
const child = testByInternalId.get(childId);
if (child) {
yield child;
}
}
})();
},
getParents: i => {
const { testByInternalId } = this;
return (function* () {
for (let parentId = i.parent; parentId;) {
const parent = testByInternalId.get(parentId);
if (!parent) {
break;
}
yield parent;
parentId = parent.parent;
}
})();
},
};
constructor(
private readonly collections: ReadonlyArray<IMainThreadTestCollection>,
private readonly testByExtId: Map<string, TestResultItem>,
private readonly testByInternalId: Map<string, TestResultItem>,
) {
this.counts[TestRunState.Unset] = testByInternalId.size;
}
/**
* @inheritdoc
*/
public getStateByExtId(extTestId: string) {
return this.testByExtId.get(extTestId);
}
/**
* Updates all tests in the collection to the given state.
*/
public setAllToState(state: ITestState, when: (_t: TestResultItem) => boolean) {
for (const test of this.testByInternalId.values()) {
if (when(test)) {
this.counts[state.state]--;
test.state = state;
this.counts[state.state]++;
refreshComputedState(this.computedStateAccessor, test, t => this.changeEmitter.fire(t));
}
}
}
/**
* Updates the state of the test by its internal ID.
*/
public updateState(testId: string, state: ITestState) {
let entry = this.testByInternalId.get(testId);
if (!entry) {
entry = this.addTestToRun(testId);
}
if (!entry) {
return;
}
if (state.state === entry.state.state) {
entry.state = state;
this.changeEmitter.fire(entry);
} else {
this.counts[entry.state.state]--;
entry.state = state;
this.counts[entry.state.state]++;
refreshComputedState(this.computedStateAccessor, entry, t => this.changeEmitter.fire(t));
}
}
/**
* 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.
*/
private addTestToRun(testId: string) {
for (const collection of this.collections) {
let test = collection.getNodeById(testId);
if (test) {
return makeNodeAndChildren(collection, test, this.testByExtId, this.testByInternalId);
}
}
return undefined;
}
/**
* Notifies the service that all tests are complete.
@ -130,109 +324,209 @@ export class TestResult {
throw new Error('cannot complete a test result multiple times');
}
// shallow clone test items to 'disconnect' them from the underlying
// connection and stop state changes. Also, marked any still-running
// tests as skipped.
this.forEachTest(test => {
test.item = { ...test.item };
if (isRunningState(test.item.state.runState)) {
test.item.state = { ...test.item.state, runState: TestRunState.Skipped };
}
});
// un-queue any tests that weren't explicitly updated
this.setAllToState(unsetState, t => t.state.state === TestRunState.Queued);
this._complete = true;
this.completeEmitter.fire();
}
/**
* Fires the 'change' event, should be called by the runner.
* @inheritdoc
*/
public notifyChanged() {
this.changeEmitter.fire();
}
private forEachTest(fn: (test: TestResultItem) => void) {
const queue = [this.tests];
while (queue.length) {
for (const test of queue.pop()!) {
fn(test);
queue.push(test.children);
}
}
public toJSON(): ISerializedResults {
return { id: this.id, counts: this.counts, items: [...this.testByExtId.entries()] };
}
}
/**
* Test results hydrated from a previously-serialized test run.
*/
class HydratedTestResult implements ITestResult {
/**
* @inheritdoc
*/
public readonly counts = this.serialized.counts;
/**
* @inheritdoc
*/
public readonly id = this.serialized.id;
/**
* @inheritdoc
*/
public readonly isComplete = true;
private readonly map = new Map<string, TestResultItem>();
constructor(private readonly serialized: ISerializedResults) {
for (const [key, value] of serialized.items) {
this.map.set(key, value);
for (const message of value.state.messages) {
if (message.location) {
message.location.uri = URI.revive(message.location.uri);
}
}
}
}
/**
* @inheritdoc
*/
public getStateByExtId(extTestId: string) {
return this.map.get(extTestId);
}
/**
* @inheritdoc
*/
public toJSON(): ISerializedResults {
return this.serialized;
}
}
export type ResultChangeEvent =
| { completed: LiveTestResult }
| { started: LiveTestResult }
| { removed: ITestResult[] };
export interface ITestResultService {
readonly _serviceBrand: undefined;
/**
* Fired after any results are added, removed, or completed.
*/
readonly onResultsChanged: Event<ResultChangeEvent>;
/**
* List of test results. Currently running tests are always at the top.
* Fired when a test changed it state, or its computed state is updated.
*/
readonly results: TestResult[];
readonly onTestChanged: Event<[results: ITestResult, item: TestResultItem]>;
/**
* Fired after a new event is added to the 'active' array.
* List of known test results.
*/
readonly onNewTestResult: Event<TestResult>;
readonly results: ReadonlyArray<ITestResult>;
/**
* Discards all completed test results.
*/
clear(): void;
/**
* Adds a new test result to the collection.
*/
push(result: TestResult): TestResult;
push(result: LiveTestResult): LiveTestResult;
/**
* Looks up a set of test results by ID.
*/
lookup(resultId: string): TestResult | undefined;
getResult(resultId: string): ITestResult | undefined;
/**
* Looks up a test's most recent state, by its extension-assigned ID.
*/
getStateByExtId(extId: string): [results: ITestResult, item: TestResultItem] | undefined;
}
export const ITestResultService = createDecorator<ITestResultService>('testResultService');
const RETAIN_LAST_RESULTS = 16;
const RETAIN_LAST_RESULTS = 64;
export class TestResultService implements ITestResultService {
declare _serviceBrand: undefined;
private newResultEmitter = new Emitter<TestResult>();
private changeResultEmitter = new Emitter<ResultChangeEvent>();
private testChangeEmitter = new Emitter<[results: ITestResult, item: TestResultItem]>();
/**
* @inheritdoc
*/
public results: TestResult[] = [];
public results: ITestResult[] = [];
/**
* @inheritdoc
*/
public readonly onNewTestResult = this.newResultEmitter.event;
public readonly onResultsChanged = this.changeResultEmitter.event;
/**
* @inheritdoc
*/
public readonly onTestChanged = this.testChangeEmitter.event;
private readonly isRunning: IContextKey<boolean>;
private readonly serializedResults: StoredValue<ISerializedResults[]>;
constructor(@IContextKeyService contextKeyService: IContextKeyService) {
constructor(@IContextKeyService contextKeyService: IContextKeyService, @IStorageService storage: IStorageService) {
this.isRunning = TestingContextKeys.isRunning.bindTo(contextKeyService);
this.serializedResults = new StoredValue({
key: 'testResults',
scope: StorageScope.WORKSPACE,
target: StorageTarget.MACHINE
}, storage);
for (const value of this.serializedResults.get([])) {
this.results.push(new HydratedTestResult(value));
}
}
/**
* @inheritdoc
*/
public push(result: TestResult): TestResult {
public getStateByExtId(extId: string): [results: ITestResult, item: TestResultItem] | undefined {
for (const result of this.results) {
const lookup = result.getStateByExtId(extId);
if (lookup && lookup.computedState !== TestRunState.Unset) {
return [result, lookup];
}
}
return undefined;
}
/**
* @inheritdoc
*/
public push(result: LiveTestResult): LiveTestResult {
this.results.unshift(result);
if (this.results.length > RETAIN_LAST_RESULTS) {
this.results.pop();
}
result.onComplete(this.onComplete, this);
result.onComplete(() => this.onComplete(result));
result.onChange(t => this.testChangeEmitter.fire([result, t]), this.testChangeEmitter);
this.isRunning.set(true);
this.newResultEmitter.fire(result);
this.changeResultEmitter.fire({ started: result });
result.setAllToState(queuedState, () => true);
return result;
}
/**
* @inheritdoc
*/
public lookup(id: string) {
public getResult(id: string) {
return this.results.find(r => r.id === id);
}
private onComplete() {
/**
* @inheritdoc
*/
public clear() {
const keep: ITestResult[] = [];
const removed: ITestResult[] = [];
for (const result of this.results) {
if (result.isComplete) {
removed.push(result);
} else {
keep.push(result);
}
}
this.results = keep;
this.serializedResults.store(this.results.map(r => r.toJSON()));
this.changeResultEmitter.fire({ removed });
}
private onComplete(result: LiveTestResult) {
// move the complete test run down behind any still-running ones
for (let i = 0; i < this.results.length - 2; i++) {
if (this.results[i].isComplete && !this.results[i + 1].isComplete) {
@ -241,5 +535,7 @@ export class TestResultService implements ITestResultService {
}
this.isRunning.set(!this.results[0]?.isComplete);
this.serializedResults.store(this.results.map(r => r.toJSON()));
this.changeResultEmitter.fire({ completed: result });
}
}

View File

@ -9,13 +9,14 @@ import { IDisposable, IReference } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResultService';
export const ITestService = createDecorator<ITestService>('testService');
export interface MainTestController {
lookupTest(test: TestIdWithProvider): Promise<InternalTestItem | undefined>;
runTests(request: RunTestForProviderRequest, token: CancellationToken): Promise<RunTestsResult>;
runTests(request: RunTestForProviderRequest, token: CancellationToken): Promise<void>;
}
export type TestDiffListener = (diff: TestsDiff) => void;
@ -84,7 +85,7 @@ export interface ITestService {
registerTestController(id: string, controller: MainTestController): void;
unregisterTestController(id: string): void;
runTests(req: RunTestsRequest, token?: CancellationToken): Promise<RunTestsResult>;
runTests(req: RunTestsRequest, token?: CancellationToken): Promise<ITestResult>;
cancelTestRun(req: RunTestsRequest): void;
publishDiff(resource: ExtHostTestingResource, uri: URI, diff: TestsDiff): void;
subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference<IMainThreadTestCollection>;

View File

@ -8,15 +8,14 @@ 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 } 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';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { AbstractIncrementalTestCollection, collectTestResults, EMPTY_TEST_RESULT, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { AbstractIncrementalTestCollection, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestResultService, TestResult } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestResult, ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService';
import { IMainThreadTestCollection, ITestService, MainTestController, TestDiffListener } from 'vs/workbench/contrib/testing/common/testService';
type TestLocationIdent = { resource: ExtHostTestingResource, uri: URI };
@ -40,14 +39,9 @@ export class TestService extends Disposable implements ITestService {
private readonly busyStateChangeEmitter = new Emitter<TestLocationIdent & { busy: boolean }>();
private readonly changeProvidersEmitter = new Emitter<{ delta: number }>();
private readonly providerCount: IContextKey<number>;
private readonly runStartedEmitter = new Emitter<RunTestsRequest>();
private readonly runCompletedEmitter = new Emitter<{ req: RunTestsRequest, result: RunTestsResult }>();
private readonly runningTests = new Map<RunTestsRequest, CancellationTokenSource>();
private rootProviderCount = 0;
public readonly onTestRunStarted = this.runStartedEmitter.event;
public readonly onTestRunCompleted = this.runCompletedEmitter.event;
constructor(@IContextKeyService contextKeyService: IContextKeyService, @INotificationService private readonly notificationService: INotificationService, @ITestResultService private readonly testResults: ITestResultService) {
super();
this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService);
@ -126,35 +120,32 @@ export class TestService extends Disposable implements ITestService {
/**
* @inheritdoc
*/
public async runTests(req: RunTestsRequest, token = CancellationToken.None): Promise<RunTestsResult> {
let result: TestResult | undefined;
public async runTests(req: RunTestsRequest, token = CancellationToken.None): Promise<ITestResult> {
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, () => result?.notifyChanged()));
result = this.testResults.push(TestResult.from(subscriptions.map(s => s.object), req.tests));
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri));
const result = this.testResults.push(LiveTestResult.from(subscriptions.map(s => s.object), req.tests));
try {
const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1);
const cancelSource = new CancellationTokenSource(token);
this.runningTests.set(req, cancelSource);
const requests = tests.map(group => {
const providerId = group[0].providerId;
const controller = this.testControllers.get(providerId);
return controller?.runTests({ providerId, debug: req.debug, ids: group.map(t => t.testId) }, cancelSource.token).catch(err => {
return controller?.runTests(
{ runId: result.id, providerId, debug: req.debug, ids: group.map(t => t.testId) },
cancelSource.token,
).catch(err => {
this.notificationService.error(localize('testError', 'An error occurred attempting to run tests: {0}', err.message));
return EMPTY_TEST_RESULT;
});
}).filter(isDefined);
if (requests.length === 0) {
return EMPTY_TEST_RESULT;
}
this.runningTests.set(req, cancelSource);
const result = collectTestResults(await Promise.all(requests));
this.runningTests.delete(req);
});
await Promise.all(requests);
return result;
} finally {
this.runningTests.delete(req);
subscriptions.forEach(s => s.dispose());
result.markComplete();
}

View File

@ -3,12 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TestItem, TestRunState, TestState } from 'vs/workbench/api/common/extHostTypes';
import { TestItem, TestRunState } from 'vs/workbench/api/common/extHostTypes';
export const stubTest = (label: string): TestItem => ({
label,
location: undefined,
state: new TestState(TestRunState.Unset),
debuggable: true,
runnable: true,
description: ''

View File

@ -10,7 +10,6 @@ import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/s
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
/**
* A content provider that returns various outputs for tests. This is used
@ -20,8 +19,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
constructor(
@ITextModelService textModelResolverService: ITextModelService,
@IModelService private readonly modelService: IModelService,
@ITestService private readonly testService: ITestService,
@ITestService private readonly resultService: ITestResultService,
@ITestResultService private readonly resultService: ITestResultService,
) {
textModelResolverService.registerTextModelContentProvider(TEST_DATA_SCHEME, this);
}
@ -40,9 +38,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
return null;
}
const test = 'providerId' in parsed
? await this.testService.lookupTest({ providerId: parsed.providerId, testId: parsed.testId })
: this.resultService.lookup(parsed.resultId)?.tests.find(t => t.id === parsed.testId);
const test = this.resultService.getResult(parsed.resultId)?.getStateByExtId(parsed.testId);
if (!test) {
return null;
@ -51,16 +47,13 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
let text: string | undefined;
switch (parsed.type) {
case TestUriType.ResultActualOutput:
case TestUriType.LiveActualOutput:
text = test.item.state.messages[parsed.messageIndex]?.actualOutput;
text = test.state.messages[parsed.messageIndex]?.actualOutput;
break;
case TestUriType.ResultExpectedOutput:
case TestUriType.LiveExpectedOutput:
text = test.item.state.messages[parsed.messageIndex]?.expectedOutput;
text = test.state.messages[parsed.messageIndex]?.expectedOutput;
break;
case TestUriType.ResultMessage:
case TestUriType.LiveMessage:
text = test.item.state.messages[parsed.messageIndex]?.message.toString();
text = test.state.messages[parsed.messageIndex]?.message.toString();
break;
}

View File

@ -5,12 +5,12 @@
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { ViewContainerLocation } from 'vs/workbench/common/views';
import { TestExplorerViewMode, TestExplorerViewGrouping } from 'vs/workbench/contrib/testing/common/constants';
import { TestExplorerViewMode, TestExplorerViewSorting } from 'vs/workbench/contrib/testing/common/constants';
export namespace TestingContextKeys {
export const providerCount = new RawContextKey('testing.providerCount', 0);
export const viewMode = new RawContextKey('testing.explorerViewMode', TestExplorerViewMode.List);
export const viewGrouping = new RawContextKey('testing.explorerViewGrouping', TestExplorerViewGrouping.ByLocation);
export const viewSorting = new RawContextKey('testing.explorerViewSorting', TestExplorerViewSorting.ByLocation);
export const isRunning = new RawContextKey('testing.isRunning', false);
export const isInPeek = new RawContextKey('testing.isInPeek', true);
export const isPeekVisible = new RawContextKey('testing.isPeekVisible', false);

View File

@ -14,12 +14,12 @@ export type TreeStateNode = { statusNode: true; state: TestRunState; priority: n
*/
export const statePriority: { [K in TestRunState]: number } = {
[TestRunState.Running]: 6,
[TestRunState.Queued]: 5,
[TestRunState.Errored]: 4,
[TestRunState.Failed]: 3,
[TestRunState.Passed]: 2,
[TestRunState.Skipped]: 1,
[TestRunState.Unset]: 0,
[TestRunState.Errored]: 5,
[TestRunState.Failed]: 4,
[TestRunState.Passed]: 3,
[TestRunState.Queued]: 2,
[TestRunState.Unset]: 1,
[TestRunState.Skipped]: 0,
};
export const isFailedState = (s: TestRunState) => s === TestRunState.Errored || s === TestRunState.Failed;

View File

@ -8,29 +8,11 @@ import { URI } from 'vs/base/common/uri';
export const TEST_DATA_SCHEME = 'vscode-test-data';
export const enum TestUriType {
LiveMessage,
LiveActualOutput,
LiveExpectedOutput,
ResultMessage,
ResultActualOutput,
ResultExpectedOutput,
}
interface ILiveTestUri {
providerId: string;
testId: string;
}
interface ILiveTestMessageReference extends ILiveTestUri {
type: TestUriType.LiveMessage;
messageIndex: number;
}
interface ILiveTestOutputReference extends ILiveTestUri {
type: TestUriType.LiveActualOutput | TestUriType.LiveExpectedOutput;
messageIndex: number;
}
interface IResultTestUri {
resultId: string;
testId: string;
@ -48,13 +30,10 @@ interface IResultTestOutputReference extends IResultTestUri {
export type ParsedTestUri =
| IResultTestMessageReference
| IResultTestOutputReference
| ILiveTestMessageReference
| ILiveTestOutputReference;
| IResultTestOutputReference;
const enum TestUriParts {
Results = 'results',
Live = 'live',
Messages = 'message',
Text = 'text',
@ -78,15 +57,6 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
case TestUriParts.ExpectedOutput:
return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultExpectedOutput };
}
} else if (type === TestUriParts.Live) {
switch (part) {
case TestUriParts.Text:
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveMessage };
case TestUriParts.ActualOutput:
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveActualOutput };
case TestUriParts.ExpectedOutput:
return { providerId: locationId, testId, messageIndex: index, type: TestUriType.LiveExpectedOutput };
}
}
}
@ -96,7 +66,7 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
export const buildTestUri = (parsed: ParsedTestUri): URI => {
const uriParts = {
scheme: TEST_DATA_SCHEME,
authority: 'resultId' in parsed ? TestUriParts.Results : TestUriParts.Live
authority: TestUriParts.Results
};
const msgRef = (locationId: string, index: number, ...remaining: string[]) =>
URI.from({
@ -111,12 +81,6 @@ export const buildTestUri = (parsed: ParsedTestUri): URI => {
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput);
case TestUriType.ResultMessage:
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.Text);
case TestUriType.LiveActualOutput:
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.ActualOutput);
case TestUriType.LiveExpectedOutput:
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.ExpectedOutput);
case TestUriType.LiveMessage:
return msgRef(parsed.providerId, parsed.messageIndex, TestUriParts.Text);
default:
throw new Error('Invalid test uri');
}

View File

@ -13,7 +13,11 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => {
const folder1 = makeTestWorkspaceFolder('f1');
const folder2 = makeTestWorkspaceFolder('f2');
setup(() => {
harness = new TestTreeTestHarness(l => new HierarchicalByLocationProjection(l));
harness = new TestTreeTestHarness(l => new HierarchicalByLocationProjection(l, {
onResultsChanged: () => undefined,
onTestChanged: () => undefined,
getStateByExtId: () => ({ state: { state: 0 }, computedState: 0 }),
} as any));
});
teardown(() => {

View File

@ -13,7 +13,11 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => {
const folder1 = makeTestWorkspaceFolder('f1');
const folder2 = makeTestWorkspaceFolder('f2');
setup(() => {
harness = new TestTreeTestHarness(l => new HierarchicalByNameProjection(l));
harness = new TestTreeTestHarness(l => new HierarchicalByNameProjection(l, {
onResultsChanged: () => undefined,
onTestChanged: () => undefined,
getStateByExtId: () => ({ state: { state: 0 }, computedState: 0 }),
} as any));
});
teardown(() => {

View File

@ -1,154 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { StateByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByLocation';
import { ReExportedTestRunState as TestRunState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs';
import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree';
suite('Workbench - Testing Explorer State by Location Projection', () => {
let harness: TestTreeTestHarness;
setup(() => {
harness = new TestTreeTestHarness(l => new StateByLocationProjection(l));
});
teardown(() => {
harness.dispose();
});
test('renders initial tree', () => {
harness.c.addRoot(testStubs.nested(), 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }
]);
});
test('expands if second root is added', () => {
harness.c.addRoot(testStubs.nested(), 'a');
harness.flush();
harness.c.addRoot({
...testStubs.test('root2'),
children: [testStubs.test('c')]
}, 'b');
assert.deepStrictEqual(harness.flush(), [
{
e: 'Unset', children: [
{ e: 'root', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] },
{ e: 'root2', children: [{ e: 'c' }] },
]
}
]);
});
test('recompacts if second root children are removed', () => {
harness.c.addRoot(testStubs.nested(), 'a');
harness.flush();
const root2 = {
...testStubs.test('root2'),
children: [testStubs.test('c')]
};
harness.c.addRoot(root2, 'b');
harness.flush();
root2.children.pop();
harness.c.onItemChange(root2, 'b');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }
]);
});
test('updates nodes if they change', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
tests.children[0].label = 'changed';
harness.c.onItemChange(tests.children[0], 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'changed', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] }
]);
});
test('updates nodes if they add children', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
tests.children[0].children?.push(testStubs.test('ac'));
harness.c.onItemChange(tests.children[0], 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] }, { e: 'b' }] }
]);
});
test('updates nodes if they remove children', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
tests.children[0].children?.pop();
harness.c.onItemChange(tests.children[0], 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }] }, { e: 'b' }] }
]);
});
test('moves nodes when states change', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
const subchild = tests.children[0].children![0];
subchild.state = { runState: TestRunState.Passed, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Passed', children: [{ e: 'a', children: [{ e: 'aa' }] }] },
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] },
]);
subchild.state = { runState: TestRunState.Failed, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Failed', children: [{ e: 'a', children: [{ e: 'aa' }] }] },
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] },
]);
subchild.state = { runState: TestRunState.Unset, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] },
]);
});
test('does not move when state is running', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
const subchild = tests.children[0].children![0];
subchild.state = { runState: TestRunState.Running, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'aa' }, { e: 'ab' }] }, { e: 'b' }] },
]);
subchild.state = { runState: TestRunState.Failed, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Failed', children: [{ e: 'a', children: [{ e: 'aa' }] }] },
{ e: 'Unset', children: [{ e: 'a', children: [{ e: 'ab' }] }, { e: 'b' }] },
]);
});
});

View File

@ -1,122 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { StateByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateByName';
import { ReExportedTestRunState as TestRunState, testStubs } from 'vs/workbench/contrib/testing/common/testStubs';
import { TestTreeTestHarness } from 'vs/workbench/contrib/testing/test/browser/testObjectTree';
suite('Workbench - Testing Explorer State by Name Projection', () => {
let harness: TestTreeTestHarness;
setup(() => {
harness = new TestTreeTestHarness(l => new StateByNameProjection(l));
});
teardown(() => {
harness.dispose();
});
test('renders initial tree', () => {
harness.c.addRoot(testStubs.nested(), 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }
]);
});
test('swaps when node becomes leaf', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
tests.children[0].children = [];
harness.c.onItemChange(tests.children[0], 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'a' }, { e: 'b' }] }
]);
});
test('swaps when node is no longer leaf', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
tests.children[1].children = [testStubs.test('ba')];
harness.c.onItemChange(tests.children[1], 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ba' }] }
]);
});
test('swaps when node is no longer runnable', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
tests.children[1].children = [testStubs.test('ba')];
harness.c.onItemChange(tests.children[0], 'a');
harness.flush();
tests.children[1].children[0].runnable = false;
harness.c.onItemChange(tests.children[1].children[0], 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }
]);
});
test('moves nodes when states change', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
const subchild = tests.children[0].children![0];
subchild.state = { runState: TestRunState.Passed, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Passed', children: [{ e: 'aa' }] },
{ e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] },
]);
subchild.state = { runState: TestRunState.Failed, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Failed', children: [{ e: 'aa' }] },
{ e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] },
]);
subchild.state = { runState: TestRunState.Unset, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }
]);
});
test('does not move when state is running', () => {
const tests = testStubs.nested();
harness.c.addRoot(tests, 'a');
harness.flush();
const subchild = tests.children[0].children![0];
subchild.state = { runState: TestRunState.Running, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Unset', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'b' }] }
]);
subchild.state = { runState: TestRunState.Failed, messages: [] };
harness.c.onItemChange(subchild, 'a');
assert.deepStrictEqual(harness.flush(), [
{ e: 'Failed', children: [{ e: 'aa' }] },
{ e: 'Unset', children: [{ e: 'ab' }, { e: 'b' }] },
]);
});
});

View File

@ -9,9 +9,6 @@ import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workb
suite('Workbench - Testing URIs', () => {
test('round trip', () => {
const uris: ParsedTestUri[] = [
{ type: TestUriType.LiveActualOutput, messageIndex: 42, providerId: 'p', testId: 't' },
{ type: TestUriType.LiveExpectedOutput, messageIndex: 42, providerId: 'p', testId: 't' },
{ type: TestUriType.LiveMessage, messageIndex: 42, providerId: 'p', testId: 't' },
{ type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testId: 't' },
{ type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testId: 't' },
{ type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testId: 't' },

View File

@ -326,7 +326,6 @@ suite('ExtHost Testing', () => {
assert.strictEqual(testItem.label, wrapper.label);
assert.strictEqual(testItem.location, wrapper.location);
assert.strictEqual(testItem.runnable, wrapper.runnable);
assert.strictEqual(testItem.state, wrapper.state);
});
test('gets no children if nothing matches Uri filter', () => {