testing: add support for jumping to test messages in terminal output (#162406)

This commit is contained in:
Connor Peet 2022-09-30 16:05:17 -07:00 committed by GitHub
parent 9fb452c485
commit 64c7d125c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 62 additions and 11 deletions

View file

@ -1166,6 +1166,7 @@ class TestMessageElement implements ITreeElement {
public readonly uri: URI;
public readonly location?: IRichLocation;
public readonly description?: string;
public readonly marker?: number;
constructor(
public readonly result: ITestResult,
@ -1173,9 +1174,10 @@ class TestMessageElement implements ITreeElement {
public readonly taskIndex: number,
public readonly messageIndex: number,
) {
const { message, location } = test.tasks[taskIndex].messages[messageIndex];
const m = test.tasks[taskIndex].messages[messageIndex];
this.location = location;
this.location = m.location;
this.marker = m.type === TestMessageType.Output ? m.marker : undefined;
this.uri = this.context = buildTestUri({
type: TestUriType.ResultMessage,
messageIndex,
@ -1186,7 +1188,7 @@ class TestMessageElement implements ITreeElement {
this.id = this.uri.toString();
const asPlaintext = renderStringAsPlaintext(message);
const asPlaintext = renderStringAsPlaintext(m.message);
const lines = count(asPlaintext.trimRight(), '\n');
this.label = firstLine(asPlaintext);
if (lines > 0) {
@ -1597,6 +1599,18 @@ class TreeActionsProvider {
}
}
if (element instanceof TestMessageElement) {
if (element.marker !== undefined) {
primary.push(new Action(
'testing.outputPeek.showMessageInTerminal',
localize('testing.showMessageInTerminal', "Show Output in Terminal"),
Codicon.terminal.classNames,
undefined,
() => this.testTerminalService.open(element.result, element.marker),
));
}
}
const result = { primary, secondary };
createAndFillInActionBarActions(menu, {
shouldForwardArgs: true,

View file

@ -17,15 +17,17 @@ import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'
import { testingViewIcon } from 'vs/workbench/contrib/testing/browser/icons';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { getMarkId } from 'vs/workbench/contrib/testing/common/testTypes';
export interface ITestingOutputTerminalService {
_serviceBrand: undefined;
/**
* Opens a terminal for the given test's output.
* Opens a terminal for the given test's output. Optionally, scrolls to and
* selects the given marker in the test results.
*/
open(result: ITestResult): Promise<void>;
open(result: ITestResult, marker?: number): Promise<void>;
}
const friendlyDate = (date: number) => {
@ -78,7 +80,7 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi
/**
* @inheritdoc
*/
public async open(result: ITestResult | undefined): Promise<void> {
public async open(result: ITestResult | undefined, marker?: number): Promise<void> {
const testOutputPtys = this.terminalService.instances
.map(t => {
const output = this.outputTerminals.get(t);
@ -95,6 +97,8 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi
} else {
this.terminalGroupService.showPanel();
}
this.revealMarker(existing[0], marker);
return;
}
@ -114,10 +118,10 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi
customPtyImplementation: () => output,
name: getTitle(result),
},
}), output, result);
}), output, result, marker);
}
private async showResultsInTerminal(terminal: ITerminalInstance, output: TestOutputProcess, result: ITestResult | undefined) {
private async showResultsInTerminal(terminal: ITerminalInstance, output: TestOutputProcess, result: ITestResult | undefined, thenSelectMarker?: number) {
this.outputTerminals.set(terminal, output);
output.resetFor(result?.id, getTitle(result));
this.terminalService.setActiveInstance(terminal);
@ -151,9 +155,16 @@ export class TestingOutputTerminalService implements ITestingOutputTerminalServi
const text = localize('runFinished', 'Test run finished at {0}', completedAt.toLocaleString());
output.pushData(`\r\n\r\n\x1b[1m> ${text} <\x1b[0m\r\n\r\n`);
output.ended = true;
this.revealMarker(terminal, thenSelectMarker);
},
});
}
private revealMarker(terminal: ITerminalInstance, marker?: number) {
if (marker !== undefined) {
terminal.scrollToMark(getMarkId(marker, true), getMarkId(marker, false), true);
}
}
}
class TestOutputProcess extends Disposable implements ITerminalChildProcess {

View file

@ -11,7 +11,7 @@ import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { IComputedStateAccessor, refreshComputedState } from 'vs/workbench/contrib/testing/common/getComputedState';
import { IObservableValue, MutableObservableValue, staticObservableValue } from 'vs/workbench/contrib/testing/common/observableValue';
import { IRichLocation, ISerializedTestResults, ITestItem, ITestMessage, ITestOutputMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestItemExpandState, TestMessageType, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes';
import { getMarkId, IRichLocation, ISerializedTestResults, ITestItem, ITestMessage, ITestOutputMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestItemExpandState, TestMessageType, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testTypes';
import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
import { maxPriority, statesInOrder, terminalStatePriorities } from 'vs/workbench/contrib/testing/common/testingStates';
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
@ -130,6 +130,7 @@ export const maxCountPriority = (counts: Readonly<TestStateCount>) => {
return TestResultState.Unset;
};
const getMarkCode = (marker: number, start: boolean) => `\x1b]633;SetMark;Id=${getMarkId(marker, start)};Hidden\x07`;
/**
* Deals with output of a {@link LiveTestResult}. By default we pass-through
@ -162,11 +163,19 @@ export class LiveOutputController {
/**
* Appends data to the output.
*/
public append(data: VSBuffer): Promise<void> | void {
public append(data: VSBuffer, marker?: number): Promise<void> | void {
if (this.closed) {
return this.closed;
}
if (marker !== undefined) {
data = VSBuffer.concat([
VSBuffer.fromString(getMarkCode(marker, true)),
data,
VSBuffer.fromString(getMarkCode(marker, false)),
]);
}
this.previouslyWritten?.push(data);
this.dataEmitter.fire(data);
this._offset += data.byteLength;
@ -285,6 +294,7 @@ export class LiveTestResult implements ITestResult {
private readonly completeEmitter = new Emitter<void>();
private readonly changeEmitter = new Emitter<TestResultItemChange>();
private readonly testById = new Map<string, TestResultItemWithChildren>();
private testMarkerCounter = 0;
private _completedAt?: number;
public readonly onChange = this.changeEmitter.event;
@ -352,15 +362,24 @@ export class LiveTestResult implements ITestResult {
*/
public appendOutput(output: VSBuffer, taskId: string, location?: IRichLocation, testId?: string): void {
const preview = output.byteLength > 100 ? output.slice(0, 100).toString() + '…' : output.toString();
let marker: number | undefined;
// currently, the UI only exposes jump-to-message from tests or locations,
// so no need to mark outputs that don't come from either of those.
if (testId || location) {
marker = this.testMarkerCounter++;
}
const message: ITestOutputMessage = {
location,
message: removeAnsiEscapeCodes(preview),
offset: this.output.offset,
length: output.byteLength,
marker: marker,
type: TestMessageType.Output,
};
this.output.append(output);
this.output.append(output, marker);
const index = this.mustGetTaskIndex(taskId);
if (testId) {

View file

@ -163,9 +163,16 @@ export interface ITestOutputMessage {
type: TestMessageType.Output;
offset: number;
length: number;
marker?: number;
location: IRichLocation | undefined;
}
/**
* Gets the TTY marker ID for either starting or ending
* an ITestOutputMessage.marker of the given ID.
*/
export const getMarkId = (marker: number, start: boolean) => `${start ? 's' : 'e'}${marker}`;
export namespace ITestOutputMessage {
export interface Serialized {
message: string;