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

wip

上级 6229e7a5
......@@ -243,6 +243,12 @@ export interface IShellLaunchConfig {
*/
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.
*/
......
......@@ -2323,6 +2323,9 @@ declare module 'vscode' {
* Appends raw output from the test runner. On the user's request, the
* output will be displayed in a terminal. ANSI escape sequences,
* 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;
}
......
......@@ -3,6 +3,7 @@
* 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 { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { isDefined } from 'vs/base/common/types';
......@@ -71,7 +72,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
* @inheritdoc
*/
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
}
}
/**
* @inheritdoc
*/
public $appendOutputToRun(runId: string, output: VSBuffer): void {
const r = this.resultService.getResult(runId);
if (r && r instanceof LiveTestResult) {
r.output.append(output);
}
}
/**
* @inheritdoc
*/
......
......@@ -1977,6 +1977,7 @@ export interface MainThreadTestingShape {
$publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void;
$updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void;
$appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void;
$appendOutputToRun(runId: string, output: VSBuffer): void;
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<string>;
$publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void;
}
......
......@@ -5,6 +5,7 @@
import { mapFind } from 'vs/base/common/arrays';
import { disposableTimeout } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
......@@ -274,8 +275,8 @@ export class ExtHostTesting implements ExtHostTestingShape {
try {
await provider.runTests({
appendOutput() {
// todo
appendOutput: message => {
this.proxy.$appendOutputToRun(req.runId, VSBuffer.fromString(message));
},
appendMessage: (test, message) => {
if (!isExcluded(test)) {
......
......@@ -42,7 +42,7 @@ const LATENCY_MEASURING_INTERVAL = 1000;
enum ProcessType {
Process,
ExtensionTerminal
PsuedoTerminal
}
/**
......@@ -191,9 +191,9 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
let newProcess: ITerminalChildProcess;
if (shellLaunchConfig.isExtensionCustomPtyTerminal) {
this._processType = ProcessType.ExtensionTerminal;
newProcess = this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._instanceId, shellLaunchConfig, cols, rows);
if (shellLaunchConfig.isExtensionCustomPtyTerminal || shellLaunchConfig.customPtyImplementation) {
this._processType = ProcessType.PsuedoTerminal;
newProcess = shellLaunchConfig.customPtyImplementation || this._instantiationService.createInstance(TerminalProcessExtHostProxy, this._instanceId, shellLaunchConfig, cols, rows);
} else {
const forceExtHostProcess = (this._configHelper.config as any).extHostProcess;
if (shellLaunchConfig.cwd && typeof shellLaunchConfig.cwd === 'object') {
......@@ -488,7 +488,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
await this.ptyProcessReady;
this._dataFilter.triggerSwap();
this._hasWrittenData = true;
if (this.shellProcessId || this._processType === ProcessType.ExtensionTerminal) {
if (this.shellProcessId || this._processType === ProcessType.PsuedoTerminal) {
if (this._process) {
// Send data if the pty is ready
this._process.input(data);
......
......@@ -31,6 +31,7 @@ import * as icons from 'vs/workbench/contrib/testing/browser/icons';
import { ITestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { TestingExplorerView, TestingExplorerViewModel } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
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 { InternalTestItem, ITestItem, TestIdPath, TestIdWithSrc, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestingAutoRun } from 'vs/workbench/contrib/testing/common/testingAutoRun';
......@@ -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> {
constructor() {
super({
......
......@@ -20,6 +20,7 @@ import { TestingDecorations } from 'vs/workbench/contrib/testing/browser/testing
import { ITestExplorerFilterState, TestExplorerFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
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 { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { testingConfiguation } from 'vs/workbench/contrib/testing/common/configuration';
......@@ -41,6 +42,7 @@ registerSingleton(ITestResultStorage, TestResultStorage);
registerSingleton(ITestResultService, TestResultService);
registerSingleton(ITestExplorerFilterState, TestExplorerFilterState);
registerSingleton(ITestingAutoRun, TestingAutoRun, true);
registerSingleton(ITestingOutputTerminalService, TestingOutputTerminalService, true);
registerSingleton(ITestingPeekOpener, TestingPeekOpener);
registerSingleton(ITestingProgressUiService, TestingProgressUiService);
registerSingleton(IWorkspaceTestCollectionService, WorkspaceTestCollectionService);
......@@ -86,30 +88,31 @@ viewsRegistry.registerViews([{
when: ContextKeyExpr.greater(TestingContextKeys.providerCount.key, 0),
}], viewContainer);
registerAction2(Action.TestingViewAsListAction);
registerAction2(Action.TestingViewAsTreeAction);
registerAction2(Action.AutoRunOffAction);
registerAction2(Action.AutoRunOnAction);
registerAction2(Action.CancelTestRunAction);
registerAction2(Action.RunSelectedAction);
registerAction2(Action.DebugSelectedAction);
registerAction2(Action.TestingSortByNameAction);
registerAction2(Action.TestingSortByLocationAction);
registerAction2(Action.RefreshTestsAction);
registerAction2(Action.ClearTestResultsAction);
registerAction2(Action.CollapseAllAction);
registerAction2(Action.RunAllAction);
registerAction2(Action.DebugAllAction);
registerAction2(Action.EditFocusedTest);
registerAction2(Action.ClearTestResultsAction);
registerAction2(Action.AutoRunOffAction);
registerAction2(Action.AutoRunOnAction);
registerAction2(Action.DebugAtCursor);
registerAction2(Action.RunAtCursor);
registerAction2(Action.DebugCurrentFile);
registerAction2(Action.RunCurrentFile);
registerAction2(Action.ReRunFailedTests);
registerAction2(Action.DebugFailedTests);
registerAction2(Action.ReRunLastRun);
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.ShowMostRecentOutputAction);
registerAction2(Action.TestingSortByLocationAction);
registerAction2(Action.TestingSortByNameAction);
registerAction2(Action.TestingViewAsListAction);
registerAction2(Action.TestingViewAsTreeAction);
registerAction2(CloseTestPeek);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored);
......
......@@ -25,7 +25,7 @@ import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'
import { ViewContainerLocation } from 'vs/workbench/common/views';
import { testingFilterIcon } from 'vs/workbench/contrib/testing/browser/icons';
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 { TestIdPath } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
......@@ -33,17 +33,17 @@ import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
export interface ITestExplorerFilterState {
_serviceBrand: undefined;
readonly text: ObservableValue<string>;
readonly text: MutableObservableValue<string>;
/**
* 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
* items are its parents.
*/
readonly reveal: ObservableValue<TestIdPath | undefined>;
readonly stateFilter: ObservableValue<TestExplorerStateFilter>;
readonly currentDocumentOnly: ObservableValue<boolean>;
readonly reveal: MutableObservableValue<TestIdPath | undefined>;
readonly stateFilter: MutableObservableValue<TestExplorerStateFilter>;
readonly currentDocumentOnly: MutableObservableValue<boolean>;
/** Whether excluded test should be shown in the view */
readonly showExcludedTests: ObservableValue<boolean>;
readonly showExcludedTests: MutableObservableValue<boolean>;
readonly onDidRequestInputFocus: Event<void>;
focusInput(): void;
......@@ -54,20 +54,20 @@ export const ITestExplorerFilterState = createDecorator<ITestExplorerFilterState
export class TestExplorerFilterState implements ITestExplorerFilterState {
declare _serviceBrand: undefined;
private readonly focusEmitter = new Emitter<void>();
public readonly text = new ObservableValue('');
public readonly stateFilter = ObservableValue.stored(new StoredValue<TestExplorerStateFilter>({
public readonly text = new MutableObservableValue('');
public readonly stateFilter = MutableObservableValue.stored(new StoredValue<TestExplorerStateFilter>({
key: 'testStateFilter',
scope: StorageScope.WORKSPACE,
target: StorageTarget.USER
}, this.storage), TestExplorerStateFilter.All);
public readonly currentDocumentOnly = ObservableValue.stored(new StoredValue<boolean>({
public readonly currentDocumentOnly = MutableObservableValue.stored(new StoredValue<boolean>({
key: 'testsByCurrentDocumentOnly',
scope: StorageScope.WORKSPACE,
target: StorageTarget.USER
}, this.storage), false);
public readonly showExcludedTests = new ObservableValue(false);
public readonly reveal = new ObservableValue<TestIdPath | undefined>(undefined);
public readonly showExcludedTests = new MutableObservableValue(false);
public readonly reveal = new MutableObservableValue<TestIdPath | undefined>(undefined);
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 @@
* 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';
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>();
public readonly onDidChange = this.changeEmitter.event;
......@@ -23,7 +33,7 @@ export class ObservableValue<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));
return o;
}
......
......@@ -3,10 +3,11 @@
* 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 { Lazy } from 'vs/base/common/lazy';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { Range } from 'vs/editor/common/core/range';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
......@@ -46,6 +47,11 @@ export interface ITestResult {
*/
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
* in the workspace.
......@@ -78,6 +84,88 @@ export const sumCounts = (counts: Iterable<TestStateCount>) => {
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 = (
item: IncrementalTestCollectionItem,
......@@ -172,7 +260,9 @@ export class LiveTestResult implements ITestResult {
* of collections.
*/
public static from(
resultId: string,
collections: ReadonlyArray<IMainThreadTestCollection>,
output: LiveOutputController,
req: RunTestsRequest,
) {
const testByExtId = new Map<string, TestResultItem>();
......@@ -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>();
......@@ -199,11 +289,6 @@ export class LiveTestResult implements ITestResult {
public readonly onChange = this.changeEmitter.event;
public readonly onComplete = this.completeEmitter.event;
/**
* Unique ID for referring to this set of test results.
*/
public readonly id = generateUuid();
/**
* @inheritdoc
*/
......@@ -255,9 +340,11 @@ export class LiveTestResult implements ITestResult {
};
constructor(
public readonly id: string,
private readonly collections: ReadonlyArray<IMainThreadTestCollection>,
private readonly testById: Map<string, TestResultItem>,
private readonly excluded: ReadonlySet<string>,
public readonly output: LiveOutputController,
public readonly isAutoRun: boolean,
) {
this.counts[TestResultState.Unset] = testById.size;
......@@ -315,6 +402,13 @@ export class LiveTestResult implements ITestResult {
});
}
/**
* @inheritdoc
*/
public getOutput() {
return this.output.read();
}
private fireUpdateAndRefresh(entry: TestResultItem, newState: TestResultState) {
const previous = entry.state.state;
if (newState === previous) {
......@@ -445,7 +539,11 @@ export class HydratedTestResult implements ITestResult {
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.completedAt = serialized.completedAt;
......@@ -472,6 +570,13 @@ export class HydratedTestResult implements ITestResult {
return this.testById.get(extTestId);
}
/**
* @inheritdoc
*/
public getOutput() {
return this.outputLoader();
}
/**
* @inheritdoc
*/
......
......@@ -9,13 +9,15 @@ import { Emitter, Event } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
import { Iterable } from 'vs/base/common/iterator';
import { equals } from 'vs/base/common/objects';
import { generateUuid } from 'vs/base/common/uuid';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { TestResultState } from 'vs/workbench/api/common/extHostTypes';
import { 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 { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService';
export type ResultChangeEvent =
| { completed: LiveTestResult }
......@@ -45,6 +47,11 @@ export interface ITestResultService {
*/
clear(): void;
/**
* Creates a new, live test result.
*/
createLiveResult(collections: ReadonlyArray<IMainThreadTestCollection>, req: RunTestsRequest): LiveTestResult;
/**
* Adds a new test result to the collection.
*/
......@@ -125,6 +132,14 @@ export class TestResultService implements ITestResultService {
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
*/
......
......@@ -3,7 +3,8 @@
* 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 { URI } from 'vs/base/common/uri';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
......@@ -14,11 +15,12 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
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;
const RETAIN_MIN_RESULTS = 16;
const RETAIN_MAX_BYTES = 1024 * 128;
const CLEANUP_PROBABILITY = 0.2;
export interface ITestResultStorage {
_serviceBrand: undefined;
......@@ -32,6 +34,11 @@ export interface ITestResultStorage {
* Persists the list of test results.
*/
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');
......@@ -39,7 +46,7 @@ export const ITestResultStorage = createDecorator('ITestResultStorage');
export abstract class BaseTestResultStorage implements ITestResultStorage {
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',
scope: StorageScope.WORKSPACE,
target: StorageTarget.MACHINE
......@@ -62,7 +69,7 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
return undefined;
}
return new HydratedTestResult(contents);
return new HydratedTestResult(contents, () => this.readOutputForResultId(id));
} catch (e) {
this.logService.warn(`Error deserializing stored test result ${id}`, e);
return undefined;
......@@ -72,6 +79,29 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
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
*/
......@@ -108,6 +138,10 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
todo.push(this.storeForResultId(result.id, obj));
toStore.push({ id: result.id, bytes: contents.byteLength });
budget -= contents.byteLength;
if (result instanceof LiveTestResult && result.completedAt !== undefined) {
todo.push(result.output.close());
}
}
for (const id of toDelete.keys()) {
......@@ -123,6 +157,11 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
*/
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.
*/
......@@ -132,6 +171,11 @@ export abstract class BaseTestResultStorage implements ITestResultStorage {
* Stores test results by ID.
*/
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 {
......@@ -150,6 +194,14 @@ export class InMemoryResultStorage extends BaseTestResultStorage {
this.cache.delete(id);
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 {
......@@ -179,7 +231,53 @@ export class TestResultStorage extends BaseTestResultStorage {
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) {
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
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol';
import { 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 { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
......@@ -157,7 +157,7 @@ export interface ITestService {
/**
* 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.
......
......@@ -14,7 +14,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
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 { AbstractIncrementalTestCollection, getTestSubscriptionKey, IncrementalTestCollectionItem, InternalTestItem, RunTestsRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
......@@ -48,7 +48,7 @@ export class TestService extends Disposable implements ITestService {
private readonly runningTests = new Map<RunTestsRequest, CancellationTokenSource>();
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',
scope: StorageScope.WORKSPACE,
target: StorageTarget.USER,
......@@ -196,7 +196,7 @@ export class TestService extends Disposable implements ITestService {
const subscriptions = [...this.testSubscriptions.values()]
.filter(v => req.tests.some(t => v.collection.getNodeById(t.testId)))
.map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri));
const result = this.testResults.push(LiveTestResult.from(subscriptions.map(s => s.object), req));
const result = this.testResults.createLiveResult(subscriptions.map(s => s.object), req);
try {
const tests = groupBy(req.tests, (a, b) => a.src.provider === b.src.provider ? 0 : 1);
......
......@@ -5,16 +5,23 @@
import * as assert from 'assert';
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 { NullLogService } from 'vs/platform/log/common/log';
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 { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage';
import { ReExportedTestRunState as TestRunState } from 'vs/workbench/contrib/testing/common/testStubs';
import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
export const emptyOutputController = () => new LiveOutputController(
new Lazy(() => [newWriteableBufferStream(), Promise.resolve()]),
() => Promise.resolve(bufferToStream(VSBuffer.alloc(0))),
);
suite('Workbench - Test Results Service', () => {
const getLabelsIn = (it: Iterable<InternalTestItem>) => [...it].map(t => t.item.label).sort();
const getChangeSummary = () => [...changed]
......@@ -27,8 +34,10 @@ suite('Workbench - Test Results Service', () => {
setup(async () => {
changed = new Set();
r = LiveTestResult.from(
'foo',
[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));
......@@ -36,7 +45,7 @@ suite('Workbench - Test Results Service', () => {
suite('LiveTestResult', () => {
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), []);
});
......@@ -181,7 +190,9 @@ suite('Workbench - Test Results Service', () => {
r.markComplete();
const r2 = results.push(LiveTestResult.from(
'',
[await getInitializedMainTestCollection()],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false }
));
results.clear();
......@@ -192,7 +203,9 @@ suite('Workbench - Test Results Service', () => {
test('keeps ongoing tests on top', async () => {
results.push(r);
const r2 = results.push(LiveTestResult.from(
'',
[await getInitializedMainTestCollection()],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false }
));
......@@ -213,7 +226,7 @@ suite('Workbench - Test Results Service', () => {
retired: undefined,
children: [],
}]
});
}, () => Promise.resolve(bufferToStream(VSBuffer.alloc(0))));
test('pushes hydrated results', async () => {
results.push(r);
......
......@@ -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 { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testServiceImpl';
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';
suite('Workbench - Test Result Storage', () => {
......@@ -18,7 +19,9 @@ suite('Workbench - Test Result Storage', () => {
const makeResult = (addMessage?: string) => {
const t = LiveTestResult.from(
'',
[collection],
emptyOutputController(),
{ tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false }
);
if (addMessage) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册