1- import { CancellationToken , CancellationTokenSource , Disposable , OutputChannel , Uri } from 'vscode' ;
1+ import { CancellationToken , CancellationTokenSource , Diagnostic , DiagnosticCollection , DiagnosticRelatedInformation , Disposable , languages , OutputChannel , Uri } from 'vscode' ;
22import { IWorkspaceService } from '../../../common/application/types' ;
33import { isNotInstalledError } from '../../../common/helpers' ;
4+ import { IFileSystem } from '../../../common/platform/types' ;
45import { IConfigurationService , IDisposableRegistry , IInstaller , IOutputChannel , IPythonSettings , Product } from '../../../common/types' ;
56import { getNamesAndValues } from '../../../common/utils/enum' ;
67import { IServiceContainer } from '../../../ioc/types' ;
78import { UNITTEST_DISCOVER , UNITTEST_RUN } from '../../../telemetry/constants' ;
89import { sendTelemetryEvent } from '../../../telemetry/index' ;
910import { TestDiscoverytTelemetry , TestRunTelemetry } from '../../../telemetry/types' ;
11+ import { IPythonUnitTestMessage , IUnitTestDiagnosticService } from '../../types' ;
1012import { CANCELLATION_REASON , CommandSource , TEST_OUTPUT_CHANNEL } from './../constants' ;
1113import { ITestCollectionStorageService , ITestDiscoveryService , ITestManager , ITestResultsService , ITestsHelper , TestDiscoveryOptions , TestProvider , Tests , TestStatus , TestsToRun } from './../types' ;
1214
@@ -16,7 +18,9 @@ enum CancellationTokenType {
1618}
1719
1820export abstract class BaseTestManager implements ITestManager {
21+ public diagnosticCollection : DiagnosticCollection ;
1922 protected readonly settings : IPythonSettings ;
23+ private readonly unitTestDiagnosticService : IUnitTestDiagnosticService ;
2024 public abstract get enabled ( ) : boolean ;
2125 protected get outputChannel ( ) {
2226 return this . _outputChannel ;
@@ -50,6 +54,8 @@ export abstract class BaseTestManager implements ITestManager {
5054 this . testCollectionStorage = this . serviceContainer . get < ITestCollectionStorageService > ( ITestCollectionStorageService ) ;
5155 this . _testResultsService = this . serviceContainer . get < ITestResultsService > ( ITestResultsService ) ;
5256 this . workspaceService = this . serviceContainer . get < IWorkspaceService > ( IWorkspaceService ) ;
57+ this . diagnosticCollection = languages . createDiagnosticCollection ( this . testProvider ) ;
58+ this . unitTestDiagnosticService = serviceContainer . get < IUnitTestDiagnosticService > ( IUnitTestDiagnosticService ) ;
5359 disposables . push ( this ) ;
5460 }
5561 protected get testDiscoveryCancellationToken ( ) : CancellationToken | undefined {
@@ -246,6 +252,42 @@ export abstract class BaseTestManager implements ITestManager {
246252 return Promise . reject < Tests > ( reason ) ;
247253 } ) ;
248254 }
255+ public async updateDiagnostics ( tests : Tests , messages : IPythonUnitTestMessage [ ] ) : Promise < void > {
256+ await this . stripStaleDiagnostics ( tests , messages ) ;
257+
258+ // Update relevant file diagnostics for tests that have problems.
259+ const uniqueMsgFiles = messages . reduce ( ( filtered , msg ) => {
260+ if ( filtered . indexOf ( msg . testFilePath ) === - 1 && msg . testFilePath !== undefined ) {
261+ filtered . push ( msg . testFilePath ) ;
262+ }
263+ return filtered ;
264+ } , [ ] ) ;
265+ const fs = this . serviceContainer . get < IFileSystem > ( IFileSystem ) ;
266+ for ( const msgFile of uniqueMsgFiles ) {
267+ // Check all messages against each test file.
268+ const fileUri = Uri . file ( msgFile ) ;
269+ if ( ! this . diagnosticCollection . has ( fileUri ) ) {
270+ // Create empty diagnostic for file URI so the rest of the logic can assume one already exists.
271+ const diagnostics : Diagnostic [ ] = [ ] ;
272+ this . diagnosticCollection . set ( fileUri , diagnostics ) ;
273+ }
274+ // Get the diagnostics for this file's URI before updating it so old tests that weren't run can still show problems.
275+ const oldDiagnostics = this . diagnosticCollection . get ( fileUri ) ;
276+ const newDiagnostics : Diagnostic [ ] = [ ] ;
277+ for ( const diagnostic of oldDiagnostics ) {
278+ newDiagnostics . push ( diagnostic ) ;
279+ }
280+ for ( const msg of messages ) {
281+ if ( fs . arePathsSame ( fileUri . fsPath , Uri . file ( msg . testFilePath ) . fsPath ) && msg . status !== TestStatus . Pass ) {
282+ const diagnostic = this . createDiagnostics ( msg ) ;
283+ newDiagnostics . push ( diagnostic ) ;
284+ }
285+ }
286+
287+ // Set the diagnostics for the file.
288+ this . diagnosticCollection . set ( fileUri , newDiagnostics ) ;
289+ }
290+ }
249291 // tslint:disable-next-line:no-any
250292 protected abstract runTestImpl ( tests : Tests , testsToRun ?: TestsToRun , runFailedTests ?: boolean , debug ?: boolean ) : Promise < any > ;
251293 protected abstract getDiscoveryOptions ( ignoreCache : boolean ) : TestDiscoveryOptions ;
@@ -270,4 +312,51 @@ export abstract class BaseTestManager implements ITestManager {
270312 this . testRunnerCancellationTokenSource = undefined ;
271313 }
272314 }
315+ /**
316+ * Whenever a test is run, any previous problems it had should be removed. This runs through
317+ * every already existing set of diagnostics for any that match the tests that were just run
318+ * so they can be stripped out (as they are now no longer relevant). If the tests pass, then
319+ * there is no need to have a diagnostic for it. If they fail, the stale diagnostic will be
320+ * replaced by an up-to-date diagnostic showing the most recent problem with that test.
321+ *
322+ * In order to identify diagnostics associated with the tests that were run, the `nameToRun`
323+ * property of each messages is compared to the `code` property of each diagnostic.
324+ *
325+ * @param messages Details about the tests that were just run.
326+ */
327+ private async stripStaleDiagnostics ( tests : Tests , messages : IPythonUnitTestMessage [ ] ) : Promise < void > {
328+ this . diagnosticCollection . forEach ( ( diagnosticUri , oldDiagnostics , collection ) => {
329+ const newDiagnostics : Diagnostic [ ] = [ ] ;
330+ for ( const diagnostic of oldDiagnostics ) {
331+ const matchingMsg = messages . find ( ( msg ) => msg . code === diagnostic . code ) ;
332+ if ( matchingMsg === undefined ) {
333+ // No matching message was found, so this test was not included in the test run.
334+ const matchingTest = tests . testFunctions . find ( ( tf ) => tf . testFunction . nameToRun === diagnostic . code ) ;
335+ if ( matchingTest !== undefined ) {
336+ // Matching test was found, so the diagnostic is still relevant.
337+ newDiagnostics . push ( diagnostic ) ;
338+ }
339+ }
340+ }
341+ // Set the diagnostics for the file.
342+ collection . set ( diagnosticUri , newDiagnostics ) ;
343+ } ) ;
344+ }
345+
346+ private createDiagnostics ( message : IPythonUnitTestMessage ) : Diagnostic {
347+ const stackStart = message . locationStack [ 0 ] ;
348+ const diagPrefix = this . unitTestDiagnosticService . getMessagePrefix ( message . status ) ;
349+ const severity = this . unitTestDiagnosticService . getSeverity ( message . severity ) ! ;
350+ const diagMsg = message . message . split ( '\n' ) [ 0 ] ;
351+ const diagnostic = new Diagnostic ( stackStart . location . range , `${ diagPrefix ? `${ diagPrefix } : ` : '' } ${ diagMsg } ` , severity ) ;
352+ diagnostic . code = message . code ;
353+ diagnostic . source = message . provider ;
354+ const relatedInfoArr : DiagnosticRelatedInformation [ ] = [ ] ;
355+ for ( const frameDetails of message . locationStack ) {
356+ const relatedInfo = new DiagnosticRelatedInformation ( frameDetails . location , frameDetails . lineText ) ;
357+ relatedInfoArr . push ( relatedInfo ) ;
358+ }
359+ diagnostic . relatedInformation = relatedInfoArr ;
360+ return diagnostic ;
361+ }
273362}
0 commit comments