@theia/test 1.53.0-next.55 → 1.53.0-next.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/src/browser/constants.ts +71 -71
- package/src/browser/style/index.css +41 -41
- package/src/browser/test-execution-progress-service.ts +53 -53
- package/src/browser/test-preferences.ts +58 -58
- package/src/browser/test-service.ts +402 -402
- package/src/browser/view/test-context-key-service.ts +36 -36
- package/src/browser/view/test-execution-state-manager.ts +147 -147
- package/src/browser/view/test-output-ui-model.ts +156 -156
- package/src/browser/view/test-output-view-contribution.ts +34 -34
- package/src/browser/view/test-output-widget.ts +148 -148
- package/src/browser/view/test-result-view-contribution.ts +34 -34
- package/src/browser/view/test-result-widget.ts +92 -92
- package/src/browser/view/test-run-view-contribution.ts +89 -89
- package/src/browser/view/test-run-widget.tsx +271 -271
- package/src/browser/view/test-tree-widget.tsx +360 -360
- package/src/browser/view/test-view-contribution.ts +328 -328
- package/src/browser/view/test-view-frontend-module.ts +136 -136
- package/src/common/collections.ts +223 -223
- package/src/common/tree-delta.spec.ts +166 -166
- package/src/common/tree-delta.ts +259 -259
|
@@ -1,402 +1,402 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2022 STMicroelectronics and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
import { CancellationToken, ContributionProvider, Disposable, Emitter, Event, QuickPickService, isObject, nls } from '@theia/core/lib/common';
|
|
18
|
-
import { CancellationTokenSource, Location, Range } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
19
|
-
import { CollectionDelta, TreeDelta } from '../common/tree-delta';
|
|
20
|
-
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
|
|
21
|
-
import URI from '@theia/core/lib/common/uri';
|
|
22
|
-
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
|
|
23
|
-
import { groupBy } from '../common/collections';
|
|
24
|
-
import { codiconArray } from '@theia/core/lib/browser';
|
|
25
|
-
|
|
26
|
-
export enum TestRunProfileKind {
|
|
27
|
-
Run = 1,
|
|
28
|
-
Debug = 2,
|
|
29
|
-
Coverage = 3
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface TestRunProfile {
|
|
33
|
-
readonly kind: TestRunProfileKind;
|
|
34
|
-
readonly label: string,
|
|
35
|
-
isDefault: boolean;
|
|
36
|
-
readonly canConfigure: boolean;
|
|
37
|
-
readonly tag: string;
|
|
38
|
-
run(name: string, included: readonly TestItem[], excluded: readonly TestItem[], preserveFocus: boolean): void;
|
|
39
|
-
configure(): void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface TestOutputItem {
|
|
43
|
-
readonly output: string;
|
|
44
|
-
readonly location?: Location;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export enum TestExecutionState {
|
|
48
|
-
Queued = 1,
|
|
49
|
-
Running = 2,
|
|
50
|
-
Passed = 3,
|
|
51
|
-
Failed = 4,
|
|
52
|
-
Skipped = 5,
|
|
53
|
-
Errored = 6
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface TestMessage {
|
|
57
|
-
readonly expected?: string;
|
|
58
|
-
readonly actual?: string;
|
|
59
|
-
readonly location: Location;
|
|
60
|
-
readonly message: string | MarkdownString;
|
|
61
|
-
readonly contextValue?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export namespace TestMessage {
|
|
65
|
-
export function is(obj: unknown): obj is TestMessage {
|
|
66
|
-
return isObject<TestMessage>(obj) && (MarkdownString.is(obj.message) || typeof obj.message === 'string');
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface TestState {
|
|
71
|
-
readonly state: TestExecutionState;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface TestFailure extends TestState {
|
|
75
|
-
readonly state: TestExecutionState.Failed | TestExecutionState.Errored;
|
|
76
|
-
readonly messages: TestMessage[];
|
|
77
|
-
readonly duration?: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export namespace TestFailure {
|
|
81
|
-
export function is(obj: unknown): obj is TestFailure {
|
|
82
|
-
return isObject<TestFailure>(obj) && (obj.state === TestExecutionState.Failed || obj.state === TestExecutionState.Errored) && Array.isArray(obj.messages);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface TestSuccess extends TestState {
|
|
87
|
-
readonly state: TestExecutionState.Passed;
|
|
88
|
-
readonly duration?: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface TestStateChangedEvent {
|
|
92
|
-
test: TestItem;
|
|
93
|
-
oldState: TestState | undefined;
|
|
94
|
-
newState: TestState | undefined;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface TestRun {
|
|
98
|
-
cancel(): void;
|
|
99
|
-
readonly id: string;
|
|
100
|
-
readonly name: string;
|
|
101
|
-
readonly isRunning: boolean;
|
|
102
|
-
readonly controller: TestController;
|
|
103
|
-
|
|
104
|
-
onDidChangeProperty: Event<{ name?: string, isRunning?: boolean }>;
|
|
105
|
-
|
|
106
|
-
getTestState(item: TestItem): TestState | undefined;
|
|
107
|
-
onDidChangeTestState: Event<TestStateChangedEvent[]>;
|
|
108
|
-
|
|
109
|
-
getOutput(item?: TestItem): readonly TestOutputItem[];
|
|
110
|
-
onDidChangeTestOutput: Event<[TestItem | undefined, TestOutputItem][]>;
|
|
111
|
-
|
|
112
|
-
readonly items: readonly TestItem[];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export namespace TestRun {
|
|
116
|
-
export function is(obj: unknown): obj is TestRun {
|
|
117
|
-
return isObject<TestRun>(obj)
|
|
118
|
-
&& typeof obj.cancel === 'function'
|
|
119
|
-
&& typeof obj.name === 'string'
|
|
120
|
-
&& typeof obj.isRunning === 'boolean'
|
|
121
|
-
&& typeof obj.controller === 'object'
|
|
122
|
-
&& typeof obj.onDidChangeProperty === 'function'
|
|
123
|
-
&& typeof obj.getTestState === 'function'
|
|
124
|
-
&& typeof obj.onDidChangeTestState === 'function'
|
|
125
|
-
&& typeof obj.onDidChangeTestState === 'function'
|
|
126
|
-
&& typeof obj.getOutput === 'function'
|
|
127
|
-
&& typeof obj.onDidChangeTestOutput === 'function'
|
|
128
|
-
&& Array.isArray(obj.items);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export interface TestItem {
|
|
133
|
-
readonly id: string;
|
|
134
|
-
readonly label: string;
|
|
135
|
-
readonly range?: Range;
|
|
136
|
-
readonly sortKey?: string;
|
|
137
|
-
readonly tags: string[];
|
|
138
|
-
readonly uri?: URI;
|
|
139
|
-
readonly busy: boolean;
|
|
140
|
-
readonly tests: readonly TestItem[];
|
|
141
|
-
readonly description?: string;
|
|
142
|
-
readonly error?: string | MarkdownString;
|
|
143
|
-
readonly parent: TestItem | undefined;
|
|
144
|
-
readonly controller: TestController | undefined;
|
|
145
|
-
readonly canResolveChildren: boolean;
|
|
146
|
-
resolveChildren(): void;
|
|
147
|
-
readonly path: string[];
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export namespace TestItem {
|
|
151
|
-
export function is(obj: unknown): obj is TestItem {
|
|
152
|
-
return isObject<TestItem>(obj)
|
|
153
|
-
&& obj.id !== undefined
|
|
154
|
-
&& obj.label !== undefined
|
|
155
|
-
&& Array.isArray(obj.tags)
|
|
156
|
-
&& Array.isArray(obj.tests)
|
|
157
|
-
&& obj.busy !== undefined
|
|
158
|
-
&& obj.canResolveChildren !== undefined
|
|
159
|
-
&& typeof obj.resolveChildren === 'function';
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface TestController {
|
|
164
|
-
readonly id: string;
|
|
165
|
-
readonly label: string;
|
|
166
|
-
readonly tests: readonly TestItem[];
|
|
167
|
-
readonly testRunProfiles: readonly TestRunProfile[];
|
|
168
|
-
readonly testRuns: readonly TestRun[];
|
|
169
|
-
|
|
170
|
-
readonly onItemsChanged: Event<TreeDelta<string, TestItem>[]>;
|
|
171
|
-
readonly onRunsChanged: Event<CollectionDelta<TestRun, TestRun>>;
|
|
172
|
-
readonly onProfilesChanged: Event<CollectionDelta<TestRunProfile, TestRunProfile>>;
|
|
173
|
-
|
|
174
|
-
refreshTests(token: CancellationToken): Promise<void>;
|
|
175
|
-
clearRuns(): void;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export interface TestService {
|
|
179
|
-
clearResults(): void;
|
|
180
|
-
configureProfile(): void;
|
|
181
|
-
selectDefaultProfile(): void;
|
|
182
|
-
runTestsWithProfile(tests: TestItem[]): void;
|
|
183
|
-
runTests(profileKind: TestRunProfileKind, tests: TestItem[]): void;
|
|
184
|
-
runAllTests(profileKind: TestRunProfileKind): void;
|
|
185
|
-
getControllers(): TestController[];
|
|
186
|
-
registerTestController(controller: TestController): Disposable;
|
|
187
|
-
onControllersChanged: Event<CollectionDelta<string, TestController>>;
|
|
188
|
-
|
|
189
|
-
refresh(): void;
|
|
190
|
-
cancelRefresh(): void;
|
|
191
|
-
isRefreshing: boolean;
|
|
192
|
-
onDidChangeIsRefreshing: Event<void>;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export namespace TestServices {
|
|
196
|
-
export function withTestRun(service: TestService, controllerId: string, runId: string): TestRun {
|
|
197
|
-
const controller = service.getControllers().find(c => c.id === controllerId);
|
|
198
|
-
if (!controller) {
|
|
199
|
-
throw new Error(`No test controller with id '${controllerId}' found`);
|
|
200
|
-
}
|
|
201
|
-
const run = controller.testRuns.find(r => r.id === runId);
|
|
202
|
-
if (!run) {
|
|
203
|
-
throw new Error(`No test run with id '${runId}' found`);
|
|
204
|
-
}
|
|
205
|
-
return run;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export const TestContribution = Symbol('TestContribution');
|
|
210
|
-
|
|
211
|
-
export interface TestContribution {
|
|
212
|
-
registerTestControllers(service: TestService): void;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export const TestService = Symbol('TestService');
|
|
216
|
-
|
|
217
|
-
@injectable()
|
|
218
|
-
export class DefaultTestService implements TestService {
|
|
219
|
-
@inject(QuickPickService) quickpickService: QuickPickService;
|
|
220
|
-
|
|
221
|
-
private testRunCounter = 0;
|
|
222
|
-
|
|
223
|
-
private onDidChangeIsRefreshingEmitter = new Emitter<void>();
|
|
224
|
-
onDidChangeIsRefreshing: Event<void> = this.onDidChangeIsRefreshingEmitter.event;
|
|
225
|
-
|
|
226
|
-
private controllers: Map<string, TestController> = new Map();
|
|
227
|
-
private refreshing: Set<CancellationTokenSource> = new Set();
|
|
228
|
-
private onControllersChangedEmitter = new Emitter<CollectionDelta<string, TestController>>();
|
|
229
|
-
|
|
230
|
-
@inject(ContributionProvider) @named(TestContribution)
|
|
231
|
-
protected readonly contributionProvider: ContributionProvider<TestContribution>;
|
|
232
|
-
|
|
233
|
-
@postConstruct()
|
|
234
|
-
protected registerContributions(): void {
|
|
235
|
-
this.contributionProvider.getContributions().forEach(contribution => contribution.registerTestControllers(this));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
onControllersChanged: Event<CollectionDelta<string, TestController>> = this.onControllersChangedEmitter.event;
|
|
239
|
-
|
|
240
|
-
registerTestController(controller: TestController): Disposable {
|
|
241
|
-
if (this.controllers.has(controller.id)) {
|
|
242
|
-
throw new Error('TestController already registered: ' + controller.id);
|
|
243
|
-
}
|
|
244
|
-
this.controllers.set(controller.id, controller);
|
|
245
|
-
this.onControllersChangedEmitter.fire({ added: [controller] });
|
|
246
|
-
return Disposable.create(() => {
|
|
247
|
-
this.controllers.delete(controller.id);
|
|
248
|
-
this.onControllersChangedEmitter.fire({ removed: [controller.id] });
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
getControllers(): TestController[] {
|
|
253
|
-
return Array.from(this.controllers.values());
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
refresh(): void {
|
|
257
|
-
const cts = new CancellationTokenSource();
|
|
258
|
-
this.refreshing.add(cts);
|
|
259
|
-
|
|
260
|
-
Promise.all(this.getControllers().map(controller => controller.refreshTests(cts.token))).then(() => {
|
|
261
|
-
this.refreshing.delete(cts);
|
|
262
|
-
if (this.refreshing.size === 0) {
|
|
263
|
-
this.onDidChangeIsRefreshingEmitter.fire();
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
if (this.refreshing.size === 1) {
|
|
268
|
-
this.onDidChangeIsRefreshingEmitter.fire();
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
cancelRefresh(): void {
|
|
273
|
-
if (this.refreshing.size > 0) {
|
|
274
|
-
this.refreshing.forEach(cts => cts.cancel());
|
|
275
|
-
this.refreshing.clear();
|
|
276
|
-
this.onDidChangeIsRefreshingEmitter.fire();
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
get isRefreshing(): boolean {
|
|
281
|
-
return this.refreshing.size > 0;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
runAllTests(profileKind: TestRunProfileKind): void {
|
|
285
|
-
this.getControllers().forEach(controller => {
|
|
286
|
-
this.runTestForController(controller, profileKind, controller.tests);
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
protected async runTestForController(controller: TestController, profileKind: TestRunProfileKind, items: readonly TestItem[]): Promise<void> {
|
|
291
|
-
const runProfiles = controller.testRunProfiles.filter(profile => profile.kind === profileKind);
|
|
292
|
-
let activeProfile;
|
|
293
|
-
if (runProfiles.length === 1) {
|
|
294
|
-
activeProfile = runProfiles[0];
|
|
295
|
-
} else if (runProfiles.length > 1) {
|
|
296
|
-
const defaultProfile = runProfiles.find(p => p.isDefault);
|
|
297
|
-
if (defaultProfile) {
|
|
298
|
-
activeProfile = defaultProfile;
|
|
299
|
-
} else {
|
|
300
|
-
|
|
301
|
-
activeProfile = await this.pickProfile(runProfiles, nls.localizeByDefault('Pick a test profile to use'));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
if (activeProfile) {
|
|
305
|
-
activeProfile.run(`Test run #${this.testRunCounter++}`, items, [], true);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
protected async pickProfile(runProfiles: readonly TestRunProfile[], title: string): Promise<TestRunProfile | undefined> {
|
|
310
|
-
if (runProfiles.length === 0) {
|
|
311
|
-
return undefined;
|
|
312
|
-
}
|
|
313
|
-
// eslint-disable-next-line arrow-body-style
|
|
314
|
-
const picks = runProfiles.map(profile => {
|
|
315
|
-
let iconClasses;
|
|
316
|
-
if (profile.kind === TestRunProfileKind.Run) {
|
|
317
|
-
iconClasses = codiconArray('run');
|
|
318
|
-
} else if (profile.kind === TestRunProfileKind.Debug) {
|
|
319
|
-
iconClasses = codiconArray('debug-alt');
|
|
320
|
-
}
|
|
321
|
-
return {
|
|
322
|
-
iconClasses,
|
|
323
|
-
label: `${profile.label}${profile.isDefault ? ' (default)' : ''}`,
|
|
324
|
-
profile: profile
|
|
325
|
-
};
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
return (await this.quickpickService.show(picks, { title: title }))?.profile;
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
protected async pickProfileKind(): Promise<TestRunProfileKind | undefined> {
|
|
333
|
-
// eslint-disable-next-line arrow-body-style
|
|
334
|
-
const picks = [{
|
|
335
|
-
iconClasses: codiconArray('run'),
|
|
336
|
-
label: 'Run',
|
|
337
|
-
kind: TestRunProfileKind.Run
|
|
338
|
-
}, {
|
|
339
|
-
iconClasses: codiconArray('debug-alt'),
|
|
340
|
-
label: 'Debug',
|
|
341
|
-
kind: TestRunProfileKind.Debug
|
|
342
|
-
}];
|
|
343
|
-
|
|
344
|
-
return (await this.quickpickService.show(picks, { title: 'Select the kind of profiles' }))?.kind;
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
runTests(profileKind: TestRunProfileKind, items: TestItem[]): void {
|
|
349
|
-
groupBy(items, item => item.controller).forEach((tests, controller) => {
|
|
350
|
-
if (controller) {
|
|
351
|
-
this.runTestForController(controller, profileKind, tests);
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
runTestsWithProfile(items: TestItem[]): void {
|
|
357
|
-
groupBy(items, item => item.controller).forEach((tests, controller) => {
|
|
358
|
-
if (controller) {
|
|
359
|
-
this.pickProfile(controller.testRunProfiles, nls.localizeByDefault('Pick a test profile to use')).then(activeProfile => {
|
|
360
|
-
if (activeProfile) {
|
|
361
|
-
activeProfile.run(`Test run #${this.testRunCounter++}`, items, [], true);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
selectDefaultProfile(): void {
|
|
369
|
-
this.pickProfileKind().then(kind => {
|
|
370
|
-
const profiles = this.getControllers().flatMap(c => c.testRunProfiles).filter(profile => profile.kind === kind);
|
|
371
|
-
this.pickProfile(profiles, nls.localizeByDefault('Pick a test profile to use')).then(activeProfile => {
|
|
372
|
-
if (activeProfile) {
|
|
373
|
-
// only change the default for the controller containing selected profile for default and its profiles with same kind
|
|
374
|
-
const controller = this.getControllers().find(c => c.testRunProfiles.includes(activeProfile));
|
|
375
|
-
controller?.testRunProfiles.filter(profile => profile.kind === activeProfile.kind).forEach(profile => {
|
|
376
|
-
profile.isDefault = profile === activeProfile;
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
configureProfile(): void {
|
|
384
|
-
const profiles: TestRunProfile[] = [];
|
|
385
|
-
|
|
386
|
-
for (const controller of this.controllers.values()) {
|
|
387
|
-
profiles.push(...controller.testRunProfiles);
|
|
388
|
-
}
|
|
389
|
-
;
|
|
390
|
-
this.pickProfile(profiles.filter(profile => profile.canConfigure), nls.localizeByDefault('Select a profile to update')).then(profile => {
|
|
391
|
-
if (profile) {
|
|
392
|
-
profile.configure();
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
clearResults(): void {
|
|
398
|
-
for (const controller of this.controllers.values()) {
|
|
399
|
-
controller.clearRuns();
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2022 STMicroelectronics and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { CancellationToken, ContributionProvider, Disposable, Emitter, Event, QuickPickService, isObject, nls } from '@theia/core/lib/common';
|
|
18
|
+
import { CancellationTokenSource, Location, Range } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
19
|
+
import { CollectionDelta, TreeDelta } from '../common/tree-delta';
|
|
20
|
+
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
|
|
21
|
+
import URI from '@theia/core/lib/common/uri';
|
|
22
|
+
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
|
|
23
|
+
import { groupBy } from '../common/collections';
|
|
24
|
+
import { codiconArray } from '@theia/core/lib/browser';
|
|
25
|
+
|
|
26
|
+
export enum TestRunProfileKind {
|
|
27
|
+
Run = 1,
|
|
28
|
+
Debug = 2,
|
|
29
|
+
Coverage = 3
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TestRunProfile {
|
|
33
|
+
readonly kind: TestRunProfileKind;
|
|
34
|
+
readonly label: string,
|
|
35
|
+
isDefault: boolean;
|
|
36
|
+
readonly canConfigure: boolean;
|
|
37
|
+
readonly tag: string;
|
|
38
|
+
run(name: string, included: readonly TestItem[], excluded: readonly TestItem[], preserveFocus: boolean): void;
|
|
39
|
+
configure(): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TestOutputItem {
|
|
43
|
+
readonly output: string;
|
|
44
|
+
readonly location?: Location;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export enum TestExecutionState {
|
|
48
|
+
Queued = 1,
|
|
49
|
+
Running = 2,
|
|
50
|
+
Passed = 3,
|
|
51
|
+
Failed = 4,
|
|
52
|
+
Skipped = 5,
|
|
53
|
+
Errored = 6
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TestMessage {
|
|
57
|
+
readonly expected?: string;
|
|
58
|
+
readonly actual?: string;
|
|
59
|
+
readonly location: Location;
|
|
60
|
+
readonly message: string | MarkdownString;
|
|
61
|
+
readonly contextValue?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export namespace TestMessage {
|
|
65
|
+
export function is(obj: unknown): obj is TestMessage {
|
|
66
|
+
return isObject<TestMessage>(obj) && (MarkdownString.is(obj.message) || typeof obj.message === 'string');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface TestState {
|
|
71
|
+
readonly state: TestExecutionState;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TestFailure extends TestState {
|
|
75
|
+
readonly state: TestExecutionState.Failed | TestExecutionState.Errored;
|
|
76
|
+
readonly messages: TestMessage[];
|
|
77
|
+
readonly duration?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export namespace TestFailure {
|
|
81
|
+
export function is(obj: unknown): obj is TestFailure {
|
|
82
|
+
return isObject<TestFailure>(obj) && (obj.state === TestExecutionState.Failed || obj.state === TestExecutionState.Errored) && Array.isArray(obj.messages);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TestSuccess extends TestState {
|
|
87
|
+
readonly state: TestExecutionState.Passed;
|
|
88
|
+
readonly duration?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TestStateChangedEvent {
|
|
92
|
+
test: TestItem;
|
|
93
|
+
oldState: TestState | undefined;
|
|
94
|
+
newState: TestState | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface TestRun {
|
|
98
|
+
cancel(): void;
|
|
99
|
+
readonly id: string;
|
|
100
|
+
readonly name: string;
|
|
101
|
+
readonly isRunning: boolean;
|
|
102
|
+
readonly controller: TestController;
|
|
103
|
+
|
|
104
|
+
onDidChangeProperty: Event<{ name?: string, isRunning?: boolean }>;
|
|
105
|
+
|
|
106
|
+
getTestState(item: TestItem): TestState | undefined;
|
|
107
|
+
onDidChangeTestState: Event<TestStateChangedEvent[]>;
|
|
108
|
+
|
|
109
|
+
getOutput(item?: TestItem): readonly TestOutputItem[];
|
|
110
|
+
onDidChangeTestOutput: Event<[TestItem | undefined, TestOutputItem][]>;
|
|
111
|
+
|
|
112
|
+
readonly items: readonly TestItem[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export namespace TestRun {
|
|
116
|
+
export function is(obj: unknown): obj is TestRun {
|
|
117
|
+
return isObject<TestRun>(obj)
|
|
118
|
+
&& typeof obj.cancel === 'function'
|
|
119
|
+
&& typeof obj.name === 'string'
|
|
120
|
+
&& typeof obj.isRunning === 'boolean'
|
|
121
|
+
&& typeof obj.controller === 'object'
|
|
122
|
+
&& typeof obj.onDidChangeProperty === 'function'
|
|
123
|
+
&& typeof obj.getTestState === 'function'
|
|
124
|
+
&& typeof obj.onDidChangeTestState === 'function'
|
|
125
|
+
&& typeof obj.onDidChangeTestState === 'function'
|
|
126
|
+
&& typeof obj.getOutput === 'function'
|
|
127
|
+
&& typeof obj.onDidChangeTestOutput === 'function'
|
|
128
|
+
&& Array.isArray(obj.items);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface TestItem {
|
|
133
|
+
readonly id: string;
|
|
134
|
+
readonly label: string;
|
|
135
|
+
readonly range?: Range;
|
|
136
|
+
readonly sortKey?: string;
|
|
137
|
+
readonly tags: string[];
|
|
138
|
+
readonly uri?: URI;
|
|
139
|
+
readonly busy: boolean;
|
|
140
|
+
readonly tests: readonly TestItem[];
|
|
141
|
+
readonly description?: string;
|
|
142
|
+
readonly error?: string | MarkdownString;
|
|
143
|
+
readonly parent: TestItem | undefined;
|
|
144
|
+
readonly controller: TestController | undefined;
|
|
145
|
+
readonly canResolveChildren: boolean;
|
|
146
|
+
resolveChildren(): void;
|
|
147
|
+
readonly path: string[];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export namespace TestItem {
|
|
151
|
+
export function is(obj: unknown): obj is TestItem {
|
|
152
|
+
return isObject<TestItem>(obj)
|
|
153
|
+
&& obj.id !== undefined
|
|
154
|
+
&& obj.label !== undefined
|
|
155
|
+
&& Array.isArray(obj.tags)
|
|
156
|
+
&& Array.isArray(obj.tests)
|
|
157
|
+
&& obj.busy !== undefined
|
|
158
|
+
&& obj.canResolveChildren !== undefined
|
|
159
|
+
&& typeof obj.resolveChildren === 'function';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface TestController {
|
|
164
|
+
readonly id: string;
|
|
165
|
+
readonly label: string;
|
|
166
|
+
readonly tests: readonly TestItem[];
|
|
167
|
+
readonly testRunProfiles: readonly TestRunProfile[];
|
|
168
|
+
readonly testRuns: readonly TestRun[];
|
|
169
|
+
|
|
170
|
+
readonly onItemsChanged: Event<TreeDelta<string, TestItem>[]>;
|
|
171
|
+
readonly onRunsChanged: Event<CollectionDelta<TestRun, TestRun>>;
|
|
172
|
+
readonly onProfilesChanged: Event<CollectionDelta<TestRunProfile, TestRunProfile>>;
|
|
173
|
+
|
|
174
|
+
refreshTests(token: CancellationToken): Promise<void>;
|
|
175
|
+
clearRuns(): void;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface TestService {
|
|
179
|
+
clearResults(): void;
|
|
180
|
+
configureProfile(): void;
|
|
181
|
+
selectDefaultProfile(): void;
|
|
182
|
+
runTestsWithProfile(tests: TestItem[]): void;
|
|
183
|
+
runTests(profileKind: TestRunProfileKind, tests: TestItem[]): void;
|
|
184
|
+
runAllTests(profileKind: TestRunProfileKind): void;
|
|
185
|
+
getControllers(): TestController[];
|
|
186
|
+
registerTestController(controller: TestController): Disposable;
|
|
187
|
+
onControllersChanged: Event<CollectionDelta<string, TestController>>;
|
|
188
|
+
|
|
189
|
+
refresh(): void;
|
|
190
|
+
cancelRefresh(): void;
|
|
191
|
+
isRefreshing: boolean;
|
|
192
|
+
onDidChangeIsRefreshing: Event<void>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export namespace TestServices {
|
|
196
|
+
export function withTestRun(service: TestService, controllerId: string, runId: string): TestRun {
|
|
197
|
+
const controller = service.getControllers().find(c => c.id === controllerId);
|
|
198
|
+
if (!controller) {
|
|
199
|
+
throw new Error(`No test controller with id '${controllerId}' found`);
|
|
200
|
+
}
|
|
201
|
+
const run = controller.testRuns.find(r => r.id === runId);
|
|
202
|
+
if (!run) {
|
|
203
|
+
throw new Error(`No test run with id '${runId}' found`);
|
|
204
|
+
}
|
|
205
|
+
return run;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const TestContribution = Symbol('TestContribution');
|
|
210
|
+
|
|
211
|
+
export interface TestContribution {
|
|
212
|
+
registerTestControllers(service: TestService): void;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const TestService = Symbol('TestService');
|
|
216
|
+
|
|
217
|
+
@injectable()
|
|
218
|
+
export class DefaultTestService implements TestService {
|
|
219
|
+
@inject(QuickPickService) quickpickService: QuickPickService;
|
|
220
|
+
|
|
221
|
+
private testRunCounter = 0;
|
|
222
|
+
|
|
223
|
+
private onDidChangeIsRefreshingEmitter = new Emitter<void>();
|
|
224
|
+
onDidChangeIsRefreshing: Event<void> = this.onDidChangeIsRefreshingEmitter.event;
|
|
225
|
+
|
|
226
|
+
private controllers: Map<string, TestController> = new Map();
|
|
227
|
+
private refreshing: Set<CancellationTokenSource> = new Set();
|
|
228
|
+
private onControllersChangedEmitter = new Emitter<CollectionDelta<string, TestController>>();
|
|
229
|
+
|
|
230
|
+
@inject(ContributionProvider) @named(TestContribution)
|
|
231
|
+
protected readonly contributionProvider: ContributionProvider<TestContribution>;
|
|
232
|
+
|
|
233
|
+
@postConstruct()
|
|
234
|
+
protected registerContributions(): void {
|
|
235
|
+
this.contributionProvider.getContributions().forEach(contribution => contribution.registerTestControllers(this));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
onControllersChanged: Event<CollectionDelta<string, TestController>> = this.onControllersChangedEmitter.event;
|
|
239
|
+
|
|
240
|
+
registerTestController(controller: TestController): Disposable {
|
|
241
|
+
if (this.controllers.has(controller.id)) {
|
|
242
|
+
throw new Error('TestController already registered: ' + controller.id);
|
|
243
|
+
}
|
|
244
|
+
this.controllers.set(controller.id, controller);
|
|
245
|
+
this.onControllersChangedEmitter.fire({ added: [controller] });
|
|
246
|
+
return Disposable.create(() => {
|
|
247
|
+
this.controllers.delete(controller.id);
|
|
248
|
+
this.onControllersChangedEmitter.fire({ removed: [controller.id] });
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
getControllers(): TestController[] {
|
|
253
|
+
return Array.from(this.controllers.values());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
refresh(): void {
|
|
257
|
+
const cts = new CancellationTokenSource();
|
|
258
|
+
this.refreshing.add(cts);
|
|
259
|
+
|
|
260
|
+
Promise.all(this.getControllers().map(controller => controller.refreshTests(cts.token))).then(() => {
|
|
261
|
+
this.refreshing.delete(cts);
|
|
262
|
+
if (this.refreshing.size === 0) {
|
|
263
|
+
this.onDidChangeIsRefreshingEmitter.fire();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (this.refreshing.size === 1) {
|
|
268
|
+
this.onDidChangeIsRefreshingEmitter.fire();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
cancelRefresh(): void {
|
|
273
|
+
if (this.refreshing.size > 0) {
|
|
274
|
+
this.refreshing.forEach(cts => cts.cancel());
|
|
275
|
+
this.refreshing.clear();
|
|
276
|
+
this.onDidChangeIsRefreshingEmitter.fire();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get isRefreshing(): boolean {
|
|
281
|
+
return this.refreshing.size > 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
runAllTests(profileKind: TestRunProfileKind): void {
|
|
285
|
+
this.getControllers().forEach(controller => {
|
|
286
|
+
this.runTestForController(controller, profileKind, controller.tests);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected async runTestForController(controller: TestController, profileKind: TestRunProfileKind, items: readonly TestItem[]): Promise<void> {
|
|
291
|
+
const runProfiles = controller.testRunProfiles.filter(profile => profile.kind === profileKind);
|
|
292
|
+
let activeProfile;
|
|
293
|
+
if (runProfiles.length === 1) {
|
|
294
|
+
activeProfile = runProfiles[0];
|
|
295
|
+
} else if (runProfiles.length > 1) {
|
|
296
|
+
const defaultProfile = runProfiles.find(p => p.isDefault);
|
|
297
|
+
if (defaultProfile) {
|
|
298
|
+
activeProfile = defaultProfile;
|
|
299
|
+
} else {
|
|
300
|
+
|
|
301
|
+
activeProfile = await this.pickProfile(runProfiles, nls.localizeByDefault('Pick a test profile to use'));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (activeProfile) {
|
|
305
|
+
activeProfile.run(`Test run #${this.testRunCounter++}`, items, [], true);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
protected async pickProfile(runProfiles: readonly TestRunProfile[], title: string): Promise<TestRunProfile | undefined> {
|
|
310
|
+
if (runProfiles.length === 0) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
// eslint-disable-next-line arrow-body-style
|
|
314
|
+
const picks = runProfiles.map(profile => {
|
|
315
|
+
let iconClasses;
|
|
316
|
+
if (profile.kind === TestRunProfileKind.Run) {
|
|
317
|
+
iconClasses = codiconArray('run');
|
|
318
|
+
} else if (profile.kind === TestRunProfileKind.Debug) {
|
|
319
|
+
iconClasses = codiconArray('debug-alt');
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
iconClasses,
|
|
323
|
+
label: `${profile.label}${profile.isDefault ? ' (default)' : ''}`,
|
|
324
|
+
profile: profile
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return (await this.quickpickService.show(picks, { title: title }))?.profile;
|
|
329
|
+
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
protected async pickProfileKind(): Promise<TestRunProfileKind | undefined> {
|
|
333
|
+
// eslint-disable-next-line arrow-body-style
|
|
334
|
+
const picks = [{
|
|
335
|
+
iconClasses: codiconArray('run'),
|
|
336
|
+
label: 'Run',
|
|
337
|
+
kind: TestRunProfileKind.Run
|
|
338
|
+
}, {
|
|
339
|
+
iconClasses: codiconArray('debug-alt'),
|
|
340
|
+
label: 'Debug',
|
|
341
|
+
kind: TestRunProfileKind.Debug
|
|
342
|
+
}];
|
|
343
|
+
|
|
344
|
+
return (await this.quickpickService.show(picks, { title: 'Select the kind of profiles' }))?.kind;
|
|
345
|
+
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
runTests(profileKind: TestRunProfileKind, items: TestItem[]): void {
|
|
349
|
+
groupBy(items, item => item.controller).forEach((tests, controller) => {
|
|
350
|
+
if (controller) {
|
|
351
|
+
this.runTestForController(controller, profileKind, tests);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
runTestsWithProfile(items: TestItem[]): void {
|
|
357
|
+
groupBy(items, item => item.controller).forEach((tests, controller) => {
|
|
358
|
+
if (controller) {
|
|
359
|
+
this.pickProfile(controller.testRunProfiles, nls.localizeByDefault('Pick a test profile to use')).then(activeProfile => {
|
|
360
|
+
if (activeProfile) {
|
|
361
|
+
activeProfile.run(`Test run #${this.testRunCounter++}`, items, [], true);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
selectDefaultProfile(): void {
|
|
369
|
+
this.pickProfileKind().then(kind => {
|
|
370
|
+
const profiles = this.getControllers().flatMap(c => c.testRunProfiles).filter(profile => profile.kind === kind);
|
|
371
|
+
this.pickProfile(profiles, nls.localizeByDefault('Pick a test profile to use')).then(activeProfile => {
|
|
372
|
+
if (activeProfile) {
|
|
373
|
+
// only change the default for the controller containing selected profile for default and its profiles with same kind
|
|
374
|
+
const controller = this.getControllers().find(c => c.testRunProfiles.includes(activeProfile));
|
|
375
|
+
controller?.testRunProfiles.filter(profile => profile.kind === activeProfile.kind).forEach(profile => {
|
|
376
|
+
profile.isDefault = profile === activeProfile;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
configureProfile(): void {
|
|
384
|
+
const profiles: TestRunProfile[] = [];
|
|
385
|
+
|
|
386
|
+
for (const controller of this.controllers.values()) {
|
|
387
|
+
profiles.push(...controller.testRunProfiles);
|
|
388
|
+
}
|
|
389
|
+
;
|
|
390
|
+
this.pickProfile(profiles.filter(profile => profile.canConfigure), nls.localizeByDefault('Select a profile to update')).then(profile => {
|
|
391
|
+
if (profile) {
|
|
392
|
+
profile.configure();
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
clearResults(): void {
|
|
398
|
+
for (const controller of this.controllers.values()) {
|
|
399
|
+
controller.clearRuns();
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|