/* eslint-disable @typescript-eslint/no-explicit-any */ // ***************************************************************************** // Copyright (C) 2022 STMicroelectronics and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { CancellationToken, Emitter, Event, URI } from '@theia/core'; import { Range, Location, CancellationTokenSource } from '@theia/core/shared/vscode-languageserver-protocol'; import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; import { SimpleObservableCollection, TreeCollection, observableProperty } from '@theia/test/lib/common/collections'; import { TestController, TestExecutionState, TestFailure, TestItem, TestOutputItem, TestRun, TestRunProfile, TestState, TestStateChangedEvent } from '@theia/test/lib/browser/test-service'; import { AccumulatingTreeDeltaEmitter, CollectionDelta, TreeDelta, TreeDeltaBuilder } from '@theia/test/lib/common/tree-delta'; import { timeout } from '@theia/core/lib/common/promise-util'; export class TestItemCollection extends TreeCollection { override add(item: TestItemImpl): TestItemImpl | undefined { item.realParent = this.owner; return super.add(item); } } export class TestItemImpl implements TestItem { constructor(readonly uri: URI, readonly id: string) { this._children = new TestItemCollection(this, (v: TestItemImpl) => v.path, (v: TestItemImpl) => v.deltaBuilder); } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected notifyPropertyChange(property: keyof TestItemImpl, value: any): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any const val: any = {}; val[property] = value; if (this.path) { this.deltaBuilder?.reportChanged(this.path, val); } } _deltaBuilder: TreeDeltaBuilder | undefined; get deltaBuilder(): TreeDeltaBuilder | undefined { if (this._deltaBuilder) { return this._deltaBuilder; } else if (this.realParent) { this._deltaBuilder = this.realParent.deltaBuilder; return this._deltaBuilder; } else { return undefined; } } _path: string[] | undefined; get path(): string[] { if (this._path) { return this._path; } else if (this.realParent instanceof TestItemImpl) { this._path = [...this.realParent.path, this.id]; return this._path; } else { return [this.id]; } }; private _parent?: TestItemImpl | TestControllerImpl; get realParent(): TestItemImpl | TestControllerImpl | undefined { return this._parent; } set realParent(v: TestItemImpl | TestControllerImpl | undefined) { this.iterate(item => { item._path = undefined; return true; }); this._parent = v; } get parent(): TestItem | undefined { const realParent = this.realParent; if (realParent instanceof TestItemImpl) { return realParent; } return undefined; } get controller(): TestControllerImpl | undefined { if (this.realParent instanceof TestItemImpl) { return this.realParent.controller; } return this.realParent; } protected iterate(toDo: (v: TestItemImpl) => boolean): boolean { if (toDo(this)) { for (let i = 0; i < this._children.values.length; i++) { if (!this._children.values[i].iterate(toDo)) { return false; } } return true; } else { return false; } } @observableProperty('notifyPropertyChange') label: string = ''; @observableProperty('notifyPropertyChange') range?: Range; @observableProperty('notifyPropertyChange') sortKey?: string | undefined; @observableProperty('notifyPropertyChange') tags: string[] = []; @observableProperty('notifyPropertyChange') busy: boolean = false; @observableProperty('notifyPropertyChange') canResolveChildren: boolean = false; @observableProperty('notifyPropertyChange') description?: string | undefined; @observableProperty('notifyPropertyChange') error?: string | MarkdownString | undefined; _children: TestItemCollection; get tests(): readonly TestItemImpl[] { return this._children.values; } resolveChildren(): void { // do nothing } } export class TestRunImpl implements TestRun { private testStates: Map = new Map(); private outputIndices: Map = new Map(); private outputs: TestOutputItem[] = []; private onDidChangePropertyEmitter = new Emitter<{ name?: string; isRunning?: boolean; }>(); onDidChangeProperty: Event<{ name?: string; isRunning?: boolean; }> = this.onDidChangePropertyEmitter.event; private cts: CancellationTokenSource; constructor(readonly controller: TestControllerImpl, readonly id: string, name: string) { this.name = name; this.isRunning = false; this.start(); } private start(): void { this.cts = new CancellationTokenSource(); Promise.allSettled(this.collectTestsForRun().map(item => this.simulateTestRun(item, this.cts.token))).then(() => this.ended()); } collectTestsForRun(): TestItemImpl[] { const result: TestItemImpl[] = []; this.collectTests(this.controller.tests, result); return result; } collectTests(tests: readonly TestItemImpl[], result: TestItemImpl[]): void { tests.forEach(test => this.collectTest(test, result)); } collectTest(test: TestItemImpl, result: TestItemImpl[]): void { if (test.tests.length > 0) { this.collectTests(test.tests, result); } else if (Math.random() < 0.8) { result.push(test); } } simulateTestRun(item: TestItemImpl, token: CancellationToken): Promise { let outputCounter = 0; let messageCounter = 0; return timeout(Math.random() * 3000, token) .then(() => this.setTestState(item, { state: TestExecutionState.Queued })) .then(() => timeout(Math.random() * 3000, token)) .then(() => this.setTestState(item, { state: TestExecutionState.Running })) .then(() => timeout(Math.random() * 3000, token)) .then(() => { this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`); }) .then(() => timeout(Math.random() * 3000, token)) .then(() => { this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`); }) .then(() => timeout(Math.random() * 3000, token)) .then(() => { this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`); }) .then(() => timeout(Math.random() * 3000, token)) .then(() => { this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`); }).then(() => { const random = Math.random(); if (random > 0.9) { this.setTestState(item, { state: TestExecutionState.Skipped }); } else if (random > 0.8) { const failure: TestFailure = { state: TestExecutionState.Errored, messages: [ { message: { value: `**Error** from Test ${item.label} nr ${messageCounter++}` }, location: { uri: item.uri.toString(), range: item.range! }, } ], duration: 33 }; this.setTestState(item, failure); } else if (random > 0.7) { const failure: TestFailure = { state: TestExecutionState.Failed, messages: [ { message: { value: `**Failure** from Test ${item.label} nr ${messageCounter++}` }, location: { uri: item.uri.toString(), range: item.range! }, } ], duration: 33 }; this.setTestState(item, failure); } else { this.setTestState(item, { state: TestExecutionState.Passed }); } }); } @observableProperty('notifyPropertyChange') isRunning: boolean; @observableProperty('notifyPropertyChange') name: string; protected notifyPropertyChange(property: 'name' | 'isRunning', value: unknown): void { // eslint-disable-next-line @typescript-eslint/no-explicit-any const val: any = {}; val[property] = value; this.onDidChangePropertyEmitter.fire(val); } cancel(): void { this.cts.cancel(); } getTestState(item: TestItem): TestState | undefined { return this.testStates.get(item); } private onDidChangeTestStateEmitter: Emitter = new Emitter(); onDidChangeTestState: Event = this.onDidChangeTestStateEmitter.event; getOutput(item?: TestItem | undefined): readonly TestOutputItem[] { if (!item) { return this.outputs; } else { const indices = this.outputIndices.get(item); if (!indices) { return []; } else { return indices.map(index => this.outputs[index]); } } } private onDidChangeTestOutputEmitter: Emitter<[TestItem | undefined, TestOutputItem][]> = new Emitter(); onDidChangeTestOutput: Event<[TestItem | undefined, TestOutputItem][]> = this.onDidChangeTestOutputEmitter.event; setTestState(test: TestItemImpl, newState: TestState): void { const oldState = this.testStates.get(test); this.testStates.set(test, newState); this.onDidChangeTestStateEmitter.fire([{ oldState: oldState, newState: newState, test: test }]); } appendOutput(text: string, location?: Location, item?: TestItem): void { const output = { output: text, location: location }; this.outputs.push(output); if (item) { let indices = this.outputIndices.get(item); if (!indices) { indices = []; this.outputIndices.set(item, indices); } indices.push(this.outputs.length - 1); } this.onDidChangeTestOutputEmitter.fire([[item, output]]); } get items(): readonly TestItem[] { return [...this.testStates.keys()]; } ended(): void { const stateEvents: TestStateChangedEvent[] = []; this.testStates.forEach((state, item) => { if (state.state <= TestExecutionState.Running) { stateEvents.push({ oldState: state, newState: undefined, test: item }); this.testStates.delete(item); } }); if (stateEvents.length > 0) { this.onDidChangeTestStateEmitter.fire(stateEvents); } this.isRunning = false; } } export class TestControllerImpl implements TestController { private _profiles = new SimpleObservableCollection(); private _runs = new SimpleObservableCollection(); readonly deltaBuilder = new AccumulatingTreeDeltaEmitter(300); items = new TestItemCollection(this, item => item.path, () => this.deltaBuilder); constructor(readonly id: string, readonly label: string) { } refreshTests(token: CancellationToken): Promise { // not implemented return Promise.resolve(); } get testRunProfiles(): readonly TestRunProfile[] { return this._profiles.values; } addProfile(profile: TestRunProfile): void { this._profiles.add(profile); } onProfilesChanged: Event> = this._profiles.onChanged; get testRuns(): readonly TestRun[] { return this._runs.values; } addRun(run: TestRun): void { this._runs.add(run); } onRunsChanged: Event> = this._runs.onChanged; get tests(): readonly TestItemImpl[] { return this.items.values; } onItemsChanged: Event[]> = this.deltaBuilder.onDidFlush; resolveChildren(item: TestItem): void { // nothing to do } clearRuns(): void { this._runs.clear(); } }