testing: polish up test output terminal handling
Fixes https://github.com/microsoft/vscode/issues/119642
This commit is contained in:
parent
8e183f0d2e
commit
1f3acad632
7 changed files with 100 additions and 30 deletions
|
@ -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>.
|
||||
*/
|
||||
|
|
5
src/vs/vscode.proposed.d.ts
vendored
5
src/vs/vscode.proposed.d.ts
vendored
|
@ -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[];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue