Skip to content

Commit d4ea4b6

Browse files
authored
Preserve status of tests when re-discovering automatically (#4638)
* Display test explorer when discovery has been run * Preserve state when adding removing tests * Retain state of tests when auto discovering tests * Delete unwanted test * Fix test * Address code review comments * Remove unnecessary file * Fixed broken tests
1 parent 7b03192 commit d4ea4b6

File tree

8 files changed

+240
-49
lines changed

8 files changed

+240
-49
lines changed

news/1 Enhancements/4576.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Retain state of tests when auto discovering tests.

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

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { ICommandManager, IWorkspaceService } from '../../../common/application/types';
1515
import '../../../common/extensions';
1616
import { isNotInstalledError } from '../../../common/helpers';
17+
import { traceError } from '../../../common/logger';
1718
import { IFileSystem } from '../../../common/platform/types';
1819
import { IConfigurationService, IDisposableRegistry, IInstaller, IOutputChannel, IPythonSettings, Product } from '../../../common/types';
1920
import { getNamesAndValues } from '../../../common/utils/enum';
@@ -23,14 +24,15 @@ import { EventName } from '../../../telemetry/constants';
2324
import { sendTelemetryEvent } from '../../../telemetry/index';
2425
import { TestDiscoverytTelemetry, TestRunTelemetry } from '../../../telemetry/types';
2526
import { IPythonUnitTestMessage, IUnitTestDiagnosticService, WorkspaceTestStatus } from '../../types';
26-
import { ITestsStatusUpdaterService } from '../types';
27+
import { copyTestResults } from '../testUtils';
2728
import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './../constants';
2829
import {
2930
ITestCollectionStorageService,
3031
ITestDiscoveryService,
3132
ITestManager,
3233
ITestResultsService,
3334
ITestsHelper,
35+
ITestsStatusUpdaterService,
3436
TestDiscoveryOptions,
3537
TestProvider,
3638
Tests,
@@ -56,19 +58,19 @@ export abstract class BaseTestManager implements ITestManager {
5658
protected get testResultsService() {
5759
return this._testResultsService;
5860
}
59-
private testCollectionStorage: ITestCollectionStorageService;
60-
private _testResultsService: ITestResultsService;
61-
private commandManager: ICommandManager;
62-
private workspaceService: IWorkspaceService;
63-
private _outputChannel: OutputChannel;
61+
private readonly testCollectionStorage: ITestCollectionStorageService;
62+
private readonly _testResultsService: ITestResultsService;
63+
private readonly commandManager: ICommandManager;
64+
private readonly workspaceService: IWorkspaceService;
65+
private readonly _outputChannel: OutputChannel;
6466
protected tests?: Tests;
6567
private _status: TestStatus = TestStatus.Unknown;
6668
private testDiscoveryCancellationTokenSource?: CancellationTokenSource;
6769
private testRunnerCancellationTokenSource?: CancellationTokenSource;
6870
private _installer!: IInstaller;
69-
private testsStatusUpdaterService: ITestsStatusUpdaterService;
71+
private readonly testsStatusUpdaterService: ITestsStatusUpdaterService;
7072
private discoverTestsPromise?: Promise<Tests>;
71-
private _onDidStatusChange = new EventEmitter<WorkspaceTestStatus>();
73+
private readonly _onDidStatusChange = new EventEmitter<WorkspaceTestStatus>();
7274
private get installer(): IInstaller {
7375
if (!this._installer) {
7476
this._installer = this.serviceContainer.get<IInstaller>(IInstaller);
@@ -77,7 +79,7 @@ export abstract class BaseTestManager implements ITestManager {
7779
}
7880
constructor(
7981
public readonly testProvider: TestProvider,
80-
private product: Product,
82+
private readonly product: Product,
8183
public readonly workspaceFolder: Uri,
8284
protected rootDirectory: string,
8385
protected serviceContainer: IServiceContainer
@@ -133,15 +135,15 @@ export abstract class BaseTestManager implements ITestManager {
133135

134136
this.testResultsService.resetResults(this.tests!);
135137
}
136-
public async discoverTests(cmdSource: CommandSource, ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false): Promise<Tests> {
138+
public async discoverTests(cmdSource: CommandSource, ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false, clearTestStatus: boolean = false): Promise<Tests> {
137139
if (this.discoverTestsPromise) {
138140
return this.discoverTestsPromise;
139141
}
140-
this.discoverTestsPromise = this._discoverTests(cmdSource, ignoreCache, quietMode, userInitiated);
142+
this.discoverTestsPromise = this._discoverTests(cmdSource, ignoreCache, quietMode, userInitiated, clearTestStatus);
141143
this.discoverTestsPromise.catch(noop).then(() => this.discoverTestsPromise = undefined).ignoreErrors();
142144
return this.discoverTestsPromise;
143145
}
144-
private async _discoverTests(cmdSource: CommandSource, ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false): Promise<Tests> {
146+
private async _discoverTests(cmdSource: CommandSource, ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false, clearTestStatus: boolean = false): Promise<Tests> {
145147
if (!ignoreCache && this.tests! && this.tests!.testFunctions.length > 0) {
146148
this.updateStatus(TestStatus.Idle);
147149
return Promise.resolve(this.tests!);
@@ -168,8 +170,16 @@ export abstract class BaseTestManager implements ITestManager {
168170
return discoveryService
169171
.discoverTests(discoveryOptions)
170172
.then(tests => {
173+
const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri;
174+
const existingTests = this.testCollectionStorage.getTests(wkspace)!;
175+
if (clearTestStatus) {
176+
this.resetTestResults();
177+
} else if (existingTests) {
178+
copyTestResults(existingTests, tests);
179+
this._testResultsService.updateResults(tests);
180+
}
181+
this.testCollectionStorage.storeTests(wkspace, tests);
171182
this.tests = tests;
172-
this.resetTestResults();
173183
this.updateStatus(TestStatus.Idle);
174184
this.discoverTestsPromise = undefined;
175185

@@ -188,8 +198,6 @@ export abstract class BaseTestManager implements ITestManager {
188198
const testsHelper = this.serviceContainer.get<ITestsHelper>(ITestsHelper);
189199
testsHelper.displayTestErrorMessage('There were some errors in discovering unit tests');
190200
}
191-
const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri;
192-
this.testCollectionStorage.storeTests(wkspace, tests);
193201
this.disposeCancellationToken(CancellationTokenType.testDiscovery);
194202
sendTelemetryEvent(EventName.UNITTEST_DISCOVER, undefined, telementryProperties);
195203
return tests;
@@ -200,7 +208,7 @@ export abstract class BaseTestManager implements ITestManager {
200208
}
201209
if (isNotInstalledError(reason as Error) && !quietMode) {
202210
this.installer.promptToInstall(this.product, this.workspaceFolder)
203-
.catch(ex => console.error('Python Extension: isNotInstalledError', ex));
211+
.catch(ex => traceError('isNotInstalledError', ex));
204212
}
205213

206214
this.tests = undefined;
@@ -216,7 +224,7 @@ export abstract class BaseTestManager implements ITestManager {
216224
this.outputChannel.appendLine(reason.toString());
217225
}
218226
const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri;
219-
this.testCollectionStorage.storeTests(wkspace, null);
227+
this.testCollectionStorage.storeTests(wkspace, undefined);
220228
this.disposeCancellationToken(CancellationTokenType.testDiscovery);
221229
return Promise.reject(reason);
222230
});
@@ -240,7 +248,6 @@ export abstract class BaseTestManager implements ITestManager {
240248
};
241249

242250
if (!runFailedTests && !testsToRun) {
243-
this.resetTestResults();
244251
this.testsStatusUpdaterService.updateStatusAsRunning(this.workspaceFolder, this.tests);
245252
}
246253

@@ -419,7 +426,7 @@ export abstract class BaseTestManager implements ITestManager {
419426
const stackStart = message.locationStack![0];
420427
const diagPrefix = this.unitTestDiagnosticService.getMessagePrefix(message.status!);
421428
const severity = this.unitTestDiagnosticService.getSeverity(message.severity)!;
422-
const diagMsg = message.message!.split('\n')[0];
429+
const diagMsg = message.message ? message.message.split('\n')[0] : '';
423430
const diagnostic = new Diagnostic(stackStart.location.range, `${diagPrefix ? `${diagPrefix}: ` : ''}${diagMsg}`, severity);
424431
diagnostic.code = message.code;
425432
diagnostic.source = message.provider;
Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
import { inject, injectable } from 'inversify';
2-
import { Disposable, Event, EventEmitter, Uri, workspace } from 'vscode';
2+
import { Disposable, Event, EventEmitter, Uri } from 'vscode';
3+
import { IWorkspaceService } from '../../../common/application/types';
34
import { IDisposableRegistry } from '../../../common/types';
45
import { TestDataItem } from '../../types';
56
import { FlattenedTestFunction, FlattenedTestSuite, ITestCollectionStorageService, TestFunction, Tests, TestSuite } from './../types';
67

78
@injectable()
89
export class TestCollectionStorageService implements ITestCollectionStorageService {
9-
private _onDidChange = new EventEmitter<{ uri: Uri; data?: TestDataItem }>();
10-
private testsIndexedByWorkspaceUri = new Map<string, Tests | undefined>();
10+
private readonly _onDidChange = new EventEmitter<{ uri: Uri; data?: TestDataItem }>();
11+
private readonly testsIndexedByWorkspaceUri = new Map<string, Tests | undefined>();
1112

12-
constructor(@inject(IDisposableRegistry) disposables: Disposable[]) {
13+
constructor(@inject(IDisposableRegistry) disposables: Disposable[],
14+
@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {
1315
disposables.push(this);
1416
}
1517
public get onDidChange(): Event<{ uri: Uri; data?: TestDataItem }> {
1618
return this._onDidChange.event;
1719
}
18-
public getTests(wkspace: Uri): Tests | undefined {
19-
const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || '';
20+
public getTests(resource: Uri): Tests | undefined {
21+
const workspaceFolder = this.workspaceService.getWorkspaceFolderIdentifier(resource);
2022
return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined;
2123
}
22-
public storeTests(wkspace: Uri, tests: Tests | undefined): void {
23-
const workspaceFolder = this.getWorkspaceFolderPath(wkspace) || '';
24+
public storeTests(resource: Uri, tests: Tests | undefined): void {
25+
const workspaceFolder = this.workspaceService.getWorkspaceFolderIdentifier(resource);
2426
this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests);
25-
this._onDidChange.fire({ uri: wkspace });
27+
this._onDidChange.fire({ uri: resource });
2628
}
2729
public findFlattendTestFunction(resource: Uri, func: TestFunction): FlattenedTestFunction | undefined {
2830
const tests = this.getTests(resource);
@@ -41,11 +43,7 @@ export class TestCollectionStorageService implements ITestCollectionStorageServi
4143
public dispose() {
4244
this.testsIndexedByWorkspaceUri.clear();
4345
}
44-
public update(resource: Uri, item: TestDataItem): void{
46+
public update(resource: Uri, item: TestDataItem): void {
4547
this._onDidChange.fire({ uri: resource, data: item });
4648
}
47-
private getWorkspaceFolderPath(resource: Uri): string | undefined {
48-
const folder = workspace.getWorkspaceFolder(resource);
49-
return folder ? folder.uri.path : undefined;
50-
}
5149
}

src/client/unittests/common/testUtils.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class TestsHelper implements ITestsHelper {
5454
private readonly appShell: IApplicationShell;
5555
private readonly commandManager: ICommandManager;
5656
constructor(
57-
@inject(ITestVisitor) @named('TestFlatteningVisitor') private flatteningVisitor: TestFlatteningVisitor,
57+
@inject(ITestVisitor) @named('TestFlatteningVisitor') private readonly flatteningVisitor: TestFlatteningVisitor,
5858
@inject(IServiceContainer) serviceContainer: IServiceContainer
5959
) {
6060
this.appShell = serviceContainer.get<IApplicationShell>(IApplicationShell);
@@ -451,3 +451,62 @@ export function getChildren(item: TestDataItem): TestDataItem[] {
451451
}
452452
}
453453
}
454+
455+
export function copyTestResults(source: Tests, target: Tests): void {
456+
copyResultsForFolders(source.testFolders, target.testFolders);
457+
}
458+
459+
function copyResultsForFolders(source: TestFolder[], target: TestFolder[]): void {
460+
source.forEach(sourceFolder => {
461+
const targetFolder = target.find(folder => folder.name === sourceFolder.name && folder.nameToRun === sourceFolder.nameToRun);
462+
if (!targetFolder) {
463+
return;
464+
}
465+
copyValueTypes<TestFolder>(sourceFolder, targetFolder);
466+
copyResultsForFiles(sourceFolder.testFiles, targetFolder.testFiles);
467+
});
468+
}
469+
function copyResultsForFiles(source: TestFile[], target: TestFile[]): void {
470+
source.forEach(sourceFile => {
471+
const targetFile = target.find(file => file.name === sourceFile.name && file.nameToRun === sourceFile.nameToRun);
472+
if (!targetFile) {
473+
return;
474+
}
475+
copyValueTypes<TestFile>(sourceFile, targetFile);
476+
copyResultsForFunctions(sourceFile.functions, targetFile.functions);
477+
copyResultsForSuites(sourceFile.suites, targetFile.suites);
478+
});
479+
}
480+
481+
function copyResultsForFunctions(source: TestFunction[], target: TestFunction[]): void {
482+
source.forEach(sourceFn => {
483+
const targetFn = target.find(fn => fn.name === sourceFn.name && fn.nameToRun === sourceFn.nameToRun);
484+
if (!targetFn) {
485+
return;
486+
}
487+
copyValueTypes<TestFunction>(sourceFn, targetFn);
488+
});
489+
}
490+
491+
function copyResultsForSuites(source: TestSuite[], target: TestSuite[]): void {
492+
source.forEach(sourceSuite => {
493+
const targetSuite = target.find(suite => suite.name === sourceSuite.name &&
494+
suite.nameToRun === sourceSuite.nameToRun &&
495+
suite.xmlName === sourceSuite.xmlName);
496+
if (!targetSuite) {
497+
return;
498+
}
499+
copyValueTypes<TestSuite>(sourceSuite, targetSuite);
500+
copyResultsForFunctions(sourceSuite.functions, targetSuite.functions);
501+
copyResultsForSuites(sourceSuite.suites, targetSuite.suites);
502+
});
503+
}
504+
505+
function copyValueTypes<T>(source: T, target: T): void {
506+
Object.keys(source).forEach(key => {
507+
const value = source[key];
508+
if (['boolean', 'number', 'string', 'undefined'].indexOf(typeof value) >= 0) {
509+
target[key] = value;
510+
}
511+
});
512+
}

src/client/unittests/common/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ export interface ITestManager extends Disposable {
232232
readonly onDidStatusChange: Event<WorkspaceTestStatus>;
233233
stop(): void;
234234
resetTestResults(): void;
235-
discoverTests(cmdSource: CommandSource, ignoreCache?: boolean, quietMode?: boolean, userInitiated?: boolean): Promise<Tests>;
235+
discoverTests(cmdSource: CommandSource, ignoreCache?: boolean, quietMode?: boolean, userInitiated?: boolean, clearTestStatus?: boolean): Promise<Tests>;
236236
runTest(cmdSource: CommandSource, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise<Tests>;
237237
}
238238

src/client/unittests/main.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
174174

175175
this.discoverTests(CommandSource.auto, this.workspaceService.workspaceFolders![0].uri, true).ignoreErrors();
176176
}
177-
public async discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean) {
177+
public async discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean, clearTestStatus?: boolean) {
178178
const testManager = await this.getTestManager(true, resource);
179179
if (!testManager) {
180180
return;
@@ -187,7 +187,7 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
187187
if (!this.testResultDisplay) {
188188
this.testResultDisplay = this.serviceContainer.get<ITestResultDisplay>(ITestResultDisplay);
189189
}
190-
const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, quietMode, userInitiated);
190+
const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, quietMode, userInitiated, clearTestStatus);
191191
this.testResultDisplay.displayDiscoverStatus(discoveryPromise, quietMode)
192192
.catch(ex => console.error('Python Extension: displayDiscoverStatus', ex));
193193
await discoveryPromise;
@@ -354,7 +354,7 @@ export class UnitTestManagementService implements IUnitTestManagementService, Di
354354
commandManager.registerCommand(constants.Commands.Tests_Discover, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => {
355355
// Ignore the exceptions returned.
356356
// This command will be invoked from other places of the extension.
357-
this.discoverTests(cmdSource, resource, true, true)
357+
this.discoverTests(cmdSource, resource, true, true, false, true)
358358
.ignoreErrors();
359359
}),
360360
commandManager.registerCommand(constants.Commands.Tests_Configure, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => {

0 commit comments

Comments
 (0)