Skip to content

Commit

Permalink
Support TestMessageStackFrame API
Browse files Browse the repository at this point in the history
Also properly convert TestState back from TestStateChangeDTO.

fixes #14111

contributed on behalf of STMicroelectronics

Signed-off-by: Remi Schnekenburger <[email protected]>
  • Loading branch information
rschnekenbu committed Sep 11, 2024
1 parent 19556f4 commit 8bfe22d
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 21 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

<!-- ## Unreleased
- [test] support TestMessage stack traces [#14154](https://github.com/eclipse-theia/theia/pull/14154) - Contributed on behalf of STMicroelectronics
<a name="breaking_changes_1.54.0">[Breaking Changes:](#breaking_changes_1.54.0)</a> -->

## 1.53.0 - 08/29/2024
Expand Down
11 changes: 11 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ export interface Range {
readonly endColumn: number;
}

export interface Position {
/**
* line number (starts at 1)
*/
readonly lineNumber: number,
/**
* column (starts at 1)
*/
readonly column: number
}

export { MarkdownStringDTO as MarkdownString };

export interface SerializedDocumentFilter {
Expand Down
15 changes: 14 additions & 1 deletion packages/plugin-ext/src/common/test-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { UriComponents } from './uri-components';
import { Location, Range } from './plugin-api-rpc-model';
import { Location, Range, Position } from './plugin-api-rpc-model';
import { isObject } from '@theia/core';

export enum TestRunProfileKind {
Expand Down Expand Up @@ -74,17 +74,30 @@ export interface TestFailureDTO extends TestStateChangeDTO {
readonly duration?: number;
}

export namespace TestFailureDTO {
export function is(ref: unknown): ref is TestFailureDTO {
return isObject<TestFailureDTO>(ref)
&& ref.state === (TestExecutionState.Failed || TestExecutionState.Errored);
}
}
export interface TestSuccessDTO extends TestStateChangeDTO {
readonly state: TestExecutionState.Passed;
readonly duration?: number;
}

export interface TestMessageStackFrameDTO {
uri?: UriComponents;
position?: Position;
label: string;
}

export interface TestMessageDTO {
readonly expected?: string;
readonly actual?: string;
readonly location?: Location;
readonly message: string | MarkdownString;
readonly contextValue?: string;
readonly stackTrace?: TestMessageStackFrameDTO[];
}

export interface TestItemDTO {
Expand Down
68 changes: 62 additions & 6 deletions packages/plugin-ext/src/main/browser/test-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@

import { SimpleObservableCollection, TreeCollection, observableProperty } from '@theia/test/lib/common/collections';
import {
TestController, TestItem, TestOutputItem, TestRun, TestRunProfile, TestService, TestState, TestStateChangedEvent
TestController, TestItem, TestOutputItem, TestRun, TestRunProfile, TestService, TestState, TestStateChangedEvent, TestMessage,
TestFailure, TestMessageStackFrame
} from '@theia/test/lib/browser/test-service';
import { TestExecutionProgressService } from '@theia/test/lib/browser/test-execution-progress-service';
import { AccumulatingTreeDeltaEmitter, CollectionDelta, DeltaKind, TreeDelta, TreeDeltaBuilder } from '@theia/test/lib/common/tree-delta';
import { Emitter, Location, Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { Range as PluginRange, Location as PluginLocation } from '../../common/plugin-api-rpc-model';
import { Emitter, Location, Range, Position } from '@theia/core/shared/vscode-languageserver-protocol';
import { Range as PluginRange, Location as PluginLocation, Position as PluginPosition } from '../../common/plugin-api-rpc-model';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { CancellationToken, Disposable, Event, URI } from '@theia/core';
import { MAIN_RPC_CONTEXT, TestControllerUpdate, TestingExt, TestingMain } from '../../common';
import { RPCProtocol } from '../../common/rpc-protocol';
import { interfaces } from '@theia/core/shared/inversify';
import { TestExecutionState, TestItemDTO, TestItemReference, TestOutputDTO, TestRunDTO, TestRunProfileDTO, TestStateChangeDTO } from '../../common/test-types';
import {
TestExecutionState, TestFailureDTO, TestItemDTO, TestItemReference, TestMessageDTO, TestMessageStackFrameDTO, TestOutputDTO,
TestRunDTO, TestRunProfileDTO, TestStateChangeDTO
} from '../../common/test-types';
import { TestRunProfileKind } from '../../plugin/types-impl';
import { CommandRegistryMainImpl } from './command-registry-main';
import { UriComponents } from '../../common/uri-components';

export class TestItemCollection extends TreeCollection<string, TestItemImpl, TestItemImpl | TestControllerImpl> {
override add(item: TestItemImpl): TestItemImpl | undefined {
Expand Down Expand Up @@ -333,8 +338,10 @@ class TestRunImpl implements TestRun {
const item = this.controller.findItem(change.itemPath);
if (item) {
const oldState = this.testStates.get(item);
this.testStates.set(item, change);
stateEvents.push({ test: item, oldState: oldState, newState: change });
// convert back changes (for example, convert back location from DTO)
const convertedState = convertTestState(change);
this.testStates.set(item, convertedState);
stateEvents.push({ test: item, oldState: oldState, newState: convertedState});
}
});
const outputEvents: [TestItem | undefined, TestOutputItem][] = [];
Expand Down Expand Up @@ -368,6 +375,55 @@ class TestRunImpl implements TestRun {
}
}

function convertTestState(testStateDto: TestStateChangeDTO): TestState | TestFailure {
if (TestFailureDTO.is(testStateDto)) {
return {
state: testStateDto.state,
messages: testStateDto.messages.map(message => convertTestMessage(message)),
duration: testStateDto.duration
};
}
return {
state: testStateDto.state
};
}

function convertTestMessage(dto: TestMessageDTO): TestMessage {
return {
message: dto.message,
actual: dto.actual,
location: dto.location ? convertLocation(dto.location) : undefined,
contextValue: dto.contextValue,
expected: dto.expected,
stackTrace: dto.stackTrace && dto.stackTrace.map(stackFrame => convertStackFrame(stackFrame))
};
}

function convertStackFrame(dto: TestMessageStackFrameDTO): TestMessageStackFrame {
return {
label: dto.label,
position: convertPosition(dto.position),
uri: convertURI(dto.uri),
};
}

function convertPosition(position: PluginPosition | undefined): Position | undefined {
if (!position) {
return undefined;
}
return {
line: position.lineNumber - 1,
character: position.column - 1
};
}

function convertURI(uri: UriComponents | undefined): URI | undefined {
if (!uri) {
return undefined;
}
return URI.fromComponents(uri);
}

function convertLocation(location: PluginLocation | undefined): Location | undefined {
if (!location) {
return undefined;
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ import {
TestTag,
TestRunRequest,
TestMessage,
TestMessageStackFrame,
ExtensionKind,
InlineCompletionItem,
InlineCompletionList,
Expand Down Expand Up @@ -1460,6 +1461,7 @@ export function createAPIFactory(
TestTag,
TestRunRequest,
TestMessage,
TestMessageStackFrame,
ExtensionKind,
InlineCompletionItem,
InlineCompletionList,
Expand Down
16 changes: 13 additions & 3 deletions packages/plugin-ext/src/plugin/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ import { TestItemImpl, TestItemCollection } from './test-item';
import { AccumulatingTreeDeltaEmitter, TreeDelta } from '@theia/test/lib/common/tree-delta';
import {
TestItemDTO, TestOutputDTO, TestExecutionState, TestRunProfileDTO,
TestRunProfileKind, TestRunRequestDTO, TestStateChangeDTO, TestItemReference, TestMessageArg, TestMessageDTO
TestRunProfileKind, TestRunRequestDTO, TestStateChangeDTO, TestItemReference, TestMessageArg, TestMessageDTO,
TestMessageStackFrameDTO
} from '../common/test-types';
import { ChangeBatcher, observableProperty } from '@theia/test/lib/common/collections';
import { TestRunRequest } from './types-impl';
import { TestRunRequest, URI } from './types-impl';
import { MarkdownString } from '../common/plugin-api-rpc-model';

type RefreshHandler = (token: theia.CancellationToken) => void | theia.Thenable<void>;
Expand Down Expand Up @@ -374,7 +375,16 @@ export class TestingExtImpl implements TestingExt {
actualOutput: testMessage.actual,
expectedOutput: testMessage.expected,
contextValue: testMessage.contextValue,
location: testMessage.location ? Convert.toLocation(testMessage.location) : undefined
location: testMessage.location ? Convert.toLocation(testMessage.location) : undefined,
stackTrace: testMessage.stackTrace ? testMessage.stackTrace.map(frame => this.toStackFrame(frame)) : undefined
};
}

toStackFrame(stackFrame: TestMessageStackFrameDTO): theia.TestMessageStackFrame {
return {
label: stackFrame.label,
position: Convert.toPosition(stackFrame.position),
uri: stackFrame.uri ? URI.revive(stackFrame.uri) : undefined
};
}

Expand Down
30 changes: 25 additions & 5 deletions packages/plugin-ext/src/plugin/type-converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { CellRange, isTextStreamMime } from '@theia/notebook/lib/common';
import { MarkdownString as MarkdownStringDTO } from '@theia/core/lib/common/markdown-rendering';

import { TestItemDTO, TestMessageDTO } from '../common/test-types';
import { TestItemDTO, TestMessageDTO, TestMessageStackFrameDTO } from '../common/test-types';
import { PluginIconPath } from './plugin-icon-path';

const SIDE_GROUP = -2;
Expand Down Expand Up @@ -134,12 +134,21 @@ export function fromRange(range: theia.Range | undefined): model.Range | undefin
endColumn: end.character + 1
};
}

export function fromPosition(position: types.Position | theia.Position): Position {
export function fromPosition(position: types.Position | theia.Position): Position;
export function fromPosition(position: types.Position | theia.Position | undefined): Position | undefined;
export function fromPosition(position: types.Position | theia.Position | undefined): Position | undefined {
if (!position) {
return undefined;
}
return { lineNumber: position.line + 1, column: position.character + 1 };
}

export function toPosition(position: Position): types.Position {
export function toPosition(position: Position): types.Position;
export function toPosition(position: Position | undefined): types.Position | undefined;
export function toPosition(position: Position | undefined): types.Position | undefined {
if (!position) {
return undefined;
}
return new types.Position(position.lineNumber - 1, position.column - 1);
}

Expand Down Expand Up @@ -1681,11 +1690,22 @@ export namespace TestMessage {
message: fromMarkdown(message.message)!,
expected: message.expectedOutput,
actual: message.actualOutput,
contextValue: message.contextValue
contextValue: message.contextValue,
stackTrace: message.stackTrace && message.stackTrace.map(frame => TestMessageStackFrame.from(frame))
}];
}
}

export namespace TestMessageStackFrame {
export function from(stackTrace: theia.TestMessageStackFrame): TestMessageStackFrameDTO {
return {
label: stackTrace.label,
position: stackTrace.position && fromPosition(stackTrace.position),
uri: stackTrace?.uri
};
}
}

export namespace TestItem {
export function from(test: theia.TestItem): TestItemDTO {
return <TestItemDTO>TestItem.fromPartial(test);
Expand Down
11 changes: 10 additions & 1 deletion packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3036,7 +3036,7 @@ export class DebugThread implements theia.DebugThread {
}

export class DebugStackFrame implements theia.DebugStackFrame {
constructor(readonly session: theia.DebugSession, readonly threadId: number, readonly frameId: number) { }
constructor(readonly session: theia.DebugSession, readonly threadId: number, readonly frameId: number) { }
}

@es5ClassCompat
Expand Down Expand Up @@ -3350,6 +3350,7 @@ export class TestMessage implements theia.TestMessage {
public actualOutput?: string;
public location?: theia.Location;
public contextValue?: string;
public stackTrace?: theia.TestMessageStackFrame[] | undefined;

public static diff(message: string | theia.MarkdownString, expected: string, actual: string): theia.TestMessage {
const msg = new TestMessage(message);
Expand All @@ -3366,6 +3367,14 @@ export class TestCoverageCount {
constructor(public covered: number, public total: number) { }
}

export class TestMessageStackFrame implements theia.TestMessageStackFrame {
constructor(
public label: string,
public uri?: theia.Uri,
public position?: Position
) { }
}

@es5ClassCompat
export class FileCoverage {

Expand Down
33 changes: 33 additions & 0 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16573,6 +16573,34 @@ export module '@theia/plugin' {
error: string | MarkdownString | undefined;
}

/**
* A stack frame found in the {@link TestMessage.stackTrace}.
*/
export class TestMessageStackFrame {
/**
* The location of this stack frame. This should be provided as a URI if the
* location of the call frame can be accessed by the editor.
*/
uri?: Uri;

/**
* Position of the stack frame within the file.
*/
position?: Position;

/**
* The name of the stack frame, typically a method or function name.
*/
label: string;

/**
* @param label The name of the stack frame
* @param file The file URI of the stack frame
* @param position The position of the stack frame within the file
*/
constructor(label: string, uri?: Uri, position?: Position);
}

/**
* Message associated with the test state. Can be linked to a specific
* source range -- useful for assertion failures, for example.
Expand Down Expand Up @@ -16629,6 +16657,11 @@ export module '@theia/plugin' {
*/
contextValue?: string;

/**
* The stack trace associated with the message or failure.
*/
stackTrace?: TestMessageStackFrame[];

/**
* Creates a new TestMessage that will present as a diff in the editor.
* @param message Message to display to the user.
Expand Down
13 changes: 10 additions & 3 deletions packages/test/src/browser/test-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// *****************************************************************************

import { CancellationToken, ContributionProvider, Disposable, Emitter, Event, QuickPickService, isObject, nls } from '@theia/core/lib/common';
import { CancellationTokenSource, Location, Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { CancellationTokenSource, Location, Range, Position } from '@theia/core/shared/vscode-languageserver-protocol';
import { CollectionDelta, TreeDelta } from '../common/tree-delta';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import URI from '@theia/core/lib/common/uri';
Expand Down Expand Up @@ -56,9 +56,16 @@ export enum TestExecutionState {
export interface TestMessage {
readonly expected?: string;
readonly actual?: string;
readonly location: Location;
readonly location?: Location;
readonly message: string | MarkdownString;
readonly contextValue?: string;
readonly stackTrace?: TestMessageStackFrame[];
}

export interface TestMessageStackFrame {
readonly label: string,
readonly uri?: URI,
readonly position?: Position,
}

export namespace TestMessage {
Expand Down Expand Up @@ -367,7 +374,7 @@ export class DefaultTestService implements TestService {

selectDefaultProfile(): void {
this.pickProfileKind().then(kind => {
const profiles = this.getControllers().flatMap(c => c.testRunProfiles).filter(profile => profile.kind === kind);
const profiles = this.getControllers().flatMap(c => c.testRunProfiles).filter(profile => profile.kind === kind);
this.pickProfile(profiles, nls.localizeByDefault('Pick a test profile to use')).then(activeProfile => {
if (activeProfile) {
// only change the default for the controller containing selected profile for default and its profiles with same kind
Expand Down
Loading

0 comments on commit 8bfe22d

Please sign in to comment.