Skip to content

Commit a650612

Browse files
authored
Raise event when status of a test manager changes (#4319)
For #4276
1 parent 8635eda commit a650612

File tree

8 files changed

+102
-54
lines changed

8 files changed

+102
-54
lines changed

src/client/unittests/common/managers/baseTestManager.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CancellationToken, CancellationTokenSource, Diagnostic, DiagnosticCollection, DiagnosticRelatedInformation, Disposable, languages, OutputChannel, Uri } from 'vscode';
1+
import { CancellationToken, CancellationTokenSource, Diagnostic, DiagnosticCollection, DiagnosticRelatedInformation, Disposable, Event, EventEmitter, languages, OutputChannel, Uri } from 'vscode';
22
import { IWorkspaceService } from '../../../common/application/types';
33
import { isNotInstalledError } from '../../../common/helpers';
44
import { IFileSystem } from '../../../common/platform/types';
@@ -38,6 +38,7 @@ export abstract class BaseTestManager implements ITestManager {
3838
private testRunnerCancellationTokenSource?: CancellationTokenSource;
3939
private _installer!: IInstaller;
4040
private discoverTestsPromise?: Promise<Tests>;
41+
private _onDidStatusChange = new EventEmitter<TestStatus>();
4142
private get installer(): IInstaller {
4243
if (!this._installer) {
4344
this._installer = this.serviceContainer.get<IInstaller>(IInstaller);
@@ -46,7 +47,7 @@ export abstract class BaseTestManager implements ITestManager {
4647
}
4748
constructor(public readonly testProvider: TestProvider, private product: Product, public readonly workspaceFolder: Uri, protected rootDirectory: string,
4849
protected serviceContainer: IServiceContainer) {
49-
this._status = TestStatus.Unknown;
50+
this.updateStatus(TestStatus.Unknown);
5051
const configService = serviceContainer.get<IConfigurationService>(IConfigurationService);
5152
this.settings = configService.getSettings(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined);
5253
const disposables = serviceContainer.get<Disposable[]>(IDisposableRegistry);
@@ -70,6 +71,9 @@ export abstract class BaseTestManager implements ITestManager {
7071
public get status(): TestStatus {
7172
return this._status;
7273
}
74+
public get onDidStatusChange(): Event<TestStatus> {
75+
return this._onDidStatusChange.event;
76+
}
7377
public get workingDirectory(): string {
7478
return this.settings.unitTest.cwd && this.settings.unitTest.cwd.length > 0 ? this.settings.unitTest.cwd : this.rootDirectory;
7579
}
@@ -82,8 +86,8 @@ export abstract class BaseTestManager implements ITestManager {
8286
}
8387
}
8488
public reset() {
85-
this._status = TestStatus.Unknown;
8689
this.tests = undefined;
90+
this.updateStatus(TestStatus.Unknown);
8791
}
8892
public resetTestResults() {
8993
if (!this.tests) {
@@ -98,11 +102,10 @@ export abstract class BaseTestManager implements ITestManager {
98102
}
99103

100104
if (!ignoreCache && this.tests! && this.tests!.testFunctions.length > 0) {
101-
this._status = TestStatus.Idle;
105+
this.updateStatus(TestStatus.Idle);
102106
return Promise.resolve(this.tests!);
103107
}
104-
this._status = TestStatus.Discovering;
105-
108+
this.updateStatus(TestStatus.Discovering);
106109
// If ignoreCache is true, its an indication of the fact that its a user invoked operation.
107110
// Hence we can stop the debugger.
108111
if (userInitiated) {
@@ -121,8 +124,8 @@ export abstract class BaseTestManager implements ITestManager {
121124
return discoveryService.discoverTests(discoveryOptions)
122125
.then(tests => {
123126
this.tests = tests;
124-
this._status = TestStatus.Idle;
125127
this.resetTestResults();
128+
this.updateStatus(TestStatus.Idle);
126129
this.discoverTestsPromise = undefined;
127130

128131
// have errors in Discovering
@@ -155,11 +158,11 @@ export abstract class BaseTestManager implements ITestManager {
155158
this.discoverTestsPromise = undefined;
156159
if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) {
157160
reason = CANCELLATION_REASON;
158-
this._status = TestStatus.Idle;
161+
this.updateStatus(TestStatus.Idle);
159162
} else {
160163
telementryProperties.failed = true;
161164
sendTelemetryEvent(EventName.UNITTEST_DISCOVER, undefined, telementryProperties);
162-
this._status = TestStatus.Error;
165+
this.updateStatus(TestStatus.Error);
163166
this.outputChannel.appendLine('Test Discovery failed: ');
164167
// tslint:disable-next-line:prefer-template
165168
this.outputChannel.appendLine(reason.toString());
@@ -211,7 +214,7 @@ export abstract class BaseTestManager implements ITestManager {
211214
this.resetTestResults();
212215
}
213216

214-
this._status = TestStatus.Running;
217+
this.updateStatus(TestStatus.Running);
215218
if (this.testRunnerCancellationTokenSource) {
216219
this.testRunnerCancellationTokenSource.cancel();
217220
}
@@ -235,16 +238,16 @@ export abstract class BaseTestManager implements ITestManager {
235238
this.createCancellationToken(CancellationTokenType.testRunner);
236239
return this.runTestImpl(tests, testsToRun, runFailedTests, debug);
237240
}).then(() => {
238-
this._status = TestStatus.Idle;
241+
this.updateStatus(TestStatus.Idle);
239242
this.disposeCancellationToken(CancellationTokenType.testRunner);
240243
sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, telementryProperties);
241244
return this.tests!;
242245
}).catch(reason => {
243246
if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) {
244247
reason = CANCELLATION_REASON;
245-
this._status = TestStatus.Idle;
248+
this.updateStatus(TestStatus.Idle);
246249
} else {
247-
this._status = TestStatus.Error;
250+
this.updateStatus(TestStatus.Error);
248251
telementryProperties.failed = true;
249252
sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, telementryProperties);
250253
}
@@ -291,6 +294,15 @@ export abstract class BaseTestManager implements ITestManager {
291294
// tslint:disable-next-line:no-any
292295
protected abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<any>;
293296
protected abstract getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions;
297+
private updateStatus(status: TestStatus): void {
298+
if (status === this._status) {
299+
return;
300+
}
301+
this._status = status;
302+
// Fire after 1ms, let existing code run to completion,
303+
// We need to allow for code to get into a consistent state.
304+
setTimeout(() => this._onDidStatusChange.fire(status), 1);
305+
}
294306
private createCancellationToken(tokenType: CancellationTokenType) {
295307
this.disposeCancellationToken(tokenType);
296308
if (tokenType === CancellationTokenType.testDiscovery) {

src/client/unittests/common/services/storageService.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
import { inject, injectable } from 'inversify';
2-
import {
3-
Disposable, Event, EventEmitter,
4-
Uri, workspace
5-
} from 'vscode';
2+
import { Disposable, Uri, workspace } from 'vscode';
63
import { IDisposableRegistry } from '../../../common/types';
74
import { FlattenedTestFunction, FlattenedTestSuite, ITestCollectionStorageService, TestFunction, Tests, TestSuite } from './../types';
85

96
@injectable()
107
export class TestCollectionStorageService implements ITestCollectionStorageService {
11-
public readonly onUpdated: Event<Uri>;
128

139
private testsIndexedByWorkspaceUri = new Map<string, Tests | undefined>();
14-
private _onTestStoreUpdated: EventEmitter<Uri> = new EventEmitter<Uri>();
1510

1611
constructor(@inject(IDisposableRegistry) disposables: Disposable[]) {
1712
disposables.push(this);
18-
this.onUpdated = this._onTestStoreUpdated.event;
1913
}
2014
public getTests(wkspace: Uri): Tests | undefined {
2115
const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || '';
@@ -24,7 +18,6 @@ export class TestCollectionStorageService implements ITestCollectionStorageServi
2418
public storeTests(wkspace: Uri, tests: Tests | undefined): void {
2519
const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || '';
2620
this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests);
27-
this._onTestStoreUpdated.fire(wkspace);
2821
}
2922
public findFlattendTestFunction(resource: Uri, func: TestFunction): FlattenedTestFunction | undefined {
3023
const tests = this.getTests(resource);
@@ -42,7 +35,6 @@ export class TestCollectionStorageService implements ITestCollectionStorageServi
4235
}
4336
public dispose() {
4437
this.testsIndexedByWorkspaceUri.clear();
45-
this._onTestStoreUpdated.dispose();
4638
}
4739
private getWorkspaceFolderPath(resource: Uri): string | undefined {
4840
const folder = workspace.getWorkspaceFolder(resource);

src/client/unittests/common/services/workspaceTestManagerService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { ITestManager, ITestManagerService, ITestManagerServiceFactory, IWorkspa
77
@injectable()
88
export class WorkspaceTestManagerService implements IWorkspaceTestManagerService, Disposable {
99
private workspaceTestManagers = new Map<string, ITestManagerService>();
10-
constructor( @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private outChannel: OutputChannel,
10+
constructor(@inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private outChannel: OutputChannel,
1111
@inject(ITestManagerServiceFactory) private testManagerServiceFactory: ITestManagerServiceFactory,
1212
@inject(IDisposableRegistry) disposables: Disposable[]) {
1313
disposables.push(this);

src/client/unittests/common/testUtils.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IUnitTestSettings, Product } from '../../common/types';
77
import { IServiceContainer } from '../../ioc/types';
88
import { CommandSource } from './constants';
99
import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor';
10-
import { ITestsHelper, ITestVisitor, TestFile, TestFolder, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, UnitTestProduct } from './types';
10+
import { ITestsHelper, ITestVisitor, TestFile, TestFolder, TestFunction, TestProvider, Tests, TestSettingsPropertyNames, TestsToRun, TestSuite, UnitTestProduct } from './types';
1111

1212
export async function selectTestWorkspace(appShell: IApplicationShell): Promise<Uri | undefined> {
1313
if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) {
@@ -205,4 +205,22 @@ export class TestsHelper implements ITestsHelper {
205205

206206
return true;
207207
}
208+
public getTestFile(test: TestFile | TestFolder | TestSuite | TestFunction): TestFile | undefined {
209+
// Only TestFile has a `fullPath` property.
210+
return Array.isArray((test as TestFile).fullPath) ? test as TestFile : undefined;
211+
}
212+
public getTestSuite(test: TestFile | TestFolder | TestSuite | TestFunction): TestSuite | undefined {
213+
// Only TestSuite has a `suites` property.
214+
return Array.isArray((test as TestSuite).suites) ? test as TestSuite : undefined;
215+
}
216+
public getTestFolder(test: TestFile | TestFolder | TestSuite | TestFunction): TestFolder | undefined {
217+
// Only TestFolder has a `folders` property.
218+
return Array.isArray((test as TestFolder).folders) ? test as TestFolder : undefined;
219+
}
220+
public getTestFunction(test: TestFile | TestFolder | TestSuite | TestFunction): TestFunction | undefined {
221+
if (this.getTestFile(test) || this.getTestSuite(test) || this.getTestSuite(test)) {
222+
return;
223+
}
224+
return test as TestFunction;
225+
}
208226
}

src/client/unittests/common/types.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import {
2-
CancellationToken, DiagnosticCollection, Disposable,
3-
Event, OutputChannel, Uri
4-
} from 'vscode';
1+
import { CancellationToken, DiagnosticCollection, Disposable, Event, OutputChannel, Uri } from 'vscode';
52
import { IUnitTestSettings, Product } from '../../common/types';
63
import { IPythonUnitTestMessage } from '../types';
74
import { CommandSource } from './constants';
@@ -37,7 +34,12 @@ export type TestFolder = TestResult & {
3734
status?: TestStatus;
3835
folders: TestFolder[];
3936
};
40-
37+
export enum TestType {
38+
testFile = 'testFile',
39+
testFolder = 'testFolder',
40+
testSuite = 'testSuite',
41+
testFunction = 'testFunction'
42+
}
4143
export type TestFile = TestResult & {
4244
name: string;
4345
fullPath: string;
@@ -184,7 +186,6 @@ export interface ITestVisitor {
184186
export const ITestCollectionStorageService = Symbol('ITestCollectionStorageService');
185187

186188
export interface ITestCollectionStorageService extends Disposable {
187-
onUpdated: Event<Uri>;
188189
getTests(wkspace: Uri): Tests | undefined;
189190
storeTests(wkspace: Uri, tests: Tests | null | undefined): void;
190191
findFlattendTestFunction(resource: Uri, func: TestFunction): FlattenedTestFunction | undefined;
@@ -232,6 +233,7 @@ export interface ITestManager extends Disposable {
232233
readonly workingDirectory: string;
233234
readonly workspaceFolder: Uri;
234235
diagnosticCollection: DiagnosticCollection;
236+
readonly onDidStatusChange: Event<TestStatus>;
235237
stop(): void;
236238
resetTestResults(): void;
237239
discoverTests(cmdSource: CommandSource, ignoreCache?: boolean, quietMode?: boolean, userInitiated?: boolean): Promise<Tests>;

src/client/unittests/main.ts

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import { inject, injectable } from 'inversify';
66
import {
77
ConfigurationChangeEvent, Disposable,
8-
DocumentSymbolProvider, EventEmitter,
9-
OutputChannel, TextDocument, Uri, window
8+
DocumentSymbolProvider, Event,
9+
EventEmitter, OutputChannel, TextDocument, Uri, window
1010
} from 'vscode';
1111
import {
1212
IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService
@@ -26,15 +26,8 @@ import {
2626
CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL
2727
} from './common/constants';
2828
import { selectTestWorkspace } from './common/testUtils';
29-
import {
30-
ITestCollectionStorageService, ITestManager,
31-
IWorkspaceTestManagerService, TestFile,
32-
TestFunction, TestStatus, TestsToRun
33-
} from './common/types';
34-
import {
35-
ITestDisplay, ITestResultDisplay,
36-
IUnitTestConfigurationService, IUnitTestManagementService
37-
} from './types';
29+
import { ITestCollectionStorageService, ITestManager, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types';
30+
import { ITestDisplay, ITestResultDisplay, IUnitTestConfigurationService, IUnitTestManagementService, WorkspaceTestStatus } from './types';
3831

3932
@injectable()
4033
export class UnitTestManagementService implements IUnitTestManagementService, Disposable {
@@ -46,7 +39,8 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
4639
private testResultDisplay?: ITestResultDisplay;
4740
private autoDiscoverTimer?: NodeJS.Timer;
4841
private configChangedTimer?: NodeJS.Timer;
49-
private readonly onDidChange: EventEmitter<void> = new EventEmitter<void>();
42+
private testManagers = new Set<ITestManager>();
43+
private readonly _onDidStatusChange: EventEmitter<WorkspaceTestStatus> = new EventEmitter<WorkspaceTestStatus>();
5044

5145
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {
5246
this.disposableRegistry = serviceContainer.get<Disposable[]>(IDisposableRegistry);
@@ -61,6 +55,9 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
6155
this.workspaceTestManagerService.dispose();
6256
}
6357
}
58+
public get onDidStatusChange(): Event<WorkspaceTestStatus> {
59+
return this._onDidStatusChange.event;
60+
}
6461
public async activate(symbolProvider: DocumentSymbolProvider): Promise<void> {
6562
this.workspaceTestManagerService = this.serviceContainer.get<IWorkspaceTestManagerService>(IWorkspaceTestManagerService);
6663
const disposablesRegistry = this.serviceContainer.get<Disposable[]>(IDisposableRegistry);
@@ -78,11 +75,6 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
7875
await this.registerSymbolProvider(symbolProvider);
7976
}
8077

81-
public async activateCodeLenses(symbolProvider: DocumentSymbolProvider): Promise<void> {
82-
const testCollectionStorage = this.serviceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService);
83-
this.disposableRegistry.push(activateCodeLenses(this.onDidChange, symbolProvider, testCollectionStorage));
84-
}
85-
8678
public async getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise<ITestManager | undefined | void> {
8779
let wkspace: Uri | undefined;
8880
if (resource) {
@@ -97,6 +89,13 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
9789
}
9890
const testManager = this.workspaceTestManagerService!.getTestManager(wkspace);
9991
if (testManager) {
92+
if (!this.testManagers.has(testManager)) {
93+
this.testManagers.add(testManager);
94+
const handler = testManager.onDidStatusChange(e => {
95+
this._onDidStatusChange.fire({ workspace: testManager.workspaceFolder, status: e });
96+
});
97+
this.disposableRegistry.push(handler);
98+
}
10099
return testManager;
101100
}
102101
if (displayTestNotConfiguredMessage) {
@@ -178,7 +177,6 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
178177

179178
if (!this.testResultDisplay) {
180179
this.testResultDisplay = this.serviceContainer.get<ITestResultDisplay>(ITestResultDisplay);
181-
this.testResultDisplay.onDidChange(() => this.onDidChange.fire());
182180
}
183181
const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, quietMode, userInitiated);
184182
this.testResultDisplay.displayDiscoverStatus(discoveryPromise, quietMode)
@@ -296,7 +294,6 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
296294

297295
if (!this.testResultDisplay) {
298296
this.testResultDisplay = this.serviceContainer.get<ITestResultDisplay>(ITestResultDisplay);
299-
this.testResultDisplay.onDidChange(() => this.onDidChange.fire());
300297
}
301298

302299
const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug)
@@ -313,7 +310,15 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
313310

314311
private async registerSymbolProvider(symbolProvider: DocumentSymbolProvider): Promise<void> {
315312
const testCollectionStorage = this.serviceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService);
316-
this.disposableRegistry.push(activateCodeLenses(this.onDidChange, symbolProvider, testCollectionStorage));
313+
const event = new EventEmitter<void>();
314+
this.disposableRegistry.push(event);
315+
const handler = this._onDidStatusChange.event(e => {
316+
if (e.status !== TestStatus.Discovering && e.status !== TestStatus.Running) {
317+
event.fire();
318+
}
319+
});
320+
this.disposableRegistry.push(handler);
321+
this.disposableRegistry.push(activateCodeLenses(event, symbolProvider, testCollectionStorage));
317322
}
318323

319324
@captureTelemetry(EventName.UNITTEST_CONFIGURE, undefined, false)
@@ -341,13 +346,13 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
341346
// Ignore the exceptions returned.
342347
// This command will be invoked from other places of the extension.
343348
this.discoverTests(cmdSource, resource, true, true)
344-
.ignoreErrors();
349+
.ignoreErrors();
345350
}),
346351
commandManager.registerCommand(constants.Commands.Tests_Configure, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => {
347352
// Ignore the exceptions returned.
348353
// This command will be invoked from other places of the extension.
349354
this.configureTests(resource)
350-
.ignoreErrors();
355+
.ignoreErrors();
351356
}),
352357
commandManager.registerCommand(constants.Commands.Tests_Run_Failed, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => this.runTestsImpl(cmdSource, resource, undefined, true)),
353358
commandManager.registerCommand(constants.Commands.Tests_Run, (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testToRun?: TestsToRun) => this.runTestsImpl(cmdSource, file, testToRun)),

0 commit comments

Comments
 (0)