diff --git a/core/citrus-api/src/main/java/org/citrusframework/report/TestResults.java b/core/citrus-api/src/main/java/org/citrusframework/report/TestResults.java index 4ef5c208d1..c8ff702ad1 100644 --- a/core/citrus-api/src/main/java/org/citrusframework/report/TestResults.java +++ b/core/citrus-api/src/main/java/org/citrusframework/report/TestResults.java @@ -20,7 +20,9 @@ import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.citrusframework.TestResult; @@ -34,11 +36,11 @@ public class TestResults { private static final long serialVersionUID = 1L; /** Common decimal format for percentage calculation in report **/ - private static DecimalFormat decFormat = new DecimalFormat("0.0"); + private static final DecimalFormat decFormat = new DecimalFormat("0.0"); private static final String ZERO_PERCENTAGE = "0.0"; /** Collected test results */ - private List results = Collections.synchronizedList(new ArrayList()); + private final Set results = Collections.synchronizedSet(new LinkedHashSet<>()); /** * Provides access to results as list generated from synchronized result list. diff --git a/core/citrus-base/src/main/java/org/citrusframework/CitrusContext.java b/core/citrus-base/src/main/java/org/citrusframework/CitrusContext.java index b5f5b2a748..798e45d61a 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/CitrusContext.java +++ b/core/citrus-base/src/main/java/org/citrusframework/CitrusContext.java @@ -16,23 +16,7 @@ import org.citrusframework.log.DefaultLogModifier; import org.citrusframework.log.LogModifier; import org.citrusframework.message.MessageProcessors; -import org.citrusframework.report.DefaultTestReporters; -import org.citrusframework.report.FailureStackTestListener; -import org.citrusframework.report.MessageListener; -import org.citrusframework.report.MessageListenerAware; -import org.citrusframework.report.MessageListeners; -import org.citrusframework.report.TestActionListener; -import org.citrusframework.report.TestActionListenerAware; -import org.citrusframework.report.TestActionListeners; -import org.citrusframework.report.TestListener; -import org.citrusframework.report.TestListenerAware; -import org.citrusframework.report.TestListeners; -import org.citrusframework.report.TestReporter; -import org.citrusframework.report.TestReporterAware; -import org.citrusframework.report.TestReporters; -import org.citrusframework.report.TestSuiteListener; -import org.citrusframework.report.TestSuiteListenerAware; -import org.citrusframework.report.TestSuiteListeners; +import org.citrusframework.report.*; import org.citrusframework.spi.ReferenceRegistry; import org.citrusframework.spi.ReferenceResolver; import org.citrusframework.spi.SimpleReferenceResolver; @@ -329,6 +313,16 @@ public void bind(String name, Object value) { } } + public TestResults getTestResults() { + return testReporters.getTestResults(); + } + + public void handleTestResults(TestResults testResults) { + if (!getTestResults().equals(testResults)) { + testResults.doWithResults(result -> getTestResults().addResult(result)); + } + } + /** * Citrus context builder. */ diff --git a/core/citrus-base/src/main/java/org/citrusframework/DefaultTestCaseRunner.java b/core/citrus-base/src/main/java/org/citrusframework/DefaultTestCaseRunner.java index ab7c2a1852..40583698c9 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/DefaultTestCaseRunner.java +++ b/core/citrus-base/src/main/java/org/citrusframework/DefaultTestCaseRunner.java @@ -38,6 +38,10 @@ public DefaultTestCaseRunner(TestCase testCase, TestContext context) { this.testCase.setIncremental(true); } + public TestContext getContext() { + return context; + } + @Override public void start() { testCase.start(context); diff --git a/core/citrus-base/src/main/java/org/citrusframework/report/LoggingReporter.java b/core/citrus-base/src/main/java/org/citrusframework/report/LoggingReporter.java index 7246415d2a..0c9ec3d9a6 100644 --- a/core/citrus-base/src/main/java/org/citrusframework/report/LoggingReporter.java +++ b/core/citrus-base/src/main/java/org/citrusframework/report/LoggingReporter.java @@ -27,24 +27,45 @@ import org.citrusframework.message.Message; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.helpers.NOPLoggerFactory; import org.springframework.util.StringUtils; /** * Simple logging reporter printing test start and ending to the console/logger. + *

+ * This class provides an option for disablement, allowing you to suppress logging for specific instances + * and delegate the logging to another facility, which could potentially be a subclass of {@link LoggingReporter}. + * It's important to note that when an instance of this class is disabled, it will not perform any logging, + * irrespective of the severity level. + *

+ * Implementation note: The disablement of the reporter is achieved by using a {@link org.slf4j.helpers.NOPLogger}, + * meaning that this class should primarily focus on logging operations and not extend beyond that functionality. * * @author Christoph Deppisch */ public class LoggingReporter extends AbstractTestReporter implements MessageListener, TestSuiteListener, TestListener, TestActionListener { /** Inbound message logger */ - private static final Logger INBOUND_MSG_LOGGER = LoggerFactory.getLogger("Logger.Message_IN"); + private static Logger inboundMessageLogger = LoggerFactory.getLogger("Logger.Message_IN"); + + /** The inbound message logger used when the reporter is enabled */ + private static final Logger enabledInboundMessageLogger = inboundMessageLogger; /** Outbound message logger */ - private static final Logger OUTBOUND_MSG_LOGGER = LoggerFactory.getLogger("Logger.Message_OUT"); + private static Logger outboundMessageLogger = LoggerFactory.getLogger("Logger.Message_OUT"); + + /** The inbound message logger used when the reporter is enabled */ + private static final Logger enabledOutboundMessageLogger = outboundMessageLogger; /** Logger */ private static Logger log = LoggerFactory.getLogger(LoggingReporter.class); + /** The standard logger used when the reporter is enabled */ + private static final Logger enabledLog = log; + + /** A {@link org.slf4j.helpers.NOPLogger} used in case the reporter is not enabled. */ + private static final Logger noOpLogger = new NOPLoggerFactory().getLogger(LoggingReporter.class.getName()); + @Override public void generate(TestResults testResults) { separator(); @@ -228,12 +249,12 @@ public void onTestActionSkipped(TestCase testCase, TestAction testAction) { @Override public void onInboundMessage(Message message, TestContext context) { - INBOUND_MSG_LOGGER.debug(message.print(context)); + inboundMessageLogger.debug(message.print(context)); } @Override public void onOutboundMessage(Message message, TestContext context) { - OUTBOUND_MSG_LOGGER.debug(message.print(context)); + outboundMessageLogger.debug(message.print(context)); } /** @@ -284,4 +305,23 @@ protected void debug(String line) { protected boolean isDebugEnabled() { return log.isDebugEnabled(); } + + /** + * Sets the enablement state of the reporter. + */ + public void setEnabled(boolean enabled) { + if (enabled) { + log = enabledLog; + inboundMessageLogger = enabledInboundMessageLogger; + outboundMessageLogger = enabledOutboundMessageLogger; + } else { + log = noOpLogger; + inboundMessageLogger = noOpLogger; + outboundMessageLogger = noOpLogger; + } + } + + protected boolean isEnabled() { + return log != noOpLogger; + } } diff --git a/runtime/citrus-junit/src/main/java/org/citrusframework/junit/JUnit4CitrusSupport.java b/runtime/citrus-junit/src/main/java/org/citrusframework/junit/JUnit4CitrusSupport.java index df3ecc54d4..d18df488ac 100644 --- a/runtime/citrus-junit/src/main/java/org/citrusframework/junit/JUnit4CitrusSupport.java +++ b/runtime/citrus-junit/src/main/java/org/citrusframework/junit/JUnit4CitrusSupport.java @@ -74,13 +74,12 @@ public void run(CitrusFrameworkMethod frameworkMethod) { if (frameworkMethod.getMethod().getAnnotation(CitrusTestSource.class) != null) { testLoader = createTestLoader(frameworkMethod.getTestName(), frameworkMethod.getPackageName(), frameworkMethod.getSource(), frameworkMethod.getSourceType()); - - CitrusAnnotations.injectTestRunner(testLoader, runner); } else { testLoader = new DefaultTestLoader(); } CitrusAnnotations.injectAll(testLoader, citrus, ctx); + CitrusAnnotations.injectTestRunner(testLoader, runner); testLoader.doWithTestCase(t -> JUnit4Helper.invokeTestMethod(this, frameworkMethod, ctx)); testLoader.load(); } else if (frameworkMethod.getMethod().getAnnotation(CitrusXmlTest.class) != null) { diff --git a/runtime/citrus-junit/src/main/java/org/citrusframework/junit/spring/JUnit4CitrusSpringSupport.java b/runtime/citrus-junit/src/main/java/org/citrusframework/junit/spring/JUnit4CitrusSpringSupport.java index 7d78bf9ae3..adcbe91464 100644 --- a/runtime/citrus-junit/src/main/java/org/citrusframework/junit/spring/JUnit4CitrusSpringSupport.java +++ b/runtime/citrus-junit/src/main/java/org/citrusframework/junit/spring/JUnit4CitrusSpringSupport.java @@ -100,14 +100,13 @@ public void run(CitrusFrameworkMethod frameworkMethod) { if (frameworkMethod.getMethod().getAnnotation(CitrusTestSource.class) != null) { testLoader = createTestLoader(frameworkMethod.getTestName(), frameworkMethod.getPackageName(), frameworkMethod.getSource(), frameworkMethod.getSourceType()); - - CitrusAnnotations.injectTestRunner(testLoader, runner); } else { testLoader = new DefaultTestLoader(); testLoader.configureTestCase(t -> testCase = t); } CitrusAnnotations.injectAll(testLoader, citrus, ctx); + CitrusAnnotations.injectTestRunner(testLoader, runner); testLoader.doWithTestCase(t -> JUnit4Helper.invokeTestMethod(this, frameworkMethod, ctx)); testLoader.load(); } diff --git a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtension.java b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtension.java index 2c26950254..3075b0d6e6 100644 --- a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtension.java +++ b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/CitrusExtension.java @@ -29,7 +29,6 @@ import org.citrusframework.common.TestLoader; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; -import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -55,7 +54,7 @@ * @author Christoph Deppisch */ public class CitrusExtension implements BeforeAllCallback, InvocationInterceptor, - AfterTestExecutionCallback, ParameterResolver, TestInstancePostProcessor, TestExecutionExceptionHandler, AfterEachCallback, AfterAllCallback { + AfterTestExecutionCallback, ParameterResolver, TestInstancePostProcessor, TestExecutionExceptionHandler, AfterEachCallback { /** Test suite name */ private static final String SUITE_NAME = "citrus-junit5-suite"; @@ -76,41 +75,25 @@ public void beforeAll(ExtensionContext extensionContext) { if (beforeSuite) { beforeSuite = false; - // Assertion: If the beforeAll callback is called for a test, the annotated tags are currently - // included by the groups filter of surefire / failsafe or no specific filter is defined and - // all groups / tags will run anyway. - final String[] tags = extensionContext.getTags().toArray(new String[0]); - extensionContext.getTestClass().map(Class::getName).or(() -> - extensionContext.getTestMethod().map(method -> method.getDeclaringClass().getName()+":"+method.getName()) - ).ifPresentOrElse(suiteName -> - CitrusExtensionHelper - .getCitrus(extensionContext) - .beforeSuite(suiteName, tags), - () -> CitrusExtensionHelper - .getCitrus(extensionContext) - .beforeSuite(SUITE_NAME, tags) - ); - } - } - @Override - public void afterAll(ExtensionContext extensionContext) throws Exception { - if (afterSuite) { - afterSuite = false; - // Assertion: If the afterAll callback is called for a test, the annotated tags are currently - // included by the groups filter of surefire / failsafe or no specific filter is defined and - // all groups / tags did run anyway. - final String[] tags = extensionContext.getTags().toArray(new String[0]); - extensionContext.getTestClass().map(Class::getName).or(() -> - extensionContext.getTestMethod().map(meth -> meth.getDeclaringClass().getName()+":"+meth.getName()) - ).ifPresentOrElse(suiteName -> - CitrusExtensionHelper - .getCitrus(extensionContext) - .afterSuite(suiteName, tags), - () -> CitrusExtensionHelper - .getCitrus(extensionContext) - .afterSuite(SUITE_NAME, tags) - ); + // Assertion: If the beforeAll callback is called for a test, the annotated tags are currently + // included by the groups filter of surefire / failsafe or no specific filter is defined and + // all groups / tags will run anyway. + + //initialize "after all test run hook" + String[] tags = extensionContext.getTags().toArray(new String[0]); + String suiteName = extensionContext.getTestClass() + .map(Class::getName) + .orElseGet(() -> + extensionContext.getTestMethod() + .map(meth -> meth.getDeclaringClass().getName()+ ":" + meth.getName()) + .orElse(SUITE_NAME) + ); + + extensionContext.getRoot().getStore(ExtensionContext.Namespace.GLOBAL).put("afterSuiteCallback", + new AfterSuiteCallback(extensionContext, suiteName, tags)); + + CitrusExtensionHelper.getCitrus(extensionContext).beforeSuite(suiteName, tags); } } @@ -211,4 +194,25 @@ default void before(CitrusContext context) { default void after(CitrusContext context) { } } + + private static class AfterSuiteCallback implements ExtensionContext.Store.CloseableResource { + + private final ExtensionContext extensionContext; + private final String suiteName; + private final String[] tags; + + public AfterSuiteCallback(ExtensionContext extensionContext, String suiteName, String... tags) { + this.extensionContext = extensionContext; + this.suiteName = suiteName; + this.tags = tags; + } + + @Override + public void close() throws Throwable { + if (afterSuite) { + afterSuite = false; + CitrusExtensionHelper.getCitrus(extensionContext).afterSuite(suiteName, tags); + } + } + } } diff --git a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/spring/CitrusSpringExtension.java b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/spring/CitrusSpringExtension.java index b1f5ae7516..9831b9be3b 100644 --- a/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/spring/CitrusSpringExtension.java +++ b/runtime/citrus-junit5/src/main/java/org/citrusframework/junit/jupiter/spring/CitrusSpringExtension.java @@ -22,11 +22,11 @@ import java.lang.reflect.Method; import org.citrusframework.Citrus; +import org.citrusframework.CitrusSpringContext; import org.citrusframework.CitrusSpringContextProvider; import org.citrusframework.TestCaseRunner; import org.citrusframework.junit.jupiter.CitrusExtension; import org.citrusframework.junit.jupiter.CitrusExtensionHelper; -import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; @@ -55,7 +55,7 @@ * @author Christoph Deppisch */ public class CitrusSpringExtension implements BeforeAllCallback, BeforeTestExecutionCallback, InvocationInterceptor, - AfterTestExecutionCallback, ParameterResolver, TestInstancePostProcessor, TestExecutionExceptionHandler, AfterEachCallback, AfterAllCallback { + AfterTestExecutionCallback, ParameterResolver, TestInstancePostProcessor, TestExecutionExceptionHandler, AfterEachCallback { private Citrus citrus; private ApplicationContext applicationContext; @@ -67,11 +67,6 @@ public void beforeAll(ExtensionContext extensionContext) { delegate.beforeAll(extensionContext); } - @Override - public void afterAll(ExtensionContext extensionContext) throws Exception { - delegate.afterAll(extensionContext); - } - @Override public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable { delegate.handleTestExecutionException(extensionContext, throwable); @@ -119,12 +114,30 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte */ protected Citrus getCitrus(ExtensionContext extensionContext) { ApplicationContext ctx = SpringExtension.getApplicationContext(extensionContext); - if (applicationContext == null) { - applicationContext = ctx; - citrus = Citrus.newInstance(new CitrusSpringContextProvider(ctx)); - } else if (!applicationContext.equals(ctx)) { + Citrus existing = null; + if (!CitrusExtensionHelper.requiresCitrus(extensionContext)) { + existing = CitrusExtensionHelper.getCitrus(extensionContext); + } + + if (applicationContext == null || !applicationContext.equals(ctx)) { applicationContext = ctx; - citrus = Citrus.newInstance(new CitrusSpringContextProvider(ctx)); + } + + if (citrus == null) { + if (existing != null && existing.getCitrusContext() instanceof CitrusSpringContext && + ((CitrusSpringContext) existing.getCitrusContext()).getApplicationContext().equals(applicationContext)) { + citrus = existing; + } else { + citrus = Citrus.newInstance(new CitrusSpringContextProvider(applicationContext)); + + if (existing != null) { + citrus.getCitrusContext().handleTestResults(existing.getCitrusContext().getTestResults()); + } + } + } else if (existing == null || + !(existing.getCitrusContext() instanceof CitrusSpringContext) || + !((CitrusSpringContext) existing.getCitrusContext()).getApplicationContext().equals(applicationContext)) { + citrus = Citrus.newInstance(new CitrusSpringContextProvider(applicationContext)); } return citrus; diff --git a/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/JsonPathVariableExtractor.java b/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/JsonPathVariableExtractor.java index 51ce7cb873..dacede2998 100644 --- a/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/JsonPathVariableExtractor.java +++ b/validation/citrus-validation-json/src/main/java/org/citrusframework/validation/json/JsonPathVariableExtractor.java @@ -112,6 +112,10 @@ public void extractVariables(Message message, TestContext context) { public static final class Builder implements VariableExtractor.Builder, MessageProcessorAdapter, ValidationContextAdapter { private final Map expressions = new LinkedHashMap<>(); + public static Builder fromJsonPath() { + return new Builder(); + } + @Override public Builder expressions(Map expressions) { this.expressions.putAll(expressions); diff --git a/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/XpathPayloadVariableExtractor.java b/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/XpathPayloadVariableExtractor.java index 92d47c4053..659bdd265d 100644 --- a/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/XpathPayloadVariableExtractor.java +++ b/validation/citrus-validation-xml/src/main/java/org/citrusframework/validation/xml/XpathPayloadVariableExtractor.java @@ -51,6 +51,8 @@ * @author Christoph Deppisch */ public class XpathPayloadVariableExtractor implements VariableExtractor { + /** Logger */ + private static final Logger LOG = LoggerFactory.getLogger(XpathPayloadVariableExtractor.class); /** Map defines xpath expressions and target variable names */ private final Map xPathExpressions; @@ -58,9 +60,6 @@ public class XpathPayloadVariableExtractor implements VariableExtractor { /** Namespace definitions used in xpath expressions */ private final Map namespaces; - /** Logger */ - private static final Logger LOG = LoggerFactory.getLogger(XpathPayloadVariableExtractor.class); - public XpathPayloadVariableExtractor() { this(new Builder()); } @@ -143,6 +142,10 @@ public static final class Builder implements VariableExtractor.Builder expressions = new HashMap<>(); private final Map namespaces = new HashMap<>(); + public static Builder fromXpath() { + return new Builder(); + } + /** * Adds explicit namespace declaration for later path validation expressions. *