@@ -420,6 +420,143 @@ async function parseSuite(
420420 }
421421}
422422
423+ /**
424+ * Helper function to create an annotation for a test case
425+ */
426+ async function createTestCaseAnnotation (
427+ testcase : any ,
428+ failure : any | null ,
429+ failureIndex : number ,
430+ totalFailures : number ,
431+ suiteName : string ,
432+ suiteFile : string | null ,
433+ suiteLine : string | null ,
434+ breadCrumb : string ,
435+ testTime : number ,
436+ skip : boolean ,
437+ success : boolean ,
438+ annotationLevel : 'failure' | 'notice' | 'warning' ,
439+ flakyFailuresCount : number ,
440+ annotateNotice : boolean ,
441+ failed : boolean ,
442+ excludeSources : string [ ] ,
443+ checkTitleTemplate : string | undefined ,
444+ testFilesPrefix : string ,
445+ transformer : Transformer [ ] ,
446+ followSymlink : boolean ,
447+ truncateStackTraces : boolean ,
448+ resolveIgnoreClassname : boolean
449+ ) : Promise < Annotation > {
450+ // Extract stack trace based on whether we have a failure or error
451+ const stackTrace : string = (
452+ ( failure && failure . _cdata ) ||
453+ ( failure && failure . _text ) ||
454+ ( testcase . error && testcase . error . _cdata ) ||
455+ ( testcase . error && testcase . error . _text ) ||
456+ ''
457+ )
458+ . toString ( )
459+ . trim ( )
460+
461+ const stackTraceMessage = truncateStackTraces ? stackTrace . split ( '\n' ) . slice ( 0 , 2 ) . join ( '\n' ) : stackTrace
462+
463+ // Extract message based on failure or error
464+ const message : string = (
465+ ( failure && failure . _attributes && failure . _attributes . message ) ||
466+ ( testcase . error && testcase . error . _attributes && testcase . error . _attributes . message ) ||
467+ stackTraceMessage ||
468+ testcase . _attributes . name
469+ ) . trim ( )
470+
471+ // Determine class name for resolution
472+ let resolveClassname = testcase . _attributes . name
473+ if ( ! resolveIgnoreClassname && testcase . _attributes . classname ) {
474+ resolveClassname = testcase . _attributes . classname
475+ }
476+
477+ // Resolve file and line information
478+ const pos = await resolveFileAndLine (
479+ testcase . _attributes . file || failure ?. _attributes ?. file || suiteFile ,
480+ testcase . _attributes . line || failure ?. _attributes ?. line || suiteLine ,
481+ resolveClassname ,
482+ stackTrace
483+ )
484+
485+ // Apply transformations to filename
486+ let transformedFileName = pos . fileName
487+ for ( const r of transformer ) {
488+ transformedFileName = applyTransformer ( r , transformedFileName )
489+ }
490+
491+ // Resolve the full path
492+ const githubWorkspacePath = process . env [ 'GITHUB_WORKSPACE' ]
493+ let resolvedPath : string = transformedFileName
494+ if ( failed || ( annotateNotice && success ) ) {
495+ if ( fs . existsSync ( transformedFileName ) ) {
496+ resolvedPath = transformedFileName
497+ } else if ( githubWorkspacePath && fs . existsSync ( `${ githubWorkspacePath } ${ transformedFileName } ` ) ) {
498+ resolvedPath = `${ githubWorkspacePath } ${ transformedFileName } `
499+ } else {
500+ resolvedPath = await resolvePath ( githubWorkspacePath || '' , transformedFileName , excludeSources , followSymlink )
501+ }
502+ }
503+
504+ core . debug ( `Path prior to stripping: ${ resolvedPath } ` )
505+ if ( githubWorkspacePath ) {
506+ resolvedPath = resolvedPath . replace ( `${ githubWorkspacePath } /` , '' ) // strip workspace prefix, make the path relative
507+ }
508+
509+ // Generate title
510+ let title = ''
511+ if ( checkTitleTemplate ) {
512+ // ensure to not duplicate the test_name if file_name is equal
513+ const fileName = pos . fileName !== testcase . _attributes . name ? pos . fileName : ''
514+ const baseClassName = testcase . _attributes . classname ? testcase . _attributes . classname : testcase . _attributes . name
515+ const className = baseClassName . split ( '.' ) . slice ( - 1 ) [ 0 ]
516+ title = checkTitleTemplate
517+ . replace ( templateVar ( 'FILE_NAME' ) , fileName )
518+ . replace ( templateVar ( 'BREAD_CRUMB' ) , breadCrumb ?? '' )
519+ . replace ( templateVar ( 'SUITE_NAME' ) , suiteName ?? '' )
520+ . replace ( templateVar ( 'TEST_NAME' ) , testcase . _attributes . name )
521+ . replace ( templateVar ( 'CLASS_NAME' ) , className )
522+ } else if ( pos . fileName !== testcase . _attributes . name ) {
523+ // special handling to use class name only for title in case class name was ignored for `resolveClassname`
524+ if ( resolveIgnoreClassname && testcase . _attributes . classname ) {
525+ title = `${ testcase . _attributes . classname } .${ testcase . _attributes . name } `
526+ } else {
527+ title = `${ pos . fileName } .${ testcase . _attributes . name } `
528+ }
529+ } else {
530+ title = `${ testcase . _attributes . name } `
531+ }
532+
533+ // Add failure index to title if multiple failures exist
534+ if ( totalFailures > 1 ) {
535+ title = `${ title } (failure ${ failureIndex + 1 } /${ totalFailures } )`
536+ }
537+
538+ // optionally attach the prefix to the path
539+ resolvedPath = testFilesPrefix ? pathHelper . join ( testFilesPrefix , resolvedPath ) : resolvedPath
540+
541+ const testTimeString = testTime > 0 ? `${ testTime } s` : ''
542+ core . info ( `${ resolvedPath } :${ pos . line } | ${ message . split ( '\n' , 1 ) [ 0 ] } ${ testTimeString } ` )
543+
544+ return {
545+ path : resolvedPath ,
546+ start_line : pos . line ,
547+ end_line : pos . line ,
548+ start_column : 0 ,
549+ end_column : 0 ,
550+ retries : ( testcase . retries || 0 ) + flakyFailuresCount ,
551+ annotation_level : annotationLevel ,
552+ status : skip ? 'skipped' : success ? 'success' : 'failure' ,
553+ title : escapeEmoji ( title ) ,
554+ message : escapeEmoji ( message ) ,
555+ raw_details : escapeEmoji ( stackTrace ) ,
556+ time : testTime
557+ }
558+ }
559+
423560async function parseTestCases (
424561 suiteName : string ,
425562 suiteFile : string | null ,
@@ -482,7 +619,7 @@ async function parseTestCases(
482619 const testFailure = testcase . failure || testcase . error // test failed
483620 const skip =
484621 testcase . skipped || testcase . _attributes . status === 'disabled' || testcase . _attributes . status === 'ignored'
485- const failed = testFailure && ! skip // test faiure , but was skipped -> don't fail if a ignored test failed
622+ const failed = testFailure && ! skip // test failure , but was skipped -> don't fail if a ignored test failed
486623 const success = ! testFailure // not a failure -> thus a success
487624 const annotationLevel = success || skip ? 'notice' : 'failure' // a skipped test shall not fail the run
488625
@@ -501,9 +638,7 @@ async function parseTestCases(
501638 ? Array . isArray ( testcase . failure )
502639 ? testcase . failure
503640 : [ testcase . failure ]
504- : undefined
505- // the action only supports 1 failure per testcase
506- const failure = failures ? failures [ 0 ] : undefined
641+ : [ ]
507642
508643 // identify the number of flaky failures
509644 const flakyFailuresCount = testcase . flakyFailure
@@ -512,103 +647,43 @@ async function parseTestCases(
512647 : 1
513648 : 0
514649
515- const stackTrace : string = (
516- ( failure && failure . _cdata ) ||
517- ( failure && failure . _text ) ||
518- ( testcase . error && testcase . error . _cdata ) ||
519- ( testcase . error && testcase . error . _text ) ||
520- ''
521- )
522- . toString ( )
523- . trim ( )
524-
525- const stackTraceMessage = truncateStackTraces ? stackTrace . split ( '\n' ) . slice ( 0 , 2 ) . join ( '\n' ) : stackTrace
526-
527- const message : string = (
528- ( failure && failure . _attributes && failure . _attributes . message ) ||
529- ( testcase . error && testcase . error . _attributes && testcase . error . _attributes . message ) ||
530- stackTraceMessage ||
531- testcase . _attributes . name
532- ) . trim ( )
533-
534- let resolveClassname = testcase . _attributes . name
535- if ( ! resolveIgnoreClassname && testcase . _attributes . classname ) {
536- resolveClassname = testcase . _attributes . classname
537- }
538-
539- const pos = await resolveFileAndLine (
540- testcase . _attributes . file || failure ?. _attributes ?. file || suiteFile ,
541- testcase . _attributes . line || failure ?. _attributes ?. line || suiteLine ,
542- resolveClassname ,
543- stackTrace
544- )
545-
546- let transformedFileName = pos . fileName
547- for ( const r of transformer ) {
548- transformedFileName = applyTransformer ( r , transformedFileName )
549- }
550-
551- const githubWorkspacePath = process . env [ 'GITHUB_WORKSPACE' ]
552- let resolvedPath : string = transformedFileName
553- if ( failed || ( annotateNotice && success ) ) {
554- if ( fs . existsSync ( transformedFileName ) ) {
555- resolvedPath = transformedFileName
556- } else if ( githubWorkspacePath && fs . existsSync ( `${ githubWorkspacePath } ${ transformedFileName } ` ) ) {
557- resolvedPath = `${ githubWorkspacePath } ${ transformedFileName } `
558- } else {
559- resolvedPath = await resolvePath ( githubWorkspacePath || '' , transformedFileName , excludeSources , followSymlink )
560- }
561- }
562-
563- core . debug ( `Path prior to stripping: ${ resolvedPath } ` )
564- if ( githubWorkspacePath ) {
565- resolvedPath = resolvedPath . replace ( `${ githubWorkspacePath } /` , '' ) // strip workspace prefix, make the path relative
650+ // Handle multiple failures or single case (success/skip/error)
651+ const failuresToProcess = failures . length > 0 ? failures : [ null ] // Process at least once for non-failure cases
652+
653+ for ( let failureIndex = 0 ; failureIndex < failuresToProcess . length ; failureIndex ++ ) {
654+ const failure = failuresToProcess [ failureIndex ]
655+
656+ const annotation = await createTestCaseAnnotation (
657+ testcase ,
658+ failure ,
659+ failureIndex ,
660+ failures . length ,
661+ suiteName ,
662+ suiteFile ,
663+ suiteLine ,
664+ breadCrumb ,
665+ testTime ,
666+ skip ,
667+ success ,
668+ annotationLevel ,
669+ flakyFailuresCount ,
670+ annotateNotice ,
671+ failed ,
672+ excludeSources ,
673+ checkTitleTemplate ,
674+ testFilesPrefix ,
675+ transformer ,
676+ followSymlink ,
677+ truncateStackTraces ,
678+ resolveIgnoreClassname
679+ )
680+
681+ annotations . push ( annotation )
682+
683+ if ( limit >= 0 && annotations . length >= limit ) break
566684 }
567685
568- let title = ''
569- if ( checkTitleTemplate ) {
570- // ensure to not duplicate the test_name if file_name is equal
571- const fileName = pos . fileName !== testcase . _attributes . name ? pos . fileName : ''
572- const baseClassName = testcase . _attributes . classname ? testcase . _attributes . classname : testcase . _attributes . name
573- const className = baseClassName . split ( '.' ) . slice ( - 1 ) [ 0 ]
574- title = checkTitleTemplate
575- . replace ( templateVar ( 'FILE_NAME' ) , fileName )
576- . replace ( templateVar ( 'BREAD_CRUMB' ) , breadCrumb ?? '' )
577- . replace ( templateVar ( 'SUITE_NAME' ) , suiteName ?? '' )
578- . replace ( templateVar ( 'TEST_NAME' ) , testcase . _attributes . name )
579- . replace ( templateVar ( 'CLASS_NAME' ) , className )
580- } else if ( pos . fileName !== testcase . _attributes . name ) {
581- // special handling to use class name only for title in face class name was ignored for `resolveClassname1
582- if ( resolveIgnoreClassname && testcase . _attributes . classname ) {
583- title = `${ testcase . _attributes . classname } .${ testcase . _attributes . name } `
584- } else {
585- title = `${ pos . fileName } .${ testcase . _attributes . name } `
586- }
587- } else {
588- title = `${ testcase . _attributes . name } `
589- }
590-
591- // optionally attach the prefix to the path
592- resolvedPath = testFilesPrefix ? pathHelper . join ( testFilesPrefix , resolvedPath ) : resolvedPath
593-
594- const testTimeString = testTime > 0 ? `${ testTime } s` : ''
595- core . info ( `${ resolvedPath } :${ pos . line } | ${ message . split ( '\n' , 1 ) [ 0 ] } ${ testTimeString } ` )
596-
597- annotations . push ( {
598- path : resolvedPath ,
599- start_line : pos . line ,
600- end_line : pos . line ,
601- start_column : 0 ,
602- end_column : 0 ,
603- retries : ( testcase . retries || 0 ) + flakyFailuresCount ,
604- annotation_level : annotationLevel ,
605- status : skip ? 'skipped' : success ? 'success' : 'failure' ,
606- title : escapeEmoji ( title ) ,
607- message : escapeEmoji ( message ) ,
608- raw_details : escapeEmoji ( stackTrace ) ,
609- time : testTime
610- } )
611-
686+ // Break from the outer testcase loop if we've reached the limit
612687 if ( limit >= 0 && annotations . length >= limit ) break
613688 }
614689
0 commit comments