@@ -420,6 +420,143 @@ async function parseSuite(
420
420
}
421
421
}
422
422
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
+
423
560
async function parseTestCases (
424
561
suiteName : string ,
425
562
suiteFile : string | null ,
@@ -482,7 +619,7 @@ async function parseTestCases(
482
619
const testFailure = testcase . failure || testcase . error // test failed
483
620
const skip =
484
621
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
486
623
const success = ! testFailure // not a failure -> thus a success
487
624
const annotationLevel = success || skip ? 'notice' : 'failure' // a skipped test shall not fail the run
488
625
@@ -501,9 +638,7 @@ async function parseTestCases(
501
638
? Array . isArray ( testcase . failure )
502
639
? testcase . failure
503
640
: [ testcase . failure ]
504
- : undefined
505
- // the action only supports 1 failure per testcase
506
- const failure = failures ? failures [ 0 ] : undefined
641
+ : [ ]
507
642
508
643
// identify the number of flaky failures
509
644
const flakyFailuresCount = testcase . flakyFailure
@@ -512,103 +647,43 @@ async function parseTestCases(
512
647
: 1
513
648
: 0
514
649
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
566
684
}
567
685
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
612
687
if ( limit >= 0 && annotations . length >= limit ) break
613
688
}
614
689
0 commit comments