Skip to content

Commit 5b7da44

Browse files
authored
Use handle global failures in citrus integration
1 parent 30d0913 commit 5b7da44

2 files changed

Lines changed: 191 additions & 2 deletions

File tree

‎allure-citrus/src/main/java/io/qameta/allure/citrus/AllureCitrus.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import java.util.stream.Stream;
5555

5656
import static io.qameta.allure.util.ResultsUtils.createFrameworkLabel;
57+
import static io.qameta.allure.util.ResultsUtils.createGlobalError;
5758
import static io.qameta.allure.util.ResultsUtils.createHostLabel;
5859
import static io.qameta.allure.util.ResultsUtils.createLanguageLabel;
5960
import static io.qameta.allure.util.ResultsUtils.createParameter;
@@ -124,7 +125,7 @@ public void onStartSuccess() {
124125
*/
125126
@Override
126127
public void onStartFailure(final Throwable cause) {
127-
//do nothing
128+
getLifecycle().writeGlobals(createGlobalError("Citrus suite setup failed", cause));
128129
}
129130

130131
/**
@@ -148,7 +149,7 @@ public void onFinishSuccess() {
148149
*/
149150
@Override
150151
public void onFinishFailure(final Throwable cause) {
151-
//do nothing
152+
getLifecycle().writeGlobals(createGlobalError("Citrus suite teardown failed", cause));
152153
}
153154

154155
/**

‎allure-citrus/src/test/java/io/qameta/allure/citrus/AllureCitrusTest.java‎

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,18 @@
2121
import com.consol.citrus.TestCaseMetaInfo;
2222
import com.consol.citrus.actions.AbstractTestAction;
2323
import com.consol.citrus.actions.FailAction;
24+
import com.consol.citrus.container.SequenceAfterSuite;
25+
import com.consol.citrus.container.SequenceBeforeSuite;
2426
import com.consol.citrus.context.TestContext;
2527
import com.consol.citrus.dsl.design.DefaultTestDesigner;
2628
import com.consol.citrus.dsl.design.TestDesigner;
29+
import com.consol.citrus.report.TestReporters;
2730
import io.qameta.allure.Allure;
2831
import io.qameta.allure.AllureLifecycle;
32+
import io.qameta.allure.Description;
2933
import io.qameta.allure.Step;
34+
import io.qameta.allure.model.GlobalError;
35+
import io.qameta.allure.model.Globals;
3036
import io.qameta.allure.model.Label;
3137
import io.qameta.allure.model.Parameter;
3238
import io.qameta.allure.model.Stage;
@@ -40,11 +46,16 @@
4046
import io.qameta.allure.test.IsolatedLifecycle;
4147
import io.qameta.allure.test.RunUtils;
4248
import org.junit.jupiter.api.Test;
49+
import org.opentest4j.AssertionFailedError;
4350

4451
import java.time.Instant;
52+
import java.util.List;
53+
import java.util.function.Consumer;
4554

55+
import static io.qameta.allure.test.AllureTestCommonsUtils.attach;
4656
import static io.qameta.allure.util.ResultsUtils.md5;
4757
import static org.assertj.core.api.Assertions.assertThat;
58+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
4859
import 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

Comments
 (0)