未验证 提交 6b666279 编写于 作者: C Connor Peet

wip

上级 6229e7a5
...@@ -243,6 +243,12 @@ export interface IShellLaunchConfig { ...@@ -243,6 +243,12 @@ export interface IShellLaunchConfig {
*/ */
isExtensionCustomPtyTerminal?: boolean; isExtensionCustomPtyTerminal?: boolean;
/**
* Custom PTY/pseudoterminal process to use.
* @todo should `TerminalProcessExtHostProxy` be passed to here and remove `isExtensionCustomPtyTerminal`?
*/
customPtyImplementation?: ITerminalChildProcess;
/** /**
* A UUID generated by the extension host process for terminals created on the extension host process. * A UUID generated by the extension host process for terminals created on the extension host process.
*/ */
......
...@@ -2323,6 +2323,9 @@ declare module 'vscode' { ...@@ -2323,6 +2323,9 @@ declare module 'vscode' {
* Appends raw output from the test runner. On the user's request, the * Appends raw output from the test runner. On the user's request, the
* output will be displayed in a terminal. ANSI escape sequences, * output will be displayed in a terminal. ANSI escape sequences,
* such as colors and text styles, are supported. * such as colors and text styles, are supported.
*
* @param output Output text to append
* @param associateTo Optionally, associate the given segment of output
*/ */
appendOutput(output: string): void; appendOutput(output: string): void;
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
* 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 { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { isDefined } from 'vs/base/common/types'; import { isDefined } from 'vs/base/common/types';
...@@ -71,7 +72,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh ...@@ -71,7 +72,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
* @inheritdoc * @inheritdoc
*/ */
public $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void { public $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void {
this.resultService.push(new HydratedTestResult(results, persist)); // todo
this.resultService.push(new HydratedTestResult(results, () => Promise.resolve(bufferToStream(VSBuffer.alloc(0))), persist));
} }
/** /**
...@@ -84,6 +86,17 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh ...@@ -84,6 +86,17 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
} }
} }
/**
* @inheritdoc
*/
public $appendOutputToRun(runId: string, output: VSBuffer): void {
const r = this.resultService.getResult(runId);
if (r && r instanceof LiveTestResult) {
r.output.append(output);
}
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
......
...@@ -1977,6 +1977,7 @@ export interface MainThreadTestingShape { ...@@ -1977,6 +1977,7 @@ export interface MainThreadTestingShape {
$publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void;
$updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void; $updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void;
$appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void; $appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void;
$appendOutputToRun(runId: string, output: VSBuffer): void;
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<string>; $runTests(req: RunTestsRequest, token: CancellationToken): Promise<string>;
$publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void; $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void;
} }
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import { mapFind } from 'vs/base/common/arrays'; import { mapFind } from 'vs/base/common/arrays';
import { disposableTimeout } from 'vs/base/common/async'; import { disposableTimeout } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
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 { once } from 'vs/base/common/functional'; import { once } from 'vs/base/common/functional';
...@@ -274,8 +275,8 @@ export class ExtHostTesting implements ExtHostTestingShape { ...@@ -274,8 +275,8 @@ export class ExtHostTesting implements ExtHostTestingShape {
try { try {
await provider.runTests({ await provider.runTests({
appendOutput() { appendOutput: message => {
// todo this.proxy.$appendOutputToRun(req.runId, VSBuffer.fromString(message));
}, },
appendMessage: (test, message) => { appendMessage: (test, message) => {
if (!isExcluded(test)) { if (!isExcluded(test)) {
......
...@@ -42,7 +42,7 @@ const LATENCY_MEASURING_INTERVAL = 1000; ...@@ -42,7 +42,7 @@ const LATENCY_MEASURING_INTERVAL = 1000;
enum ProcessType { enum ProcessType {
Process, Process,
ExtensionTerminal PsuedoTerminal
} }
/** /**
...@@ -191,9 +191,9 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce ...@@ -191,9 +191,9 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
let newProcess: ITerminalChildProcess; let newProcess: ITerminalChildProcess;
if (shellLaunchConfig.isExtensionCustomPtyTerminal) { if (shellLaunchConfig.isExtensionCustomPtyTerminal || shellLaunchConfig.customPtyImplementation) {
this._processType = ProcessType.ExtensionTerminal; this._processType = ProcessType.PsuedoTerminal;
newProcess = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._instanceId, shellLaunchConfig, cols, rows); newProcess = shellLaunchConfig.customPtyImplementation || this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._instanceId, shellLaunchConfig, cols, rows);
} else { } else {
const forceExtHostProcess = (this._configHelper.config as any).extHostProcess; const forceExtHostProcess = (this._configHelper.config as any).extHostProcess;
if (shellLaunchConfig.cwd && typeof shellLaunchConfig.cwd === 'object') { if (shellLaunchConfig.cwd && typeof shellLaunchConfig.cwd === 'object') {
...@@ -488,7 +488,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce ...@@ -488,7 +488,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
await this.ptyProcessReady; await this.ptyProcessReady;
this._dataFilter.triggerSwap(); this._dataFilter.triggerSwap();
this._hasWrittenData = true; this._hasWrittenData = true;
if (this.shellProcessId || this._processType === ProcessType.ExtensionTerminal) { if (this.shellProcessId || this._processType === ProcessType.PsuedoTerminal) {
if (this._process) { if (this._process) {
// Send data if the pty is ready // Send data if the pty is ready
this._process.input(data); this._process.input(data);
......
...@@ -31,6 +31,7 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons'; ...@@ -31,6 +31,7 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService';
import { TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestExplorerViewMode, TestExplorerViewSorting, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { InternalTestItem, ITestItem, TestIdPath, TestIdWithSrc, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { InternalTestItem, ITestItem, TestIdPath, TestIdWithSrc, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun'; import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
...@@ -435,6 +436,29 @@ export class TestingSortByLocationAction extends ViewAction<TestingExplorerView> ...@@ -435,6 +436,29 @@ export class TestingSortByLocationAction extends ViewAction<TestingExplorerView>
} }
} }
export class ShowMostRecentOutputAction extends Action2 {
constructor() {
super({
id: 'testing.showMostRecentOutput',
title: localize('testing.showMostRecentOutput', "Collapse All Tests"),
f1: false,
icon: Codicon.terminal,
menu: {
id: MenuId.ViewTitle,
order: ActionOrder.Collapse,
group: 'navigation',
when: ContextKeyEqualsExpr.create('view', Testing.ExplorerViewId)
}
});
}
public run(accessor: ServicesAccessor) {
const result = accessor.get(ITestResultService).results[0];
accessor.get(ITestingOutputTerminalService).open(result);
}
}
export class CollapseAllAction extends ViewAction<TestingExplorerView> { export class CollapseAllAction extends ViewAction<TestingExplorerView> {
constructor() { constructor() {
super({ super({
......
...@@ -20,6 +20,7 @@ import { TestingDecorations } from 'vs/workbench/contrib/testing/browser/testing ...@@ -20,6 +20,7 @@ import { TestingDecorations } from 'vs/workbench/contrib/testing/browser/testing
import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter'; import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView'; import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { CloseTestPeek, ITestingPeekOpener, TestingOutputPeekController, TestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek'; import { CloseTestPeek, ITestingPeekOpener, TestingOutputPeekController, TestingPeekOpener } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { ITestingOutputTerminalService, TestingOutputTerminalService } from 'vs/workbench/contrib/testing/browser/testingOutputTerminalService';
import { ITestingProgressUiService, TestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService'; import { ITestingProgressUiService, TestingProgressUiService } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer'; import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration'; import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration';
...@@ -41,6 +42,7 @@ registerSingleton(ITestResultStorage, TestResultStorage); ...@@ -41,6 +42,7 @@ registerSingleton(ITestResultStorage, TestResultStorage);
registerSingleton(ITestResultService, TestResultService); registerSingleton(ITestResultService, TestResultService);
registerSingleton(ITestExplorerFilterState, TestExplorerFilterState); registerSingleton(ITestExplorerFilterState, TestExplorerFilterState);
registerSingleton(ITestingAutoRun, TestingAutoRun, true); registerSingleton(ITestingAutoRun, TestingAutoRun, true);
registerSingleton(ITestingOutputTerminalService, TestingOutputTerminalService, true);
registerSingleton(ITestingPeekOpener, TestingPeekOpener); registerSingleton(ITestingPeekOpener, TestingPeekOpener);
registerSingleton(ITestingProgressUiService, TestingProgressUiService); registerSingleton(ITestingProgressUiService, TestingProgressUiService);
registerSingleton(IWorkspaceTestCollectionService, WorkspaceTestCollectionService); registerSingleton(IWorkspaceTestCollectionService, WorkspaceTestCollectionService);
...@@ -86,30 +88,31 @@ viewsRegistry.registerViews([{ ...@@ -86,30 +88,31 @@ viewsRegistry.registerViews([{
when: ContextKeyExpr.greater(TestingContextKeys.providerCount.key, 0), when: ContextKeyExpr.greater(TestingContextKeys.providerCount.key, 0),
}], viewContainer); }], viewContainer);
registerAction2(Action.TestingViewAsListAction); registerAction2(Action.AutoRunOffAction);
registerAction2(Action.TestingViewAsTreeAction); registerAction2(Action.AutoRunOnAction);
registerAction2(Action.CancelTestRunAction); registerAction2(Action.CancelTestRunAction);
registerAction2(Action.RunSelectedAction); registerAction2(Action.ClearTestResultsAction);
registerAction2(Action.DebugSelectedAction);
registerAction2(Action.TestingSortByNameAction);
registerAction2(Action.TestingSortByLocationAction);
registerAction2(Action.RefreshTestsAction);
registerAction2(Action.CollapseAllAction); registerAction2(Action.CollapseAllAction);
registerAction2(Action.RunAllAction);
registerAction2(Action.DebugAllAction); registerAction2(Action.DebugAllAction);
registerAction2(Action.EditFocusedTest);
registerAction2(Action.ClearTestResultsAction);
registerAction2(Action.AutoRunOffAction);
registerAction2(Action.AutoRunOnAction);
registerAction2(Action.DebugAtCursor); registerAction2(Action.DebugAtCursor);
registerAction2(Action.RunAtCursor);
registerAction2(Action.DebugCurrentFile); registerAction2(Action.DebugCurrentFile);
registerAction2(Action.RunCurrentFile);
registerAction2(Action.ReRunFailedTests);
registerAction2(Action.DebugFailedTests); registerAction2(Action.DebugFailedTests);
registerAction2(Action.ReRunLastRun);
registerAction2(Action.DebugLastRun); registerAction2(Action.DebugLastRun);
registerAction2(Action.DebugSelectedAction);
registerAction2(Action.EditFocusedTest);
registerAction2(Action.RefreshTestsAction);
registerAction2(Action.ReRunFailedTests);
registerAction2(Action.ReRunLastRun);
registerAction2(Action.RunAllAction);
registerAction2(Action.RunAtCursor);
registerAction2(Action.RunCurrentFile);
registerAction2(Action.RunSelectedAction);
registerAction2(Action.SearchForTestExtension); registerAction2(Action.SearchForTestExtension);
registerAction2(Action.ShowMostRecentOutputAction);
registerAction2(Action.TestingSortByLocationAction);
registerAction2(Action.TestingSortByNameAction);
registerAction2(Action.TestingViewAsListAction);
registerAction2(Action.TestingViewAsTreeAction);
registerAction2(CloseTestPeek); registerAction2(CloseTestPeek);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored); Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored);
......
...@@ -25,7 +25,7 @@ import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService' ...@@ -25,7 +25,7 @@ import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'
import { ViewContainerLocation } from 'vs/workbench/common/views'; import { ViewContainerLocation } from 'vs/workbench/common/views';
import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons';
import { TestExplorerStateFilter, Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestExplorerStateFilter, Testing } from 'vs/workbench/contrib/testing/common/constants';
import { ObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { TestIdPath } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestIdPath } 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';
...@@ -33,17 +33,17 @@ import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; ...@@ -33,17 +33,17 @@ import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
export interface ITestExplorerFilterState { export interface ITestExplorerFilterState {
_serviceBrand: undefined; _serviceBrand: undefined;
readonly text: ObservableValue<string>; readonly text: MutableObservableValue<string>;
/** /**
* Reveal request: the path to the test to reveal. The last element of the * Reveal request: the path to the test to reveal. The last element of the
* array is the test the user wanted to reveal, and the previous * array is the test the user wanted to reveal, and the previous
* items are its parents. * items are its parents.
*/ */
readonly reveal: ObservableValue<TestIdPath | undefined>; readonly reveal: MutableObservableValue<TestIdPath | undefined>;
readonly stateFilter: ObservableValue<TestExplorerStateFilter>; readonly stateFilter: MutableObservableValue<TestExplorerStateFilter>;
readonly currentDocumentOnly: ObservableValue<boolean>; readonly currentDocumentOnly: MutableObservableValue<boolean>;
/** Whether excluded test should be shown in the view */ /** Whether excluded test should be shown in the view */
readonly showExcludedTests: ObservableValue<boolean>; readonly showExcludedTests: MutableObservableValue<boolean>;
readonly onDidRequestInputFocus: Event<void>; readonly onDidRequestInputFocus: Event<void>;
focusInput(): void; focusInput(): void;
...@@ -54,20 +54,20 @@ export const ITestExplorerFilterState = createDecorator<ITestExplorerFilterState ...@@ -54,20 +54,20 @@ export const ITestExplorerFilterState = createDecorator<ITestExplorerFilterState
export class TestExplorerFilterState implements ITestExplorerFilterState { export class TestExplorerFilterState implements ITestExplorerFilterState {
declare _serviceBrand: undefined; declare _serviceBrand: undefined;
private readonly focusEmitter = new Emitter<void>(); private readonly focusEmitter = new Emitter<void>();
public readonly text = new ObservableValue(''); public readonly text = new MutableObservableValue('');
public readonly stateFilter = ObservableValue.stored(new StoredValue<TestExplorerStateFilter>({ public readonly stateFilter = MutableObservableValue.stored(new StoredValue<TestExplorerStateFilter>({
key: 'testStateFilter', key: 'testStateFilter',
scope: StorageScope.WORKSPACE, scope: StorageScope.WORKSPACE,
target: StorageTarget.USER target: StorageTarget.USER
}, this.storage), TestExplorerStateFilter.All); }, this.storage), TestExplorerStateFilter.All);
public readonly currentDocumentOnly = ObservableValue.stored(new StoredValue<boolean>({ public readonly currentDocumentOnly = MutableObservableValue.stored(new StoredValue<boolean>({
key: 'testsByCurrentDocumentOnly', key: 'testsByCurrentDocumentOnly',
scope: StorageScope.WORKSPACE, scope: StorageScope.WORKSPACE,
target: StorageTarget.USER target: StorageTarget.USER
}, this.storage), false); }, this.storage), false);
public readonly showExcludedTests = new ObservableValue(false); public readonly showExcludedTests = new MutableObservableValue(false);
public readonly reveal = new ObservableValue<TestIdPath | undefined>(undefined); public readonly reveal = new MutableObservableValue<TestIdPath | undefined>(undefined);
public readonly onDidRequestInputFocus = this.focusEmitter.event; public readonly onDidRequestInputFocus = this.focusEmitter.event;
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { listenStream } from 'vs/base/common/stream';
import { localize } from 'vs/nls';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProcessDataEvent, ITerminalChildProcess, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal';
import { ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
export interface ITestingOutputTerminalService {
_serviceBrand: undefined;
/**
* Opens a terminal for the given test's output.
*/
open(result: ITestResult): Promise<void>;
}
const friendlyDate = (date: number) => {
const d = new Date(date);
return d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0') + ':' + String(d.getSeconds()).padStart(2, '0');
};
const genericTitle = localize('testOutputTerminalTitle', 'Test Output');
type TestOutputTerminalInstance = ITerminalInstance & { shellLaunchConfig: { customPtyImplementation: TestOutputProcess } };
export const ITestingOutputTerminalService = createDecorator<ITestingOutputTerminalService>('ITestingOutputTerminalService');
export class TestingOutputTerminalService implements ITestingOutputTerminalService {
_serviceBrand: undefined;
constructor(@ITerminalService private readonly terminalService: ITerminalService) { }
/**
* @inheritdoc
*/
public async open(result: ITestResult | undefined): Promise<void> {
const title = result
? localize('testOutputTerminalTitleWithDate', 'Test Output at {0}', friendlyDate(result.completedAt ?? Date.now()))
: genericTitle;
const testOutputPtys = this.terminalService.terminalInstances.filter(
(i): i is TestOutputTerminalInstance => i.shellLaunchConfig.customPtyImplementation instanceof TestOutputProcess);
// If there's an existing terminal for the attempted reveal, show that instead.
const existing = testOutputPtys.find(i => i.shellLaunchConfig.customPtyImplementation.resultId === result?.id);
if (existing) {
this.terminalService.setActiveInstance(existing);
this.terminalService.showPanel();
return;
}
// Try to reuse ended terminals, otherwise make a new one
let output: TestOutputProcess;
let terminal = testOutputPtys.find(i => i.shellLaunchConfig.customPtyImplementation.ended);
if (terminal) {
output = terminal.shellLaunchConfig.customPtyImplementation;
} else {
output = new TestOutputProcess();
terminal = this.terminalService.createTerminal({
isFeatureTerminal: true,
customPtyImplementation: output,
name: title,
}) as TestOutputTerminalInstance;
}
output.resetFor(result?.id, title);
this.terminalService.setActiveInstance(terminal);
this.terminalService.showPanel();
if (!result) {
// seems like it takes a tick for listeners to be registered
output.ended = true;
setTimeout(() => output.pushData(localize('testNoRunYet', '\r\nNo tests have been run, yet.\r\n')));
return;
}
listenStream(await result.getOutput(), {
onData: d => output.pushData(d.toString()),
onError: err => output.pushData(`\r\n\r\n${err.stack || err.message}`),
onEnd: () => {
const completedAt = result.completedAt ? new Date(result.completedAt) : new Date();
const text = localize('runFinished', 'Test run finished at {0}', completedAt.toLocaleString());
output.pushData(`\r\n\r\n\x1b[1m> ${text} <\x1b[0m\r\n`);
output.ended = true;
},
});
}
}
class TestOutputProcess extends Disposable implements ITerminalChildProcess {
private processDataEmitter = this._register(new Emitter<string | IProcessDataEvent>());
private titleEmitter = this._register(new Emitter<string>());
/** Whether the associated test has ended (indicating the terminal can be reused) */
public ended = true;
/** Result currently being displayed */
public resultId: string | undefined;
public pushData(data: string | IProcessDataEvent) {
this.processDataEmitter.fire(data);
}
public resetFor(resultId: string | undefined, title: string) {
this.ended = false;
this.resultId = resultId;
this.processDataEmitter.fire('\x1bc');
this.titleEmitter.fire(title);
}
//#region implementation
public readonly id = 0;
public readonly shouldPersist = false;
public readonly onProcessData = this.processDataEmitter.event;
public readonly onProcessExit = this._register(new Emitter<number | undefined>()).event;
public readonly onProcessReady = this._register(new Emitter<{ pid: number; cwd: string; }>()).event;
public readonly onProcessTitleChanged = this.titleEmitter.event;
public readonly onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>()).event;
public start(): Promise<ITerminalLaunchError | undefined> {
return Promise.resolve(undefined);
}
public shutdown(): void {
// no-op
}
public input(): void {
// not supported
}
public processBinary(): Promise<void> {
return Promise.resolve();
}
public resize(): void {
// no-op
}
public acknowledgeDataEvent(): void {
// no-op, flow control not currently implemented
}
public getInitialCwd(): Promise<string> {
return Promise.resolve('');
}
public getCwd(): Promise<string> {
return Promise.resolve('');
}
public getLatency(): Promise<number> {
return Promise.resolve(0);
}
//#endregion
}
...@@ -3,10 +3,20 @@ ...@@ -3,10 +3,20 @@
* 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 { Emitter } from 'vs/base/common/event'; import { Emitter, Event } from 'vs/base/common/event';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
export class ObservableValue<T> { export interface IObservableValue<T> {
onDidChange: Event<T>;
readonly value: T;
}
export const staticObservableValue = <T>(value: T): IObservableValue<T> => ({
onDidChange: Event.None,
value,
});
export class MutableObservableValue<T> implements IObservableValue<T> {
private readonly changeEmitter = new Emitter<T>(); private readonly changeEmitter = new Emitter<T>();
public readonly onDidChange = this.changeEmitter.event; public readonly onDidChange = this.changeEmitter.event;
...@@ -23,7 +33,7 @@ export class ObservableValue<T> { ...@@ -23,7 +33,7 @@ export class ObservableValue<T> {
} }
public static stored<T>(stored: StoredValue<T>, defaultValue: T) { public static stored<T>(stored: StoredValue<T>, defaultValue: T) {
const o = new ObservableValue(stored.get(defaultValue)); const o = new MutableObservableValue(stored.get(defaultValue));
o.onDidChange(value => stored.store(value)); o.onDidChange(value => stored.store(value));
return o; return o;
} }
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +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 { newWriteableBufferStream, VSBuffer, VSBufferReadableStream, VSBufferWriteableStream } from 'vs/base/common/buffer';
import { Emitter } from 'vs/base/common/event'; import { Emitter } from 'vs/base/common/event';
import { Lazy } from 'vs/base/common/lazy'; import { Lazy } from 'vs/base/common/lazy';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { Range } from 'vs/editor/common/core/range'; import { Range } from 'vs/editor/common/core/range';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState'; import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
...@@ -46,6 +47,11 @@ export interface ITestResult { ...@@ -46,6 +47,11 @@ export interface ITestResult {
*/ */
getStateById(testExtId: string): TestResultItem | undefined; getStateById(testExtId: string): TestResultItem | undefined;
/**
* Loads the output of the result as a stream.
*/
getOutput(): Promise<VSBufferReadableStream>;
/** /**
* Serializes the test result. Used to save and restore results * Serializes the test result. Used to save and restore results
* in the workspace. * in the workspace.
...@@ -78,6 +84,88 @@ export const sumCounts = (counts: Iterable<TestStateCount>) => { ...@@ -78,6 +84,88 @@ export const sumCounts = (counts: Iterable<TestStateCount>) => {
return total; return total;
}; };
/**
* Deals with output of a {@link LiveTestResult}. By default we pass-through
* data into the underlying write stream, but if a client requests to read it
* we splice in the written data and then continue streaming incoming data.
*/
export class LiveOutputController {
/** Set on close() to a promise that is resolved once closing is complete */
private closed?: Promise<void>;
/** Data written so far. This is available until the file closes. */
private previouslyWritten: VSBuffer[] | undefined = [];
private readonly dataEmitter = new Emitter<VSBuffer>();
private readonly endEmitter = new Emitter<void>();
constructor(
private readonly writer: Lazy<[VSBufferWriteableStream, Promise<void>]>,
private readonly reader: () => Promise<VSBufferReadableStream>,
) { }
/**
* Appends data to the output.
*/
public append(data: VSBuffer): Promise<void> | void {
if (this.closed) {
return this.closed;
}
this.previouslyWritten?.push(data);
this.dataEmitter.fire(data);
return this.writer.getValue()[0].write(data);
}
/**
* Reads the value of the stream.
*/
public read() {
if (!this.previouslyWritten) {
return this.reader();
}
const stream = newWriteableBufferStream();
for (const chunk of this.previouslyWritten) {
stream.write(chunk);
}
const disposable = new DisposableStore();
disposable.add(this.dataEmitter.event(d => stream.write(d)));
disposable.add(this.endEmitter.event(() => stream.end()));
stream.on('end', () => disposable.dispose());
return Promise.resolve(stream);
}
/**
* Closes the output, signalling no more writes will be made.
* @returns a promise that resolves when the output is written
*/
public close(): Promise<void> {
if (this.closed) {
return this.closed;
}
if (!this.writer.hasValue()) {
this.closed = Promise.resolve();
} else {
const [stream, ended] = this.writer.getValue();
stream.end();
this.closed = ended;
}
this.endEmitter.fire();
this.closed.then(() => {
this.previouslyWritten = undefined;
this.dataEmitter.dispose();
this.endEmitter.dispose();
});
return this.closed;
}
}
const itemToNode = ( const itemToNode = (
item: IncrementalTestCollectionItem, item: IncrementalTestCollectionItem,
...@@ -172,7 +260,9 @@ export class LiveTestResult implements ITestResult { ...@@ -172,7 +260,9 @@ export class LiveTestResult implements ITestResult {
* of collections. * of collections.
*/ */
public static from( public static from(
resultId: string,
collections: ReadonlyArray<IMainThreadTestCollection>, collections: ReadonlyArray<IMainThreadTestCollection>,
output: LiveOutputController,
req: RunTestsRequest, req: RunTestsRequest,
) { ) {
const testByExtId = new Map<string, TestResultItem>(); const testByExtId = new Map<string, TestResultItem>();
...@@ -189,7 +279,7 @@ export class LiveTestResult implements ITestResult { ...@@ -189,7 +279,7 @@ export class LiveTestResult implements ITestResult {
} }
} }
return new LiveTestResult(collections, testByExtId, excludeSet, !!req.isAutoRun); return new LiveTestResult(resultId, collections, testByExtId, excludeSet, output, !!req.isAutoRun);
} }
private readonly completeEmitter = new Emitter<void>(); private readonly completeEmitter = new Emitter<void>();
...@@ -199,11 +289,6 @@ export class LiveTestResult implements ITestResult { ...@@ -199,11 +289,6 @@ export class LiveTestResult implements ITestResult {
public readonly onChange = this.changeEmitter.event; public readonly onChange = this.changeEmitter.event;
public readonly onComplete = this.completeEmitter.event; public readonly onComplete = this.completeEmitter.event;
/**
* Unique ID for referring to this set of test results.
*/
public readonly id = generateUuid();
/** /**
* @inheritdoc * @inheritdoc
*/ */
...@@ -255,9 +340,11 @@ export class LiveTestResult implements ITestResult { ...@@ -255,9 +340,11 @@ export class LiveTestResult implements ITestResult {
}; };
constructor( constructor(
public readonly id: string,
private readonly collections: ReadonlyArray<IMainThreadTestCollection>, private readonly collections: ReadonlyArray<IMainThreadTestCollection>,
private readonly testById: Map<string, TestResultItem>, private readonly testById: Map<string, TestResultItem>,
private readonly excluded: ReadonlySet<string>, private readonly excluded: ReadonlySet<string>,
public readonly output: LiveOutputController,
public readonly isAutoRun: boolean, public readonly isAutoRun: boolean,
) { ) {
this.counts[TestResultState.Unset] = testById.size; this.counts[TestResultState.Unset] = testById.size;
...@@ -315,6 +402,13 @@ export class LiveTestResult implements ITestResult { ...@@ -315,6 +402,13 @@ export class LiveTestResult implements ITestResult {
}); });
} }
/**
* @inheritdoc
*/
public getOutput() {
return this.output.read();
}
private fireUpdateAndRefresh(entry: TestResultItem, newState: TestResultState) { private fireUpdateAndRefresh(entry: TestResultItem, newState: TestResultState) {
const previous = entry.state.state; const previous = entry.state.state;
if (newState === previous) { if (newState === previous) {
...@@ -445,7 +539,11 @@ export class HydratedTestResult implements ITestResult { ...@@ -445,7 +539,11 @@ export class HydratedTestResult implements ITestResult {
private readonly testById = new Map<string, TestResultItem>(); private readonly testById = new Map<string, TestResultItem>();
constructor(private readonly serialized: ISerializedTestResults, private readonly persist = true) { constructor(
private readonly serialized: ISerializedTestResults,
private readonly outputLoader: () => Promise<VSBufferReadableStream>,
private readonly persist = true,
) {
this.id = serialized.id; this.id = serialized.id;
this.completedAt = serialized.completedAt; this.completedAt = serialized.completedAt;
...@@ -472,6 +570,13 @@ export class HydratedTestResult implements ITestResult { ...@@ -472,6 +570,13 @@ export class HydratedTestResult implements ITestResult {
return this.testById.get(extTestId); return this.testById.get(extTestId);
} }
/**
* @inheritdoc
*/
public getOutput() {
return this.outputLoader();
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
......
...@@ -9,13 +9,15 @@ import { Emitter, Event } from 'vs/base/common/event'; ...@@ -9,13 +9,15 @@ import { Emitter, Event } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional'; import { once } from 'vs/base/common/functional';
import { Iterable } from 'vs/base/common/iterator'; import { Iterable } from 'vs/base/common/iterator';
import { equals } from 'vs/base/common/objects'; import { equals } from 'vs/base/common/objects';
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 { TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { RunTestsRequest, TestResultItem } 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, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService';
export type ResultChangeEvent = export type ResultChangeEvent =
| { completed: LiveTestResult } | { completed: LiveTestResult }
...@@ -45,6 +47,11 @@ export interface ITestResultService { ...@@ -45,6 +47,11 @@ export interface ITestResultService {
*/ */
clear(): void; clear(): void;
/**
* Creates a new, live test result.
*/
createLiveResult(collections: ReadonlyArray<IMainThreadTestCollection>, req: RunTestsRequest): LiveTestResult;
/** /**
* Adds a new test result to the collection. * Adds a new test result to the collection.
*/ */
...@@ -125,6 +132,14 @@ export class TestResultService implements ITestResultService { ...@@ -125,6 +132,14 @@ export class TestResultService implements ITestResultService {
return undefined; return undefined;
} }
/**
* @inheritdoc
*/
public createLiveResult(collections: ReadonlyArray<IMainThreadTestCollection>, req: RunTestsRequest) {
const id = generateUuid();
return this.push(LiveTestResult.from(id, collections, this.storage.getOutputController(id), req));
}
/** /**
* @inheritdoc * @inheritdoc
*/ */
......
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
* 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 { VSBuffer } from 'vs/base/common/buffer'; import { bufferToStream, newWriteableBufferStream, VSBuffer, VSBufferReadableStream, VSBufferWriteableStream } from 'vs/base/common/buffer';
import { Lazy } from 'vs/base/common/lazy';
import { isDefined } from 'vs/base/common/types'; import { isDefined } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri'; import { URI } from 'vs/base/common/uri';
import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IEnvironmentService } from 'vs/platform/environment/common/environment';
...@@ -14,11 +15,12 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag ...@@ -14,11 +15,12 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { ISerializedTestResults } from 'vs/workbench/contrib/testing/common/testCollection'; import { ISerializedTestResults } from 'vs/workbench/contrib/testing/common/testCollection';
import { HydratedTestResult, ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { HydratedTestResult, ITestResult, LiveOutputController, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
export const RETAIN_MAX_RESULTS = 128; export const RETAIN_MAX_RESULTS = 128;
const RETAIN_MIN_RESULTS = 16; const RETAIN_MIN_RESULTS = 16;
const RETAIN_MAX_BYTES = 1024 * 128; const RETAIN_MAX_BYTES = 1024 * 128;
const CLEANUP_PROBABILITY = 0.2;
export interface ITestResultStorage { export interface ITestResultStorage {
_serviceBrand: undefined; _serviceBrand: undefined;
...@@ -32,6 +34,11 @@ export interface ITestResultStorage { ...@@ -32,6 +34,11 @@ export interface ITestResultStorage {
* Persists the list of test results. * Persists the list of test results.
*/ */
persist(results: ReadonlyArray<ITestResult>): Promise<void>; persist(results: ReadonlyArray<ITestResult>): Promise<void>;
/**
* Gets the output controller for a new or existing test result.
*/
getOutputController(resultId: string): LiveOutputController;
} }
export const ITestResultStorage = createDecorator('ITestResultStorage'); export const ITestResultStorage = createDecorator('ITestResultStorage');
...@@ -39,7 +46,7 @@ export const ITestResultStorage = createDecorator('ITestResultStorage'); ...@@ -39,7 +46,7 @@ export const ITestResultStorage = createDecorator('ITestResultStorage');
export abstract class BaseTestResultStorage implements ITestResultStorage { export abstract class BaseTestResultStorage implements ITestResultStorage {
declare readonly _serviceBrand: undefined; declare readonly _serviceBrand: undefined;
private readonly stored = new StoredValue<ReadonlyArray<{ id: string, bytes: number }>>({ protected readonly stored = new StoredValue<ReadonlyArray<{ id: string, bytes: number }>>({
key: 'storedTestResults', key: 'storedTestResults',
scope: StorageScope.WORKSPACE, scope: StorageScope.WORKSPACE,
target: StorageTarget.MACHINE target: StorageTarget.MACHINE
...@@ -62,7 +69,7 @@ export abstract class BaseTestResultStorage implements ITestResultStorage { ...@@ -62,7 +69,7 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
return undefined; return undefined;
} }
return new HydratedTestResult(contents); return new HydratedTestResult(contents, () => this.readOutputForResultId(id));
} catch (e) { } catch (e) {
this.logService.warn(`Error deserializing stored test result ${id}`, e); this.logService.warn(`Error deserializing stored test result ${id}`, e);
return undefined; return undefined;
...@@ -72,6 +79,29 @@ export abstract class BaseTestResultStorage implements ITestResultStorage { ...@@ -72,6 +79,29 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
return results.filter(isDefined); return results.filter(isDefined);
} }
/**
* @override
*/
public getOutputController(resultId: string) {
return new LiveOutputController(
new Lazy(() => {
const stream = newWriteableBufferStream();
const promise = this.storeOutputForResultId(resultId, stream);
return [stream, promise];
}),
() => this.readOutputForResultId(resultId),
);
}
/**
* @override
*/
public getResultOutputWriter(resultId: string) {
const stream = newWriteableBufferStream();
this.storeOutputForResultId(resultId, stream);
return stream;
}
/** /**
* @override * @override
*/ */
...@@ -108,6 +138,10 @@ export abstract class BaseTestResultStorage implements ITestResultStorage { ...@@ -108,6 +138,10 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
todo.push(this.storeForResultId(result.id, obj)); todo.push(this.storeForResultId(result.id, obj));
toStore.push({ id: result.id, bytes: contents.byteLength }); toStore.push({ id: result.id, bytes: contents.byteLength });
budget -= contents.byteLength; budget -= contents.byteLength;
if (result instanceof LiveTestResult && result.completedAt !== undefined) {
todo.push(result.output.close());
}
} }
for (const id of toDelete.keys()) { for (const id of toDelete.keys()) {
...@@ -123,6 +157,11 @@ export abstract class BaseTestResultStorage implements ITestResultStorage { ...@@ -123,6 +157,11 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
*/ */
protected abstract readForResultId(id: string): Promise<ISerializedTestResults | undefined>; protected abstract readForResultId(id: string): Promise<ISerializedTestResults | undefined>;
/**
* Reads serialized results for the test. Is allowed to throw.
*/
protected abstract readOutputForResultId(id: string): Promise<VSBufferReadableStream>;
/** /**
* Deletes serialized results for the test. * Deletes serialized results for the test.
*/ */
...@@ -132,6 +171,11 @@ export abstract class BaseTestResultStorage implements ITestResultStorage { ...@@ -132,6 +171,11 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
* Stores test results by ID. * Stores test results by ID.
*/ */
protected abstract storeForResultId(id: string, data: ISerializedTestResults): Promise<unknown>; protected abstract storeForResultId(id: string, data: ISerializedTestResults): Promise<unknown>;
/**
* Reads serialized results for the test. Is allowed to throw.
*/
protected abstract storeOutputForResultId(id: string, input: VSBufferWriteableStream): Promise<void>;
} }
export class InMemoryResultStorage extends BaseTestResultStorage { export class InMemoryResultStorage extends BaseTestResultStorage {
...@@ -150,6 +194,14 @@ export class InMemoryResultStorage extends BaseTestResultStorage { ...@@ -150,6 +194,14 @@ export class InMemoryResultStorage extends BaseTestResultStorage {
this.cache.delete(id); this.cache.delete(id);
return Promise.resolve(); return Promise.resolve();
} }
protected readOutputForResultId(id: string): Promise<VSBufferReadableStream> {
throw new Error('Method not implemented.');
}
protected storeOutputForResultId(id: string, input: VSBufferWriteableStream): Promise<void> {
throw new Error('Method not implemented.');
}
} }
export class TestResultStorage extends BaseTestResultStorage { export class TestResultStorage extends BaseTestResultStorage {
...@@ -179,7 +231,53 @@ export class TestResultStorage extends BaseTestResultStorage { ...@@ -179,7 +231,53 @@ export class TestResultStorage extends BaseTestResultStorage {
return this.fileService.del(this.getResultJsonPath(id)).catch(() => undefined); return this.fileService.del(this.getResultJsonPath(id)).catch(() => undefined);
} }
protected async readOutputForResultId(id: string): Promise<VSBufferReadableStream> {
try {
const { value } = await this.fileService.readFileStream(this.getResultOutputPath(id));
return value;
} catch {
return bufferToStream(VSBuffer.alloc(0));
}
}
protected async storeOutputForResultId(id: string, input: VSBufferWriteableStream) {
await this.fileService.createFile(this.getResultOutputPath(id), input);
}
/**
* @inheritdoc
*/
public override async persist(results: ReadonlyArray<ITestResult>) {
await super.persist(results);
if (Math.random() < CLEANUP_PROBABILITY) {
await this.cleanupDereferenced();
}
}
/**
* Cleans up orphaned files. For instance, output can get orphaned if it's
* written but the editor is closed before the test run is complete.
*/
private async cleanupDereferenced() {
const { children } = await this.fileService.resolve(this.directory);
if (!children) {
return;
}
const stored = new Set(this.stored.get()?.map(({ id }) => id));
await Promise.all(
children
.filter(child => !stored.has(child.name.replace(/\.[a-z]+$/, '')))
.map(child => this.fileService.del(child.resource))
);
}
private getResultJsonPath(id: string) { private getResultJsonPath(id: string) {
return URI.joinPath(this.directory, `${id}.json`); return URI.joinPath(this.directory, `${id}.json`);
} }
private getResultOutputPath(id: string) {
return URI.joinPath(this.directory, `${id}.output`);
}
} }
...@@ -9,7 +9,7 @@ import { DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecyc ...@@ -9,7 +9,7 @@ import { DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecyc
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 { ObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdPath, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdPath, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
...@@ -157,7 +157,7 @@ export interface ITestService { ...@@ -157,7 +157,7 @@ export interface ITestService {
/** /**
* Set of test IDs the user asked to exclude. * Set of test IDs the user asked to exclude.
*/ */
readonly excludeTests: ObservableValue<ReadonlySet<string>>; readonly excludeTests: MutableObservableValue<ReadonlySet<string>>;
/** /**
* Sets whether a test is excluded. * Sets whether a test is excluded.
......
...@@ -14,7 +14,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c ...@@ -14,7 +14,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
import { INotificationService } from 'vs/platform/notification/common/notification'; import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { ObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
import { AbstractIncrementalTestCollection, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import { AbstractIncrementalTestCollection, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, TestDiffOpType, TestIdWithSrc, 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';
...@@ -48,7 +48,7 @@ export class TestService extends Disposable implements ITestService { ...@@ -48,7 +48,7 @@ export class TestService extends Disposable implements ITestService {
private readonly runningTests = new Map<RunTestsRequest, CancellationTokenSource>(); private readonly runningTests = new Map<RunTestsRequest, CancellationTokenSource>();
private readonly rootProviders = new Set<ITestRootProvider>(); private readonly rootProviders = new Set<ITestRootProvider>();
public readonly excludeTests = ObservableValue.stored(new StoredValue<ReadonlySet<string>>({ public readonly excludeTests = MutableObservableValue.stored(new StoredValue<ReadonlySet<string>>({
key: 'excludedTestItems', key: 'excludedTestItems',
scope: StorageScope.WORKSPACE, scope: StorageScope.WORKSPACE,
target: StorageTarget.USER, target: StorageTarget.USER,
...@@ -196,7 +196,7 @@ export class TestService extends Disposable implements ITestService { ...@@ -196,7 +196,7 @@ export class TestService extends Disposable implements ITestService {
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)); .map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri));
const result = this.testResults.push(LiveTestResult.from(subscriptions.map(s => s.object), req)); const result = this.testResults.createLiveResult(subscriptions.map(s => s.object), req);
try { try {
const tests = groupBy(req.tests, (a, b) => a.src.provider === b.src.provider ? 0 : 1); const tests = groupBy(req.tests, (a, b) => a.src.provider === b.src.provider ? 0 : 1);
......
...@@ -5,16 +5,23 @@ ...@@ -5,16 +5,23 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { timeout } from 'vs/base/common/async'; import { timeout } from 'vs/base/common/async';
import { bufferToStream, newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer';
import { Lazy } from 'vs/base/common/lazy';
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
import { NullLogService } from 'vs/platform/log/common/log'; import { NullLogService } from 'vs/platform/log/common/log';
import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { HydratedTestResult, LiveTestResult, makeEmptyCounts, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage'; import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { ReExportedTestRunState as TestRunState } from 'vs/workbench/contrib/testing/common/testStubs'; import { ReExportedTestRunState as TestRunState } from 'vs/workbench/contrib/testing/common/testStubs';
import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
export const emptyOutputController = () => new LiveOutputController(
new Lazy(() => [newWriteableBufferStream(), Promise.resolve()]),
() => Promise.resolve(bufferToStream(VSBuffer.alloc(0))),
);
suite('Workbench - Test Results Service', () => { suite('Workbench - Test Results Service', () => {
const getLabelsIn = (it: Iterable<InternalTestItem>) => [...it].map(t => t.item.label).sort(); const getLabelsIn = (it: Iterable<InternalTestItem>) => [...it].map(t => t.item.label).sort();
const getChangeSummary = () => [...changed] const getChangeSummary = () => [...changed]
...@@ -27,8 +34,10 @@ suite('Workbench - Test Results Service', () => { ...@@ -27,8 +34,10 @@ suite('Workbench - Test Results Service', () => {
setup(async () => { setup(async () => {
changed = new Set(); changed = new Set();
r = LiveTestResult.from( r = LiveTestResult.from(
'foo',
[await getInitializedMainTestCollection()], [await getInitializedMainTestCollection()],
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false } emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false },
); );
r.onChange(e => changed.add(e)); r.onChange(e => changed.add(e));
...@@ -36,7 +45,7 @@ suite('Workbench - Test Results Service', () => { ...@@ -36,7 +45,7 @@ suite('Workbench - Test Results Service', () => {
suite('LiveTestResult', () => { suite('LiveTestResult', () => {
test('is empty if no tests are requesteed', async () => { test('is empty if no tests are requesteed', async () => {
const r = LiveTestResult.from([await getInitializedMainTestCollection()], { tests: [], debug: false }); const r = LiveTestResult.from('', [await getInitializedMainTestCollection()], emptyOutputController(), { tests: [], debug: false });
assert.deepStrictEqual(getLabelsIn(r.tests), []); assert.deepStrictEqual(getLabelsIn(r.tests), []);
}); });
...@@ -181,7 +190,9 @@ suite('Workbench - Test Results Service', () => { ...@@ -181,7 +190,9 @@ suite('Workbench - Test Results Service', () => {
r.markComplete(); r.markComplete();
const r2 = results.push(LiveTestResult.from( const r2 = results.push(LiveTestResult.from(
'',
[await getInitializedMainTestCollection()], [await getInitializedMainTestCollection()],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false } { tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false }
)); ));
results.clear(); results.clear();
...@@ -192,7 +203,9 @@ suite('Workbench - Test Results Service', () => { ...@@ -192,7 +203,9 @@ suite('Workbench - Test Results Service', () => {
test('keeps ongoing tests on top', async () => { test('keeps ongoing tests on top', async () => {
results.push(r); results.push(r);
const r2 = results.push(LiveTestResult.from( const r2 = results.push(LiveTestResult.from(
'',
[await getInitializedMainTestCollection()], [await getInitializedMainTestCollection()],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false } { tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false }
)); ));
...@@ -213,7 +226,7 @@ suite('Workbench - Test Results Service', () => { ...@@ -213,7 +226,7 @@ suite('Workbench - Test Results Service', () => {
retired: undefined, retired: undefined,
children: [], children: [],
}] }]
}); }, () => Promise.resolve(bufferToStream(VSBuffer.alloc(0))));
test('pushes hydrated results', async () => { test('pushes hydrated results', async () => {
results.push(r); results.push(r);
......
...@@ -10,6 +10,7 @@ import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common ...@@ -10,6 +10,7 @@ import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common
import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testServiceImpl'; import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testServiceImpl';
import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection';
import { emptyOutputController } from 'vs/workbench/contrib/testing/test/common/testResultService.test';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
suite('Workbench - Test Result Storage', () => { suite('Workbench - Test Result Storage', () => {
...@@ -18,7 +19,9 @@ suite('Workbench - Test Result Storage', () => { ...@@ -18,7 +19,9 @@ suite('Workbench - Test Result Storage', () => {
const makeResult = (addMessage?: string) => { const makeResult = (addMessage?: string) => {
const t = LiveTestResult.from( const t = LiveTestResult.from(
'',
[collection], [collection],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false } { tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false }
); );
if (addMessage) { if (addMessage) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册