From 598e055f7e2e703333914be2bd60ec00a28b871d Mon Sep 17 00:00:00 2001 From: Johannes Beck Date: Sun, 17 Mar 2024 14:31:32 +0100 Subject: [PATCH] Using TestContainers for Arquillian Remote --- Jenkinsfile | 22 +----- .../scripts/create-stomp-test.cli | 17 ++++ .../arquillian/ArquillianTestContainers.java | 25 ++++++ .../x1/arquillian/ContainerDefinition.java | 14 ++++ src/test/java/x1/arquillian/Containers.java | 69 ++++++++++++++++ .../java/x1/arquillian/SimpleLogConsumer.java | 20 +++++ .../arquillian/TestContainersExtension.java | 78 +++++++++++++++++++ .../java/x1/service/test/ResolverTest.java | 2 +- src/test/java/x1/stomp/test/AbstractIT.java | 72 +++++++++-------- ...boss.arquillian.core.spi.LoadableExtension | 1 + ...ersistence.xml => managed-persistence.xml} | 0 src/test/resources/remote-persistence.xml | 15 ++++ src/test/resources/simplelogger.properties | 3 +- 13 files changed, 285 insertions(+), 53 deletions(-) create mode 100644 src/test/java/x1/arquillian/ArquillianTestContainers.java create mode 100644 src/test/java/x1/arquillian/ContainerDefinition.java create mode 100644 src/test/java/x1/arquillian/Containers.java create mode 100644 src/test/java/x1/arquillian/SimpleLogConsumer.java create mode 100644 src/test/java/x1/arquillian/TestContainersExtension.java create mode 100644 src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension rename src/test/resources/{test-persistence.xml => managed-persistence.xml} (100%) create mode 100644 src/test/resources/remote-persistence.xml diff --git a/Jenkinsfile b/Jenkinsfile index 76295296..dcd3fdd2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -27,14 +27,8 @@ node { } stage('Run IT test') { - docker - .image('registry.x1/j7beck/x1-wildfly-stomp-test-it:1.8') - .withRun('-e MANAGEMENT=public -e HTTP=public --name stomp-test-it') { - c -> - waitFor("http://${hostIp(c)}:9990/health/ready", 20, 3) - withMaven(maven: 'Maven-3.9', mavenSettingsConfig: mavenSetting) { - sh "mvn -Parq-remote verify -Djboss.managementAddress=${hostIp(c)}" - } + withMaven(maven: 'Maven-3.9', mavenSettingsConfig: mavenSetting) { + sh "mvn -Parq-remote verify" } } @@ -65,15 +59,3 @@ node { } } } - -def hostIp(container) { - sh "docker inspect -f {{.NetworkSettings.IPAddress}} ${container.id} > hostIp" - readFile('hostIp').trim() -} - -def waitFor(target, sleepInSec, retries) { - retry (retries) { - sleep sleepInSec - httpRequest url: target, validResponseCodes: '200' - } -} diff --git a/src/main/docker/integration-tests/scripts/create-stomp-test.cli b/src/main/docker/integration-tests/scripts/create-stomp-test.cli index 60f3310a..e87de74f 100644 --- a/src/main/docker/integration-tests/scripts/create-stomp-test.cli +++ b/src/main/docker/integration-tests/scripts/create-stomp-test.cli @@ -1,5 +1,22 @@ embed-server --admin-only --server-config=profile.xml --std-out=echo +if (outcome != success) of /subsystem=datasources/data-source=stocksDS:read-resource + /subsystem=datasources/data-source=stocksDS:add( \ + jndi-name=java:jboss/datasources/stocksDS, \ + connection-url=jdbc:postgresql://${env.DB_SERVER:postgresql}:${env.DB_PORT:5432}/stocks,\ + user-name=${env.DB_USER:stocks}, \ + password=${env.DB_PASSWORD:stocks}, \ + statistics-enabled=true,\ + min-pool-size=2,\ + max-pool-size=5,\ + driver-name=postgresql,\ + driver-class=org.postgresql.Driver,\ + validate-on-match=true,\ + check-valid-connection-sql="select current_timestamp",\ + statistics-enabled=true\ + ) +end-if + /subsystem=messaging-activemq/server=default/jms-queue=stocksQueue:add(durable=true, entries=[java:/jms/queue/stocks]) /subsystem=messaging-activemq/server=default/jms-topic=quotesTopic:add(entries=[java:/jms/topic/quotes]) /subsystem=logging/logger=x1.service.registry:write-attribute(name=level,value=DEBUG) diff --git a/src/test/java/x1/arquillian/ArquillianTestContainers.java b/src/test/java/x1/arquillian/ArquillianTestContainers.java new file mode 100644 index 00000000..1f58385d --- /dev/null +++ b/src/test/java/x1/arquillian/ArquillianTestContainers.java @@ -0,0 +1,25 @@ +package x1.arquillian; + +import java.util.List; + +import org.jboss.arquillian.container.spi.ContainerRegistry; +import org.testcontainers.containers.GenericContainer; + +/** + * implementation must have a no-arg constructor and must be annotated + * with @ContainerDefinition + */ +public interface ArquillianTestContainers { + List> instances(); + + default void configureAfterStart(ContainerRegistry registry) { + }; + + default boolean followLog(GenericContainer container) { + return true; + } + + default boolean simpleLog(GenericContainer container) { + return false; + } +} diff --git a/src/test/java/x1/arquillian/ContainerDefinition.java b/src/test/java/x1/arquillian/ContainerDefinition.java new file mode 100644 index 00000000..97166974 --- /dev/null +++ b/src/test/java/x1/arquillian/ContainerDefinition.java @@ -0,0 +1,14 @@ +package x1.arquillian; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE }) +public @interface ContainerDefinition { + +} diff --git a/src/test/java/x1/arquillian/Containers.java b/src/test/java/x1/arquillian/Containers.java new file mode 100644 index 00000000..bfe083db --- /dev/null +++ b/src/test/java/x1/arquillian/Containers.java @@ -0,0 +1,69 @@ +package x1.arquillian; + +import java.util.Arrays; +import java.util.List; + +import org.jboss.arquillian.config.descriptor.api.ContainerDef; +import org.jboss.arquillian.container.spi.Container; +import org.jboss.arquillian.container.spi.ContainerRegistry; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +import jakarta.ws.rs.core.Response.Status; + +@ContainerDefinition +public final class Containers implements ArquillianTestContainers { + private Network network = Network.newNetwork(); + + private GenericContainer database = new GenericContainer<>( + DockerImageName.parse("registry.x1/j7beck/x1-postgres-stomp-test:1.8")).withNetwork(network) + .withNetworkAliases("db"); + + private GenericContainer etcd = new GenericContainer<>(DockerImageName.parse("quay.io/coreos/etcd:v3.5.11")) + .withEnv("ETCD_ENABLE_V2", "true").withNetwork(network).withNetworkAliases("etcd").withCommand("etcd", + "--listen-client-urls", "http://0.0.0.0:2379", "--advertise-client-urls", "http://etcd:2379"); + + private GenericContainer wildfly = new GenericContainer<>( + DockerImageName.parse("registry.x1/j7beck/x1-wildfly-stomp-test-it:1.8")).dependsOn(database).dependsOn(etcd) + .withNetwork(network).withEnv("DB_SERVER", "db").withEnv("DB_PORT", "5432").withEnv("DB_USER", "stocks") + .withEnv("DB_PASSWORD", "stocks").withEnv("ETCD_SERVER", "etcd").withEnv("ETCD_PORT", "2379") + .withEnv("X1_SERVICE_REGISTRY_STAGE", "docker").withExposedPorts(8080, 9990) + .waitingFor(Wait.forHttp("/health/ready").forStatusCode(Status.OK.getStatusCode())); + + private final List> instances = Arrays.asList(etcd, database, wildfly); + + @Override + public List> instances() { + return instances; + } + + @Override + public void configureAfterStart(ContainerRegistry registry) { + Container arquillianContainer = registry.getContainers().iterator().next(); + ContainerDef containerConfiguration = arquillianContainer.getContainerConfiguration(); + containerConfiguration.property("managementPort", Integer.toString(wildfly.getMappedPort(9990))); + + // if we would run the test as client, we would need to access the servlet from the host + // ProtocolDef protocolConfiguration = arquillianContainer.getProtocolConfiguration(new ProtocolDescription(ServletProtocolDefinition.NAME)); + // protocolConfiguration.property("port", Integer.toString(wildfly.getMappedPort(8080))); + } + + @Override + public boolean followLog(GenericContainer container) { + if (container == etcd) { + return false; + } + return true; + } + + @Override + public boolean simpleLog(GenericContainer container) { + if (container == wildfly) { + return true; + } + return false; + } + +} diff --git a/src/test/java/x1/arquillian/SimpleLogConsumer.java b/src/test/java/x1/arquillian/SimpleLogConsumer.java new file mode 100644 index 00000000..ef4e9b6f --- /dev/null +++ b/src/test/java/x1/arquillian/SimpleLogConsumer.java @@ -0,0 +1,20 @@ +package x1.arquillian; + +import org.testcontainers.containers.output.BaseConsumer; +import org.testcontainers.containers.output.OutputFrame; + +public class SimpleLogConsumer extends BaseConsumer { + + @Override + public void accept(OutputFrame outputFrame) { + var outputType = outputFrame.getType(); + var utf8String = outputFrame.getUtf8StringWithoutLineEnding(); + + switch (outputType) { + case END -> {} + case STDOUT -> System.out.println(utf8String); + case STDERR -> System.err.println(utf8String); + default -> throw new IllegalArgumentException("Unexpected outputType " + outputType); + } + } +} diff --git a/src/test/java/x1/arquillian/TestContainersExtension.java b/src/test/java/x1/arquillian/TestContainersExtension.java new file mode 100644 index 00000000..1e5e0e70 --- /dev/null +++ b/src/test/java/x1/arquillian/TestContainersExtension.java @@ -0,0 +1,78 @@ +package x1.arquillian; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.jboss.arquillian.container.spi.ContainerRegistry; +import org.jboss.arquillian.container.spi.event.container.AfterStop; +import org.jboss.arquillian.core.api.annotation.Observes; +import org.jboss.arquillian.core.spi.LoadableExtension; +import org.jboss.arquillian.core.spi.ServiceLoader; +import org.reflections.Reflections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +public class TestContainersExtension implements LoadableExtension { + private static final Logger LOGGER = LoggerFactory.getLogger(TestContainersExtension.class); + private static final String PACKAGE_NAME = "x1.arquillian"; + + public static boolean isRemoteArquillian() { + return System.getProperty("arquillian.launch").equals("remote"); + } + + @Override + public void register(ExtensionBuilder builder) { + if (isRemoteArquillian()) { + findArquillianTestContainers().ifPresent(containerDefinition -> { + LoadContainerConfiguration.containerDefinition = containerDefinition; + builder.observer(LoadContainerConfiguration.class); + }); + } + } + + public static final class LoadContainerConfiguration { + private static ArquillianTestContainers containerDefinition; + + public void registerInstance(@Observes ContainerRegistry registry, ServiceLoader serviceLoader) { + containerDefinition.instances().forEach(container -> { + container.start(); + if (containerDefinition.followLog(container)) { + var logConsumer = containerDefinition.simpleLog(container) ? new SimpleLogConsumer() + : new Slf4jLogConsumer(LOGGER).withSeparateOutputStreams(); + container.followOutput(logConsumer); + } + }); + LOGGER.info("Started {}", getImageNames()); + containerDefinition.configureAfterStart(registry); + } + + public void stopInstance(@Observes AfterStop event) { + containerDefinition.instances().forEach(container -> container.stop()); + LOGGER.info("Stopped {}", getImageNames()); + } + + private List getImageNames() { + return containerDefinition.instances().stream().map(instance -> instance.getDockerImageName()) + .collect(Collectors.toList()); + } + } + + private Optional findArquillianTestContainers() { + var classes = new Reflections(PACKAGE_NAME).getTypesAnnotatedWith(ContainerDefinition.class); + if (classes.isEmpty()) { + return Optional.empty(); + } else if (classes.size() > 1) { + throw new IllegalArgumentException( + "Found more than one ContainerDefinition under " + PACKAGE_NAME + ": " + classes); + } + try { + LOGGER.debug("Found ContainerDefinition in {}", classes); + return Optional.of((ArquillianTestContainers) classes.iterator().next().getDeclaredConstructor().newInstance()); + } catch (Exception e) { + LOGGER.warn("Could not create ContainerDefinition", e); + return Optional.empty(); + } + } +} diff --git a/src/test/java/x1/service/test/ResolverTest.java b/src/test/java/x1/service/test/ResolverTest.java index 0bea551c..0bad2536 100644 --- a/src/test/java/x1/service/test/ResolverTest.java +++ b/src/test/java/x1/service/test/ResolverTest.java @@ -50,7 +50,7 @@ public static Archive createTestArchive() { .withTransitivity().asFile(); return ShrinkWrap.create(WebArchive.class, VersionData.APP_NAME_MAJOR_MINOR + ".war").addPackages(true, "x1.stomp") - .addAsResource("test-persistence.xml", "META-INF/persistence.xml") + .addAsResource("managed-persistence.xml", "META-INF/persistence.xml") .addAsResource("microprofile-config.properties", "META-INF/microprofile-config.properties") .addAsResource("service-registry.properties").addAsWebInfResource("beans.xml") .addAsWebInfResource("test-ds.xml").addAsWebInfResource("jboss-deployment-structure.xml") diff --git a/src/test/java/x1/stomp/test/AbstractIT.java b/src/test/java/x1/stomp/test/AbstractIT.java index 78842a84..cfd88838 100644 --- a/src/test/java/x1/stomp/test/AbstractIT.java +++ b/src/test/java/x1/stomp/test/AbstractIT.java @@ -17,46 +17,56 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.extension.ExtendWith; +import x1.arquillian.TestContainersExtension; import x1.stomp.boundary.JacksonConfig; import x1.stomp.version.VersionData; @ExtendWith(ArquillianExtension.class) @Tag("Arquillian") public abstract class AbstractIT { - - protected Client client; - - @ArquillianResource - protected URL url; - - @Deployment - public static Archive createTestArchive() { - var libraries = Maven.resolver().loadPomFromFile("pom.xml") - .resolve("org.assertj:assertj-core", "org.hamcrest:hamcrest-core").withTransitivity().asFile(); - - return ShrinkWrap.create(WebArchive.class, VersionData.APP_NAME_MAJOR_MINOR + ".war").addPackages(true, "x1.stomp") - .addAsResource("test-persistence.xml", "META-INF/persistence.xml") + + protected Client client; + + @ArquillianResource + protected URL url; + + @Deployment + public static Archive createTestArchive() { + var libraries = Maven.resolver().loadPomFromFile("pom.xml") + .resolve("org.assertj:assertj-core", "org.hamcrest:hamcrest-core").withTransitivity().asFile(); + + if (TestContainersExtension.isRemoteArquillian()) { + return ShrinkWrap.create(WebArchive.class, VersionData.APP_NAME_MAJOR_MINOR + ".war") + .addPackages(true, "x1.stomp").addAsResource("remote-persistence.xml", "META-INF/persistence.xml") .addAsResource("microprofile-config.properties", "META-INF/microprofile-config.properties") - .addAsResource("quickquoteresult.xml").addAsWebInfResource("beans.xml").addAsWebInfResource("test-ds.xml") + .addAsResource("quickquoteresult.xml").addAsWebInfResource("beans.xml") + .addAsWebInfResource("jboss-deployment-structure.xml").addAsLibraries(libraries); + } else { + return ShrinkWrap.create(WebArchive.class, VersionData.APP_NAME_MAJOR_MINOR + ".war") + .addPackages(true, "x1.stomp").addAsResource("managed-persistence.xml", "META-INF/persistence.xml") + .addAsWebInfResource("test-ds.xml") + .addAsResource("microprofile-config.properties", "META-INF/microprofile-config.properties") + .addAsResource("quickquoteresult.xml").addAsWebInfResource("beans.xml") .addAsWebInfResource("jboss-deployment-structure.xml").addAsLibraries(libraries); } - - @BeforeEach - public void setup() { - client = ClientBuilder.newClient().register(JacksonConfig.class); - } + } - @AfterEach - public void tearDown() { - client.close(); - } - - public Integer getPortOffset() { - return Integer.valueOf(System.getProperty("jboss.socket.binding.port-offset", "0")); - } + @BeforeEach + public void setup() { + client = ClientBuilder.newClient().register(JacksonConfig.class); + } + + @AfterEach + public void tearDown() { + client.close(); + } + + public Integer getPortOffset() { + return Integer.valueOf(System.getProperty("jboss.socket.binding.port-offset", "0")); + } + + public String getHost() { + return System.getProperty("jboss.bind.address", "127.0.0.1"); + } - public String getHost() { - return System.getProperty("jboss.bind.address", "127.0.0.1"); - } - } diff --git a/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 00000000..455a5250 --- /dev/null +++ b/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1 @@ +x1.arquillian.TestContainersExtension \ No newline at end of file diff --git a/src/test/resources/test-persistence.xml b/src/test/resources/managed-persistence.xml similarity index 100% rename from src/test/resources/test-persistence.xml rename to src/test/resources/managed-persistence.xml diff --git a/src/test/resources/remote-persistence.xml b/src/test/resources/remote-persistence.xml new file mode 100644 index 00000000..1817dd94 --- /dev/null +++ b/src/test/resources/remote-persistence.xml @@ -0,0 +1,15 @@ + + + + java:jboss/datasources/stocksDS + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties index cec060a7..2bcaeba3 100644 --- a/src/test/resources/simplelogger.properties +++ b/src/test/resources/simplelogger.properties @@ -1,3 +1,4 @@ org.slf4j.simpleLogger.defaultLogLevel=info org.slf4j.simpleLogger.log.x1=debug -org.slf4j.simpleLogger.log.org.hibernate=warn \ No newline at end of file +org.slf4j.simpleLogger.log.org.hibernate=warn +org.slf4j.simpleLogger.log.x1.arquillian=info \ No newline at end of file