testing: initial output correlation

This commit is contained in:
Connor Peet 2021-08-11 13:01:33 -07:00
parent cb52f1a50e
commit fa8ccff4d8
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
17 changed files with 177 additions and 121 deletions

View file

@ -1788,6 +1788,22 @@ declare module 'vscode' {
}
//#endregion
//#region non-error test output https://github.com/microsoft/vscode/issues/129201
interface TestRun {
/**
* 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 location Indicate that the output was logged at the given
* location.
* @param test Test item to associate the output with.
*/
appendOutput(output: string, location?: Location, test?: TestItem): void;
}
//#endregion
//#region test tags https://github.com/microsoft/vscode/issues/129456
/**
* Tags can be associated with {@link TestItem | TestItems} and

View file

@ -17,7 +17,7 @@ import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { IMainThreadTestController, ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ExtHostContext, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
import { ExtHostContext, ExtHostTestingShape, IExtHostContext, ILocationDto, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
const reviveDiff = (diff: TestsDiff) => {
for (const entry of diff) {
@ -163,8 +163,13 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
/**
* @inheritdoc
*/
public $appendOutputToRun(runId: string, _taskId: string, output: VSBuffer): void {
this.withLiveRun(runId, r => r.output.append(output));
public $appendOutputToRun(runId: string, taskId: string, output: VSBuffer, locationDto?: ILocationDto, testId?: string): void {
const location = locationDto && {
uri: URI.revive(locationDto.uri),
range: Range.lift(locationDto.range)
};
this.withLiveRun(runId, r => r.appendOutput(output, taskId, location, testId));
}

View file

@ -1277,7 +1277,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
TestTag: extHostTypes.TestTag,
TestRunProfileKind: extHostTypes.TestRunProfileKind,
TextSearchCompleteMessageType: TextSearchCompleteMessageType,
TestMessageSeverity: extHostTypes.TestMessageSeverity,
CoveredCount: extHostTypes.CoveredCount,
FileCoverage: extHostTypes.FileCoverage,
StatementCoverage: extHostTypes.StatementCoverage,

View file

@ -2156,7 +2156,7 @@ export interface MainThreadTestingShape {
/** Appends a message to a test in the run. */
$appendTestMessagesInRun(runId: string, taskId: string, testId: string, messages: ITestMessage[]): void;
/** Appends raw output to the test run.. */
$appendOutputToRun(runId: string, taskId: string, output: VSBuffer): void;
$appendOutputToRun(runId: string, taskId: string, output: VSBuffer, location?: ILocationDto, testId?: string): void;
/** Triggered when coverage is added to test results. */
$signalCoverageAvailable(runId: string, taskId: string): void;
/** Signals a task in a test run started. */

View file

@ -398,10 +398,26 @@ class TestRunTracker extends Disposable {
this.proxy.$updateTestStateInRun(runId, taskId, TestId.fromExtHostTestItem(test, this.dto.controllerId).toString(), TestResultState.Passed, duration);
}),
//#endregion
appendOutput: output => {
if (!ended) {
this.proxy.$appendOutputToRun(runId, taskId, VSBuffer.fromString(output));
appendOutput: (output, location?: vscode.Location, test?: vscode.TestItem) => {
if (ended) {
return;
}
if (test) {
if (this.dto.isIncluded(test)) {
this.ensureTestIsKnown(test);
} else {
test = undefined;
}
}
this.proxy.$appendOutputToRun(
runId,
taskId,
VSBuffer.fromString(output),
location && Convert.location.from(location),
test && TestId.fromExtHostTestItem(test, ctrlId).toString(),
);
},
end: () => {
if (ended) {

View file

@ -31,7 +31,7 @@ import { SaveReason } from 'vs/workbench/common/editor';
import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
import * as search from 'vs/workbench/contrib/search/common/search';
import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestItem, ITestItemContext, ITestMessage, ITestTag, ITestTagDisplayInfo, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection';
import { CoverageDetails, DetailType, ICoveredCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestItemContext, ITestTag, ITestTagDisplayInfo, SerializedTestResultItem, TestMessageType } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestId } from 'vs/workbench/contrib/testing/common/testId';
import { EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
@ -1639,20 +1639,20 @@ export namespace NotebookRendererScript {
}
export namespace TestMessage {
export function from(message: vscode.TestMessage): ITestMessage {
export function from(message: vscode.TestMessage): ITestErrorMessage {
return {
message: MarkdownString.fromStrict(message.message) || '',
severity: types.TestMessageSeverity.Error,
expectedOutput: message.expectedOutput,
actualOutput: message.actualOutput,
type: TestMessageType.Error,
expected: message.expectedOutput,
actual: message.actualOutput,
location: message.location ? location.from(message.location) as any : undefined,
};
}
export function to(item: ITestMessage): vscode.TestMessage {
export function to(item: ITestErrorMessage): vscode.TestMessage {
const message = new types.TestMessage(typeof item.message === 'string' ? item.message : MarkdownString.to(item.message));
message.actualOutput = item.actualOutput;
message.expectedOutput = item.expectedOutput;
message.actualOutput = item.actual;
message.expectedOutput = item.expected;
return message;
}
}
@ -1750,7 +1750,9 @@ export namespace TestResults {
taskStates: item.tasks.map(t => ({
state: t.state as number as types.TestResultState,
duration: t.duration,
messages: t.messages.map(TestMessage.to),
messages: t.messages
.filter((m): m is ITestErrorMessage => m.type === TestMessageType.Error)
.map(TestMessage.to),
})),
children: item.children
.map(c => byInternalId.get(c))

View file

@ -3302,13 +3302,6 @@ export enum TestResultState {
Errored = 6
}
export enum TestMessageSeverity {
Error = 0,
Warning = 1,
Information = 2,
Hint = 3
}
export enum TestRunProfileKind {
Run = 1,
Debug = 2,

View file

@ -7,8 +7,7 @@ import { Codicon } from 'vs/base/common/codicons';
import { localize } from 'vs/nls';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { TestMessageSeverity } from 'vs/workbench/api/common/extHostTypes';
import { testingColorRunAction, testMessageSeverityColors, testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme';
import { testingColorRunAction, testStatesToIconColors } from 'vs/workbench/contrib/testing/browser/theme';
import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection';
export const testingViewIcon = registerIcon('test-view-icon', Codicon.beaker, localize('testViewIcon', 'View icon of the test view.'));
@ -37,13 +36,6 @@ export const testingStatesToIcons = new Map<TestResultState, ThemeIcon>([
[TestResultState.Unset, registerIcon('testing-unset-icon', Codicon.circleOutline, localize('testingUnsetIcon', 'Icon shown for tests that are in an unset state.'))],
]);
export const testMessageSeverityToIcons = new Map<TestMessageSeverity, ThemeIcon>([
[TestMessageSeverity.Error, registerIcon('testing-error-message-icon', Codicon.error, localize('testingErrorIcon', 'Icon shown for tests that have an error.'))],
[TestMessageSeverity.Warning, registerIcon('testing-warning-message-icon', Codicon.warning, localize('testingErrorIcon', 'Icon shown for tests that have an error.'))],
[TestMessageSeverity.Information, registerIcon('testing-info-message-icon', Codicon.info, localize('testingErrorIcon', 'Icon shown for tests that have an error.'))],
[TestMessageSeverity.Hint, registerIcon('testing-hint-message-icon', Codicon.question, localize('testingErrorIcon', 'Icon shown for tests that have an error.'))],
]);
registerThemingParticipant((theme, collector) => {
for (const [state, icon] of testingStatesToIcons.entries()) {
const color = testStatesToIconColors[state];
@ -55,16 +47,6 @@ registerThemingParticipant((theme, collector) => {
}`);
}
for (const [state, { decorationForeground }] of Object.entries(testMessageSeverityColors)) {
const icon = testMessageSeverityToIcons.get(Number(state));
if (!icon) {
continue;
}
collector.addRule(`.monaco-workbench ${ThemeIcon.asCSSSelector(icon)} {
color: ${theme.getColor(decorationForeground)} !important;
}`);
}
collector.addRule(`
.monaco-editor ${ThemeIcon.asCSSSelector(testingRunIcon)},
.monaco-editor ${ThemeIcon.asCSSSelector(testingRunAllIcon)} {

View file

@ -8,6 +8,7 @@ import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/action
import { Event } from 'vs/base/common/event';
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle';
import { removeAnsiEscapeCodes } from 'vs/base/common/strings';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
@ -16,7 +17,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IRange } from 'vs/editor/common/core/range';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model';
import { overviewRulerError, overviewRulerInfo, overviewRulerWarning } from 'vs/editor/common/view/editorColorRegistry';
import { overviewRulerError, overviewRulerInfo } from 'vs/editor/common/view/editorColorRegistry';
import { localize } from 'vs/nls';
import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
@ -26,7 +27,6 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IThemeService, themeColorFromId, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { TestMessageSeverity } from 'vs/workbench/api/common/extHostTypes';
import { BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution } from 'vs/workbench/contrib/debug/common/debug';
import { getTestItemContextOverlay } from 'vs/workbench/contrib/testing/browser/explorerProjections/testItemContextOverlay';
import { testingRunAllIcon, testingRunIcon, testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
@ -34,7 +34,7 @@ import { TestingOutputPeekController } from 'vs/workbench/contrib/testing/browse
import { testMessageSeverityColors } from 'vs/workbench/contrib/testing/browser/theme';
import { DefaultGutterClickAction, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { labelForTestInState } from 'vs/workbench/contrib/testing/common/constants';
import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { IncrementalTestCollectionItem, InternalTestItem, IRichLocation, ITestMessage, ITestRunProfile, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { isFailedState, maxPriority } from 'vs/workbench/contrib/testing/common/testingStates';
import { buildTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
@ -195,7 +195,7 @@ export class TestingDecorations extends Disposable implements IEditorContributio
for (let i = 0; i < state.messages.length; i++) {
const m = state.messages[i];
if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) {
const uri = buildTestUri({
const uri = m.type === TestMessageType.Info ? undefined : buildTestUri({
type: TestUriType.ResultActualOutput,
messageIndex: i,
taskIndex: taskId,
@ -582,13 +582,14 @@ class TestMessageDecoration implements ITestDecoration {
constructor(
public readonly testMessage: ITestMessage,
private readonly messageUri: URI,
private readonly messageUri: URI | undefined,
public readonly location: IRichLocation,
private readonly editor: ICodeEditor,
@ICodeEditorService private readonly editorService: ICodeEditorService,
@IThemeService themeService: IThemeService,
) {
const { severity = TestMessageSeverity.Error, message } = testMessage;
const severity = testMessage.type;
const message = typeof testMessage.message === 'string' ? removeAnsiEscapeCodes(testMessage.message) : testMessage.message;
const colorTheme = themeService.getColorTheme();
editorService.registerDecorationType('test-message-decoration', this.decorationId, {
after: {
@ -609,13 +610,9 @@ class TestMessageDecoration implements ITestDecoration {
options.stickiness = TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges;
options.collapseOnReplaceEdit = true;
const rulerColor = severity === TestMessageSeverity.Error
const rulerColor = severity === TestMessageType.Error
? overviewRulerError
: severity === TestMessageSeverity.Warning
? overviewRulerWarning
: severity === TestMessageSeverity.Information
? overviewRulerInfo
: undefined;
: overviewRulerInfo;
if (rulerColor) {
options.overviewRuler = { color: themeColorFromId(rulerColor), position: OverviewRulerLane.Right };
@ -629,6 +626,10 @@ class TestMessageDecoration implements ITestDecoration {
return false;
}
if (!this.messageUri) {
return false;
}
if (e.target.element?.className.includes(this.decorationId)) {
TestingOutputPeekController.get(this.editor).toggle(this.messageUri);
}

View file

@ -62,7 +62,7 @@ import { ITestingOutputTerminalService } from 'vs/workbench/contrib/testing/brow
import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme';
import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { Testing } from 'vs/workbench/contrib/testing/common/constants';
import { IRichLocation, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
import { ITestingPeekOpener } from 'vs/workbench/contrib/testing/common/testingPeekOpener';
@ -646,6 +646,10 @@ class TestingOutputPeek extends PeekViewWidget {
const message = dto.messages[dto.messageIndex];
const previous = this.current;
if (message.type !== TestMessageType.Error) {
return Promise.resolve();
}
if (!dto.revealLocation && !previous) {
return Promise.resolve();
}
@ -729,8 +733,8 @@ const diffEditorOptions: IDiffEditorOptions = {
modifiedAriaLabel: localize('testingOutputActual', 'Actual result'),
};
const isDiffable = (message: ITestMessage): message is ITestMessage & { actualOutput: string; expectedOutput: string } =>
message.actualOutput !== undefined && message.expectedOutput !== undefined;
const isDiffable = (message: ITestErrorMessage): message is ITestErrorMessage & { actualOutput: string; expectedOutput: string } =>
message.actual !== undefined && message.expected !== undefined;
class DiffContentProvider extends Disposable implements IPeekOutputRenderer {
private readonly widget = this._register(new MutableDisposable<EmbeddedDiffEditorWidget>());
@ -746,7 +750,7 @@ class DiffContentProvider extends Disposable implements IPeekOutputRenderer {
super();
}
public async update({ expectedUri, actualUri }: TestDto, message: ITestMessage) {
public async update({ expectedUri, actualUri }: TestDto, message: ITestErrorMessage) {
if (!isDiffable(message)) {
return this.clear();
}
@ -772,7 +776,7 @@ class DiffContentProvider extends Disposable implements IPeekOutputRenderer {
this.widget.value.setModel(model);
this.widget.value.updateOptions(this.getOptions(
isMultiline(message.expectedOutput) || isMultiline(message.actualOutput)
isMultiline(message.expected) || isMultiline(message.actual)
));
}
@ -831,7 +835,7 @@ class MarkdownTestMessagePeek extends Disposable implements IPeekOutputRenderer
super();
}
public update(_dto: TestDto, message: ITestMessage): void {
public update(_dto: TestDto, message: ITestErrorMessage): void {
if (isDiffable(message) || typeof message.message === 'string') {
return this.textPreview.clear();
}
@ -862,7 +866,7 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer {
super();
}
public async update({ messageUri }: TestDto, message: ITestMessage) {
public async update({ messageUri }: TestDto, message: ITestErrorMessage) {
if (isDiffable(message) || typeof message.message !== 'string') {
return this.clear();
}
@ -902,8 +906,8 @@ class PlainTextMessagePeek extends Disposable implements IPeekOutputRenderer {
}
}
const hintDiffPeekHeight = (message: ITestMessage) =>
Math.max(hintPeekStrHeight(message.actualOutput), hintPeekStrHeight(message.expectedOutput));
const hintDiffPeekHeight = (message: ITestErrorMessage) =>
Math.max(hintPeekStrHeight(message.actual), hintPeekStrHeight(message.expected));
const firstLine = (str: string) => {
const index = str.indexOf('\n');
@ -1039,7 +1043,6 @@ class TestMessageElement implements ITreeElement {
public readonly context: URI;
public readonly id: string;
public readonly label: string;
public readonly icon: ThemeIcon | undefined;
public readonly uri: URI;
public readonly location: IRichLocation | undefined;
@ -1049,7 +1052,7 @@ class TestMessageElement implements ITreeElement {
public readonly taskIndex: number,
public readonly messageIndex: number,
) {
const { message, severity, location } = test.tasks[taskIndex].messages[messageIndex];
const { message, location } = test.tasks[taskIndex].messages[messageIndex];
this.location = location;
this.uri = this.context = buildTestUri({
@ -1062,7 +1065,6 @@ class TestMessageElement implements ITreeElement {
this.id = this.uri.toString();
this.label = firstLine(renderStringAsPlaintext(message));
this.icon = icons.testMessageSeverityToIcons.get(severity);
}
}

View file

@ -5,11 +5,10 @@
import { Color, RGBA } from 'vs/base/common/color';
import { localize } from 'vs/nls';
import { editorErrorForeground, editorForeground, editorHintForeground, editorInfoForeground, editorWarningForeground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, registerColor } from 'vs/platform/theme/common/colorRegistry';
import { editorErrorForeground, editorForeground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, registerColor, transparent } from 'vs/platform/theme/common/colorRegistry';
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { TestMessageSeverity } from 'vs/workbench/api/common/extHostTypes';
import { ACTIVITY_BAR_BADGE_BACKGROUND } from 'vs/workbench/common/theme';
import { TestResultState } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestMessageType, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection';
export const testingColorIconFailed = registerColor('testing.iconFailed', {
dark: '#f14c4c',
@ -60,12 +59,12 @@ export const testingPeekBorder = registerColor('testing.peekBorder', {
}, localize('testing.peekBorder', 'Color of the peek view borders and arrow.'));
export const testMessageSeverityColors: {
[K in TestMessageSeverity]: {
[K in TestMessageType]: {
decorationForeground: string,
marginBackground: string,
};
} = {
[TestMessageSeverity.Error]: {
[TestMessageType.Error]: {
decorationForeground: registerColor(
'testing.message.error.decorationForeground',
{ dark: editorErrorForeground, light: editorErrorForeground, hc: editorForeground },
@ -77,42 +76,18 @@ export const testMessageSeverityColors: {
localize('testing.message.error.marginBackground', 'Margin color beside error messages shown inline in the editor.')
),
},
[TestMessageSeverity.Warning]: {
decorationForeground: registerColor(
'testing.message.warning.decorationForeground',
{ dark: editorWarningForeground, light: editorWarningForeground, hc: editorForeground },
localize('testing.message.warning.decorationForeground', 'Text color of test warning messages shown inline in the editor.')
),
marginBackground: registerColor(
'testing.message.warning.lineBackground',
{ dark: new Color(new RGBA(255, 208, 0, 0.2)), light: new Color(new RGBA(255, 208, 0, 0.2)), hc: null },
localize('testing.message.warning.marginBackground', 'Margin color beside warning messages shown inline in the editor.')
),
},
[TestMessageSeverity.Information]: {
[TestMessageType.Info]: {
decorationForeground: registerColor(
'testing.message.info.decorationForeground',
{ dark: editorInfoForeground, light: editorInfoForeground, hc: editorForeground },
{ dark: transparent(editorForeground, 0.5), light: transparent(editorForeground, 0.5), hc: transparent(editorForeground, 0.5) },
localize('testing.message.info.decorationForeground', 'Text color of test info messages shown inline in the editor.')
),
marginBackground: registerColor(
'testing.message.info.lineBackground',
{ dark: new Color(new RGBA(0, 127, 255, 0.2)), light: new Color(new RGBA(0, 127, 255, 0.2)), hc: null },
{ dark: null, light: null, hc: null },
localize('testing.message.info.marginBackground', 'Margin color beside info messages shown inline in the editor.')
),
},
[TestMessageSeverity.Hint]: {
decorationForeground: registerColor(
'testing.message.hint.decorationForeground',
{ dark: editorHintForeground, light: editorHintForeground, hc: editorForeground },
localize('testing.message.hint.decorationForeground', 'Text color of test hint messages shown inline in the editor.')
),
marginBackground: registerColor(
'testing.message.hint.lineBackground',
{ dark: null, light: null, hc: editorForeground },
localize('testing.message.hint.marginBackground', 'Margin color beside hint messages shown inline in the editor.')
),
},
};
export const testStatesToIconColors: { [K in TestResultState]?: string } = {

View file

@ -8,7 +8,6 @@ import { MarshalledId } from 'vs/base/common/marshalling';
import { URI } from 'vs/base/common/uri';
import { IPosition } from 'vs/editor/common/core/position';
import { IRange, Range } from 'vs/editor/common/core/range';
import { TestMessageSeverity } from 'vs/workbench/api/common/extHostTypes';
export const enum TestResultState {
Unset = 0,
@ -97,15 +96,28 @@ export interface IRichLocation {
uri: URI;
}
export interface ITestMessage {
export const enum TestMessageType {
Error,
Info
}
export interface ITestErrorMessage {
message: string | IMarkdownString;
/** @deprecated */
severity: TestMessageSeverity;
expectedOutput: string | undefined;
actualOutput: string | undefined;
type: TestMessageType.Error;
expected: string | undefined;
actual: string | undefined;
location: IRichLocation | undefined;
}
export interface ITestOutputMessage {
message: string;
type: TestMessageType.Info;
offset: number;
location: IRichLocation | undefined;
}
export type ITestMessage = ITestErrorMessage | ITestOutputMessage;
export interface ITestTaskState {
state: TestResultState;
duration: number | undefined;
@ -211,12 +223,10 @@ 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[];
/** Tasks involved in the run. */
tasks: ITestRunTask[];
tasks: { id: string; name: string | undefined; messages: ITestOutputMessage[] }[];
/** Human-readable name of the test run. */
name: string;
/** Test trigger informaton */

View file

@ -12,15 +12,20 @@ import { Range } from 'vs/editor/common/core/range';
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 { ISerializedTestResults, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestItemExpandState, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection';
import { IRichLocation, ISerializedTestResults, ITestItem, ITestMessage, ITestOutputMessage, ITestRunTask, ITestTaskState, ResolvedTestRunRequest, TestItemExpandState, TestMessageType, TestResultItem, TestResultState } from 'vs/workbench/contrib/testing/common/testCollection';
import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage';
import { maxPriority, statesInOrder } from 'vs/workbench/contrib/testing/common/testingStates';
export interface ITestRunTaskWithCoverage extends ITestRunTask {
export interface ITestRunTaskResults extends ITestRunTask {
/**
* Contains test coverage for the result, if it's available.
*/
readonly coverage: IObservableValue<TestCoverage | undefined>;
/**
* Messages from the task not associated with any specific test.
*/
readonly otherMessages: ITestOutputMessage[];
}
export interface ITestResult {
@ -58,7 +63,7 @@ export interface ITestResult {
/**
* List of this result's subtasks.
*/
tasks: ReadonlyArray<ITestRunTaskWithCoverage>;
tasks: ReadonlyArray<ITestRunTaskResults>;
/**
* Gets the state of the test by its extension-assigned ID.
@ -133,6 +138,14 @@ export class LiveOutputController {
private readonly dataEmitter = new Emitter<VSBuffer>();
private readonly endEmitter = new Emitter<void>();
private _offset = 0;
/**
* Gets the number of written bytes.
*/
public get offset() {
return this._offset;
}
constructor(
private readonly writer: Lazy<[VSBufferWriteableStream, Promise<void>]>,
@ -149,6 +162,7 @@ export class LiveOutputController {
this.previouslyWritten?.push(data);
this.dataEmitter.fire(data);
this._offset += data.byteLength;
return this.writer.getValue()[0].write(data);
}
@ -243,7 +257,7 @@ export class LiveTestResult implements ITestResult {
public readonly onChange = this.changeEmitter.event;
public readonly onComplete = this.completeEmitter.event;
public readonly tasks: ITestRunTaskWithCoverage[] = [];
public readonly tasks: ITestRunTaskResults[] = [];
public readonly name = localize('runFinished', 'Test run at {0}', new Date().toLocaleString());
/**
@ -301,12 +315,32 @@ export class LiveTestResult implements ITestResult {
return this.testById.get(extTestId);
}
/**
* Appends output that occurred during the test run.
*/
public appendOutput(output: VSBuffer, taskId: string, location?: IRichLocation, testId?: string): void {
this.output.append(output);
const message: ITestOutputMessage = {
location,
message: output.toString(),
offset: this.output.offset,
type: TestMessageType.Info,
};
const index = this.mustGetTaskIndex(taskId);
if (testId) {
this.testById.get(testId)?.tasks[index].messages.push(message);
} else {
this.tasks[index].otherMessages.push(message);
}
}
/**
* Adds a new run task to the results.
*/
public addTask(task: ITestRunTask) {
const index = this.tasks.length;
this.tasks.push({ ...task, coverage: new MutableObservableValue(undefined) });
this.tasks.push({ ...task, coverage: new MutableObservableValue(undefined), otherMessages: [] });
for (const test of this.tests) {
test.tasks.push({ duration: undefined, messages: [], state: TestResultState.Unset });
@ -504,7 +538,7 @@ export class LiveTestResult implements ITestResult {
private readonly doSerialize = new Lazy((): ISerializedTestResults => ({
id: this.id,
completedAt: this.completedAt!,
tasks: this.tasks,
tasks: this.tasks.map(t => ({ id: t.id, name: t.name, messages: t.otherMessages })),
name: this.name,
request: this.request,
items: [...this.testById.values()].map(entry => ({
@ -538,7 +572,7 @@ export class HydratedTestResult implements ITestResult {
/**
* @inheritdoc
*/
public readonly tasks: ITestRunTaskWithCoverage[];
public readonly tasks: ITestRunTaskResults[];
/**
* @inheritdoc
@ -566,7 +600,21 @@ export class HydratedTestResult implements ITestResult {
) {
this.id = serialized.id;
this.completedAt = serialized.completedAt;
this.tasks = serialized.tasks.map(task => ({ ...task, coverage: staticObservableValue(undefined) }));
this.tasks = serialized.tasks.map((task, i) => ({
id: task.id,
name: task.name,
running: false,
coverage: staticObservableValue(undefined),
otherMessages: task.messages.map(m => ({
message: m.message,
type: m.type,
offset: m.offset,
location: m.location && {
uri: URI.revive(m.location.uri),
range: Range.lift(m.location.range)
},
}))
}));
this.name = serialized.name;
this.request = serialized.request;

View file

@ -43,7 +43,12 @@ export interface ITestResultStorage {
export const ITestResultStorage = createDecorator('ITestResultStorage');
const currentRevision = 0;
/**
* Data revision this version of VS Code deals with. Should be bumped whenever
* a breaking change is made to the stored results, which will cause previous
* revisions to be discarded.
*/
const currentRevision = 1;
export abstract class BaseTestResultStorage implements ITestResultStorage {
declare readonly _serviceBrand: undefined;

View file

@ -274,7 +274,7 @@ suite('Workbench - Test Results Service', () => {
const makeHydrated = async (completedAt = 42, state = TestResultState.Passed) => new HydratedTestResult({
completedAt,
id: 'some-id',
tasks: [{ id: 't', running: false, name: undefined }],
tasks: [{ id: 't', messages: [], name: undefined }],
name: 'hello world',
request: defaultOpts([]),
items: [{

View file

@ -36,10 +36,10 @@ suite('Workbench - Test Result Storage', () => {
if (addMessage) {
t.appendMessage(new TestId(['ctrlId', 'id-a']).toString(), 't', {
message: addMessage,
actualOutput: undefined,
expectedOutput: undefined,
actual: undefined,
expected: undefined,
location: undefined,
severity: 0,
type: 0,
});
}
t.markComplete();

View file

@ -7,6 +7,7 @@ const { constants } = require('mocha/lib/runner');
const BaseRunner = require('mocha/lib/reporters/base');
const {
EVENT_TEST_BEGIN,
EVENT_TEST_PASS,
EVENT_TEST_FAIL,
EVENT_RUN_BEGIN,
@ -28,6 +29,7 @@ module.exports = class FullJsonStreamReporter extends BaseRunner {
runner.once(EVENT_RUN_BEGIN, () => writeEvent(['start', { total }]));
runner.once(EVENT_RUN_END, () => writeEvent(['end', this.stats]));
runner.on(EVENT_TEST_BEGIN, test => writeEvent(['testStart', clean(test)]));
runner.on(EVENT_TEST_PASS, test => writeEvent(['pass', clean(test)]));
runner.on(EVENT_TEST_FAIL, (test, err) => {
test = clean(test);