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:
parent
783bb42362
commit
3d4cabb608
|
@ -1009,6 +1009,7 @@
|
||||||
"end",
|
"end",
|
||||||
"expand",
|
"expand",
|
||||||
"hide",
|
"hide",
|
||||||
|
"invalidate",
|
||||||
"open",
|
"open",
|
||||||
"override",
|
"override",
|
||||||
"receive",
|
"receive",
|
||||||
|
|
|
@ -3,410 +3,6 @@
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
export function activate() {
|
||||||
import * as nls from 'vscode-nls';
|
// no-op. This extension may be removed in the future
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
|
||||||
|
|
||||||
interface IDisposable {
|
|
||||||
dispose(): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
|
||||||
this.model.updateElementHeight(element, height);
|
this.model.updateElementHeight(element, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
resort(element: T, recursive = true): void {
|
resort(element: T | null, recursive = true): void {
|
||||||
this.model.resort(element, recursive);
|
this.model.resort(element, recursive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
50
src/vs/vscode.proposed.d.ts
vendored
50
src/vs/vscode.proposed.d.ts
vendored
|
@ -2109,10 +2109,12 @@ declare module 'vscode' {
|
||||||
export function registerTestProvider<T extends TestItem>(testProvider: TestProvider<T>): Disposable;
|
export function registerTestProvider<T extends TestItem>(testProvider: TestProvider<T>): Disposable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs tests with the given options. If no options are given, then
|
* Runs tests. The "run" contains the list of tests to run as well as a
|
||||||
* all tests are run. Returns the resulting test run.
|
* 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.
|
* Returns an observer that retrieves tests in the given workspace folder.
|
||||||
|
@ -2226,6 +2228,14 @@ declare module 'vscode' {
|
||||||
*/
|
*/
|
||||||
readonly discoveredInitialTests?: Thenable<unknown>;
|
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
|
* Dispose will be called when there are no longer observers interested
|
||||||
* in the hierarchy.
|
* 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.
|
* @todo this will eventually need to be able to return a summary report, coverage for example.
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line vscode-dts-provider-naming
|
// 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> {
|
export interface TestRunOptions<T extends TestItem = TestItem> {
|
||||||
/**
|
/**
|
||||||
|
@ -2289,6 +2299,17 @@ declare module 'vscode' {
|
||||||
debug: boolean;
|
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
|
* 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.
|
* 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.
|
* Optional list of nested tests for this item.
|
||||||
*/
|
*/
|
||||||
children?: TestItem[];
|
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
|
* in order to update it. This allows consumers to quickly and easily check
|
||||||
* for changes via object identity.
|
* for changes via object identity.
|
||||||
*/
|
*/
|
||||||
export class TestState {
|
export interface TestState {
|
||||||
/**
|
/**
|
||||||
* Current state of the test.
|
* Current state of the test.
|
||||||
*/
|
*/
|
||||||
readonly runState: TestRunState;
|
readonly state: TestRunState;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional duration of the test run, in milliseconds.
|
* Optional duration of the test run, in milliseconds.
|
||||||
|
@ -2392,14 +2407,7 @@ declare module 'vscode' {
|
||||||
* Associated test run message. Can, for example, contain assertion
|
* Associated test run message. Can, for example, contain assertion
|
||||||
* failure information if the test fails.
|
* failure information if the test fails.
|
||||||
*/
|
*/
|
||||||
readonly messages: ReadonlyArray<Readonly<TestMessage>>;
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,11 +4,11 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
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 { URI, UriComponents } from 'vs/base/common/uri';
|
||||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||||
import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
import { getTestSubscriptionKey, ITestState, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
import { ITestResultService, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
|
import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
|
||||||
|
|
||||||
|
@ -19,12 +19,6 @@ const reviveDiff = (diff: TestsDiff) => {
|
||||||
if (item.item.location) {
|
if (item.item.location) {
|
||||||
item.item.location.uri = URI.revive(item.item.location.uri);
|
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(
|
constructor(
|
||||||
extHostContext: IExtHostContext,
|
extHostContext: IExtHostContext,
|
||||||
@ITestService private readonly testService: ITestService,
|
@ITestService private readonly testService: ITestService,
|
||||||
@ITestResultService resultService: ITestResultService,
|
@ITestResultService private readonly resultService: ITestResultService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting);
|
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting);
|
||||||
this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri)));
|
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)));
|
this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri)));
|
||||||
|
|
||||||
const testCompleteListener = this._register(new MutableDisposable());
|
// const testCompleteListener = this._register(new MutableDisposable());
|
||||||
this._register(resultService.onNewTestResult(results => {
|
// todo(@connor4312): reimplement, maybe
|
||||||
testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests }));
|
// this._register(resultService.onResultsChanged(results => {
|
||||||
}));
|
// testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: [] }));
|
||||||
|
// }));
|
||||||
|
|
||||||
testService.updateRootProviderCount(1);
|
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) {
|
for (const { resource, uri } of this.testService.subscriptions) {
|
||||||
this.proxy.$subscribeToTests(resource, uri);
|
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
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -105,8 +111,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
|
||||||
this.testService.publishDiff(resource, URI.revive(uri), diff);
|
this.testService.publishDiff(resource, URI.revive(uri), diff);
|
||||||
}
|
}
|
||||||
|
|
||||||
public $runTests(req: RunTestsRequest, token: CancellationToken): Promise<RunTestsResult> {
|
public async $runTests(req: RunTestsRequest, token: CancellationToken): Promise<string> {
|
||||||
return this.testService.runTests(req, token);
|
const result = await this.testService.runTests(req, token);
|
||||||
|
return result.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
|
|
|
@ -1292,10 +1292,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||||
// checkProposedApiEnabled(extension);
|
// checkProposedApiEnabled(extension);
|
||||||
return extHostTypes.TestMessageSeverity;
|
return extHostTypes.TestMessageSeverity;
|
||||||
},
|
},
|
||||||
get TestState() {
|
|
||||||
// checkProposedApiEnabled(extension);
|
|
||||||
return extHostTypes.TestState;
|
|
||||||
},
|
|
||||||
get WorkspaceTrustState() {
|
get WorkspaceTrustState() {
|
||||||
// checkProposedApiEnabled(extension);
|
// checkProposedApiEnabled(extension);
|
||||||
return extHostTypes.WorkspaceTrustState;
|
return extHostTypes.WorkspaceTrustState;
|
||||||
|
|
|
@ -58,7 +58,7 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib
|
||||||
import { DebugConfigurationProviderTriggerKind, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes';
|
import { DebugConfigurationProviderTriggerKind, WorkspaceTrustState } from 'vs/workbench/api/common/extHostTypes';
|
||||||
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
|
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
|
||||||
import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync';
|
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 { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
|
||||||
import { WorkspaceTrustStateChangeEvent } from 'vs/platform/workspace/common/workspaceTrust';
|
import { WorkspaceTrustStateChangeEvent } from 'vs/platform/workspace/common/workspaceTrust';
|
||||||
|
|
||||||
|
@ -1826,7 +1826,7 @@ export const enum ExtHostTestingResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtHostTestingShape {
|
export interface ExtHostTestingShape {
|
||||||
$runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise<RunTestsResult>;
|
$runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise<void>;
|
||||||
$subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void;
|
$subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void;
|
||||||
$unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void;
|
$unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void;
|
||||||
$lookupTest(test: TestIdWithProvider): Promise<InternalTestItem | undefined>;
|
$lookupTest(test: TestIdWithProvider): Promise<InternalTestItem | undefined>;
|
||||||
|
@ -1840,7 +1840,8 @@ export interface MainThreadTestingShape {
|
||||||
$subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
|
$subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
|
||||||
$unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
|
$unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
|
||||||
$publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): 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
|
// --- proxy identifiers
|
||||||
|
|
|
@ -16,11 +16,11 @@ import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTes
|
||||||
import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData';
|
import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData';
|
||||||
import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
||||||
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
|
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 { Disposable } from 'vs/workbench/api/common/extHostTypes';
|
||||||
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
|
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
|
||||||
import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
|
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';
|
import type * as vscode from 'vscode';
|
||||||
|
|
||||||
const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`;
|
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.
|
// Find workspace items first, then owned tests, then document tests.
|
||||||
// If a test instance exists in both the workspace and document, prefer
|
// If a test instance exists in both the workspace and document, prefer
|
||||||
// the workspace because it's less ephemeral.
|
// the workspace because it's less ephemeral.
|
||||||
.map(test => this.workspaceObservers.getMirroredTestDataByReference(test)
|
.map(this.getInternalTestForReference, this)
|
||||||
?? mapFind(this.testSubscriptions.values(), c => c.collection.getTestByReference(test))
|
|
||||||
?? this.textDocumentObservers.getMirroredTestDataByReference(test))
|
|
||||||
.filter(isDefined)
|
.filter(isDefined)
|
||||||
.map(item => ({ providerId: item.providerId, testId: item.id })),
|
.map(item => ({ providerId: item.providerId, testId: item.id })),
|
||||||
debug: req.debug
|
debug: req.debug
|
||||||
|
@ -219,10 +217,10 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||||
* providers to be run.
|
* providers to be run.
|
||||||
* @override
|
* @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);
|
const provider = this.providers.get(req.providerId);
|
||||||
if (!provider || !provider.runTests) {
|
if (!provider || !provider.runTests) {
|
||||||
return EMPTY_TEST_RESULT;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual)
|
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.
|
// Only send the actual TestItem's to the user to run.
|
||||||
.map(t => t instanceof TestItemFilteredWrapper ? t.actual : t);
|
.map(t => t instanceof TestItemFilteredWrapper ? t.actual : t);
|
||||||
if (!tests.length) {
|
if (!tests.length) {
|
||||||
return EMPTY_TEST_RESULT;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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()) {
|
for (const { collection } of this.testSubscriptions.values()) {
|
||||||
collection.flushDiff(); // ensure all states are updated
|
collection.flushDiff(); // ensure all states are updated
|
||||||
}
|
}
|
||||||
|
|
||||||
return EMPTY_TEST_RESULT;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e); // so it appears to attached debuggers
|
console.error(e); // so it appears to attached debuggers
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -256,6 +263,28 @@ export class ExtHostTesting implements ExtHostTestingShape {
|
||||||
return Promise.resolve(item);
|
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 {
|
private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy<vscode.TestItem> | undefined {
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
return;
|
return;
|
||||||
|
@ -361,10 +390,6 @@ export class TestItemFilteredWrapper implements vscode.TestItem {
|
||||||
return this.actual.runnable;
|
return this.actual.runnable;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get state() {
|
|
||||||
return this.actual.state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get children() {
|
public get children() {
|
||||||
// We only want children that match the filter.
|
// We only want children that match the filter.
|
||||||
return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter);
|
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 id() { return this.#internal.revived.id!; }
|
||||||
public get label() { return this.#internal.revived.label; }
|
public get label() { return this.#internal.revived.label; }
|
||||||
public get description() { return this.#internal.revived.description; }
|
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 location() { return this.#internal.revived.location; }
|
||||||
public get runnable() { return this.#internal.revived.runnable ?? true; }
|
public get runnable() { return this.#internal.revived.runnable ?? true; }
|
||||||
public get debuggable() { return this.#internal.revived.debuggable ?? false; }
|
public get debuggable() { return this.#internal.revived.debuggable ?? false; }
|
||||||
|
@ -665,7 +689,6 @@ class TestItemFromMirror implements vscode.RequiredTestItem {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
label: this.label,
|
label: this.label,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
state: this.state,
|
|
||||||
location: this.location,
|
location: this.location,
|
||||||
runnable: this.runnable,
|
runnable: this.runnable,
|
||||||
debuggable: this.debuggable,
|
debuggable: this.debuggable,
|
||||||
|
|
|
@ -1380,22 +1380,22 @@ export namespace NotebookDecorationRenderOptions {
|
||||||
export namespace TestState {
|
export namespace TestState {
|
||||||
export function from(item: vscode.TestState): ITestState {
|
export function from(item: vscode.TestState): ITestState {
|
||||||
return {
|
return {
|
||||||
runState: item.runState,
|
state: item.state,
|
||||||
duration: item.duration,
|
duration: item.duration,
|
||||||
messages: item.messages.map(message => ({
|
messages: item.messages?.map(message => ({
|
||||||
message: MarkdownString.fromStrict(message.message) || '',
|
message: MarkdownString.fromStrict(message.message) || '',
|
||||||
severity: message.severity,
|
severity: message.severity,
|
||||||
expectedOutput: message.expectedOutput,
|
expectedOutput: message.expectedOutput,
|
||||||
actualOutput: message.actualOutput,
|
actualOutput: message.actualOutput,
|
||||||
location: message.location ? location.from(message.location) : undefined,
|
location: message.location ? location.from(message.location) : undefined,
|
||||||
})),
|
})) ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function to(item: ITestState): vscode.TestState {
|
export function to(item: ITestState): vscode.TestState {
|
||||||
return new types.TestState(
|
return {
|
||||||
item.runState,
|
state: item.state,
|
||||||
item.messages.map(message => ({
|
messages: item.messages.map(message => ({
|
||||||
message: typeof message.message === 'string' ? message.message : MarkdownString.to(message.message),
|
message: typeof message.message === 'string' ? message.message : MarkdownString.to(message.message),
|
||||||
severity: message.severity,
|
severity: message.severity,
|
||||||
expectedOutput: message.expectedOutput,
|
expectedOutput: message.expectedOutput,
|
||||||
|
@ -1405,8 +1405,8 @@ export namespace TestState {
|
||||||
uri: URI.revive(message.location.uri)
|
uri: URI.revive(message.location.uri)
|
||||||
}),
|
}),
|
||||||
})),
|
})),
|
||||||
item.duration,
|
duration: item.duration,
|
||||||
);
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1420,7 +1420,6 @@ export namespace TestItem {
|
||||||
debuggable: item.debuggable ?? false,
|
debuggable: item.debuggable ?? false,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
runnable: item.runnable ?? true,
|
runnable: item.runnable ?? true,
|
||||||
state: TestState.from(item.state),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1435,7 +1434,6 @@ export namespace TestItem {
|
||||||
debuggable: item.debuggable,
|
debuggable: item.debuggable,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
runnable: item.runnable,
|
runnable: item.runnable,
|
||||||
state: TestState.to(item.state),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2964,31 +2964,6 @@ export enum TestMessageSeverity {
|
||||||
Hint = 3
|
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 RequiredTestItem = vscode.RequiredTestItem;
|
||||||
|
|
||||||
export type TestItem = vscode.TestItem;
|
export type TestItem = vscode.TestItem;
|
||||||
|
|
|
@ -10,13 +10,28 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { Position } from 'vs/editor/common/core/position';
|
import { Position } from 'vs/editor/common/core/position';
|
||||||
import { IWorkspaceFolder, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace';
|
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 { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||||
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
|
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
|
||||||
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
|
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 { 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 { 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';
|
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.
|
* 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;
|
public readonly onUpdate = this.updateEmitter.event;
|
||||||
|
|
||||||
constructor(listener: TestSubscriptionListener) {
|
constructor(listener: TestSubscriptionListener, @ITestResultService private readonly results: ITestResultService) {
|
||||||
super();
|
super();
|
||||||
this._register(listener.onDiff(([folder, diff]) => this.applyDiff(folder, diff)));
|
this._register(listener.onDiff(([folder, diff]) => this.applyDiff(folder, diff)));
|
||||||
this._register(listener.onFolderChange(this.applyFolderChange, this));
|
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 [folder, collection] of listener.workspaceFolderCollections) {
|
||||||
for (const node of collection.all) {
|
for (const node of collection.all) {
|
||||||
this.storeItem(this.createItem(node, folder.folder));
|
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);
|
const locationChanged = !locationsEqual(existing.location, item.item.location);
|
||||||
if (locationChanged) { this.locations.remove(existing); }
|
if (locationChanged) { this.locations.remove(existing); }
|
||||||
existing.update(item, this.addUpdated);
|
existing.update(item);
|
||||||
if (locationChanged) { this.locations.add(existing); }
|
if (locationChanged) { this.locations.add(existing); }
|
||||||
this.addUpdated(existing);
|
this.addUpdated(existing);
|
||||||
break;
|
break;
|
||||||
|
@ -172,5 +218,11 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes
|
||||||
item.parentItem.children.add(item);
|
item.parentItem.children.add(item);
|
||||||
this.items.set(item.test.id, item);
|
this.items.set(item.test.id, item);
|
||||||
this.locations.add(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { HierarchicalByLocationProjection as HierarchicalByLocationProjection }
|
||||||
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
|
import { HierarchicalElement, HierarchicalFolder } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes';
|
||||||
import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
|
import { NodeRenderDirective } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
|
||||||
import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
|
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';
|
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,9 +65,9 @@ export class HierarchicalByNameElement extends HierarchicalElement {
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) {
|
public update(actual: InternalTestItem) {
|
||||||
const wasRunnable = this.test.item.runnable;
|
const wasRunnable = this.test.item.runnable;
|
||||||
super.update(actual, addUpdated);
|
super.update(actual);
|
||||||
|
|
||||||
if (this.test.item.runnable !== wasRunnable) {
|
if (this.test.item.runnable !== wasRunnable) {
|
||||||
this.updateLeafTestState();
|
this.updateLeafTestState();
|
||||||
|
@ -117,8 +118,8 @@ export class HierarchicalByNameElement extends HierarchicalElement {
|
||||||
* test root rather than the heirarchal parent.
|
* test root rather than the heirarchal parent.
|
||||||
*/
|
*/
|
||||||
export class HierarchicalByNameProjection extends HierarchicalByLocationProjection {
|
export class HierarchicalByNameProjection extends HierarchicalByLocationProjection {
|
||||||
constructor(listener: TestSubscriptionListener) {
|
constructor(listener: TestSubscriptionListener, @ITestResultService results: ITestResultService) {
|
||||||
super(listener);
|
super(listener, results);
|
||||||
|
|
||||||
const originalRenderNode = this.renderNode.bind(this);
|
const originalRenderNode = this.renderNode.bind(this);
|
||||||
this.renderNode = (node, recurse) => {
|
this.renderNode = (node, recurse) => {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import { Iterable } from 'vs/base/common/iterator';
|
||||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||||
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
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';
|
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 {
|
export class HierarchicalElement implements ITestTreeElement {
|
||||||
public readonly children = new Set<HierarchicalElement>();
|
public readonly children = new Set<HierarchicalElement>();
|
||||||
public computedState: TestRunState | undefined;
|
|
||||||
public readonly depth: number = this.parentItem.depth + 1;
|
public readonly depth: number = this.parentItem.depth + 1;
|
||||||
|
|
||||||
public get treeId() {
|
public get treeId() {
|
||||||
|
@ -26,10 +24,6 @@ export class HierarchicalElement implements ITestTreeElement {
|
||||||
return this.test.item.label;
|
return this.test.item.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get state() {
|
|
||||||
return this.test.item.state.runState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get location() {
|
public get location() {
|
||||||
return this.test.item.location;
|
return this.test.item.location;
|
||||||
}
|
}
|
||||||
|
@ -46,16 +40,15 @@ export class HierarchicalElement implements ITestTreeElement {
|
||||||
: Iterable.empty();
|
: Iterable.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public state = TestRunState.Unset;
|
||||||
|
public ownState = TestRunState.Unset;
|
||||||
|
|
||||||
constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) {
|
constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) {
|
||||||
this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese
|
this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(actual: InternalTestItem, addUpdated: (n: ITestTreeElement) => void) {
|
public update(actual: InternalTestItem) {
|
||||||
const stateChange = actual.item.state.runState !== this.state;
|
|
||||||
Object.assign(this.test, actual);
|
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));
|
return Iterable.concatNested(Iterable.map(this.children, c => c.debuggable));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public state = TestRunState.Unset;
|
||||||
|
public ownState = TestRunState.Unset;
|
||||||
|
|
||||||
constructor(private readonly folder: IWorkspaceFolder) { }
|
constructor(private readonly folder: IWorkspaceFolder) { }
|
||||||
|
|
||||||
public get label() {
|
public get label() {
|
||||||
return this.folder.name;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -40,13 +40,6 @@ export interface ITestTreeProjection extends IDisposable {
|
||||||
|
|
||||||
|
|
||||||
export interface ITestTreeElement {
|
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>;
|
readonly children: Set<ITestTreeElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,9 +78,11 @@ export interface ITestTreeElement {
|
||||||
readonly debuggable: Iterable<TestIdWithProvider>;
|
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 label: string;
|
||||||
readonly parentItem: ITestTreeElement | null;
|
readonly parentItem: ITestTreeElement | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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) { }
|
|
||||||
}
|
|
|
@ -21,9 +21,10 @@ import { ViewAction } from 'vs/workbench/browser/parts/views/viewPane';
|
||||||
import { FocusedViewContext } from 'vs/workbench/common/views';
|
import { FocusedViewContext } from 'vs/workbench/common/views';
|
||||||
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
|
import * as icons from 'vs/workbench/contrib/testing/browser/icons';
|
||||||
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
|
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
|
||||||
import { TestExplorerViewGrouping, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
import { TestExplorerViewSorting, TestExplorerViewMode, Testing } from 'vs/workbench/contrib/testing/common/constants';
|
||||||
import { EMPTY_TEST_RESULT, InternalTestItem, RunTestsResult, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
|
import { InternalTestItem, TestIdWithProvider } from 'vs/workbench/contrib/testing/common/testCollection';
|
||||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
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 { ITestService, waitForAllRoots } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
import { IWorkspaceTestCollectionService } from 'vs/workbench/contrib/testing/common/workspaceTestCollectionService';
|
||||||
|
|
||||||
|
@ -101,10 +102,10 @@ abstract class RunOrDebugAction extends ViewAction<TestingExplorerView> {
|
||||||
/**
|
/**
|
||||||
* @override
|
* @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);
|
const tests = this.getActionableTests(accessor.get(IWorkspaceTestCollectionService), view.viewModel);
|
||||||
if (!tests.length) {
|
if (!tests.length) {
|
||||||
return Promise.resolve(EMPTY_TEST_RESULT);
|
return Promise.resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
return accessor.get(ITestService).runTests({ tests, debug: this.debug });
|
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() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
id: 'testing.groupByLocation',
|
id: 'testing.sortByName',
|
||||||
viewId: Testing.ExplorerViewId,
|
viewId: Testing.ExplorerViewId,
|
||||||
title: localize('testing.groupByLocation', "Sort by Name"),
|
title: localize('testing.sortByName', "Sort by Name"),
|
||||||
f1: false,
|
f1: false,
|
||||||
toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByLocation),
|
toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByName),
|
||||||
menu: {
|
menu: {
|
||||||
id: MenuId.ViewTitle,
|
id: MenuId.ViewTitle,
|
||||||
order: 10,
|
order: 10,
|
||||||
group: 'groupBy',
|
group: 'sortBy',
|
||||||
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
|
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -348,22 +349,22 @@ export class TestingGroupByLocationAction extends ViewAction<TestingExplorerView
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
|
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() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
id: 'testing.groupByStatus',
|
id: 'testing.sortByLocation',
|
||||||
viewId: Testing.ExplorerViewId,
|
viewId: Testing.ExplorerViewId,
|
||||||
title: localize('testing.groupByStatus', "Sort by Status"),
|
title: localize('testing.sortByLocation', "Sort by Location"),
|
||||||
f1: false,
|
f1: false,
|
||||||
toggled: TestingContextKeys.viewGrouping.isEqualTo(TestExplorerViewGrouping.ByStatus),
|
toggled: TestingContextKeys.viewSorting.isEqualTo(TestExplorerViewSorting.ByLocation),
|
||||||
menu: {
|
menu: {
|
||||||
id: MenuId.ViewTitle,
|
id: MenuId.ViewTitle,
|
||||||
order: 10,
|
order: 10,
|
||||||
group: 'groupBy',
|
group: 'sortBy',
|
||||||
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
|
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -373,7 +374,7 @@ export class TestingGroupByStatusAction extends ViewAction<TestingExplorerView>
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public runInView(_accessor: ServicesAccessor, view: TestingExplorerView) {
|
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> {
|
export class EditFocusedTest extends ViewAction<TestingExplorerView> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super({
|
super({
|
||||||
|
|
|
@ -83,13 +83,14 @@ registerAction2(Action.TestingViewAsTreeAction);
|
||||||
registerAction2(Action.CancelTestRunAction);
|
registerAction2(Action.CancelTestRunAction);
|
||||||
registerAction2(Action.RunSelectedAction);
|
registerAction2(Action.RunSelectedAction);
|
||||||
registerAction2(Action.DebugSelectedAction);
|
registerAction2(Action.DebugSelectedAction);
|
||||||
registerAction2(Action.TestingGroupByLocationAction);
|
registerAction2(Action.TestingSortByNameAction);
|
||||||
registerAction2(Action.TestingGroupByStatusAction);
|
registerAction2(Action.TestingSortByLocationAction);
|
||||||
registerAction2(Action.RefreshTestsAction);
|
registerAction2(Action.RefreshTestsAction);
|
||||||
registerAction2(Action.CollapseAllAction);
|
registerAction2(Action.CollapseAllAction);
|
||||||
registerAction2(Action.RunAllAction);
|
registerAction2(Action.RunAllAction);
|
||||||
registerAction2(Action.DebugAllAction);
|
registerAction2(Action.DebugAllAction);
|
||||||
registerAction2(Action.EditFocusedTest);
|
registerAction2(Action.EditFocusedTest);
|
||||||
|
registerAction2(Action.ClearTestResultsAction);
|
||||||
registerAction2(CloseTestPeek);
|
registerAction2(CloseTestPeek);
|
||||||
|
|
||||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);
|
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Eventually);
|
||||||
|
|
|
@ -28,8 +28,8 @@ import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/work
|
||||||
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
||||||
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
|
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
|
||||||
import { IncrementalTestCollectionItem, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection';
|
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 { 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';
|
import { IMainThreadTestCollection, ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
|
|
||||||
export class TestingDecorations extends Disposable implements IEditorContribution {
|
export class TestingDecorations extends Disposable implements IEditorContribution {
|
||||||
|
@ -39,6 +39,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio
|
||||||
constructor(
|
constructor(
|
||||||
private readonly editor: ICodeEditor,
|
private readonly editor: ICodeEditor,
|
||||||
@ITestService private readonly testService: ITestService,
|
@ITestService private readonly testService: ITestService,
|
||||||
|
@ITestResultService private readonly results: ITestResultService,
|
||||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||||
) {
|
) {
|
||||||
super();
|
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.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);
|
this.setDecorations(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,19 +84,25 @@ export class TestingDecorations extends Disposable implements IEditorContributio
|
||||||
this.editor.changeDecorations(accessor => {
|
this.editor.changeDecorations(accessor => {
|
||||||
const newDecorations: ITestDecoration[] = [];
|
const newDecorations: ITestDecoration[] = [];
|
||||||
for (const test of ref.object.all) {
|
for (const test of ref.object.all) {
|
||||||
|
const stateLookup = this.results.getStateByExtId(test.item.extId);
|
||||||
if (hasValidLocation(uri, test.item)) {
|
if (hasValidLocation(uri, test.item)) {
|
||||||
newDecorations.push(this.instantiationService.createInstance(
|
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++) {
|
if (!stateLookup) {
|
||||||
const m = test.item.state.messages[i];
|
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)) {
|
if (hasValidLocation(uri, m)) {
|
||||||
const uri = buildTestUri({
|
const uri = buildTestUri({
|
||||||
type: TestUriType.LiveMessage,
|
type: TestUriType.ResultActualOutput,
|
||||||
messageIndex: i,
|
messageIndex: i,
|
||||||
providerId: test.providerId,
|
resultId: result.id,
|
||||||
testId: test.id,
|
testId: stateItem.item.extId,
|
||||||
});
|
});
|
||||||
|
|
||||||
newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor));
|
newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor));
|
||||||
|
@ -138,7 +154,7 @@ const firstLineRange = (originalRange: IRange) => ({
|
||||||
endColumn: 1,
|
endColumn: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
class RunTestDecoration implements ITestDecoration {
|
class RunTestDecoration extends Disposable implements ITestDecoration {
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
|
@ -156,25 +172,16 @@ class RunTestDecoration implements ITestDecoration {
|
||||||
private readonly collection: IMainThreadTestCollection,
|
private readonly collection: IMainThreadTestCollection,
|
||||||
private readonly location: ModeLocation,
|
private readonly location: ModeLocation,
|
||||||
private readonly editor: ICodeEditor,
|
private readonly editor: ICodeEditor,
|
||||||
|
computedState: TestRunState | undefined,
|
||||||
@ITestService private readonly testService: ITestService,
|
@ITestService private readonly testService: ITestService,
|
||||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||||
@ICommandService private readonly commandService: ICommandService,
|
@ICommandService private readonly commandService: ICommandService,
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
this.line = location.range.startLineNumber;
|
this.line = location.range.startLineNumber;
|
||||||
|
|
||||||
const queue = [test.children];
|
const icon = computedState !== undefined && computedState !== TestRunState.Unset
|
||||||
let state = this.test.item.state.runState;
|
? testingStatesToIcons.get(computedState)!
|
||||||
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)!
|
|
||||||
: test.children.size > 0 ? testingRunAllIcon : testingRunIcon;
|
: test.children.size > 0 ? testingRunAllIcon : testingRunIcon;
|
||||||
|
|
||||||
this.editorDecoration = {
|
this.editorDecoration = {
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
|
|
||||||
import * as dom from 'vs/base/browser/dom';
|
import * as dom from 'vs/base/browser/dom';
|
||||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
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 { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||||
import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
import { DefaultKeyboardNavigationDelegate, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||||
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
||||||
import { ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
|
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 { 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 { Action, IAction, IActionViewItem } from 'vs/base/common/actions';
|
||||||
import { DeferredPromise } from 'vs/base/common/async';
|
import { DeferredPromise } from 'vs/base/common/async';
|
||||||
import { Color, RGBA } from 'vs/base/common/color';
|
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 { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||||
import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation';
|
import { HierarchicalByLocationProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation';
|
||||||
import { HierarchicalByNameProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName';
|
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 { testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
|
||||||
import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
|
import { ITestExplorerFilterState, TestExplorerFilterState, TestingExplorerFilter } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
|
||||||
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
|
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 { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||||
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
|
import { cmpPriority, isFailedState } from 'vs/workbench/contrib/testing/common/testingStates';
|
||||||
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||||
|
@ -188,7 +184,7 @@ export class TestingExplorerViewModel extends Disposable {
|
||||||
public projection!: ITestTreeProjection;
|
public projection!: ITestTreeProjection;
|
||||||
|
|
||||||
private readonly _viewMode = TestingContextKeys.viewMode.bindTo(this.contextKeyService);
|
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.
|
* Fires when the selected tests change.
|
||||||
|
@ -210,18 +206,18 @@ export class TestingExplorerViewModel extends Disposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public get viewGrouping() {
|
public get viewSorting() {
|
||||||
return this._viewGrouping.get() ?? TestExplorerViewGrouping.ByLocation;
|
return this._viewSorting.get() ?? TestExplorerViewSorting.ByLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public set viewGrouping(newGrouping: TestExplorerViewGrouping) {
|
public set viewSorting(newSorting: TestExplorerViewSorting) {
|
||||||
if (newGrouping === this._viewGrouping.get()) {
|
if (newSorting === this._viewSorting.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._viewGrouping.set(newGrouping);
|
this._viewSorting.set(newSorting);
|
||||||
this.updatePreferredProjection();
|
this.tree.resort(null);
|
||||||
this.storageService.store('testing.viewGrouping', newGrouping, StorageScope.WORKSPACE, StorageTarget.USER);
|
this.storageService.store('testing.viewSorting', newSorting, StorageScope.WORKSPACE, StorageTarget.USER);
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -229,16 +225,17 @@ export class TestingExplorerViewModel extends Disposable {
|
||||||
onDidChangeVisibility: Event<boolean>,
|
onDidChangeVisibility: Event<boolean>,
|
||||||
private listener: TestSubscriptionListener | undefined,
|
private listener: TestSubscriptionListener | undefined,
|
||||||
@ITestExplorerFilterState filterState: TestExplorerFilterState,
|
@ITestExplorerFilterState filterState: TestExplorerFilterState,
|
||||||
@IInstantiationService instantiationService: IInstantiationService,
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||||
@IEditorService private readonly editorService: IEditorService,
|
@IEditorService private readonly editorService: IEditorService,
|
||||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||||
@IStorageService private readonly storageService: IStorageService,
|
@IStorageService private readonly storageService: IStorageService,
|
||||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||||
|
@ITestResultService private readonly testResults: ITestResultService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._viewMode.set(this.storageService.get('testing.viewMode', StorageScope.WORKSPACE, TestExplorerViewMode.Tree) as TestExplorerViewMode);
|
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 }));
|
const labels = this._register(instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: onDidChangeVisibility }));
|
||||||
|
|
||||||
|
@ -261,7 +258,7 @@ export class TestingExplorerViewModel extends Disposable {
|
||||||
simpleKeyboardNavigation: true,
|
simpleKeyboardNavigation: true,
|
||||||
identityProvider: instantiationService.createInstance(IdentityProvider),
|
identityProvider: instantiationService.createInstance(IdentityProvider),
|
||||||
hideTwistiesOfChildlessElements: true,
|
hideTwistiesOfChildlessElements: true,
|
||||||
sorter: instantiationService.createInstance(TreeSorter),
|
sorter: instantiationService.createInstance(TreeSorter, this),
|
||||||
keyboardNavigationLabelProvider: instantiationService.createInstance(TreeKeyboardNavigationLabelProvider),
|
keyboardNavigationLabelProvider: instantiationService.createInstance(TreeKeyboardNavigationLabelProvider),
|
||||||
accessibilityProvider: instantiationService.createInstance(ListAccessibilityProvider),
|
accessibilityProvider: instantiationService.createInstance(ListAccessibilityProvider),
|
||||||
filter: this.filter,
|
filter: this.filter,
|
||||||
|
@ -293,6 +290,10 @@ export class TestingExplorerViewModel extends Disposable {
|
||||||
tracker.deactivate();
|
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.
|
* Tries to peek the first test error, if the item is in a failed state.
|
||||||
*/
|
*/
|
||||||
private async tryPeekError(item: ITestTreeElement) {
|
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;
|
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) {
|
if (index === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = item.test.item.state.messages[index];
|
const message = test.state.messages[index];
|
||||||
const pane = await this.editorService.openEditor({
|
const pane = await this.editorService.openEditor({
|
||||||
resource: message.location!.uri,
|
resource: message.location!.uri,
|
||||||
options: { selection: message.location!.range, preserveFocus: true }
|
options: { selection: message.location!.range, preserveFocus: true }
|
||||||
|
@ -401,10 +404,10 @@ export class TestingExplorerViewModel extends Disposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
TestingOutputPeekController.get(control).show(buildTestUri({
|
TestingOutputPeekController.get(control).show(buildTestUri({
|
||||||
type: TestUriType.LiveMessage,
|
type: TestUriType.ResultMessage,
|
||||||
messageIndex: index,
|
messageIndex: index,
|
||||||
providerId: item.test.providerId,
|
resultId: result.id,
|
||||||
testId: item.test.id,
|
testId: item.test!.item.extId,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -417,18 +420,10 @@ export class TestingExplorerViewModel extends Disposable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._viewGrouping.get() === TestExplorerViewGrouping.ByLocation) {
|
|
||||||
if (this._viewMode.get() === TestExplorerViewMode.List) {
|
if (this._viewMode.get() === TestExplorerViewMode.List) {
|
||||||
this.projection = new HierarchicalByNameProjection(this.listener);
|
this.projection = this.instantiationService.createInstance(HierarchicalByNameProjection, this.listener);
|
||||||
} else {
|
} else {
|
||||||
this.projection = new HierarchicalByLocationProjection(this.listener);
|
this.projection = this.instantiationService.createInstance(HierarchicalByLocationProjection, this.listener);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this._viewMode.get() === TestExplorerViewMode.List) {
|
|
||||||
this.projection = new StateByNameProjection(this.listener);
|
|
||||||
} else {
|
|
||||||
this.projection = new StateByLocationProjection(this.listener);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.projection.onUpdate(this.deferUpdate, this);
|
this.projection.onUpdate(this.deferUpdate, this);
|
||||||
|
@ -569,9 +564,19 @@ class TestsFilter implements ITreeFilter<ITestTreeElement> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class TreeSorter implements ITreeSorter<ITestTreeElement> {
|
class TreeSorter implements ITreeSorter<ITestTreeElement> {
|
||||||
|
constructor(private readonly viewModel: TestingExplorerViewModel) { }
|
||||||
|
|
||||||
public compare(a: ITestTreeElement, b: ITestTreeElement): number {
|
public compare(a: ITestTreeElement, b: ITestTreeElement): number {
|
||||||
if (a instanceof StateElement && b instanceof StateElement) {
|
let delta = cmpPriority(a.state, b.state);
|
||||||
return cmpPriority(a.computedState, b.computedState);
|
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);
|
return a.label.localeCompare(b.label);
|
||||||
|
@ -587,7 +592,7 @@ class ListAccessibilityProvider implements IListAccessibilityProvider<ITestTreeE
|
||||||
return localize({
|
return localize({
|
||||||
key: 'testing.treeElementLabel',
|
key: 'testing.treeElementLabel',
|
||||||
comment: ['label then the unit tests state, for example "Addition Tests (Running)"'],
|
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 = {};
|
const options: IResourceLabelOptions = {};
|
||||||
data.actionBar.clear();
|
data.actionBar.clear();
|
||||||
|
|
||||||
const state = getComputedState(element);
|
const icon = testingStatesToIcons.get(element.state);
|
||||||
const icon = testingStatesToIcons.get(state);
|
|
||||||
data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : '');
|
data.icon.className = 'computed-state ' + (icon ? ThemeIcon.asClassName(icon) : '');
|
||||||
const test = element.test;
|
const test = element.test;
|
||||||
if (test) {
|
if (test) {
|
||||||
|
@ -683,7 +687,7 @@ class TestsRenderer implements ITreeRenderer<ITestTreeElement, FuzzyScore, TestT
|
||||||
options.fileKind = FileKind.ROOT_FOLDER;
|
options.fileKind = FileKind.ROOT_FOLDER;
|
||||||
}
|
}
|
||||||
|
|
||||||
const running = state === TestRunState.Running;
|
const running = element.state === TestRunState.Running;
|
||||||
if (!Iterable.isEmpty(element.runnable)) {
|
if (!Iterable.isEmpty(element.runnable)) {
|
||||||
data.actionBar.push(
|
data.actionBar.push(
|
||||||
this.instantiationService.createInstance(RunAction, element.runnable, running),
|
this.instantiationService.createInstance(RunAction, element.runnable, running),
|
||||||
|
@ -740,12 +744,16 @@ const getProgressText = ({ passed, runSoFar, skipped, failed }: CountSummary) =>
|
||||||
class TestRunProgress {
|
class TestRunProgress {
|
||||||
private current?: { update: IProgress<IProgressStep>; deferred: DeferredPromise<void> };
|
private current?: { update: IProgress<IProgressStep>; deferred: DeferredPromise<void> };
|
||||||
private badge = new MutableDisposable();
|
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.updateProgress();
|
||||||
this.updateBadge();
|
this.updateBadge();
|
||||||
|
|
||||||
result.onChange(this.throttledProgressUpdate, this);
|
result.started.onChange(this.throttledProgressUpdate, this);
|
||||||
result.onComplete(() => {
|
result.started.onComplete(() => {
|
||||||
this.throttledProgressUpdate();
|
this.throttledProgressUpdate();
|
||||||
this.updateBadge();
|
this.updateBadge();
|
||||||
});
|
});
|
||||||
|
|
|
@ -27,15 +27,15 @@ import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeServic
|
||||||
import { EditorModel } from 'vs/workbench/common/editor';
|
import { EditorModel } from 'vs/workbench/common/editor';
|
||||||
import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
|
import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
|
||||||
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
|
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 { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||||
import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
||||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
|
||||||
|
|
||||||
interface ITestDto {
|
interface ITestDto {
|
||||||
|
test: ITestItem,
|
||||||
messageIndex: number;
|
messageIndex: number;
|
||||||
test: InternalTestItem;
|
state: ITestState;
|
||||||
expectedUri: URI;
|
expectedUri: URI;
|
||||||
actualUri: URI;
|
actualUri: URI;
|
||||||
messageUri: URI;
|
messageUri: URI;
|
||||||
|
@ -66,7 +66,6 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
|
||||||
private readonly editor: ICodeEditor,
|
private readonly editor: ICodeEditor,
|
||||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||||
@ITestResultService private readonly testResults: ITestResultService,
|
@ITestResultService private readonly testResults: ITestResultService,
|
||||||
@ITestService private readonly testService: ITestService,
|
|
||||||
@IContextKeyService contextKeyService: IContextKeyService,
|
@IContextKeyService contextKeyService: IContextKeyService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
@ -83,7 +82,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = dto.test.item.state.messages[dto.messageIndex];
|
const message = dto.state.messages[dto.messageIndex];
|
||||||
if (!message?.location) {
|
if (!message?.location) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -120,30 +119,16 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('resultId' in parts) {
|
const test = this.testResults.getResult(parts.resultId)?.getStateByExtId(parts.testId);
|
||||||
const test = this.testResults.lookup(parts.resultId)?.tests.find(t => t.id === parts.testId);
|
|
||||||
return test && {
|
return test && {
|
||||||
test,
|
test: test.item,
|
||||||
|
state: test.state,
|
||||||
messageIndex: parts.messageIndex,
|
messageIndex: parts.messageIndex,
|
||||||
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
|
expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }),
|
||||||
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
|
actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }),
|
||||||
messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }),
|
messageUri: buildTestUri({ ...parts, type: TestUriType.ResultMessage }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const test = await this.testService.lookupTest({ providerId: parts.providerId, testId: parts.testId });
|
|
||||||
if (!test) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
test,
|
|
||||||
messageIndex: parts.messageIndex,
|
|
||||||
expectedUri: buildTestUri({ ...parts, type: TestUriType.LiveActualOutput }),
|
|
||||||
actualUri: buildTestUri({ ...parts, type: TestUriType.LiveExpectedOutput }),
|
|
||||||
messageUri: buildTestUri({ ...parts, type: TestUriType.LiveMessage }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class TestingOutputPeek extends PeekViewWidget {
|
abstract class TestingOutputPeek extends PeekViewWidget {
|
||||||
|
@ -236,14 +221,14 @@ class TestingDiffOutputPeek extends TestingOutputPeek {
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public async setModel({ test, messageIndex, expectedUri, actualUri }: ITestDto) {
|
public async setModel({ test, state, messageIndex, expectedUri, actualUri }: ITestDto) {
|
||||||
const message = test.item.state.messages[messageIndex];
|
const message = state.messages[messageIndex];
|
||||||
if (!message?.location) {
|
if (!message?.location) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.show(message.location.range, hintDiffPeekHeight(message));
|
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([
|
const [original, modified] = await Promise.all([
|
||||||
this.modelService.createModelReference(expectedUri),
|
this.modelService.createModelReference(expectedUri),
|
||||||
|
@ -285,14 +270,14 @@ class TestingMessageOutputPeek extends TestingOutputPeek {
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
public async setModel({ test, messageIndex, messageUri }: ITestDto) {
|
public async setModel({ state, test, messageIndex, messageUri }: ITestDto) {
|
||||||
const message = test.item.state.messages[messageIndex];
|
const message = state.messages[messageIndex];
|
||||||
if (!message?.location) {
|
if (!message?.location) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.show(message.location.range, hintPeekStrHeight(message.message.toString()));
|
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);
|
const modelRef = this.model.value = await this.modelService.createModelReference(messageUri);
|
||||||
if (this.preview.value) {
|
if (this.preview.value) {
|
||||||
|
|
|
@ -20,9 +20,9 @@ export const enum TestExplorerViewMode {
|
||||||
Tree = 'true'
|
Tree = 'true'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum TestExplorerViewGrouping {
|
export const enum TestExplorerViewSorting {
|
||||||
ByLocation = 'location',
|
ByLocation = 'location',
|
||||||
ByStatus = 'status',
|
ByName = 'name',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const testStateNames: { [K in TestRunState]: string } = {
|
export const testStateNames: { [K in TestRunState]: string } = {
|
||||||
|
|
84
src/vs/workbench/contrib/testing/common/getComputedState.ts
Normal file
84
src/vs/workbench/contrib/testing/common/getComputedState.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -204,7 +204,6 @@ const keyMap: { [K in keyof Omit<RequiredTestItem, 'children'>]: null } = {
|
||||||
id: null,
|
id: null,
|
||||||
label: null,
|
label: null,
|
||||||
location: null,
|
location: null,
|
||||||
state: null,
|
|
||||||
debuggable: null,
|
debuggable: null,
|
||||||
description: null,
|
description: null,
|
||||||
runnable: null
|
runnable: null
|
||||||
|
|
|
@ -26,24 +26,12 @@ export interface RunTestsRequest {
|
||||||
* Request from the main thread to run tests for a single provider.
|
* Request from the main thread to run tests for a single provider.
|
||||||
*/
|
*/
|
||||||
export interface RunTestForProviderRequest {
|
export interface RunTestForProviderRequest {
|
||||||
|
runId: string;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
ids: string[];
|
ids: string[];
|
||||||
debug: boolean;
|
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 {
|
export interface ITestMessage {
|
||||||
message: string | IMarkdownString;
|
message: string | IMarkdownString;
|
||||||
severity: TestMessageSeverity | undefined;
|
severity: TestMessageSeverity | undefined;
|
||||||
|
@ -53,7 +41,7 @@ export interface ITestMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITestState {
|
export interface ITestState {
|
||||||
runState: TestRunState;
|
state: TestRunState;
|
||||||
duration: number | undefined;
|
duration: number | undefined;
|
||||||
messages: ITestMessage[];
|
messages: ITestMessage[];
|
||||||
}
|
}
|
||||||
|
@ -70,7 +58,6 @@ export interface ITestItem {
|
||||||
description: string | undefined;
|
description: string | undefined;
|
||||||
runnable: boolean;
|
runnable: boolean;
|
||||||
debuggable: boolean;
|
debuggable: boolean;
|
||||||
state: ITestState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -84,7 +71,7 @@ export interface InternalTestItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternalTestItemWithChildren extends InternalTestItem {
|
export interface InternalTestItemWithChildren extends InternalTestItem {
|
||||||
children: InternalTestItemWithChildren[];
|
children: this[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InternalTestResults {
|
export interface InternalTestResults {
|
||||||
|
|
|
@ -4,17 +4,52 @@
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { Emitter, Event } from 'vs/base/common/event';
|
import { Emitter, Event } from 'vs/base/common/event';
|
||||||
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { generateUuid } from 'vs/base/common/uuid';
|
import { generateUuid } from 'vs/base/common/uuid';
|
||||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
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 { 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 { 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';
|
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 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 makeEmptyCounts = () => {
|
||||||
const o: Partial<TestStateCount> = {};
|
const o: Partial<TestStateCount> = {};
|
||||||
for (const state of statesInOrder) {
|
for (const state of statesInOrder) {
|
||||||
|
@ -24,7 +59,7 @@ const makeEmptyCounts = () => {
|
||||||
return o as TestStateCount;
|
return o as TestStateCount;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sumCounts = (counts: TestStateCount[]) => {
|
export const sumCounts = (counts: Iterable<TestStateCount>) => {
|
||||||
const total = makeEmptyCounts();
|
const total = makeEmptyCounts();
|
||||||
for (const count of counts) {
|
for (const count of counts) {
|
||||||
for (const state of statesInOrder) {
|
for (const state of statesInOrder) {
|
||||||
|
@ -35,28 +70,97 @@ export const sumCounts = (counts: TestStateCount[]) => {
|
||||||
return total;
|
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,
|
collection: IMainThreadTestCollection,
|
||||||
test: IncrementalTestCollectionItem,
|
test: IncrementalTestCollectionItem,
|
||||||
|
byExtId: Map<string, TestResultItem>,
|
||||||
|
byInternalId: Map<string, TestResultItem>,
|
||||||
): 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) {
|
for (const childId of test.children) {
|
||||||
const child = collection.getNodeById(childId);
|
const child = collection.getNodeById(childId);
|
||||||
if (child) {
|
if (child) {
|
||||||
mapped.children.push(makeNode(collection, child));
|
makeNodeAndChildren(collection, child, byExtId, byInternalId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapped;
|
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
|
* Results of a test. These are created when the test initially started running
|
||||||
* and marked as "complete" when the run finishes.
|
* 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
|
* Creates a new TestResult, pulling tests from the associated list
|
||||||
* of collections.
|
* of collections.
|
||||||
|
@ -65,27 +169,29 @@ export class TestResult {
|
||||||
collections: ReadonlyArray<IMainThreadTestCollection>,
|
collections: ReadonlyArray<IMainThreadTestCollection>,
|
||||||
tests: ReadonlyArray<TestIdWithProvider>,
|
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 test of tests) {
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
const node = collection.getNodeById(test.testId);
|
const node = collection.getNodeById(test.testId);
|
||||||
if (node) {
|
if (!node) {
|
||||||
mapped.push(makeNode(collection, node));
|
continue;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 readonly completeEmitter = new Emitter<void>();
|
||||||
private changeEmitter = new Emitter<void>();
|
private readonly changeEmitter = new Emitter<TestResultItem>();
|
||||||
private _complete = false;
|
private _complete = false;
|
||||||
private _cachedCounts?: { [K in TestRunState]: number };
|
|
||||||
|
|
||||||
public onChange = this.changeEmitter.event;
|
public readonly onChange = this.changeEmitter.event;
|
||||||
public onComplete = this.completeEmitter.event;
|
public readonly onComplete = this.completeEmitter.event;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique ID for referring to this set of test results.
|
* Unique ID for referring to this set of test results.
|
||||||
|
@ -93,34 +199,122 @@ export class TestResult {
|
||||||
public readonly id = generateUuid();
|
public readonly id = generateUuid();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets whether the test run has finished.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public get isComplete() {
|
public get isComplete() {
|
||||||
return this._complete;
|
return this._complete;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a count of tests in each state.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public get counts() {
|
public readonly counts: { [K in TestRunState]: number } = makeEmptyCounts();
|
||||||
if (this._cachedCounts) {
|
|
||||||
return this._cachedCounts;
|
/**
|
||||||
|
* Gets all tests involved in the run by ID.
|
||||||
|
*/
|
||||||
|
public get tests() {
|
||||||
|
return this.testByInternalId.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
const counts = makeEmptyCounts();
|
private readonly computedStateAccessor: IComputedStateAccessor<TestResultItem> = {
|
||||||
this.forEachTest(({ item }) => {
|
getOwnState: i => i.state.state,
|
||||||
counts[item.state.runState]++;
|
getCurrentComputedState: i => i.computedState,
|
||||||
});
|
setComputedState: (i, s) => i.computedState = s,
|
||||||
|
getChildren: i => {
|
||||||
if (this._complete) {
|
const { testByInternalId } = this;
|
||||||
this._cachedCounts = counts;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return counts;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(public readonly tests: TestResultItem[]) { }
|
/**
|
||||||
|
* @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.
|
* 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');
|
throw new Error('cannot complete a test result multiple times');
|
||||||
}
|
}
|
||||||
|
|
||||||
// shallow clone test items to 'disconnect' them from the underlying
|
// un-queue any tests that weren't explicitly updated
|
||||||
// connection and stop state changes. Also, marked any still-running
|
this.setAllToState(unsetState, t => t.state.state === TestRunState.Queued);
|
||||||
// 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 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._complete = true;
|
this._complete = true;
|
||||||
this.completeEmitter.fire();
|
this.completeEmitter.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires the 'change' event, should be called by the runner.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public notifyChanged() {
|
public toJSON(): ISerializedResults {
|
||||||
this.changeEmitter.fire();
|
return { id: this.id, counts: this.counts, items: [...this.testByExtId.entries()] };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private forEachTest(fn: (test: TestResultItem) => void) {
|
/**
|
||||||
const queue = [this.tests];
|
* Test results hydrated from a previously-serialized test run.
|
||||||
while (queue.length) {
|
*/
|
||||||
for (const test of queue.pop()!) {
|
class HydratedTestResult implements ITestResult {
|
||||||
fn(test);
|
/**
|
||||||
queue.push(test.children);
|
* @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 {
|
export interface ITestResultService {
|
||||||
readonly _serviceBrand: undefined;
|
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.
|
* 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.
|
* 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');
|
export const ITestResultService = createDecorator<ITestResultService>('testResultService');
|
||||||
|
|
||||||
const RETAIN_LAST_RESULTS = 16;
|
const RETAIN_LAST_RESULTS = 64;
|
||||||
|
|
||||||
export class TestResultService implements ITestResultService {
|
export class TestResultService implements ITestResultService {
|
||||||
declare _serviceBrand: undefined;
|
declare _serviceBrand: undefined;
|
||||||
private newResultEmitter = new Emitter<TestResult>();
|
private changeResultEmitter = new Emitter<ResultChangeEvent>();
|
||||||
|
private testChangeEmitter = new Emitter<[results: ITestResult, item: TestResultItem]>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public results: TestResult[] = [];
|
public results: ITestResult[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @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 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.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
|
* @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);
|
this.results.unshift(result);
|
||||||
if (this.results.length > RETAIN_LAST_RESULTS) {
|
if (this.results.length > RETAIN_LAST_RESULTS) {
|
||||||
this.results.pop();
|
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.isRunning.set(true);
|
||||||
this.newResultEmitter.fire(result);
|
this.changeResultEmitter.fire({ started: result });
|
||||||
|
result.setAllToState(queuedState, () => true);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public lookup(id: string) {
|
public getResult(id: string) {
|
||||||
return this.results.find(r => r.id === id);
|
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
|
// move the complete test run down behind any still-running ones
|
||||||
for (let i = 0; i < this.results.length - 2; i++) {
|
for (let i = 0; i < this.results.length - 2; i++) {
|
||||||
if (this.results[i].isComplete && !this.results[i + 1].isComplete) {
|
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.isRunning.set(!this.results[0]?.isComplete);
|
||||||
|
this.serializedResults.store(this.results.map(r => r.toJSON()));
|
||||||
|
this.changeResultEmitter.fire({ completed: result });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,14 @@ import { IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||||
import { URI } from 'vs/base/common/uri';
|
import { URI } from 'vs/base/common/uri';
|
||||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||||
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
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 const ITestService = createDecorator<ITestService>('testService');
|
||||||
|
|
||||||
export interface MainTestController {
|
export interface MainTestController {
|
||||||
lookupTest(test: TestIdWithProvider): Promise<InternalTestItem | undefined>;
|
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;
|
export type TestDiffListener = (diff: TestsDiff) => void;
|
||||||
|
@ -84,7 +85,7 @@ export interface ITestService {
|
||||||
|
|
||||||
registerTestController(id: string, controller: MainTestController): void;
|
registerTestController(id: string, controller: MainTestController): void;
|
||||||
unregisterTestController(id: string): void;
|
unregisterTestController(id: string): void;
|
||||||
runTests(req: RunTestsRequest, token?: CancellationToken): Promise<RunTestsResult>;
|
runTests(req: RunTestsRequest, token?: CancellationToken): Promise<ITestResult>;
|
||||||
cancelTestRun(req: RunTestsRequest): void;
|
cancelTestRun(req: RunTestsRequest): void;
|
||||||
publishDiff(resource: ExtHostTestingResource, uri: URI, diff: TestsDiff): void;
|
publishDiff(resource: ExtHostTestingResource, uri: URI, diff: TestsDiff): void;
|
||||||
subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference<IMainThreadTestCollection>;
|
subscribeToDiffs(resource: ExtHostTestingResource, uri: URI, acceptDiff?: TestDiffListener): IReference<IMainThreadTestCollection>;
|
||||||
|
|
|
@ -8,15 +8,14 @@ import { disposableTimeout } from 'vs/base/common/async';
|
||||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||||
import { Emitter } from 'vs/base/common/event';
|
import { Emitter } from 'vs/base/common/event';
|
||||||
import { Disposable, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
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 { URI, UriComponents } from 'vs/base/common/uri';
|
||||||
import { localize } from 'vs/nls';
|
import { localize } from 'vs/nls';
|
||||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||||
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
|
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 { 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';
|
import { IMainThreadTestCollection, ITestService, MainTestController, TestDiffListener } from 'vs/workbench/contrib/testing/common/testService';
|
||||||
|
|
||||||
type TestLocationIdent = { resource: ExtHostTestingResource, uri: URI };
|
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 busyStateChangeEmitter = new Emitter<TestLocationIdent & { busy: boolean }>();
|
||||||
private readonly changeProvidersEmitter = new Emitter<{ delta: number }>();
|
private readonly changeProvidersEmitter = new Emitter<{ delta: number }>();
|
||||||
private readonly providerCount: IContextKey<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 readonly runningTests = new Map<RunTestsRequest, CancellationTokenSource>();
|
||||||
private rootProviderCount = 0;
|
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) {
|
constructor(@IContextKeyService contextKeyService: IContextKeyService, @INotificationService private readonly notificationService: INotificationService, @ITestResultService private readonly testResults: ITestResultService) {
|
||||||
super();
|
super();
|
||||||
this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService);
|
this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService);
|
||||||
|
@ -126,35 +120,32 @@ export class TestService extends Disposable implements ITestService {
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
public async runTests(req: RunTestsRequest, token = CancellationToken.None): Promise<RunTestsResult> {
|
public async runTests(req: RunTestsRequest, token = CancellationToken.None): Promise<ITestResult> {
|
||||||
let result: TestResult | undefined;
|
|
||||||
const subscriptions = [...this.testSubscriptions.values()]
|
const subscriptions = [...this.testSubscriptions.values()]
|
||||||
.filter(v => req.tests.some(t => v.collection.getNodeById(t.testId)))
|
.filter(v => req.tests.some(t => v.collection.getNodeById(t.testId)))
|
||||||
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri, () => result?.notifyChanged()));
|
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri));
|
||||||
result = this.testResults.push(TestResult.from(subscriptions.map(s => s.object), req.tests));
|
const result = this.testResults.push(LiveTestResult.from(subscriptions.map(s => s.object), req.tests));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1);
|
const tests = groupBy(req.tests, (a, b) => a.providerId === b.providerId ? 0 : 1);
|
||||||
const cancelSource = new CancellationTokenSource(token);
|
const cancelSource = new CancellationTokenSource(token);
|
||||||
|
this.runningTests.set(req, cancelSource);
|
||||||
|
|
||||||
const requests = tests.map(group => {
|
const requests = tests.map(group => {
|
||||||
const providerId = group[0].providerId;
|
const providerId = group[0].providerId;
|
||||||
const controller = this.testControllers.get(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));
|
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;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
|
this.runningTests.delete(req);
|
||||||
subscriptions.forEach(s => s.dispose());
|
subscriptions.forEach(s => s.dispose());
|
||||||
result.markComplete();
|
result.markComplete();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,11 @@
|
||||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
* 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 => ({
|
export const stubTest = (label: string): TestItem => ({
|
||||||
label,
|
label,
|
||||||
location: undefined,
|
location: undefined,
|
||||||
state: new TestState(TestRunState.Unset),
|
|
||||||
debuggable: true,
|
debuggable: true,
|
||||||
runnable: true,
|
runnable: true,
|
||||||
description: ''
|
description: ''
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/s
|
||||||
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
import { IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||||
import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri';
|
import { parseTestUri, TestUriType, TEST_DATA_SCHEME } from 'vs/workbench/contrib/testing/common/testingUri';
|
||||||
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
|
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
|
* A content provider that returns various outputs for tests. This is used
|
||||||
|
@ -20,8 +19,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
|
||||||
constructor(
|
constructor(
|
||||||
@ITextModelService textModelResolverService: ITextModelService,
|
@ITextModelService textModelResolverService: ITextModelService,
|
||||||
@IModelService private readonly modelService: IModelService,
|
@IModelService private readonly modelService: IModelService,
|
||||||
@ITestService private readonly testService: ITestService,
|
@ITestResultService private readonly resultService: ITestResultService,
|
||||||
@ITestService private readonly resultService: ITestResultService,
|
|
||||||
) {
|
) {
|
||||||
textModelResolverService.registerTextModelContentProvider(TEST_DATA_SCHEME, this);
|
textModelResolverService.registerTextModelContentProvider(TEST_DATA_SCHEME, this);
|
||||||
}
|
}
|
||||||
|
@ -40,9 +38,7 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const test = 'providerId' in parsed
|
const test = this.resultService.getResult(parsed.resultId)?.getStateByExtId(parsed.testId);
|
||||||
? await this.testService.lookupTest({ providerId: parsed.providerId, testId: parsed.testId })
|
|
||||||
: this.resultService.lookup(parsed.resultId)?.tests.find(t => t.id === parsed.testId);
|
|
||||||
|
|
||||||
if (!test) {
|
if (!test) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -51,16 +47,13 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode
|
||||||
let text: string | undefined;
|
let text: string | undefined;
|
||||||
switch (parsed.type) {
|
switch (parsed.type) {
|
||||||
case TestUriType.ResultActualOutput:
|
case TestUriType.ResultActualOutput:
|
||||||
case TestUriType.LiveActualOutput:
|
text = test.state.messages[parsed.messageIndex]?.actualOutput;
|
||||||
text = test.item.state.messages[parsed.messageIndex]?.actualOutput;
|
|
||||||
break;
|
break;
|
||||||
case TestUriType.ResultExpectedOutput:
|
case TestUriType.ResultExpectedOutput:
|
||||||
case TestUriType.LiveExpectedOutput:
|
text = test.state.messages[parsed.messageIndex]?.expectedOutput;
|
||||||
text = test.item.state.messages[parsed.messageIndex]?.expectedOutput;
|
|
||||||
break;
|
break;
|
||||||
case TestUriType.ResultMessage:
|
case TestUriType.ResultMessage:
|
||||||
case TestUriType.LiveMessage:
|
text = test.state.messages[parsed.messageIndex]?.message.toString();
|
||||||
text = test.item.state.messages[parsed.messageIndex]?.message.toString();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
|
|
||||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||||
import { ViewContainerLocation } from 'vs/workbench/common/views';
|
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 namespace TestingContextKeys {
|
||||||
export const providerCount = new RawContextKey('testing.providerCount', 0);
|
export const providerCount = new RawContextKey('testing.providerCount', 0);
|
||||||
export const viewMode = new RawContextKey('testing.explorerViewMode', TestExplorerViewMode.List);
|
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 isRunning = new RawContextKey('testing.isRunning', false);
|
||||||
export const isInPeek = new RawContextKey('testing.isInPeek', true);
|
export const isInPeek = new RawContextKey('testing.isInPeek', true);
|
||||||
export const isPeekVisible = new RawContextKey('testing.isPeekVisible', false);
|
export const isPeekVisible = new RawContextKey('testing.isPeekVisible', false);
|
||||||
|
|
|
@ -14,12 +14,12 @@ export type TreeStateNode = { statusNode: true; state: TestRunState; priority: n
|
||||||
*/
|
*/
|
||||||
export const statePriority: { [K in TestRunState]: number } = {
|
export const statePriority: { [K in TestRunState]: number } = {
|
||||||
[TestRunState.Running]: 6,
|
[TestRunState.Running]: 6,
|
||||||
[TestRunState.Queued]: 5,
|
[TestRunState.Errored]: 5,
|
||||||
[TestRunState.Errored]: 4,
|
[TestRunState.Failed]: 4,
|
||||||
[TestRunState.Failed]: 3,
|
[TestRunState.Passed]: 3,
|
||||||
[TestRunState.Passed]: 2,
|
[TestRunState.Queued]: 2,
|
||||||
[TestRunState.Skipped]: 1,
|
[TestRunState.Unset]: 1,
|
||||||
[TestRunState.Unset]: 0,
|
[TestRunState.Skipped]: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isFailedState = (s: TestRunState) => s === TestRunState.Errored || s === TestRunState.Failed;
|
export const isFailedState = (s: TestRunState) => s === TestRunState.Errored || s === TestRunState.Failed;
|
||||||
|
|
|
@ -8,29 +8,11 @@ import { URI } from 'vs/base/common/uri';
|
||||||
export const TEST_DATA_SCHEME = 'vscode-test-data';
|
export const TEST_DATA_SCHEME = 'vscode-test-data';
|
||||||
|
|
||||||
export const enum TestUriType {
|
export const enum TestUriType {
|
||||||
LiveMessage,
|
|
||||||
LiveActualOutput,
|
|
||||||
LiveExpectedOutput,
|
|
||||||
ResultMessage,
|
ResultMessage,
|
||||||
ResultActualOutput,
|
ResultActualOutput,
|
||||||
ResultExpectedOutput,
|
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 {
|
interface IResultTestUri {
|
||||||
resultId: string;
|
resultId: string;
|
||||||
testId: string;
|
testId: string;
|
||||||
|
@ -48,13 +30,10 @@ interface IResultTestOutputReference extends IResultTestUri {
|
||||||
|
|
||||||
export type ParsedTestUri =
|
export type ParsedTestUri =
|
||||||
| IResultTestMessageReference
|
| IResultTestMessageReference
|
||||||
| IResultTestOutputReference
|
| IResultTestOutputReference;
|
||||||
| ILiveTestMessageReference
|
|
||||||
| ILiveTestOutputReference;
|
|
||||||
|
|
||||||
const enum TestUriParts {
|
const enum TestUriParts {
|
||||||
Results = 'results',
|
Results = 'results',
|
||||||
Live = 'live',
|
|
||||||
|
|
||||||
Messages = 'message',
|
Messages = 'message',
|
||||||
Text = 'text',
|
Text = 'text',
|
||||||
|
@ -78,15 +57,6 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => {
|
||||||
case TestUriParts.ExpectedOutput:
|
case TestUriParts.ExpectedOutput:
|
||||||
return { resultId: locationId, testId, messageIndex: index, type: TestUriType.ResultExpectedOutput };
|
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 => {
|
export const buildTestUri = (parsed: ParsedTestUri): URI => {
|
||||||
const uriParts = {
|
const uriParts = {
|
||||||
scheme: TEST_DATA_SCHEME,
|
scheme: TEST_DATA_SCHEME,
|
||||||
authority: 'resultId' in parsed ? TestUriParts.Results : TestUriParts.Live
|
authority: TestUriParts.Results
|
||||||
};
|
};
|
||||||
const msgRef = (locationId: string, index: number, ...remaining: string[]) =>
|
const msgRef = (locationId: string, index: number, ...remaining: string[]) =>
|
||||||
URI.from({
|
URI.from({
|
||||||
|
@ -111,12 +81,6 @@ export const buildTestUri = (parsed: ParsedTestUri): URI => {
|
||||||
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput);
|
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput);
|
||||||
case TestUriType.ResultMessage:
|
case TestUriType.ResultMessage:
|
||||||
return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.Text);
|
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:
|
default:
|
||||||
throw new Error('Invalid test uri');
|
throw new Error('Invalid test uri');
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,11 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => {
|
||||||
const folder1 = makeTestWorkspaceFolder('f1');
|
const folder1 = makeTestWorkspaceFolder('f1');
|
||||||
const folder2 = makeTestWorkspaceFolder('f2');
|
const folder2 = makeTestWorkspaceFolder('f2');
|
||||||
setup(() => {
|
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(() => {
|
teardown(() => {
|
||||||
|
|
|
@ -13,7 +13,11 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => {
|
||||||
const folder1 = makeTestWorkspaceFolder('f1');
|
const folder1 = makeTestWorkspaceFolder('f1');
|
||||||
const folder2 = makeTestWorkspaceFolder('f2');
|
const folder2 = makeTestWorkspaceFolder('f2');
|
||||||
setup(() => {
|
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(() => {
|
teardown(() => {
|
||||||
|
|
|
@ -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' }] },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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' }] },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -9,9 +9,6 @@ import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workb
|
||||||
suite('Workbench - Testing URIs', () => {
|
suite('Workbench - Testing URIs', () => {
|
||||||
test('round trip', () => {
|
test('round trip', () => {
|
||||||
const uris: ParsedTestUri[] = [
|
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.ResultActualOutput, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||||
{ type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testId: 't' },
|
{ type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||||
{ type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testId: 't' },
|
{ type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testId: 't' },
|
||||||
|
|
|
@ -326,7 +326,6 @@ suite('ExtHost Testing', () => {
|
||||||
assert.strictEqual(testItem.label, wrapper.label);
|
assert.strictEqual(testItem.label, wrapper.label);
|
||||||
assert.strictEqual(testItem.location, wrapper.location);
|
assert.strictEqual(testItem.location, wrapper.location);
|
||||||
assert.strictEqual(testItem.runnable, wrapper.runnable);
|
assert.strictEqual(testItem.runnable, wrapper.runnable);
|
||||||
assert.strictEqual(testItem.state, wrapper.state);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gets no children if nothing matches Uri filter', () => {
|
test('gets no children if nothing matches Uri filter', () => {
|
||||||
|
|
Loading…
Reference in a new issue