import * as path from 'path' ; import { ParseOptions , TestParser } from '../../test-parser' ; import { parseStringPromise } from 'xml2js' ; import { JunitReport , SingleSuiteReport , TestCase , TestSuite } from './java-junit-types' ; import { normalizeFilePath } from '../../utils/path-utils' ; import { TestExecutionResult , TestRunResult , TestSuiteResult , TestGroupResult , TestCaseResult , TestCaseError } from '../../test-results' ; export class JavaJunitParser implements TestParser { readonly trackedFiles : { [ fileName ] : string [ ] ; } ; constructor ( readonly options ) { this . trackedFiles = { } ; for ( const filePath of options . trackedFiles ) { const fileName = path . basename ( filePath ) ; const files = this . trackedFiles [ fileName ] ?? ( this . trackedFiles [ fileName ] = [ ] ) ; files . push ( normalizeFilePath ( filePath ) ) ; } } async parse ( filePath , content ) { const reportOrSuite = await this . getJunitReport ( filePath , content ) ; const isReport = ( reportOrSuite as JunitReport ) . testsuites !== undefined ; let ju ; if ( isReport ) { ju = reportOrSuite as JunitReport ; } else { const suite = ( reportOrSuite as SingleSuiteReport ) . testsuite ; ju = { testsuites : { $ : { time : suite . $ . time } , testsuite : [ suite ] } } ; } return this . getTestRunResult ( filePath , ju ) ; } private async getJunitReport ( filePath , content ) { try { return await parseStringPromise ( content ) ; } catch ( e ) { throw new Error ( `Invalid XML at ${ filePath }\n\n${ e }` ) ; } } private getTestRunResult ( filePath , junit ) { const suites = junit . testsuites . testsuite === undefined ? [ ] : junit . testsuites . testsuite . map ( ts => { const name = ts . $ . name . trim ( ) ; const time = parseFloat ( ts . $ . time ) * 1000 ; const sr = new TestSuiteResult ( name , this . getGroups ( ts ) , time ) ; return sr ; } ) ; const seconds = parseFloat ( junit . testsuites . $ ?. time ) ; const time = isNaN ( seconds ) ? undefined : seconds * 1000 ; return new TestRunResult ( filePath , suites , time ) ; } private getGroups ( suite ) { if ( suite . testcase === undefined ) { return [ ] ; } const groups = [ ] ; for ( const tc of suite . testcase ) { const className = tc . $ . classname === suite . $ . name ? '' : tc . $ . classname ; let grp = groups . find ( g => g . name === className ) ; if ( grp === undefined ) { grp = { name : className , tests : [ ] } ; groups . push ( grp ) ; } grp . tests . push ( tc ) ; } return groups . map ( grp => { const tests = grp . tests . map ( tc => { const name = tc . $ . name . trim ( ) ; const result = this . getTestCaseResult ( tc ) ; const time = parseFloat ( tc . $ . time ) * 1000 ; const error = this . getTestCaseError ( tc ) ; return new TestCaseResult ( name , result , time , error ) ; } ) ; return new TestGroupResult ( grp . name , tests ) ; } ) ; } private getTestCaseResult ( test ) { if ( test . failure || test . error ) return 'failed' ; if ( test . skipped ) return 'skipped' ; return 'success' ; } private getTestCaseError ( tc ) { if ( ! this . options . parseErrors ) { return undefined ; } const failures = tc . failure ?? tc . error ; if ( ! failures ) { return undefined ; } const failure = failures [ 0 ] ; const details = typeof failure === 'object' ? failure . _ : failure ; let filePath ; let line ; const src = this . exceptionThrowSource ( details ) ; if ( src ) { filePath = src . filePath ; line = src . line ; } return { path : filePath , line , details , message : typeof failure === 'object' ? failure . message : undefined } ; } private exceptionThrowSource ( stackTrace ) { const lines = stackTrace . split ( /\r?\n/ ) ; const re = /^at (.*)\((.*):(\d+)\)$/ ; for ( const str of lines ) { const match = str . match ( re ) ; if ( match !== null ) { const [ _ , tracePath , fileName , lineStr ] = match ; const filePath = this . getFilePath ( tracePath , fileName ) ; if ( filePath !== undefined ) { const line = parseInt ( lineStr ) ; return { filePath , line } ; } } } } private getFilePath ( tracePath , fileName ) { const files = this . trackedFiles [ fileName ] ; if ( files === undefined ) { return undefined ; } const packageParts = tracePath . split ( /\./g ) ; const packageIndex = packageParts . findIndex ( part => part [ 0 ] <= 'Z' ) ; if ( packageIndex !== - 1 ) { packageParts . splice ( packageIndex , packageParts . length - packageIndex ) ; } if ( packageParts . length === 0 ) { return undefined ; } for ( const filePath of files ) { const dirs = path . dirname ( filePath ) . split ( /\//g ) ; if ( packageParts . length > dirs . length ) { continue ; } if ( dirs . length > packageParts . length ) { dirs . splice ( 0 , dirs . length - packageParts . length ) ; } const isMatch = packageParts . every ( ( part , i ) => part === dirs [ i ] ) ; if ( isMatch ) { return filePath ; } } return undefined ; } }