diff --git a/simulator-samples/sample-combined/src/main/resources/application.properties b/simulator-samples/sample-combined/src/main/resources/application.properties index 03e7e37eb..fd37c2bac 100644 --- a/simulator-samples/sample-combined/src/main/resources/application.properties +++ b/simulator-samples/sample-combined/src/main/resources/application.properties @@ -18,3 +18,6 @@ citrus.simulator.default-scenario=Default # Should Citrus validate incoming messages on syntax and semantics citrus.simulator.template-validation=true + +# Use async mode for intermediate messaging +citrus.simulator.mode=async diff --git a/simulator-samples/sample-jms-fax/src/main/resources/application.properties b/simulator-samples/sample-jms-fax/src/main/resources/application.properties index 9a7397281..ae0c18064 100644 --- a/simulator-samples/sample-jms-fax/src/main/resources/application.properties +++ b/simulator-samples/sample-jms-fax/src/main/resources/application.properties @@ -19,3 +19,6 @@ citrus.simulator.template-validation=true # JMS destinations citrus.simulator.jms.inbound-destination=Fax.Inbound citrus.simulator.jms.status-destination=Fax.Status + +# Use async mode for intermediate messaging +citrus.simulator.mode=async diff --git a/simulator-samples/sample-jms/src/main/resources/application.properties b/simulator-samples/sample-jms/src/main/resources/application.properties index 3544bee6d..568f4ac5e 100644 --- a/simulator-samples/sample-jms/src/main/resources/application.properties +++ b/simulator-samples/sample-jms/src/main/resources/application.properties @@ -15,3 +15,6 @@ citrus.simulator.default-scenario=Default # Should Citrus validate incoming messages on syntax and semantics citrus.simulator.template-validation=true + +# Use async mode for intermediate messaging +citrus.simulator.mode=async diff --git a/simulator-samples/sample-rest/src/main/resources/application.properties b/simulator-samples/sample-rest/src/main/resources/application.properties index 5553bae6c..35c1cd8e9 100644 --- a/simulator-samples/sample-rest/src/main/resources/application.properties +++ b/simulator-samples/sample-rest/src/main/resources/application.properties @@ -15,3 +15,6 @@ citrus.simulator.default-scenario=Default # Should Citrus validate incoming messages on syntax and semantics citrus.simulator.template-validation=true + +# Use async mode for intermediate messaging +citrus.simulator.mode=async diff --git a/simulator-samples/sample-ws/src/main/resources/application.properties b/simulator-samples/sample-ws/src/main/resources/application.properties index e5d234613..bf4c667c5 100644 --- a/simulator-samples/sample-ws/src/main/resources/application.properties +++ b/simulator-samples/sample-ws/src/main/resources/application.properties @@ -16,3 +16,6 @@ citrus.simulator.default-scenario=Default # Should Citrus validate incoming messages on syntax and semantics citrus.simulator.template-validation=true + +# Use async mode for intermediate messaging +citrus.simulator.mode=async diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/ScenarioExecutorService.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/ScenarioExecutorService.java index a5efa5c78..9910dad7e 100644 --- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/ScenarioExecutorService.java +++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/ScenarioExecutorService.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,9 +19,6 @@ import jakarta.annotation.Nullable; import org.citrusframework.simulator.model.ScenarioParameter; import org.citrusframework.simulator.scenario.SimulatorScenario; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextClosedEvent; import java.util.List; @@ -34,7 +31,7 @@ * the "CRUD Service" for {@link org.citrusframework.simulator.model.ScenarioExecution} and has * nothing to do with actual {@link SimulatorScenario} execution. */ -public interface ScenarioExecutorService extends DisposableBean, ApplicationListener { +public interface ScenarioExecutorService { /** * Starts a new scenario instance using the collection of supplied parameters. @@ -43,7 +40,7 @@ public interface ScenarioExecutorService extends DisposableBean, ApplicationList * @param scenarioParameters the list of parameters to pass to the scenario when starting * @return the scenario execution id */ - public Long run(String name, @Nullable List scenarioParameters); + Long run(String name, @Nullable List scenarioParameters); /** * Starts a new scenario instance using the collection of supplied parameters. @@ -53,5 +50,5 @@ public interface ScenarioExecutorService extends DisposableBean, ApplicationList * @param scenarioParameters the list of parameters to pass to the scenario when starting * @return the scenario execution id */ - public Long run(SimulatorScenario scenario, String name, @Nullable List scenarioParameters); + Long run(SimulatorScenario scenario, String name, @Nullable List scenarioParameters); } diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AbstractLegacyScenarioExecutorService.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AbstractLegacyScenarioExecutorService.java new file mode 100644 index 000000000..3419b7d43 --- /dev/null +++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AbstractLegacyScenarioExecutorService.java @@ -0,0 +1,96 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.simulator.service.runner; + +import jakarta.annotation.Nullable; +import org.citrusframework.Citrus; +import org.citrusframework.context.TestContext; +import org.citrusframework.simulator.model.ScenarioExecution; +import org.citrusframework.simulator.model.ScenarioParameter; +import org.citrusframework.simulator.scenario.ScenarioRunner; +import org.citrusframework.simulator.scenario.SimulatorScenario; +import org.citrusframework.simulator.service.ScenarioExecutionService; +import org.citrusframework.simulator.service.ScenarioExecutorService; +import org.springframework.context.ApplicationContext; + +import java.util.List; + +import static org.citrusframework.annotations.CitrusAnnotations.injectAll; +import static org.citrusframework.simulator.model.ScenarioExecution.EXECUTION_ID; + +abstract class AbstractLegacyScenarioExecutorService implements ScenarioExecutorService { + + private final ApplicationContext applicationContext; + private final Citrus citrus; + private final ScenarioExecutionService scenarioExecutionService; + + public AbstractLegacyScenarioExecutorService(ApplicationContext applicationContext, Citrus citrus, ScenarioExecutionService scenarioExecutionService) { + this.applicationContext = applicationContext; + this.citrus = citrus; + this.scenarioExecutionService = scenarioExecutionService; + } + + @Override + public final Long run(String name, @Nullable List scenarioParameters) { + return run(applicationContext.getBean(name, SimulatorScenario.class), name, scenarioParameters); + } + + @Override + public final Long run(SimulatorScenario scenario, String name, @Nullable List scenarioParameters) { + ScenarioExecution scenarioExecution = scenarioExecutionService.createAndSaveExecutionScenario(name, scenarioParameters); + + prepareBeforeExecution(scenario); + + startScenario(scenarioExecution.getExecutionId(), name, scenario, scenarioParameters); + + return scenarioExecution.getExecutionId(); + } + + protected abstract void startScenario(Long executionId, String name, SimulatorScenario scenario, List scenarioParameters); + + /** + * Prepare scenario instance before execution. Subclasses can add custom preparation steps in + * here. + * + * @param scenario + */ + protected void prepareBeforeExecution(SimulatorScenario scenario) { + } + + protected TestContext createTestContext() { + return citrus.getCitrusContext().createTestContext(); + } + + protected void createAndRunScenarioRunner(TestContext context, Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { + var runner = new ScenarioRunner(scenario.getScenarioEndpoint(), applicationContext, context); + if (scenarioParameters != null) { + scenarioParameters.forEach(p -> runner.variable(p.getName(), p.getValue())); + } + + runner.variable(EXECUTION_ID, executionId); + runner.name(String.format("Scenario(%s)", name)); + + injectAll(scenario, citrus, context); + + try { + runner.start(); + scenario.run(runner); + } finally { + runner.stop(); + } + } +} diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImpl.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorServiceImpl.java similarity index 51% rename from simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImpl.java rename to simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorServiceImpl.java index db7ea27c9..dee4b80f2 100644 --- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImpl.java +++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,48 +14,43 @@ * limitations under the License. */ -package org.citrusframework.simulator.service.impl; +package org.citrusframework.simulator.service.runner; import com.google.common.util.concurrent.ThreadFactoryBuilder; -import jakarta.annotation.Nullable; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; import org.citrusframework.Citrus; -import org.citrusframework.annotations.CitrusAnnotations; -import org.citrusframework.context.TestContext; import org.citrusframework.simulator.config.SimulatorConfigurationProperties; -import org.citrusframework.simulator.model.ScenarioExecution; import org.citrusframework.simulator.model.ScenarioParameter; -import org.citrusframework.simulator.scenario.ScenarioRunner; import org.citrusframework.simulator.scenario.SimulatorScenario; import org.citrusframework.simulator.service.ScenarioExecutionService; -import org.citrusframework.simulator.service.ScenarioExecutorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; import org.springframework.stereotype.Service; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + /** + * Asynchronous Legay * {@inheritDoc} */ @Service -public class DefaultScenarioExecutorServiceImpl implements ScenarioExecutorService { +@ConditionalOnProperty(name= "citrus.simulator.mode", havingValue = "async") +public class AsyncScenarioExecutorServiceImpl extends AbstractLegacyScenarioExecutorService implements ApplicationListener, DisposableBean { - private static final Logger logger = LoggerFactory.getLogger( DefaultScenarioExecutorServiceImpl.class); + private static final Logger logger = LoggerFactory.getLogger( AsyncScenarioExecutorServiceImpl.class); - private final ApplicationContext applicationContext; - private final Citrus citrus; - private final ScenarioExecutionService scenarioExecutionService; private final ExecutorService executorService; - public DefaultScenarioExecutorServiceImpl(ApplicationContext applicationContext, Citrus citrus, ScenarioExecutionService scenarioExecutionService, SimulatorConfigurationProperties properties) { - this.applicationContext = applicationContext; - this.citrus = citrus; - this.scenarioExecutionService = scenarioExecutionService; + public AsyncScenarioExecutorServiceImpl(ApplicationContext applicationContext, Citrus citrus, ScenarioExecutionService scenarioExecutionService, SimulatorConfigurationProperties properties) { + super(applicationContext, citrus,scenarioExecutionService); this.executorService = Executors.newFixedThreadPool( properties.getExecutorThreads(), @@ -77,21 +72,9 @@ public void onApplicationEvent(ContextClosedEvent event) { } @Override - public final Long run(String name, @Nullable List scenarioParameters) { - return run(applicationContext.getBean(name, SimulatorScenario.class), name, scenarioParameters); - } - - @Override - public final Long run(SimulatorScenario scenario, String name, @Nullable List scenarioParameters) { + protected void startScenario(Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { logger.info("Starting scenario : {}", name); - - ScenarioExecution scenarioExecution = scenarioExecutionService.createAndSaveExecutionScenario(name, scenarioParameters); - - prepareBeforeExecution(scenario); - - startScenarioAsync(scenarioExecution.getExecutionId(), name, scenario, scenarioParameters); - - return scenarioExecution.getExecutionId(); + startScenarioAsync(executionId, name, scenario, scenarioParameters); } private Future startScenarioAsync(Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { @@ -100,7 +83,7 @@ private Future startScenarioAsync(Long executionId, String name, SimulatorSce private void startScenarioSync(Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { try { - TestContext context = citrus.getCitrusContext().createTestContext(); + var context = createTestContext(); createAndRunScenarioRunner(context, executionId, name, scenario, scenarioParameters); logger.debug("Scenario completed: {}", name); } catch (Exception e) { @@ -108,34 +91,6 @@ private void startScenarioSync(Long executionId, String name, SimulatorScenario } } - private void createAndRunScenarioRunner(TestContext context, Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { - ScenarioRunner runner = new ScenarioRunner(scenario.getScenarioEndpoint(), applicationContext, context); - if (scenarioParameters != null) { - scenarioParameters.forEach(p -> runner.variable(p.getName(), p.getValue())); - } - - runner.variable(ScenarioExecution.EXECUTION_ID, executionId); - runner.name(String.format("Scenario(%s)", name)); - - CitrusAnnotations.injectAll(scenario, citrus, context); - - try { - runner.start(); - scenario.run(runner); - } finally { - runner.stop(); - } - } - - /** - * Prepare scenario instance before execution. Subclasses can add custom preparation steps in - * here. - * - * @param scenario - */ - protected void prepareBeforeExecution(SimulatorScenario scenario) { - } - private void shutdownExecutor() { logger.debug("Request to shutdown executor"); diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/SimulatorMode.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/SimulatorMode.java new file mode 100644 index 000000000..9df96b529 --- /dev/null +++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/SimulatorMode.java @@ -0,0 +1,7 @@ +package org.citrusframework.simulator.service.runner; + +public enum SimulatorMode { + + ASYNC, + SYNC +} diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/SyncScenarioExecutorServiceImpl.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/SyncScenarioExecutorServiceImpl.java new file mode 100644 index 000000000..f7d70952e --- /dev/null +++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/SyncScenarioExecutorServiceImpl.java @@ -0,0 +1,65 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.simulator.service.runner; + +import org.citrusframework.Citrus; +import org.citrusframework.simulator.config.SimulatorConfigurationProperties; +import org.citrusframework.simulator.model.ScenarioParameter; +import org.citrusframework.simulator.scenario.SimulatorScenario; +import org.citrusframework.simulator.service.ScenarioExecutionService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * {@inheritDoc} + */ +@Service +@ConditionalOnProperty(name= "citrus.simulator.mode", havingValue = "sync", matchIfMissing = true) +public class SyncScenarioExecutorServiceImpl extends AbstractLegacyScenarioExecutorService { + + private static final Logger logger = LoggerFactory.getLogger( SyncScenarioExecutorServiceImpl.class); + + public SyncScenarioExecutorServiceImpl(ApplicationContext applicationContext, Citrus citrus, ScenarioExecutionService scenarioExecutionService, SimulatorConfigurationProperties properties) { + super(applicationContext, citrus,scenarioExecutionService); + } + + @Override + public final void startScenario(Long executionId, String name,SimulatorScenario scenario, List scenarioParameters) { + logger.info("Starting scenario : {}", name); + try { + var context = createTestContext(); + createAndRunScenarioRunner(context, executionId, name, scenario, scenarioParameters); + logger.debug("Scenario completed: {}", name); + } catch (Exception e) { + logger.error("Scenario completed with error: {}!", name, e); + } + } + + /** + * Prepare scenario instance before execution. Subclasses can add custom preparation steps in + * here. + * + * @param scenario + */ + protected void prepareBeforeExecution(SimulatorScenario scenario) { + } +} diff --git a/simulator-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/simulator-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1fa3a7f3e..c3c64aa02 100644 --- a/simulator-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/simulator-spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -12,6 +12,12 @@ "description": "Full qualified class name of additional Spring Java configuration to automatically load beans from.", "defaultValue": "org.citrusframework.simulator.SimulatorConfig" }, + { + "name": "citrus.simulator.mode", + "type": "org.citrusframework.simulator.service.runner.SimulatorMode", + "description": "Execution mode of simulator.", + "defaultValue": "sync" + }, { "name": "citrus.simulator.inbound.xml.dictionary.enabled", "type": "java.lang.Boolean", diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImplTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/AsyncScenarioExecutorServiceImplTest.java similarity index 96% rename from simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImplTest.java rename to simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/AsyncScenarioExecutorServiceImplTest.java index 897023571..7cb8805fb 100644 --- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImplTest.java +++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/AsyncScenarioExecutorServiceImplTest.java @@ -12,6 +12,7 @@ import org.citrusframework.simulator.scenario.ScenarioRunner; import org.citrusframework.simulator.scenario.SimulatorScenario; import org.citrusframework.simulator.service.ScenarioExecutionService; +import org.citrusframework.simulator.service.runner.AsyncScenarioExecutorServiceImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,7 +40,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; @ExtendWith(MockitoExtension.class) -class DefaultScenarioExecutorServiceImplTest { +class AsyncScenarioExecutorServiceImplTest { private static ScenarioEndpoint scenarioEndpointMock; @@ -60,7 +61,7 @@ class DefaultScenarioExecutorServiceImplTest { @Mock private ExecutorService executorServiceMock; - private DefaultScenarioExecutorServiceImpl fixture; + private AsyncScenarioExecutorServiceImpl fixture; private final String scenarioName = "testScenario"; private final List parameters = List.of( @@ -78,7 +79,7 @@ class DefaultScenarioExecutorServiceImplTest { public void beforeEachSetup() { doReturn(1).when(propertiesMock).getExecutorThreads(); - fixture = new DefaultScenarioExecutorServiceImpl(applicationContextMock, citrusMock, scenarioExecutionServiceMock, propertiesMock); + fixture = new AsyncScenarioExecutorServiceImpl(applicationContextMock, citrusMock, scenarioExecutionServiceMock, propertiesMock); ReflectionTestUtils.setField(fixture, "executorService", executorServiceMock, ExecutorService.class); scenarioEndpointMock = mock(ScenarioEndpoint.class);