2121import com .consol .citrus .TestCaseMetaInfo ;
2222import com .consol .citrus .actions .AbstractTestAction ;
2323import com .consol .citrus .actions .FailAction ;
24+ import com .consol .citrus .container .SequenceAfterSuite ;
25+ import com .consol .citrus .container .SequenceBeforeSuite ;
2426import com .consol .citrus .context .TestContext ;
2527import com .consol .citrus .dsl .design .DefaultTestDesigner ;
2628import com .consol .citrus .dsl .design .TestDesigner ;
29+ import com .consol .citrus .report .TestReporters ;
2730import io .qameta .allure .Allure ;
2831import io .qameta .allure .AllureLifecycle ;
32+ import io .qameta .allure .Description ;
2933import io .qameta .allure .Step ;
34+ import io .qameta .allure .model .GlobalError ;
35+ import io .qameta .allure .model .Globals ;
3036import io .qameta .allure .model .Label ;
3137import io .qameta .allure .model .Parameter ;
3238import io .qameta .allure .model .Stage ;
4046import io .qameta .allure .test .IsolatedLifecycle ;
4147import io .qameta .allure .test .RunUtils ;
4248import org .junit .jupiter .api .Test ;
49+ import org .opentest4j .AssertionFailedError ;
4350
4451import java .time .Instant ;
52+ import java .util .List ;
53+ import java .util .function .Consumer ;
4554
55+ import static io .qameta .allure .test .AllureTestCommonsUtils .attach ;
4656import static io .qameta .allure .util .ResultsUtils .md5 ;
4757import static org .assertj .core .api .Assertions .assertThat ;
58+ import static org .assertj .core .api .Assertions .assertThatThrownBy ;
4859import static org .assertj .core .api .Assertions .tuple ;
4960@ SuppressWarnings ("unchecked" )
5061@ IsolatedLifecycle
@@ -94,6 +105,7 @@ void shouldSetStatus() {
94105 assertThat (results .getTestResults ())
95106 .extracting (TestResult ::getStatus )
96107 .containsExactly (Status .PASSED );
108+ assertThat (results .getGlobals ()).isEmpty ();
97109 }
98110
99111 @ AllureFeatures .BrokenTests
@@ -107,6 +119,7 @@ void shouldSetBrokenStatus() {
107119 assertThat (results .getTestResults ())
108120 .extracting (TestResult ::getStatus )
109121 .containsExactly (Status .BROKEN );
122+ assertThat (results .getGlobals ()).isEmpty ();
110123 }
111124
112125 @ AllureFeatures .FailedTests
@@ -125,6 +138,7 @@ public void doExecute(final TestContext context) {
125138 assertThat (results .getTestResults ())
126139 .extracting (TestResult ::getStatus )
127140 .containsExactly (Status .FAILED );
141+ assertThat (results .getGlobals ()).isEmpty ();
128142 }
129143
130144 @ AllureFeatures .FailedTests
@@ -281,6 +295,180 @@ void shouldReportDisabledTestsWithIds() {
281295 });
282296 }
283297
298+ /**
299+ * A suite setup failure is reported as a global error with its phase, exception details, and timestamp.
300+ */
301+ @ Test
302+ @ Description
303+ void shouldReportBeforeSuiteFailureAsGlobalError () {
304+ final CitrusContext context = suiteContext ()
305+ .beforeSuite (
306+ SequenceBeforeSuite .Builder .beforeSuite ()
307+ .actions (FailAction .Builder .fail ("setup action failed" ))
308+ .build ()
309+ )
310+ .build ();
311+ final long started = System .currentTimeMillis ();
312+
313+ final AllureResults results = runSuite (
314+ context , citrus -> assertThatThrownBy (() -> citrus .beforeSuite ("suite" ))
315+ .isInstanceOf (AssertionError .class )
316+ .hasRootCauseMessage ("setup action failed" )
317+ );
318+
319+ final List <Globals > globals = results .getGlobals ();
320+ assertThat (globals ).hasSize (1 );
321+ final List <GlobalError > errors = globals .get (0 ).getErrors ();
322+ assertThat (errors ).hasSize (1 );
323+ final GlobalError error = errors .get (0 );
324+ assertThat (error .getMessage ()).contains ("Citrus suite setup failed" , "setup action failed" );
325+ assertThat (error .getTrace ()).contains ("setup action failed" , "com.consol.citrus.actions.FailAction" );
326+ assertThat (error .getTimestamp ()).isBetween (started , System .currentTimeMillis ());
327+ }
328+
329+ /**
330+ * A cleanup sequence failure is reported as a global error with the aggregate exception supplied by Citrus.
331+ */
332+ @ Test
333+ @ Description
334+ void shouldReportAfterSuiteFailureAsGlobalError () {
335+ final CitrusContext context = suiteContext ()
336+ .afterSuite (
337+ SequenceAfterSuite .Builder .afterSuite ()
338+ .actions (FailAction .Builder .fail ("cleanup action failed" ))
339+ .build ()
340+ )
341+ .build ();
342+
343+ final AllureResults results = runSuite (context , citrus -> {
344+ citrus .beforeSuite ("suite" );
345+ assertThatThrownBy (() -> citrus .afterSuite ("suite" ))
346+ .isInstanceOf (AssertionError .class )
347+ .hasRootCauseMessage ("Error in after suite" );
348+ });
349+
350+ final List <Globals > globals = results .getGlobals ();
351+ assertThat (globals ).hasSize (1 );
352+ final List <GlobalError > errors = globals .get (0 ).getErrors ();
353+ assertThat (errors ).hasSize (1 );
354+ final GlobalError error = errors .get (0 );
355+ assertThat (error .getMessage ()).contains ("Citrus suite teardown failed" , "Error in after suite" );
356+ assertThat (error .getTrace ()).contains ("Error in after suite" , "com.consol.citrus.container.SequenceAfterSuite" );
357+ assertThat (error .getTimestamp ()).isPositive ();
358+ }
359+
360+ /**
361+ * Setup and teardown failures in the same suite are reported as separate global errors identifying each phase.
362+ */
363+ @ Test
364+ @ Description
365+ void shouldReportBothSuiteFailuresAsGlobalErrors () {
366+ final CitrusContext context = suiteContext ()
367+ .beforeSuite (
368+ SequenceBeforeSuite .Builder .beforeSuite ()
369+ .actions (FailAction .Builder .fail ("setup action failed" ))
370+ .build ()
371+ )
372+ .afterSuite (
373+ SequenceAfterSuite .Builder .afterSuite ()
374+ .actions (FailAction .Builder .fail ("cleanup action failed" ))
375+ .build ()
376+ )
377+ .build ();
378+
379+ final AllureResults results = runSuite (
380+ context , citrus -> assertThatThrownBy (() -> citrus .beforeSuite ("suite" ))
381+ .isInstanceOf (AssertionError .class )
382+ .hasRootCauseMessage ("Error in after suite" )
383+ );
384+
385+ final List <Globals > globals = results .getGlobals ();
386+ assertThat (globals ).hasSize (2 );
387+ final List <GlobalError > errors = globals .stream ().flatMap (value -> value .getErrors ().stream ()).toList ();
388+ assertThat (errors ).hasSize (2 );
389+ final GlobalError setupError = errors .get (0 );
390+ final GlobalError teardownError = errors .get (1 );
391+ assertThat (setupError .getMessage ()).contains ("Citrus suite setup failed" , "setup action failed" );
392+ assertThat (teardownError .getMessage ()).contains ("Citrus suite teardown failed" , "Error in after suite" );
393+ }
394+
395+ /**
396+ * Suite failure callbacks with no throwable report their phase and timestamp on the supplied lifecycle.
397+ */
398+ @ Test
399+ @ Description
400+ void shouldReportSuiteFailuresWithoutThrowable () {
401+ final AllureResults results = reportSuiteFailures (listener -> {
402+ listener .onStartFailure (null );
403+ listener .onFinishFailure (null );
404+ });
405+
406+ final List <Globals > globals = results .getGlobals ();
407+ assertThat (globals ).hasSize (2 );
408+ final List <GlobalError > errors = globals .stream ().flatMap (value -> value .getErrors ().stream ()).toList ();
409+ assertThat (errors ).extracting (GlobalError ::getMessage ).containsExactly (
410+ "Citrus suite setup failed" ,
411+ "Citrus suite teardown failed"
412+ );
413+ assertThat (errors ).allSatisfy (error -> {
414+ assertThat (error .getTrace ()).isNull ();
415+ assertThat (error .getTimestamp ()).isPositive ();
416+ });
417+ }
418+
419+ /**
420+ * Suite failure callbacks report exception traces and comparison values on the supplied lifecycle.
421+ */
422+ @ Test
423+ @ Description
424+ void shouldReportSuiteFailureComparisonDetails () {
425+ final AssertionFailedError cause = new AssertionFailedError (
426+ "suite comparison failed" , "expected value" , "actual value"
427+ );
428+
429+ final AllureResults results = reportSuiteFailures (listener -> listener .onFinishFailure (cause ));
430+
431+ final List <Globals > globals = results .getGlobals ();
432+ assertThat (globals ).hasSize (1 );
433+ final List <GlobalError > errors = globals .get (0 ).getErrors ();
434+ assertThat (errors ).hasSize (1 );
435+ final GlobalError error = errors .get (0 );
436+ assertThat (error .getMessage ()).contains ("Citrus suite teardown failed" , "suite comparison failed" );
437+ assertThat (error .getTrace ()).contains ("org.opentest4j.AssertionFailedError: suite comparison failed" );
438+ assertThat (error .getExpected ()).isEqualTo (cause .getExpected ().toString ());
439+ assertThat (error .getActual ()).isEqualTo (cause .getActual ().toString ());
440+ }
441+
442+ private CitrusContext .Builder suiteContext () {
443+ // Only Allure output is needed; disable Citrus HTML and JUnit report generation.
444+ return new CitrusContext .Builder ().testReporters (new TestReporters ());
445+ }
446+
447+ @ Step ("Run Citrus suite lifecycle" )
448+ private AllureResults runSuite (final CitrusContext context , final Consumer <Citrus > execution ) {
449+ return RunUtils .runTests (lifecycle -> {
450+ final Citrus citrus = Citrus .newInstance (() -> context );
451+ citrus .addTestSuiteListener (new AllureCitrus (lifecycle ));
452+ try {
453+ execution .accept (citrus );
454+ } finally {
455+ citrus .close ();
456+ }
457+ });
458+ }
459+
460+ @ Step ("Report Citrus suite failure callbacks" )
461+ private AllureResults reportSuiteFailures (final Consumer <AllureCitrus > notification ) {
462+ final AllureResultsWriterStub results = new AllureResultsWriterStub ();
463+ final AllureCitrus listener = new AllureCitrus (new AllureLifecycle (results ));
464+ try {
465+ notification .accept (listener );
466+ } finally {
467+ attach (results );
468+ }
469+ return results ;
470+ }
471+
284472 @ Step ("Run test case {testDesigner}" )
285473 private AllureResults run (final TestDesigner testDesigner ) {
286474 // a failing citrus test is a valid outcome under test — only fail the harness when
0 commit comments