import { TestReconciler , JestTotalResults , TestFileAssertionStatus , IParseResults , parse , TestAssertionStatus , } from 'jest-editor-support' ; import { TestReconciliationState , TestReconciliationStateType } from './TestReconciliationState' ; import { TestResult , TestResultStatusInfo } from './TestResult' ; import * as match from './match-by-context' ; import { JestSessionEvents } from '../JestExt' ; import { TestStats } from '../types' ; import { emptyTestStats } from '../helpers' ; import { createTestResultEvents , TestResultEvents } from './test-result-events' ; import { ContainerNode } from './match-node' ; import { JestProcessInfo } from '../JestProcessManagement' ; export interface TestSuiteResult { status : TestReconciliationStateType ; message : string ; assertionContainer ? : ContainerNode < TestAssertionStatus > ; results ? : TestResult [ ] ; sorted ? : SortedTestResults ; } export interface SortedTestResults { fail : TestResult [ ] ; skip : TestResult [ ] ; success : TestResult [ ] ; unknown : TestResult [ ] ; } const sortByStatus = ( a , b ) : number => { if ( a . status === b . status ) { return 0 ; } return TestResultStatusInfo [ a . status ] . precedence - TestResultStatusInfo [ b . status ] . precedence ; } ; export class TestResultProvider { verbose : boolean ; events : TestResultEvents ; private reconciler : TestReconciler ; private testSuites : Map < string , TestSuiteResult > ; private testFiles ? : string [ ] ; constructor ( extEvents , verbose = false ) { this . reconciler = new TestReconciler ( ) ; this . verbose = verbose ; this . events = createTestResultEvents ( ) ; this . testSuites = new Map ( ) ; extEvents . onTestSessionStarted . event ( this . onSessionStart . bind ( this ) ) ; } dispose ( ) { this . events . testListUpdated . dispose ( ) ; this . events . testSuiteChanged . dispose ( ) ; } private onSessionStart ( ) { this . testSuites . clear ( ) ; this . reconciler = new TestReconciler ( ) ; } private groupByRange ( results ) { if ( ! results . length ) { return results ; } const byRange = new Map ( ) ; results . forEach ( ( r ) => { const key = `${ r . start . line }-${ r . start . column }-${ r . end . line }-${ r . end . column }` ; const list = byRange . get ( key ) ; if ( ! list ) { byRange . set ( key , [ r ] ) ; } else { list . push ( r ) ; } } ) ; byRange . forEach ( ( list ) => list . sort ( sortByStatus ) ) ; const consolidated = [ ] ; byRange . forEach ( ( list ) => { if ( list . length > 1 ) { list [ 0 ] . multiResults = list . slice ( 1 ) ; } consolidated . push ( list [ 0 ] ) ; } ) ; return consolidated ; } updateTestFileList ( testFiles ? ) { this . testFiles = testFiles ; this . testSuites . clear ( ) ; this . events . testListUpdated . fire ( testFiles ) ; } getTestList ( ) { if ( this . testFiles && this . testFiles . length > 0 ) { return this . testFiles ; } return Array . from ( this . testSuites . keys ( ) ) ; } isTestFile ( fileName ) { if ( this . testFiles ?. includes ( fileName ) || this . testSuites . get ( fileName ) != null ) { return 'yes' ; } if ( ! this . testFiles ) { return 'unknown' ; } return 'no' ; } public getTestSuiteResult ( filePath ) { const cache = this . testSuites . get ( filePath ) ; if ( cache && ! cache . assertionContainer ) { const assertions = this . reconciler . assertionsForTestFile ( filePath ) ; if ( assertions && assertions . length > 0 ) { cache . assertionContainer = match . buildAssertionContainer ( assertions ) ; this . testSuites . set ( filePath , cache ) ; } } return cache ; } private matchResults ( filePath , { root , itBlocks } ) { let error ; try { const cache = this . getTestSuiteResult ( filePath ) ; if ( cache ?. assertionContainer ) { cache . results = this . groupByRange ( match . matchTestAssertions ( filePath , root , cache . assertionContainer , this . verbose ) ) ; this . events . testSuiteChanged . fire ( { type : 'result-matched' , file : filePath , } ) ; return cache ; } error = 'no assertion generated for file' ; } catch ( e ) { console . warn ( `failed to match test results for ${ filePath }:` , e ) ; error = `encountered internal match error: ${ e }` ; } this . events . testSuiteChanged . fire ( { type : 'test-parsed' , file : filePath , testContainer : match . buildSourceContainer ( root ) , } ) ; return { status : 'Unknown' , message : error , results : itBlocks . map ( ( t ) => match . toMatchResult ( t , 'no assertion found' , 'match-failed' ) ) , } ; } param filePath returns throws getResults ( filePath ) { const results = this . testSuites . get ( filePath ) ?. results ; if ( results ) { return results ; } if ( this . isTestFile ( filePath ) === 'no' ) { return ; } try { const parseResult = parse ( filePath ) ; this . testSuites . set ( filePath , this . matchResults ( filePath , parseResult ) ) ; return this . testSuites . get ( filePath ) ?. results ; } catch ( e ) { const message = `failed to get test results for ${ filePath }` ; console . warn ( message , e ) ; this . testSuites . set ( filePath , { status : 'KnownFail' , message , results : [ ] } ) ; throw e ; } } param filePath returns throws getSortedResults ( filePath ) { const cached = this . testSuites . get ( filePath ) ?. sorted ; if ( cached ) { return cached ; } if ( this . isTestFile ( filePath ) === 'no' ) { return ; } const result = { fail : [ ] , skip : [ ] , success : [ ] , unknown : [ ] , } ; try { const testResults = this . getResults ( filePath ) ; if ( ! testResults ) { return ; } for ( const test of testResults ) { if ( test . status === TestReconciliationState . KnownFail ) { result . fail . push ( test ) ; } else if ( test . status === TestReconciliationState . KnownSkip ) { result . skip . push ( test ) ; } else if ( test . status === TestReconciliationState . KnownSuccess ) { result . success . push ( test ) ; } else { result . unknown . push ( test ) ; } } } finally { const cached = this . testSuites . get ( filePath ) ; if ( cached ) { cached . sorted = result ; } } return result ; } updateTestResults ( data , process ) { const results = this . reconciler . updateFileWithJestStatus ( data ) ; results ?. forEach ( ( r ) => { this . testSuites . set ( r . file , { status : r . status , message : r . message , assertionContainer : r . assertions ? match . buildAssertionContainer ( r . assertions ) : undefined , } ) ; } ) ; this . events . testSuiteChanged . fire ( { type : 'assertions-updated' , files : results . map ( ( r ) => r . file ) , process , } ) ; return results ; } removeCachedResults ( filePath ) { this . testSuites . delete ( filePath ) ; } invalidateTestResults ( filePath ) { this . removeCachedResults ( filePath ) ; this . reconciler . removeTestFile ( filePath ) ; } getTestSuiteStats ( ) { const stats = emptyTestStats ( ) ; this . testSuites . forEach ( ( suite ) => { if ( suite . status === 'KnownSuccess' ) { stats . success += 1 ; } else if ( suite . status === 'KnownFail' ) { stats . fail += 1 ; } else { stats . unknown += 1 ; } } ) ; if ( this . testFiles ) { if ( this . testFiles . length > stats . fail + stats . success + stats . unknown ) { return { ... stats , unknown : this . testFiles . length - stats . fail - stats . success , } ; } } return stats ; } }