未验证 提交 1f3acad6 编写于 作者: C Connor Peet

testing: polish up test output terminal handling

Fixes https://github.com/microsoft/vscode/issues/119642
上级 8e183f0d
......@@ -625,6 +625,15 @@ export function toStream<T>(t: T, reducer: IReducer<T>): ReadableStream<T> {
return stream;
}
/**
* Helper
*/
export function emptyStream(): ReadableStream<never> {
const stream = newWriteableStream<never>(() => { throw new Error('not supported'); });
stream.end();
return stream;
}
/**
* Helper to convert a T into a Readable<T>.
*/
......
......@@ -2554,6 +2554,11 @@ declare module 'vscode' {
*/
completedAt: number;
/**
* Optional raw output from the test run.
*/
output?: string;
/**
* List of test results. The items in this array are the items that
* were passed in the {@link test.runTests} method.
......
......@@ -6,6 +6,7 @@
import { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { emptyStream } from 'vs/base/common/stream';
import { isDefined } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { Range } from 'vs/editor/common/core/range';
......@@ -72,8 +73,15 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
* @inheritdoc
*/
public $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void {
// todo
this.resultService.push(new HydratedTestResult(results, () => Promise.resolve(bufferToStream(VSBuffer.alloc(0))), persist));
this.resultService.push(new HydratedTestResult(
results,
() => Promise.resolve(
results.output
? bufferToStream(VSBuffer.fromString(results.output))
: emptyStream(),
),
persist,
));
}
/**
......
......@@ -1707,6 +1707,7 @@ export namespace TestResults {
const serialized: ISerializedTestResults = {
completedAt: results.completedAt,
id,
output: results.output,
items: [],
};
......
......@@ -440,7 +440,7 @@ export class ShowMostRecentOutputAction extends Action2 {
constructor() {
super({
id: 'testing.showMostRecentOutput',
title: localize('testing.showMostRecentOutput', "Collapse All Tests"),
title: localize('testing.showMostRecentOutput', "Show Most Recent Output"),
f1: false,
icon: Codicon.terminal,
menu: {
......
......@@ -3,14 +3,17 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DeferredPromise } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { listenStream } from 'vs/base/common/stream';
import { isDefined } from 'vs/base/common/types';
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';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
export interface ITestingOutputTerminalService {
......@@ -27,51 +30,77 @@ const friendlyDate = (date: number) => {
return d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0') + ':' + String(d.getSeconds()).padStart(2, '0');
};
const genericTitle = localize('testOutputTerminalTitle', 'Test Output');
const getTitle = (result: ITestResult | undefined) => {
return result
? localize('testOutputTerminalTitleWithDate', 'Test Output at {0}', friendlyDate(result.completedAt ?? Date.now()))
: genericTitle;
};
type TestOutputTerminalInstance = ITerminalInstance & { shellLaunchConfig: { customPtyImplementation: TestOutputProcess } };
const genericTitle = localize('testOutputTerminalTitle', 'Test Output');
export const ITestingOutputTerminalService = createDecorator<ITestingOutputTerminalService>('ITestingOutputTerminalService');
export class TestingOutputTerminalService implements ITestingOutputTerminalService {
_serviceBrand: undefined;
constructor(@ITerminalService private readonly terminalService: ITerminalService) { }
private outputTerminals = new WeakMap<ITerminalInstance, TestOutputProcess>();
constructor(
@ITerminalService private readonly terminalService: ITerminalService,
@ITestResultService resultService: ITestResultService,
) {
// If a result terminal is currently active and we start a new test run,
// stream live results there automatically.
resultService.onResultsChanged(evt => {
const active = this.terminalService.getActiveInstance();
if (!('started' in evt) || !active) {
return;
}
const output = this.outputTerminals.get(active);
if (output && output.ended) {
this.showResultsInTerminal(active, output, evt.started);
}
});
}
/**
* @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);
const testOutputPtys = this.terminalService.terminalInstances
.map(t => {
const output = this.outputTerminals.get(t);
return output ? [t, output] as const : undefined;
})
.filter(isDefined);
// If there's an existing terminal for the attempted reveal, show that instead.
const existing = testOutputPtys.find(i => i.shellLaunchConfig.customPtyImplementation.resultId === result?.id);
const existing = testOutputPtys.find(([, o]) => o.resultId === result?.id);
if (existing) {
this.terminalService.setActiveInstance(existing);
this.terminalService.setActiveInstance(existing[0]);
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;
const ended = testOutputPtys.find(([, o]) => o.ended);
if (ended) {
ended[1].clear();
this.showResultsInTerminal(ended[0], ended[1], result);
}
output.resetFor(result?.id, title);
const output = new TestOutputProcess();
this.showResultsInTerminal(this.terminalService.createTerminal({
isFeatureTerminal: true,
customPtyImplementation: () => output,
name: getTitle(result),
}), output, result);
}
private async showResultsInTerminal(terminal: ITerminalInstance, output: TestOutputProcess, result: ITestResult | undefined) {
this.outputTerminals.set(terminal, output);
output.resetFor(result?.id, getTitle(result));
this.terminalService.setActiveInstance(terminal);
this.terminalService.showPanel();
......@@ -82,13 +111,22 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi
return;
}
listenStream(await result.getOutput(), {
onData: d => output.pushData(d.toString()),
const [stream] = await Promise.all([result.getOutput(), output.started]);
let hadData = false;
listenStream(stream, {
onData: d => {
hadData = true;
output.pushData(d.toString());
},
onError: err => output.pushData(`\r\n\r\n${err.stack || err.message}`),
onEnd: () => {
if (!hadData) {
output.pushData(`\x1b[2m${localize('runNoOutout', 'The test run did not record any output.')}\x1b[0m`);
}
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.pushData(`\r\n\r\n\x1b[1m> ${text} <\x1b[0m\r\n\r\n`);
output.ended = true;
},
});
......@@ -98,20 +136,26 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi
class TestOutputProcess extends Disposable implements ITerminalChildProcess {
private processDataEmitter = this._register(new Emitter<string | IProcessDataEvent>());
private titleEmitter = this._register(new Emitter<string>());
private readonly startedDeferred = new DeferredPromise<void>();
/** Whether the associated test has ended (indicating the terminal can be reused) */
public ended = true;
/** Result currently being displayed */
public resultId: string | undefined;
/** Promise resolved when the terminal is ready to take data */
public readonly started = this.startedDeferred.p;
public pushData(data: string | IProcessDataEvent) {
this.processDataEmitter.fire(data);
}
public clear() {
this.processDataEmitter.fire('\x1bc');
}
public resetFor(resultId: string | undefined, title: string) {
this.ended = false;
this.resultId = resultId;
this.processDataEmitter.fire('\x1bc');
this.titleEmitter.fire(title);
}
......@@ -126,6 +170,7 @@ class TestOutputProcess extends Disposable implements ITerminalChildProcess {
public readonly onProcessShellTypeChanged = this._register(new Emitter<TerminalShellType>()).event;
public start(): Promise<ITerminalLaunchError | undefined> {
this.startedDeferred.complete();
return Promise.resolve(undefined);
}
public shutdown(): void {
......
......@@ -138,6 +138,8 @@ export interface ISerializedTestResults {
id: string;
/** Time the results were compelted */
completedAt: number;
/** Raw output, given for tests published by extensiosn */
output?: string;
/** Subset of test result items */
items: SerializedTestResultItem[];
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册