>
+public abstract class SpringEntityManagerControllerFactory<
+ P extends PersistenceProvider,
+ SELF extends SpringEntityManagerControllerFactory>
extends EntityManagerControllerFactory
{
protected boolean addJarFileUrls;
+ protected P persistenceProvider;
protected SpringEntityManagerControllerFactory()
{
@@ -61,6 +67,15 @@ public SELF withAddJarFileUrls(final boolean addJarFileUrls)
return this.self();
}
+ /**
+ * Allows to manually configure a (default) persistence provider
+ */
+ public SELF withPersistenceProvider(final P persistenceProvider)
+ {
+ this.persistenceProvider = persistenceProvider;
+ return this.self();
+ }
+
@SuppressWarnings("java:S4449") // can't add annotations to source code that we do not control
protected SpringPersistenceUnitInfo instantiateSpringPersistenceUnitInfo()
{
@@ -100,12 +115,6 @@ protected SpringPersistenceUnitInfo createSpringPersistenceUnitInfo()
return pui;
}
- @Override
- protected PersistenceUnitInfo createPersistenceUnitInfo()
- {
- return this.createSpringPersistenceUnitInfo().asStandardPersistenceUnitInfo();
- }
-
protected Collection jarFileUrlsToAdd()
{
try
@@ -119,4 +128,24 @@ protected Collection jarFileUrlsToAdd()
throw new UncheckedIOException(ioe);
}
}
+
+ @Override
+ protected PersistenceUnitInfo createPersistenceUnitInfo()
+ {
+ return this.createSpringPersistenceUnitInfo().asStandardPersistenceUnitInfo();
+ }
+
+ protected abstract P createDefaultPersistenceProvider();
+
+ @Override
+ protected EntityManagerFactory createEntityManagerFactory(
+ final PersistenceUnitInfo pui,
+ final Map properties)
+ {
+ if(this.persistenceProvider == null)
+ {
+ this.persistenceProvider = this.createDefaultPersistenceProvider();
+ }
+ return this.persistenceProvider.createContainerEntityManagerFactory(pui, properties);
+ }
}
diff --git a/db-jdbc/pom.xml b/db-jdbc/pom.xml
index 34598840..fb25db29 100644
--- a/db-jdbc/pom.xml
+++ b/db-jdbc/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
db-jdbc
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
jar
db-jdbc
@@ -53,7 +53,7 @@
software.xdev.tci
base
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
@@ -72,7 +72,7 @@
org.junit.jupiter
junit-jupiter
- 6.1.0
+ 6.1.2
test
@@ -268,7 +268,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -306,12 +306,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0
diff --git a/db-jdbc/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java b/db-jdbc/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java
index 2decd1cf..62ca1a55 100644
--- a/db-jdbc/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java
+++ b/db-jdbc/src/main/java/software/xdev/tci/db/containers/WaitableJDBCContainer.java
@@ -16,22 +16,22 @@
package software.xdev.tci.db.containers;
import java.sql.Connection;
+import java.sql.SQLException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
-import org.rnorth.ducttape.TimeoutException;
import org.rnorth.ducttape.ratelimits.RateLimiter;
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
import org.rnorth.ducttape.unreliables.Unreliables;
-import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.JdbcDatabaseContainer;
-import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
-import org.testcontainers.containers.wait.strategy.Wait;
-import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;
import software.xdev.tci.envperf.EnvironmentPerformance;
+import software.xdev.tci.startup.wait.AbortMonitor;
+import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy;
+import software.xdev.tci.startup.wait.strategy.AbstractWaitAbortableStrategy;
+import software.xdev.tci.startup.wait.strategy.HostPortWaitAbortableStrategy;
public interface WaitableJDBCContainer extends WaitStrategyTarget
@@ -39,10 +39,11 @@ public interface WaitableJDBCContainer extends WaitStrategyTarget
@SuppressWarnings("checkstyle:MagicNumber")
default WaitStrategy completeJDBCWaitStrategy()
{
- return new WaitAllStrategy()
- .withStrategy(Wait.defaultWaitStrategy())
+ return FastAbortOnContainerDeathWaitStrategy.waitAll(s -> s
+ .withStartupTimeout(Duration.ofSeconds(40L + EnvironmentPerformance.cpuSlownessFactor() * 20L))
+ .withStrategy(new HostPortWaitAbortableStrategy())
.withStrategy(new JDBCWaitStrategy())
- .withStartupTimeout(Duration.ofSeconds(40L + EnvironmentPerformance.cpuSlownessFactor() * 20L));
+ );
}
WaitStrategy getWaitStrategy();
@@ -59,20 +60,19 @@ default void waitUntilContainerStarted()
/**
* @apiNote Assumes that the container is already started
*/
- class JDBCWaitStrategy extends AbstractWaitStrategy
+ class JDBCWaitStrategy extends AbstractWaitAbortableStrategy
{
@SuppressWarnings("checkstyle:MagicNumber")
public JDBCWaitStrategy()
{
this.withRateLimiter(RateLimiterBuilder.newBuilder()
- .withRate(200, TimeUnit.MILLISECONDS)
+ .withRate(5, TimeUnit.SECONDS)
.withConstantThroughput()
.build());
}
- @SuppressWarnings("PMD.PreserveStackTrace")
@Override
- protected void waitUntilReady()
+ protected void waitUntilReady(final AbortMonitor abortMonitor)
{
if(!(this.waitStrategyTarget instanceof final JdbcDatabaseContainer> container))
{
@@ -80,45 +80,33 @@ protected void waitUntilReady()
"Container must implement JdbcDatabaseContainer and WaitableJDBCContainer");
}
- try
- {
- this.waitUntilJDBCValid(container);
- }
- catch(final TimeoutException e)
- {
- throw new ContainerLaunchException(
- "JDBCContainer cannot be accessed by (JDBC URL: "
- + container.getJdbcUrl()
- + "), please check container logs");
- }
- }
-
- protected void waitUntilJDBCValid(final JdbcDatabaseContainer> container)
- {
- Unreliables.retryUntilTrue(
- (int)this.startupTimeout.getSeconds(),
- TimeUnit.SECONDS,
- // Rate limit creation of connections as this is quite an expensive operation
- () -> this.getRateLimiter().getWhenReady(() -> {
+ // Rate limit creation of connections as this is quite an expensive operation
+ this.startupRetryUntilSuccessWithRateLimitWhenNotAborted(
+ abortMonitor,
+ () -> {
try(final Connection connection = container.createConnection(""))
{
- return this.waitUntilJDBCConnectionValidated(container, connection);
+ this.waitUntilJDBCConnectionValidated(container, connection);
+ }
+ catch(final SQLException sqlEx)
+ {
+ throw new IllegalStateException("SQL failed", sqlEx);
}
- })
+ }
);
}
@SuppressWarnings({"unused", "checkstyle:MagicNumber"}) // Parameter might be used by extension
- protected boolean waitUntilJDBCConnectionValidated(
+ protected void waitUntilJDBCConnectionValidated(
final JdbcDatabaseContainer> container,
final Connection connection)
{
final RateLimiter validateJDBCRateLimiter = RateLimiterBuilder.newBuilder()
- .withRate(50, TimeUnit.MILLISECONDS)
+ .withRate(20, TimeUnit.SECONDS)
.withConstantThroughput()
.build();
- return Unreliables.retryUntilSuccess(
+ Unreliables.retryUntilSuccess(
// If this fails after startupTimeout / 2 (min=3s, max=30s)
// it might be possible that the connection is somehow corrupted or broken
// -> Build a new connection after that (see waitUntilJDBCValid)
diff --git a/image-build/README.md b/image-build/README.md
new file mode 100644
index 00000000..2430e99d
--- /dev/null
+++ b/image-build/README.md
@@ -0,0 +1,36 @@
+# Image-Build
+
+Provides common utility and configuration to build images.
+
+## Config
+
+The configuration is dynamically loaded from (sorted by highest priority)
+
+* Environment variables
+ * prefixed with `TCI_IMAGE-BUILD__`
+ * where `imageName` is the (sanitized) name of the image to build
+ * prefixed with `TCI_IMAGE-BUILD_`
+ * all properties are in UPPERCASE and use `_` instead of `.` or `-`
+* System properties
+ * prefixed with `tci.image-build..`
+ * where `imageName` is the (sanitized) name of the image to build
+ * prefixed with `tci.image-build.`
+
+_NOTE: Sanitized image-names only include alphanumeric characters, `_` or `-`. All other characters are replaced by `_`_
+
+
+
+Full list of configuration options
+
+| Property | Type | Default | Notes |
+| --- | --- | --- | --- |
+| `delete-on-exit` | `bool` | `false` | Should the image be deleted on exit? |
+| `logger-for-build-prefix` | `string` | `container.build.` | Prefix used for the build logger |
+| `cache-from` | `string` | - | Only applies to BuildKit (native) build.
See [Docker docs](https://docs.docker.com/build/cache/backends/) for details. |
+| `cache-to` | `string` | - | Only applies to BuildKit (native) build.
See [Docker docs](https://docs.docker.com/build/cache/backends/) for details. |
+| `save-cache-in-background` | `bool` | if `cache-to` set `true`
otherwise `false` | Builds the image the first time WITHOUT `cache-to` and the async in the background again WITH `cache-to`. This way saving the cache does not delay test execution. |
+| `wait-for-save-cache-in-background` | `bool` | `true` | Wait until all caches are saved before terminating |
+
+
+
+
diff --git a/image-build/pom.xml b/image-build/pom.xml
new file mode 100644
index 00000000..0efd17d3
--- /dev/null
+++ b/image-build/pom.xml
@@ -0,0 +1,311 @@
+
+
+ 4.0.0
+
+ software.xdev.tci
+ image-build
+ 4.0.0-SNAPSHOT
+ jar
+
+ image-build
+ TCI - image-build
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 21
+ ${javaVersion}
+
+ UTF-8
+ UTF-8
+
+
+
+
+ software.xdev.tci
+ base
+ 4.0.0-SNAPSHOT
+
+
+ software.xdev
+ testcontainers-advanced-imagebuilder
+ 4.1.2
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 5.0.0
+
+
+ ${project.organization.url}
+
+
+
+ com/mycila/maven/plugin/license/templates/APACHE-2.txt
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+ first
+
+ format
+
+ process-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.15.0
+
+ ${maven.compiler.release}
+
+ -proc:none
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.12.0
+
+
+ attach-javadocs
+ package
+
+ jar
+
+
+
+
+ true
+ none
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.4.0
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.6
+
+
+
+
+
+ ignore-service-loading
+
+
+
+ src/main/resources
+
+ META-INF/services/**
+
+
+
+
+
+
+ publish
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.3
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.8
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.11.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+ checkstyle
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 13.8.0
+
+
+
+ ../.config/checkstyle/checkstyle.xml
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+ pmd
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.28.0
+
+ true
+ true
+ true
+
+ ../.config/pmd/java/ruleset.xml
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+ 7.26.0
+
+
+ net.sourceforge.pmd
+ pmd-java
+ 7.26.0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 3.6.0
+
+
+
+
+
+
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java
new file mode 100644
index 00000000..aa2c0017
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImage.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild;
+
+import java.time.Duration;
+import java.util.function.UnaryOperator;
+
+import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
+import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile;
+import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile;
+
+
+public final class BuildImage
+{
+ public static String nativeImage(
+ final String dockerImage,
+ final Duration timeout,
+ final UnaryOperator configure)
+ {
+ return impl().nativeImage(dockerImage, timeout, configure);
+ }
+
+ public static String image(
+ final String dockerImage,
+ final Duration timeout,
+ final UnaryOperator configure)
+ {
+ return impl().image(dockerImage, timeout, configure);
+ }
+
+ public static BuildImageHandlerProvider impl()
+ {
+ return TCIServiceLoaderHolder.instance().service(BuildImageHandlerProvider.class);
+ }
+
+ private BuildImage()
+ {
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandlerProvider.java b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandlerProvider.java
new file mode 100644
index 00000000..ed57af9e
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/BuildImageHandlerProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild;
+
+import java.time.Duration;
+import java.util.function.UnaryOperator;
+
+import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile;
+import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile;
+
+
+public interface BuildImageHandlerProvider
+{
+ String nativeImage(
+ String dockerImage,
+ Duration timeout,
+ UnaryOperator configure);
+
+ String image(
+ String dockerImage,
+ Duration timeout,
+ UnaryOperator configure);
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandlerProvider.java b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandlerProvider.java
new file mode 100644
index 00000000..a2aa4df0
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/DefaultBuildImageHandlerProvider.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild;
+
+import java.time.Duration;
+import java.util.function.UnaryOperator;
+
+import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig;
+import software.xdev.tci.imagebuild.handler.AdvancedBuildImageHandler;
+import software.xdev.tci.imagebuild.handler.NativeAdvancedBuildImageHandler;
+import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile;
+import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile;
+
+
+public class DefaultBuildImageHandlerProvider implements BuildImageHandlerProvider
+{
+ protected final BuildImageHandlerConfig config;
+
+ public DefaultBuildImageHandlerProvider()
+ {
+ this(BuildImageHandlerConfig.instance());
+ }
+
+ public DefaultBuildImageHandlerProvider(final BuildImageHandlerConfig config)
+ {
+ this.config = config;
+ }
+
+ @Override
+ public String nativeImage(
+ final String dockerImage,
+ final Duration timeout,
+ final UnaryOperator configure)
+ {
+ return new NativeAdvancedBuildImageHandler().build(
+ dockerImage,
+ this.config,
+ timeout,
+ configure);
+ }
+
+ @Override
+ public String image(
+ final String dockerImage,
+ final Duration timeout,
+ final UnaryOperator configure)
+ {
+ return new AdvancedBuildImageHandler().build(
+ dockerImage,
+ this.config,
+ timeout,
+ configure);
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java
new file mode 100644
index 00000000..aaae1d0f
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCICacheSaveRegistry.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.cache.async;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+
+public class TCICacheSaveRegistry
+{
+ protected static TCICacheSaveRegistry instance;
+
+ public static TCICacheSaveRegistry instance()
+ {
+ if(instance == null)
+ {
+ createDefaultInstance();
+ }
+ return instance;
+ }
+
+ protected static synchronized void createDefaultInstance()
+ {
+ if(instance == null)
+ {
+ instance = new TCICacheSaveRegistry();
+ }
+ }
+
+ protected final List> cfs = Collections.synchronizedList(new ArrayList<>());
+
+ public void add(final CompletableFuture> cf)
+ {
+ this.cfs.add(cf);
+ }
+
+ public List> get()
+ {
+ return this.cfs;
+ }
+
+ protected TCICacheSaveRegistry()
+ {
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java
new file mode 100644
index 00000000..eb81e7d8
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/cache/async/TCIWaitForCacheSave.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.cache.async;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestPlan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig;
+
+
+public class TCIWaitForCacheSave implements TestExecutionListener
+{
+ private static final Logger LOG = LoggerFactory.getLogger(TCIWaitForCacheSave.class);
+
+ private BuildImageHandlerConfig config;
+
+ @Override
+ public void testPlanExecutionStarted(final TestPlan testPlan)
+ {
+ this.config = BuildImageHandlerConfig.instance();
+ }
+
+ @Override
+ public void testPlanExecutionFinished(final TestPlan testPlan)
+ {
+ if(!this.config.waitForSaveCacheInBackground())
+ {
+ return;
+ }
+
+ final List> cfs = TCICacheSaveRegistry.instance().get();
+ if(cfs.isEmpty())
+ {
+ return;
+ }
+
+ LOG.info("Waiting for cache saves to finish...");
+ final long startMs = System.currentTimeMillis();
+ try
+ {
+ CompletableFuture.allOf(cfs.toArray(CompletableFuture[]::new))
+ .get(10, TimeUnit.MINUTES);
+ }
+ catch(final InterruptedException e)
+ {
+ LOG.warn("Got interrupted", e);
+ Thread.currentThread().interrupt();
+ }
+ catch(final ExecutionException e)
+ {
+ LOG.warn("A cache save failed", e);
+ }
+ catch(final TimeoutException e)
+ {
+ LOG.warn("Timed out while waiting for cache save", e);
+ }
+
+ LOG.info(
+ "Finished waiting for {}x cache saves, took {}ms",
+ cfs.size(),
+ System.currentTimeMillis() - startMs);
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/AbstractBuildImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/AbstractBuildImageHandlerConfig.java
new file mode 100644
index 00000000..1f7ef873
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/AbstractBuildImageHandlerConfig.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.config;
+
+import java.util.Optional;
+
+import software.xdev.tci.config.DefaultConfig;
+
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public abstract class AbstractBuildImageHandlerConfig extends DefaultConfig implements BuildImageHandlerConfig
+{
+ protected static final String DELETE_ON_EXIT = "delete-on-exit";
+ protected static final String LOGGER_FOR_BUILD_PREFIX = "logger-for-build-prefix";
+ protected static final String CACHE_FROM = "cache-from";
+ protected static final String CACHE_TO = "cache-to";
+ protected static final String SAVE_CACHE_IN_BACKGROUND = "save-cache-in-background";
+ protected static final String WAIT_FOR_SAVE_CACHE_IN_BACKGROUND = "wait-for-" + SAVE_CACHE_IN_BACKGROUND;
+
+ protected Boolean deleteOnExit;
+ protected String loggerForBuildPrefix;
+ protected Optional cacheFrom;
+ protected Optional cacheTo;
+ protected Boolean saveCacheInBackground;
+ protected Boolean waitForSaveCacheInBackground;
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/BuildImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/BuildImageHandlerConfig.java
new file mode 100644
index 00000000..1a16d4d4
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/BuildImageHandlerConfig.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.config;
+
+import java.util.Optional;
+
+import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
+
+
+public interface BuildImageHandlerConfig
+{
+ boolean deleteOnExit();
+
+ String loggerForBuildPrefix();
+
+ Optional cacheFrom();
+
+ Optional cacheTo();
+
+ boolean saveCacheInBackground();
+
+ boolean waitForSaveCacheInBackground();
+
+ static BuildImageHandlerConfig instance()
+ {
+ return TCIServiceLoaderHolder.instance().service(BuildImageHandlerConfig.class);
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java
new file mode 100644
index 00000000..5cdf6bc9
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/DefaultBuildImageHandlerConfig.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.config;
+
+import java.util.Optional;
+
+
+@SuppressWarnings({"java:S2789", "OptionalAssignedToNull"})
+public class DefaultBuildImageHandlerConfig extends AbstractBuildImageHandlerConfig
+{
+ @Override
+ protected String propertyNamePrefix()
+ {
+ return "tci.image-build";
+ }
+
+ @Override
+ public boolean deleteOnExit()
+ {
+ if(this.deleteOnExit == null)
+ {
+ this.deleteOnExit = this.resolveBool(DELETE_ON_EXIT, false);
+ }
+ return this.deleteOnExit;
+ }
+
+ @Override
+ public String loggerForBuildPrefix()
+ {
+ if(this.loggerForBuildPrefix == null)
+ {
+ this.loggerForBuildPrefix = this.resolve(LOGGER_FOR_BUILD_PREFIX)
+ .orElse("container.build.");
+ }
+ return this.loggerForBuildPrefix;
+ }
+
+ @Override
+ public Optional cacheFrom()
+ {
+ if(this.cacheFrom == null)
+ {
+ this.cacheFrom = this.resolve(CACHE_FROM);
+ }
+ return this.cacheFrom;
+ }
+
+ @Override
+ public Optional cacheTo()
+ {
+ if(this.cacheTo == null)
+ {
+ this.cacheTo = this.resolve(CACHE_TO);
+ }
+ return this.cacheTo;
+ }
+
+ @Override
+ public boolean saveCacheInBackground()
+ {
+ if(this.saveCacheInBackground == null)
+ {
+ this.saveCacheInBackground =
+ this.resolveBool(SAVE_CACHE_IN_BACKGROUND, this.cacheTo().isPresent());
+ }
+ return this.saveCacheInBackground;
+ }
+
+ @Override
+ public boolean waitForSaveCacheInBackground()
+ {
+ if(this.waitForSaveCacheInBackground == null)
+ {
+ this.waitForSaveCacheInBackground =
+ this.resolveBool(WAIT_FOR_SAVE_CACHE_IN_BACKGROUND, true);
+ }
+ return this.waitForSaveCacheInBackground;
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java b/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java
new file mode 100644
index 00000000..65ffafdc
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/config/OverlayBuildImageHandlerConfig.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.config;
+
+import java.util.Optional;
+
+
+@SuppressWarnings({"java:S2789", "OptionalAssignedToNull"})
+public class OverlayBuildImageHandlerConfig extends DefaultBuildImageHandlerConfig
+{
+ private final BuildImageHandlerConfig parentConfig;
+ private final String prefixName;
+
+ public OverlayBuildImageHandlerConfig(final BuildImageHandlerConfig parentConfig, final String prefixName)
+ {
+ this.parentConfig = parentConfig;
+ this.prefixName = prefixName;
+ }
+
+ @Override
+ protected String propertyNamePrefix()
+ {
+ return super.propertyNamePrefix() + "." + this.prefixName;
+ }
+
+ @Override
+ public boolean deleteOnExit()
+ {
+ if(this.deleteOnExit == null)
+ {
+ this.deleteOnExit = this.resolveBool(DELETE_ON_EXIT, this.parentConfig.deleteOnExit());
+ }
+ return this.deleteOnExit;
+ }
+
+ @Override
+ public String loggerForBuildPrefix()
+ {
+ if(this.loggerForBuildPrefix == null)
+ {
+ this.loggerForBuildPrefix = this.resolve(LOGGER_FOR_BUILD_PREFIX)
+ .orElseGet(this.parentConfig::loggerForBuildPrefix);
+ }
+ return this.loggerForBuildPrefix;
+ }
+
+ @Override
+ public Optional cacheFrom()
+ {
+ if(this.cacheFrom == null)
+ {
+ this.cacheFrom = this.resolve(CACHE_FROM)
+ .or(this.parentConfig::cacheFrom);
+ }
+ return this.cacheFrom;
+ }
+
+ @Override
+ public Optional cacheTo()
+ {
+ if(this.cacheTo == null)
+ {
+ this.cacheTo = this.resolve(CACHE_TO)
+ .or(this.parentConfig::cacheTo);
+ }
+ return this.cacheTo;
+ }
+
+ @Override
+ public boolean waitForSaveCacheInBackground()
+ {
+ if(this.waitForSaveCacheInBackground == null)
+ {
+ this.waitForSaveCacheInBackground =
+ this.resolveBool(WAIT_FOR_SAVE_CACHE_IN_BACKGROUND, this.parentConfig.waitForSaveCacheInBackground());
+ }
+ return this.waitForSaveCacheInBackground;
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java
new file mode 100644
index 00000000..a2412174
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AbstractBuildImageHandler.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.handler;
+
+import java.time.Duration;
+import java.util.function.UnaryOperator;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig;
+import software.xdev.tci.imagebuild.config.OverlayBuildImageHandlerConfig;
+import software.xdev.testcontainers.imagebuilder.AbstractImageFromDockerfile;
+
+
+public abstract class AbstractBuildImageHandler>
+ implements BuildImageHandler
+{
+ protected static final Pattern DOCKER_IMAGE_SANITIZATION_PATTERN = Pattern.compile("[^A-Za-z0-9-_]");
+
+ protected Logger logger;
+
+ protected AbstractBuildImageHandler()
+ {
+ this.logger = LoggerFactory.getLogger(this.getClass());
+ }
+
+ @Override
+ public String build(
+ final String dockerImage,
+ final BuildImageHandlerConfig parentConfig,
+ final Duration timeout,
+ final UnaryOperator configure)
+ {
+ final String sanitizedDockerImageName = this.sanitizeDockerImageName(dockerImage);
+ return this.build(
+ dockerImage,
+ sanitizedDockerImageName,
+ new OverlayBuildImageHandlerConfig(parentConfig, sanitizedDockerImageName),
+ timeout,
+ configure);
+ }
+
+ protected abstract String build(
+ final String dockerImage,
+ final String sanitizedDockerImageName,
+ final BuildImageHandlerConfig parentConfig,
+ final Duration timeout,
+ final UnaryOperator configure);
+
+ protected void configureAbstract(
+ final I builder,
+ final BuildImageHandlerConfig config,
+ final String sanitizeDockerImageName)
+ {
+ builder.withLoggerForBuild(
+ LoggerFactory.getLogger(config.loggerForBuildPrefix() + sanitizeDockerImageName));
+ }
+
+ protected String buildImage(
+ final AbstractImageFromDockerfile> builder,
+ final Duration timeout)
+ {
+ this.logger.info("Building image {}...", builder.getDockerImageName());
+ final String builtImageName = builder.build(timeout);
+ this.logger.info("Built image {}", builtImageName);
+
+ return builtImageName;
+ }
+
+ protected String sanitizeDockerImageName(final String dockerImage)
+ {
+ return DOCKER_IMAGE_SANITIZATION_PATTERN.matcher(dockerImage).replaceAll("_");
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java
new file mode 100644
index 00000000..cd013563
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/AdvancedBuildImageHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.handler;
+
+import java.time.Duration;
+import java.util.function.UnaryOperator;
+
+import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig;
+import software.xdev.testcontainers.imagebuilder.AdvancedImageFromDockerFile;
+
+
+public class AdvancedBuildImageHandler extends AbstractBuildImageHandler
+{
+ @Override
+ protected String build(
+ final String dockerImage,
+ final String sanitizedDockerImageName,
+ final BuildImageHandlerConfig config,
+ final Duration timeout,
+ final UnaryOperator configure)
+ {
+ this.logger.info("Starting build of image {}", dockerImage);
+
+ final AdvancedImageFromDockerFile builder =
+ new AdvancedImageFromDockerFile(dockerImage, config.deleteOnExit());
+
+ this.configureAbstract(builder, config, sanitizedDockerImageName);
+
+ return this.buildImage(configure.apply(builder), timeout);
+ }
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java
new file mode 100644
index 00000000..399c9b82
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/BuildImageHandler.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.handler;
+
+import java.time.Duration;
+import java.util.function.UnaryOperator;
+
+import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig;
+
+
+public interface BuildImageHandler
+{
+ String build(
+ String dockerImage,
+ BuildImageHandlerConfig parentConfig,
+ Duration timeout,
+ UnaryOperator configure);
+}
diff --git a/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java
new file mode 100644
index 00000000..112c9b8f
--- /dev/null
+++ b/image-build/src/main/java/software/xdev/tci/imagebuild/handler/NativeAdvancedBuildImageHandler.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.imagebuild.handler;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.UnaryOperator;
+
+import software.xdev.tci.concurrent.TCIExecutorServiceHolder;
+import software.xdev.tci.imagebuild.cache.async.TCICacheSaveRegistry;
+import software.xdev.tci.imagebuild.config.BuildImageHandlerConfig;
+import software.xdev.testcontainers.imagebuilder.buildxnative.NativeAdvancedImageFromDockerfile;
+
+
+public class NativeAdvancedBuildImageHandler extends AbstractBuildImageHandler
+{
+ @Override
+ protected String build(
+ final String dockerImage,
+ final String sanitizedDockerImageName,
+ final BuildImageHandlerConfig config,
+ final Duration timeout,
+ final UnaryOperator configure)
+ {
+ this.logger.info("Starting build of (native) image {}", dockerImage);
+
+ final NativeAdvancedImageFromDockerfile builder =
+ new NativeAdvancedImageFromDockerfile(dockerImage, config.deleteOnExit());
+
+ this.configureAbstract(builder, config, sanitizedDockerImageName);
+
+ this.templateCacheConfigValue(config.cacheFrom(), sanitizedDockerImageName)
+ .ifPresent(builder::withCacheFrom);
+
+ if(config.saveCacheInBackground())
+ {
+ builder.withCreateTransferFilesCache(true);
+ }
+ else
+ {
+ this.configureCacheTo(sanitizedDockerImageName, config, builder);
+ }
+
+ final NativeAdvancedImageFromDockerfile customizedBuilder = configure.apply(builder);
+
+ final String builtImageName = this.buildImage(customizedBuilder, timeout);
+
+ if(config.saveCacheInBackground() && config.cacheTo().isPresent())
+ {
+ this.logger.info("Rebuilding image {} in background to save cache", builtImageName);
+ TCICacheSaveRegistry.instance().add(
+ CompletableFuture.runAsync(
+ () -> {
+ try
+ {
+ this.buildImage(
+ this.configureCacheTo(
+ sanitizedDockerImageName,
+ config,
+ customizedBuilder.copyForExactRebuild(dockerImage + "-cache")
+ // Don't load it into docker because it's not needed there
+ .withLoad(false)),
+ timeout);
+ }
+ catch(final Exception ex)
+ {
+ this.logger.warn("Rebuilding {} to save cache failed", builtImageName, ex);
+ }
+ finally
+ {
+ try
+ {
+ builder.cleanCreatedTransferFilesCache();
+ }
+ catch(final Exception ex)
+ {
+ this.logger.warn(
+ "Failed to clean created transfer file cache for {}",
+ builtImageName,
+ ex);
+ }
+ }
+ },
+ TCIExecutorServiceHolder.instance()));
+ }
+
+ return builtImageName;
+ }
+
+ protected NativeAdvancedImageFromDockerfile configureCacheTo(
+ final String sanitizeDockerImageName,
+ final BuildImageHandlerConfig config,
+ final NativeAdvancedImageFromDockerfile builder)
+ {
+ this.templateCacheConfigValue(config.cacheTo(), sanitizeDockerImageName)
+ .ifPresent(builder::withCacheTo);
+ return builder;
+ }
+
+ protected Optional templateCacheConfigValue(
+ final Optional configValue,
+ final String sanitizeDockerImageName)
+ {
+ return configValue
+ .map(s -> s.replace("$image", sanitizeDockerImageName))
+ .map(s -> s.replace("§image", sanitizeDockerImageName));
+ }
+}
diff --git a/image-build/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/image-build/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
new file mode 100644
index 00000000..8604e1ab
--- /dev/null
+++ b/image-build/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
@@ -0,0 +1 @@
+software.xdev.tci.imagebuild.cache.async.TCIWaitForCacheSave
diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandlerProvider b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandlerProvider
new file mode 100644
index 00000000..bae2b157
--- /dev/null
+++ b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.BuildImageHandlerProvider
@@ -0,0 +1 @@
+software.xdev.tci.imagebuild.DefaultBuildImageHandlerProvider
diff --git a/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.BuildImageHandlerConfig b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.BuildImageHandlerConfig
new file mode 100644
index 00000000..b71d6a3e
--- /dev/null
+++ b/image-build/src/main/resources/META-INF/services/software.xdev.tci.imagebuild.config.BuildImageHandlerConfig
@@ -0,0 +1 @@
+software.xdev.tci.imagebuild.config.DefaultBuildImageHandlerConfig
diff --git a/jacoco/README.md b/jacoco/README.md
index 61c1b198..c746e030 100644
--- a/jacoco/README.md
+++ b/jacoco/README.md
@@ -7,6 +7,30 @@ Provides support for [JaCoCo](https://github.com/jacoco/jacoco) Code Coverage in
NOTE: Using this is a bit complex as it also requires some changes to the java application in the container.
Please have a look at the [advanced-demo](../advanced-demo/) and the corresponding [GitHub Actions workflow](../.github/workflows/run-integration-tests.yml) for details.
-### Example Report
+## Config
+
+The configuration is dynamically loaded from (sorted by highest priority)
+
+* Environment variables
+ * prefixed with `TCI_JACOCO_`
+ * all properties are in UPPERCASE and use `_` instead of `.` or `-`
+* System properties
+ * prefixed with `tci.jacoco.`
+
+
+
+Full list of configuration options
+
+| Property | Type | Default | Notes |
+| --- | --- | --- | --- |
+| `enabled` | `bool` | `false` | You should probably set this to `true` |
+| `execution-data-files-dir` | `string` | `target/jacoco-execution-data-files` | Location where the jacoco execution reports/execution data files are stored |
+| `move-old-execution-data-files-dir` | `bool` | `true` | If `execution-data-files-dir` already exists it will be moved to `execution-data-files-dir-old` |
+| `execution-data-files-dir-old` | `string` | `target/jacoco-execution-data-files-old` | Location where previous jacoco execution reports/execution data files are moved to. If the directory already exists it will be deleted. |
+| `execution-data-file-suffix` | `string` | `-jacoco.exec` | Suffix for the jacoco reports/execution data files |
+
+
+
+## Example Report


diff --git a/jacoco/pom.xml b/jacoco/pom.xml
index 250c1adf..253a09de 100644
--- a/jacoco/pom.xml
+++ b/jacoco/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
jacoco
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
jar
jacoco
@@ -53,7 +53,7 @@
software.xdev.tci
base
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
@@ -242,7 +242,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -280,12 +280,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0
diff --git a/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java b/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java
index 428271eb..455e851f 100644
--- a/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java
+++ b/jacoco/src/main/java/software/xdev/tci/jacoco/testbase/JaCoCoRecorder.java
@@ -46,11 +46,19 @@ public static JaCoCoRecorder instance()
{
if(instance == null)
{
- instance = new JaCoCoRecorder();
+ createDefaultInstance();
}
return instance;
}
+ protected static synchronized void createDefaultInstance()
+ {
+ if(instance == null)
+ {
+ instance = new JaCoCoRecorder();
+ }
+ }
+
protected JaCoCoConfig config;
protected Map cachedDirForExecutionDataFilesVariantPaths = new ConcurrentHashMap<>();
@@ -79,7 +87,6 @@ public CompletableFuture afterTestAsync(
return this.afterTestAsync(optTCI, null, fileSystemFriendlyNameSupplier, variantName);
}
- @SuppressWarnings("resource")
public CompletableFuture afterTestAsync(
final Optional> optTCI,
final String jaCoCoExecutionDataFilePathInContainer,
@@ -124,18 +131,7 @@ protected void stopContainerAndCopy(
final GenericContainer> container,
final String containerPath)
{
- if(container.isRunning())
- {
- // Shutdown container so that jacoco agent dumps the execution data file
- try
- {
- DockerClientFactory.lazyClient().stopContainerCmd(container.getContainerId()).exec();
- }
- catch(final Exception ex)
- {
- LOG.warn("Failed to stop container", ex);
- }
- }
+ this.stopContainer(container);
try
{
@@ -160,6 +156,23 @@ protected void stopContainerAndCopy(
}
}
+ @SuppressWarnings("resource")
+ protected void stopContainer(final GenericContainer> container)
+ {
+ if(container.isRunning())
+ {
+ // Shutdown container so that jacoco agent dumps the execution data file
+ try
+ {
+ DockerClientFactory.lazyClient().stopContainerCmd(container.getContainerId()).exec();
+ }
+ catch(final Exception ex)
+ {
+ LOG.warn("Failed to stop container", ex);
+ }
+ }
+ }
+
protected Path resolveDirForExecutionDataFiles(final String variantName)
{
return this.cachedDirForExecutionDataFilesVariantPaths.computeIfAbsent(
diff --git a/jul-to-slf4j/README.md b/jul-to-slf4j/README.md
index d6074722..3959daa0 100644
--- a/jul-to-slf4j/README.md
+++ b/jul-to-slf4j/README.md
@@ -1,3 +1,3 @@
# JUL to SLF4J
-Logging Adapter to redirect [JUL](https://docs.oracle.com/en/java/javase/21/docs/api/java.logging/java/util/logging/package-summary.html) to [SLF4J](https://github.com/qos-ch/slf4j)
+Logging Adapter to redirect [JUL](https://docs.oracle.com/en/java/javase/25/docs/api/java.logging/java/util/logging/package-summary.html) to [SLF4J](https://github.com/qos-ch/slf4j)
diff --git a/jul-to-slf4j/pom.xml b/jul-to-slf4j/pom.xml
index 975557c2..f60c13fe 100644
--- a/jul-to-slf4j/pom.xml
+++ b/jul-to-slf4j/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
jul-to-slf4j
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
jar
jul-to-slf4j
@@ -242,7 +242,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -280,12 +280,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0
diff --git a/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java b/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java
index be2d0675..6c41107d 100644
--- a/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java
+++ b/jul-to-slf4j/src/main/java/software/xdev/tci/logging/JULtoSLF4JRedirector.java
@@ -35,6 +35,17 @@ protected void redirectInternal()
{
return;
}
+
+ this.redirectInternalSync();
+ }
+
+ protected synchronized void redirectInternalSync()
+ {
+ if(this.installed)
+ {
+ return;
+ }
+
if(SLF4JBridgeHandler.isInstalled())
{
this.installed = true;
diff --git a/junit-jupiter-api-support/pom.xml b/junit-jupiter-api-support/pom.xml
index 1d5a9864..b950b2e4 100644
--- a/junit-jupiter-api-support/pom.xml
+++ b/junit-jupiter-api-support/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
junit-jupiter-api-support
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
jar
junit-jupiter-api-support
@@ -53,7 +53,7 @@
org.junit.jupiter
junit-jupiter-api
- 6.1.0
+ 6.1.2
compile
@@ -243,7 +243,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -281,12 +281,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0
diff --git a/mailpit/Dockerfile b/mailpit/Dockerfile
new file mode 100644
index 00000000..3c9c49a3
--- /dev/null
+++ b/mailpit/Dockerfile
@@ -0,0 +1,3 @@
+# This file is just used as a reminder to update the latest minor version in
+# MailpitContainer.java from time to time
+FROM axllent/mailpit:v1.30
diff --git a/mailpit/README.md b/mailpit/README.md
new file mode 100644
index 00000000..9d251cbb
--- /dev/null
+++ b/mailpit/README.md
@@ -0,0 +1,3 @@
+# Mailpit
+
+TCI for [Mailpit](https://github.com/axllent/mailpit).
diff --git a/mailpit/pom.xml b/mailpit/pom.xml
new file mode 100644
index 00000000..1edbaf10
--- /dev/null
+++ b/mailpit/pom.xml
@@ -0,0 +1,354 @@
+
+
+ 4.0.0
+
+ software.xdev.tci
+ mailpit
+ 4.0.0-SNAPSHOT
+ jar
+
+ mailpit
+ TCI - mailpit
+ https://github.com/xdev-software/tci
+
+
+ https://github.com/xdev-software/tci
+ scm:git:https://github.com/xdev-software/tci.git
+
+
+ 2025
+
+
+ XDEV Software
+ https://xdev.software
+
+
+
+
+ XDEV Software
+ XDEV Software
+ https://xdev.software
+
+
+
+
+
+ Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ 21
+ ${javaVersion}
+
+ UTF-8
+ UTF-8
+
+
+ true
+
+ 2.26.1
+
+
+
+
+ software.xdev.tci
+ base
+ 4.0.0-SNAPSHOT
+
+
+
+ software.xdev
+ mailpit-java-client
+ 1.0.0
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 6.1.2
+ test
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ ${log4j.version}
+ test
+
+
+ org.apache.logging.log4j
+ log4j-slf4j2-impl
+ ${log4j.version}
+ test
+
+
+
+ org.simplejavamail
+ simple-java-mail
+ 9.1.0
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 4.0.0-M16
+
+
+ org.apache.maven.plugins
+ maven-project-info-reports-plugin
+ 3.9.0
+
+
+
+
+
+ com.mycila
+ license-maven-plugin
+ 5.0.0
+
+
+ ${project.organization.url}
+
+
+
+ com/mycila/maven/plugin/license/templates/APACHE-2.txt
+
+ src/main/java/**
+ src/test/java/**
+
+
+
+
+
+
+ first
+
+ format
+
+ process-sources
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.15.0
+
+ ${maven.compiler.release}
+
+ -proc:none
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.12.0
+
+
+ attach-javadocs
+ package
+
+ jar
+
+
+
+
+ true
+ none
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.4.0
+
+
+ attach-sources
+ package
+
+ jar-no-fork
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.6
+
+ ${skipTests}
+
+
+
+
+
+
+ run-it
+
+ false
+
+
+
+ ignore-service-loading
+
+
+
+ src/main/resources
+
+ META-INF/services/**
+
+
+
+
+
+
+ publish
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.3
+
+ ossrh
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.2.8
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+
+
+ publish-sonatype-central-portal
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.11.0
+ true
+
+ sonatype-central-portal
+ true
+
+
+
+
+
+
+ checkstyle
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.6.0
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 13.8.0
+
+
+
+ ../.config/checkstyle/checkstyle.xml
+ true
+
+
+
+
+ check
+
+
+
+
+
+
+
+
+ pmd
+
+
+
+ org.apache.maven.plugins
+ maven-pmd-plugin
+ 3.28.0
+
+ true
+ true
+ true
+
+ ../.config/pmd/java/ruleset.xml
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+ 7.26.0
+
+
+ net.sourceforge.pmd
+ pmd-java
+ 7.26.0
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jxr-plugin
+ 3.6.0
+
+
+
+
+
+
diff --git a/mailpit/src/main/java/software/xdev/tci/mailpit/MailpitTCI.java b/mailpit/src/main/java/software/xdev/tci/mailpit/MailpitTCI.java
new file mode 100644
index 00000000..e63c7639
--- /dev/null
+++ b/mailpit/src/main/java/software/xdev/tci/mailpit/MailpitTCI.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.mailpit;
+
+import java.time.Duration;
+
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
+import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
+import org.apache.hc.core5.util.Timeout;
+import org.slf4j.LoggerFactory;
+
+import software.xdev.mailpit.client.ApiClient;
+import software.xdev.tci.TCI;
+import software.xdev.tci.mailpit.containers.MailpitContainer;
+
+
+public class MailpitTCI extends TCI
+{
+ protected ApiClient apiClient;
+
+ public MailpitTCI(final MailpitContainer container, final String networkAlias)
+ {
+ super(container, networkAlias);
+ }
+
+ public String getExternalHTTPEndpoint()
+ {
+ return "http://"
+ + this.getContainer().getHost()
+ + ":"
+ + this.getContainer().getMappedPort(MailpitContainer.WEB_PORT);
+ }
+
+ public ApiClient apiClient()
+ {
+ if(this.apiClient != null)
+ {
+ return this.apiClient;
+ }
+
+ final Duration defaultTimeout = Duration.ofSeconds(30);
+
+ this.apiClient = new ApiClient();
+ this.apiClient.setHttpClient(HttpClientBuilder.create()
+ .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create()
+ .setDefaultConnectionConfig(ConnectionConfig.custom()
+ .setConnectTimeout(Timeout.of(defaultTimeout))
+ .setSocketTimeout(Timeout.of(defaultTimeout))
+ .build())
+ .build())
+ .build());
+ this.apiClient.setBasePath(this.getExternalHTTPEndpoint());
+
+ return this.apiClient;
+ }
+
+ @Override
+ public void stop()
+ {
+ if(this.apiClient != null)
+ {
+ try
+ {
+ this.apiClient.getHttpClient().close();
+ }
+ catch(final Exception e)
+ {
+ LoggerFactory.getLogger(this.getClass()).warn("Failed to close API client", e);
+ }
+ }
+ super.stop();
+ }
+}
diff --git a/mailpit/src/main/java/software/xdev/tci/mailpit/containers/MailpitContainer.java b/mailpit/src/main/java/software/xdev/tci/mailpit/containers/MailpitContainer.java
new file mode 100644
index 00000000..785088f2
--- /dev/null
+++ b/mailpit/src/main/java/software/xdev/tci/mailpit/containers/MailpitContainer.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.mailpit.containers;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.utility.DockerImageName;
+
+
+public class MailpitContainer extends GenericContainer
+{
+ public static final DockerImageName DEFAULT_IMAGE = DockerImageName.parse("axllent/mailpit:v1.30");
+
+ public static final int WEB_PORT = 8025;
+ public static final int SMTP_PORT = 1025;
+
+ public MailpitContainer()
+ {
+ super(DEFAULT_IMAGE);
+
+ // Version check is not needed in tests
+ this.addEnv("MP_DISABLE_VERSION_CHECK", "true");
+ // Resolving client reverse dns is not needed
+ this.addEnv("MP_SMTP_DISABLE_RDNS", "true");
+
+ // Enforce CSP (only important when debugging)
+ this.addEnv("MP_BLOCK_REMOTE_CSS_AND_FONTS", "true");
+
+ // Use normal SMTP
+ this.addEnv("MP_SMTP_AUTH_ALLOW_INSECURE", "true");
+
+ this.addExposedPort(WEB_PORT);
+ }
+
+ public MailpitContainer withSmtpAuth(final Map usernamePasswords)
+ {
+ this.addEnv(
+ "MP_SMTP_AUTH",
+ usernamePasswords.entrySet()
+ .stream()
+ .map(e -> e.getKey() + ":" + e.getValue())
+ .collect(Collectors.joining(" ")));
+ return this;
+ }
+}
diff --git a/mailpit/src/main/java/software/xdev/tci/mailpit/factory/MailpitTCIFactory.java b/mailpit/src/main/java/software/xdev/tci/mailpit/factory/MailpitTCIFactory.java
new file mode 100644
index 00000000..a5d172fc
--- /dev/null
+++ b/mailpit/src/main/java/software/xdev/tci/mailpit/factory/MailpitTCIFactory.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.mailpit.factory;
+
+import java.util.Map;
+import java.util.function.Supplier;
+
+import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
+import software.xdev.tci.mailpit.MailpitTCI;
+import software.xdev.tci.mailpit.containers.MailpitContainer;
+import software.xdev.tci.misc.ContainerMemory;
+
+
+public class MailpitTCIFactory extends PreStartableTCIFactory
+{
+ public static final String DEFAULT_USER = "no-reply@test.localhost";
+ public static final String DEFAULT_PW = "test";
+ public static final int SMTP_PORT = MailpitContainer.SMTP_PORT;
+
+ public MailpitTCIFactory()
+ {
+ this(MailpitTCIFactory::createDefaultContainer);
+ }
+
+ public MailpitTCIFactory(final Supplier mailpitContainerSupplier)
+ {
+ super(
+ MailpitTCI::new,
+ mailpitContainerSupplier,
+ "mailpit",
+ "container.mailpit",
+ "Mailpit"
+ );
+ }
+
+ @SuppressWarnings("resource")
+ public static MailpitContainer createDefaultContainer()
+ {
+ return new MailpitContainer()
+ .withSmtpAuth(Map.of(DEFAULT_USER, DEFAULT_PW))
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M));
+ }
+}
diff --git a/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java b/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java
new file mode 100644
index 00000000..ff666022
--- /dev/null
+++ b/mailpit/src/test/java/software/xdev/mailpit/SimpleMailpitTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.mailpit;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.simplejavamail.api.mailer.Mailer;
+import org.simplejavamail.api.mailer.config.TransportStrategy;
+import org.simplejavamail.email.EmailBuilder;
+import org.simplejavamail.mailer.MailerBuilder;
+import org.simplejavamail.recipient.RecipientBuilder;
+
+import software.xdev.mailpit.api.MessageApi;
+import software.xdev.mailpit.client.ApiClient;
+import software.xdev.mailpit.model.Message;
+import software.xdev.tci.concurrent.TCIExecutorServiceHolder;
+import software.xdev.tci.factory.registry.TCIFactoryRegistry;
+import software.xdev.tci.mailpit.MailpitTCI;
+import software.xdev.tci.mailpit.containers.MailpitContainer;
+import software.xdev.tci.mailpit.factory.MailpitTCIFactory;
+import software.xdev.tci.network.LazyNetwork;
+import software.xdev.tci.network.LazyNetworkPool;
+
+
+class SimpleMailpitTest
+{
+ protected static final MailpitTCIFactory FACTORY = new MailpitTCIFactory(() -> {
+ final MailpitContainer container = MailpitTCIFactory.createDefaultContainer();
+ // For testing we send mails form the host so SMTP must be exposed
+ container.addExposedPort(MailpitTCIFactory.SMTP_PORT);
+ return container;
+ });
+ protected static final LazyNetworkPool LAZY_NETWORK_POOL = new LazyNetworkPool();
+
+ protected LazyNetwork network;
+ protected MailpitTCI mailpitInfra;
+
+ @BeforeAll
+ static void beforeAll()
+ {
+ LAZY_NETWORK_POOL.managePoolAsync();
+
+ TCIFactoryRegistry.instance().warmUp();
+
+ // Preload classes
+ CompletableFuture.runAsync(ApiClient::new, TCIExecutorServiceHolder.instance());
+ }
+
+ @BeforeEach
+ void beforeEach()
+ {
+ this.network = LAZY_NETWORK_POOL.getNew();
+ this.mailpitInfra = FACTORY.getNew(this.network);
+ }
+
+ @Test
+ void check()
+ {
+ final String toAddress = "m.mustermann@test.localhost";
+ final String subject = "Test";
+ final String plainText = "This is a test";
+
+ try(final Mailer mailer = MailerBuilder.withSMTPServer(
+ this.mailpitInfra.getContainer().getHost(),
+ this.mailpitInfra.getContainer().getMappedPort(MailpitTCIFactory.SMTP_PORT),
+ MailpitTCIFactory.DEFAULT_USER,
+ MailpitTCIFactory.DEFAULT_PW)
+ .withTransportStrategy(TransportStrategy.SMTP)
+ .withDebugLogging(true)
+ .buildMailer())
+ {
+ mailer.sendMail(EmailBuilder.startingBlank()
+ .from(MailpitTCIFactory.DEFAULT_USER)
+ .withRecipients(new RecipientBuilder()
+ .withType(jakarta.mail.Message.RecipientType.TO)
+ .withAddress(toAddress)
+ .withName("Max Mustermann")
+ .build())
+ .withSubject(subject)
+ .withPlainText(plainText)
+ .buildEmail());
+ }
+ catch(final Exception ex)
+ {
+ throw new IllegalStateException("Mailer failed", ex);
+ }
+
+ final Message message = new MessageApi(this.mailpitInfra.apiClient()).getMessageParams("latest");
+ assertNotNull(message);
+ assertAll(
+ () -> assertEquals(toAddress, message.getTo().getFirst().getAddress()),
+ () -> assertEquals(subject, message.getSubject()),
+ () -> assertEquals(
+ plainText,
+ Arrays.stream(message.getText().split("\n"))
+ .map(String::trim)
+ .findFirst()
+ .orElse(null))
+ );
+ }
+
+ @AfterEach
+ void afterEach()
+ {
+ if(this.mailpitInfra != null)
+ {
+ this.mailpitInfra.stop();
+ this.mailpitInfra = null;
+ }
+ if(this.network != null)
+ {
+ this.network.close();
+ this.network = null;
+ }
+ }
+
+ @AfterAll
+ static void afterAll()
+ {
+ FACTORY.close();
+ }
+}
diff --git a/mailpit/src/test/resources/log4j2-test.xml b/mailpit/src/test/resources/log4j2-test.xml
new file mode 100644
index 00000000..3b76bc01
--- /dev/null
+++ b/mailpit/src/test/resources/log4j2-test.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ %d{HH:mm:ss} %-5p [%t] [%-25.25c] %m %n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mockserver/pom.xml b/mockserver/pom.xml
index 7a908c8f..c63e30e7 100644
--- a/mockserver/pom.xml
+++ b/mockserver/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
mockserver
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
jar
mockserver
@@ -54,7 +54,7 @@
software.xdev.mockserver
bom
- 2.51.0
+ 2.51.1
pom
import
@@ -65,7 +65,7 @@
software.xdev.tci
base
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
@@ -264,7 +264,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -302,12 +302,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0
diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java b/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java
new file mode 100644
index 00000000..44f0eaef
--- /dev/null
+++ b/mockserver/src/main/java/software/xdev/tci/mockserver/containers/TCIMockserverContainer.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright © 2025 XDEV Software (https://xdev.software)
+ *
+ * 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 software.xdev.tci.mockserver.containers;
+
+import org.testcontainers.images.RemoteDockerImage;
+import org.testcontainers.utility.DockerImageName;
+
+import com.github.dockerjava.api.command.InspectContainerResponse;
+
+import software.xdev.tci.misc.ContainerMemory;
+import software.xdev.tci.startup.error.java.fatal.HsErrPidStartUpCrashReporter;
+import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy;
+import software.xdev.tci.startup.wait.strategy.LogMessageWaitAbortableStrategy;
+import software.xdev.testcontainers.mockserver.containers.MockServerContainer;
+
+
+@SuppressWarnings("java:S2160")
+public class TCIMockserverContainer extends MockServerContainer
+{
+ private final HsErrPidStartUpCrashReporter hsErrPidStartUpCrashReporter;
+
+ public TCIMockserverContainer(final RemoteDockerImage image)
+ {
+ super(image);
+ this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this);
+ }
+
+ public TCIMockserverContainer(final DockerImageName dockerImageName)
+ {
+ super(dockerImageName);
+ this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this);
+ }
+
+ public TCIMockserverContainer(final String tag)
+ {
+ super(tag);
+ this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this);
+ }
+
+ public TCIMockserverContainer()
+ {
+ this.hsErrPidStartUpCrashReporter = new HsErrPidStartUpCrashReporter(this);
+ }
+
+ @Override
+ protected void containerIsStarted(final InspectContainerResponse containerInfo, final boolean reused)
+ {
+ this.hsErrPidStartUpCrashReporter.containerIsStarted();
+ super.containerIsStarted(containerInfo, reused);
+ }
+
+ @Override
+ protected void containerIsStopping(final InspectContainerResponse containerInfo)
+ {
+ this.hsErrPidStartUpCrashReporter.containerIsStopping(this.logger());
+ super.containerIsStopping(containerInfo);
+ }
+
+ public static TCIMockserverContainer createDefaultForFactory()
+ {
+ final TCIMockserverContainer container = new TCIMockserverContainer();
+ container
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M))
+ .waitingFor(new FastAbortOnContainerDeathWaitStrategy(new LogMessageWaitAbortableStrategy()
+ .withRegEx(MockServerContainer.LOG_MSG_WAIT_STRATEGY_REGEX)
+ ));
+ return container;
+ }
+}
diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java
index d3d8b25e..5cb0bf03 100644
--- a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java
+++ b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/MockServerTCIFactory.java
@@ -22,8 +22,8 @@
import org.testcontainers.containers.Network;
import software.xdev.tci.factory.ondemand.OnDemandTCIFactory;
-import software.xdev.tci.misc.ContainerMemory;
import software.xdev.tci.mockserver.MockServerTCI;
+import software.xdev.tci.mockserver.containers.TCIMockserverContainer;
import software.xdev.testcontainers.mockserver.containers.MockServerContainer;
@@ -39,7 +39,6 @@ protected MockServerTCIFactory(
super(infraBuilder, containerBuilder, containerBaseName, containerLoggerName);
}
- @SuppressWarnings("resource")
protected MockServerTCIFactory(
final BiFunction infraBuilder,
final String additionalContainerBaseName,
@@ -47,8 +46,7 @@ protected MockServerTCIFactory(
{
super(
infraBuilder,
- () -> new MockServerContainer()
- .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)),
+ TCIMockserverContainer::createDefaultForFactory,
"mockserver-" + additionalContainerBaseName,
"container.mockserver." + additionalLoggerName);
}
diff --git a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java
index 14d0346b..465ff0ee 100644
--- a/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java
+++ b/mockserver/src/main/java/software/xdev/tci/mockserver/factory/PreStartableMockServerTCIFactory.java
@@ -18,15 +18,14 @@
import java.util.function.BiFunction;
import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
-import software.xdev.tci.misc.ContainerMemory;
import software.xdev.tci.mockserver.MockServerTCI;
+import software.xdev.tci.mockserver.containers.TCIMockserverContainer;
import software.xdev.testcontainers.mockserver.containers.MockServerContainer;
public abstract class PreStartableMockServerTCIFactory
extends PreStartableTCIFactory
{
- @SuppressWarnings("resource")
protected PreStartableMockServerTCIFactory(
final BiFunction infraBuilder,
final String additionalContainerBaseName,
@@ -35,8 +34,7 @@ protected PreStartableMockServerTCIFactory(
{
super(
infraBuilder,
- () -> new MockServerContainer()
- .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M)),
+ TCIMockserverContainer::createDefaultForFactory,
"mockserver-" + additionalContainerBaseName,
"container.mockserver." + additionalLoggerName,
prestartName);
diff --git a/oidc-server-mock/pom.xml b/oidc-server-mock/pom.xml
index 25ba063d..fad26177 100644
--- a/oidc-server-mock/pom.xml
+++ b/oidc-server-mock/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
oidc-server-mock
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
jar
oidc-server-mock
@@ -53,13 +53,13 @@
software.xdev.tci
base
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
org.apache.httpcomponents.client5
httpclient5
- 5.6.1
+ 5.6.2
@@ -248,7 +248,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -286,12 +286,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0
diff --git a/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/BaseOIDCTCIFactory.java b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/BaseOIDCTCIFactory.java
index 62a55a75..37a1e49a 100644
--- a/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/BaseOIDCTCIFactory.java
+++ b/oidc-server-mock/src/main/java/software/xdev/tci/oidc/factory/BaseOIDCTCIFactory.java
@@ -21,9 +21,6 @@
import java.util.function.Supplier;
import org.apache.hc.core5.http.HttpStatus;
-import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
-import org.testcontainers.containers.wait.strategy.HttpWaitStrategy;
-import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import software.xdev.tci.envperf.EnvironmentPerformance;
import software.xdev.tci.factory.prestart.PreStartableTCIFactory;
@@ -32,6 +29,9 @@
import software.xdev.tci.oidc.BaseOIDCTCI;
import software.xdev.tci.oidc.containers.BaseOIDCServerContainer;
import software.xdev.tci.oidc.containers.OIDCServerContainer;
+import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy;
+import software.xdev.tci.startup.wait.strategy.HostPortWaitAbortableStrategy;
+import software.xdev.tci.startup.wait.strategy.HttpWaitAbortableStrategy;
@SuppressWarnings("java:S119")
@@ -92,22 +92,21 @@ public static OIDCServerContainer createDefaultContainer()
return createDefaultContainer(null);
}
- @SuppressWarnings({"resource", "checkstyle:MagicNumber"})
+ @SuppressWarnings({"checkstyle:MagicNumber"})
public static OIDCServerContainer createDefaultContainer(final Consumer customizer)
{
final OIDCServerContainer oidcServerContainer = new OIDCServerContainer()
.withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M))
- .waitingFor(
- new WaitAllStrategy()
- .withStartupTimeout(Duration.ofSeconds(40L + 20L * EnvironmentPerformance.cpuSlownessFactor()))
- .withStrategy(new HostPortWaitStrategy())
- .withStrategy(
- new HttpWaitStrategy()
- .forPort(BaseOIDCServerContainer.PORT)
- .forPath("/")
- .forStatusCode(HttpStatus.SC_OK)
- .withReadTimeout(Duration.ofSeconds(10))
- )
+ .waitingFor(FastAbortOnContainerDeathWaitStrategy.waitAll(s -> s
+ .withStartupTimeout(Duration.ofSeconds(40L + 20L * EnvironmentPerformance.cpuSlownessFactor()))
+ .withStrategy(new HostPortWaitAbortableStrategy())
+ .withStrategy(
+ new HttpWaitAbortableStrategy()
+ .forPort(BaseOIDCServerContainer.PORT)
+ .forPath("/")
+ .forStatusCode(HttpStatus.SC_OK)
+ .withReadTimeout(Duration.ofSeconds(10))
+ ))
);
if(customizer != null)
diff --git a/pom.xml b/pom.xml
index 16a6ad85..d4bf205d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
root
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
pom
@@ -21,9 +21,11 @@
db-jdbc-spring-orm
db-jdbc-spring-orm-eclipselink
db-jdbc-spring-orm-hibernate
+ image-build
jacoco
jul-to-slf4j
junit-jupiter-api-support
+ mailpit
mockserver
oidc-server-mock
selenium
@@ -77,7 +79,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -115,12 +117,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0
diff --git a/selenium/README.md b/selenium/README.md
index 75811157..274838d8 100644
--- a/selenium/README.md
+++ b/selenium/README.md
@@ -9,3 +9,28 @@ TCI for [Selenium](https://github.com/SeleniumHQ/selenium).
* NoVNC support
* Full scale support for recording videos
* Browser Logs can be enabled if required
+
+## Config
+
+The configuration is dynamically loaded from (sorted by highest priority)
+
+* Environment variables
+ * prefixed with `TCI_SELENIUM_`
+ * all properties are in UPPERCASE and use `_` instead of `.` or `-`
+* System properties
+ * prefixed with `tci.selenium.`
+
+
+
+Full list of configuration options
+
+| Property | Type | Default | Notes |
+| --- | --- | --- | --- |
+| `record-mode` | `Enum` | `RECORD_FAILING` | Recording mode.
Available:- SKIP - Do not record any videos
- RECORD_ALL - Record all tests
- RECORD_FAILING - Record failing tests only
|
+| `dir-for-records` | `String` | `target/records` | Directory for storing the recorded videos |
+| `vnc-enabled` | `bool` | `false` | Enable [VNC](https://en.wikipedia.org/wiki/VNC) and [NoVNC](https://github.com/novnc/novnc). This is usually only needed during debugging. |
+| `bidi-enabled` | `bool` | `true` | Use [Selenium BiDirectional functionality](https://www.selenium.dev/documentation/webdriver/bidi/) instead of legacy Chrome DevTools Protocol (CDP).
Disabling this will make certain operations unavailable e.g. listening for browser logs. |
+| `deactivate-cdp-if-possible` | `bool` | `true` | Disable Chrome DevTools Protocol (CDP) if possible.
CDP requires additional maven dependencies (e.g. `selenium-devtools-v137)` that are not present by default and will result in a warning. |
+| `min-browser-console-log-level` | `Enum` | `ERROR` | Prints out browser console logs. Configures the MINIMUM log level.
Available options:
+
+
diff --git a/selenium/pom.xml b/selenium/pom.xml
index 9ae5d3ca..1119fe4a 100644
--- a/selenium/pom.xml
+++ b/selenium/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
selenium
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
jar
selenium
@@ -54,7 +54,7 @@
org.seleniumhq.selenium
selenium-dependencies-bom
- 4.45.0
+ 4.46.0
pom
import
@@ -65,24 +65,24 @@
software.xdev.tci
base
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
software.xdev.tci
jul-to-slf4j
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
software.xdev.tci
junit-jupiter-api-support
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
software.xdev
testcontainers-selenium
- 2.0.0
+ 2.0.2
@@ -296,7 +296,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -334,12 +334,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java b/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java
index c88cdc11..7009160d 100644
--- a/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java
+++ b/selenium/src/main/java/software/xdev/tci/selenium/BrowserTCI.java
@@ -62,7 +62,7 @@ public class BrowserTCI extends TCI
// https://www.selenium.dev/documentation/webdriver/bidi
protected boolean bidiEnabled = true;
- // Disables the (not standardized) Chrome Dev Tools (CDP) protocol (when bidi is enabled).
+ // Disables the (not standardized) Chrome DevTools protocol (CDP) when bidi is enabled.
// CDP requires additional maven dependencies (e.g. selenium-devtools-v137) that are
// NOT present and result in a warning.
protected boolean deactivateCDPIfPossible = true;
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java
index 055484ae..f153a52d 100644
--- a/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java
+++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/BrowserTCIFactory.java
@@ -31,9 +31,6 @@
import org.rnorth.ducttape.unreliables.Unreliables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
-import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
-import org.testcontainers.containers.wait.strategy.WaitAllStrategy;
import org.testcontainers.images.RemoteDockerImage;
import software.xdev.tci.concurrent.TCIExecutorServiceHolder;
@@ -45,6 +42,9 @@
import software.xdev.tci.selenium.containers.SeleniumBrowserWebDriverContainer;
import software.xdev.tci.selenium.factory.config.BrowserTCIFactoryConfig;
import software.xdev.tci.serviceloading.TCIServiceLoaderHolder;
+import software.xdev.tci.startup.wait.FastAbortOnContainerDeathWaitStrategy;
+import software.xdev.tci.startup.wait.strategy.HostPortWaitAbortableStrategy;
+import software.xdev.tci.startup.wait.strategy.LogMessageWaitAbortableStrategy;
import software.xdev.testcontainers.selenium.containers.browser.BrowserWebDriverContainer;
import software.xdev.testcontainers.selenium.containers.recorder.SeleniumRecordingContainer;
@@ -88,8 +88,8 @@ public static BrowserTCI createDefaultBrowserTCI(
.withWebDriverRetryCount(Math.max(Math.min(cpuSlownessFactor(), 5), 1))
.withWebDriverRetrySec(25 + cpuSlownessFactor() * 5)
.withBrowserConsoleLog(
- logBrowserConsoleConsumer(config.browserConsoleLogLevel()),
- config.browserConsoleLogLevel().logLevels());
+ logBrowserConsoleConsumer(config.minBrowserConsoleLogLevel()),
+ config.minBrowserConsoleLogLevel().logLevels());
}
@SuppressWarnings({"resource", "checkstyle:MagicNumber"})
@@ -119,12 +119,12 @@ public static SeleniumBrowserWebDriverContainer createDefaultContainer(
// https://github.com/SeleniumHQ/docker-selenium/issues/2355
.withEnv("SE_ENABLE_TRACING", "false")
// Some (AWS) CPUs are completely overloaded with the default 15s timeout -> increase it
- .waitingFor(new WaitAllStrategy()
- .withStrategy(new LogMessageWaitStrategy()
- .withRegEx(".*(Started Selenium Standalone).*\n")
- .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor())))
- .withStrategy(new HostPortWaitStrategy())
- .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor())));
+ .waitingFor(FastAbortOnContainerDeathWaitStrategy.waitAll(s -> s
+ .withStartupTimeout(Duration.ofSeconds(30 + 20L * cpuSlownessFactor()))
+ .withStrategy(new LogMessageWaitAbortableStrategy()
+ .withRegEx(BrowserWebDriverContainer.LOG_MSG_WAIT_STRATEGY_REGEX))
+ .withStrategy(new HostPortWaitAbortableStrategy())
+ ));
}
public static SeleniumRecordingContainer createDefaultRecordingContainer(
@@ -133,7 +133,11 @@ public static SeleniumRecordingContainer createDefaultRecordingContainer(
{
return new SeleniumRecordingContainer(browserContainer)
.withLogConsumer(getLogConsumer("container.browserrecorder." + capabilities.getBrowserName()))
- .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M));
+ .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig().withMemory(ContainerMemory.M512M))
+ .waitingFor(new FastAbortOnContainerDeathWaitStrategy(
+ new LogMessageWaitAbortableStrategy()
+ .withRegEx(SeleniumRecordingContainer.LOG_MSG_WAIT_STRATEGY_REGEX)
+ ));
}
public BrowserTCIFactory(
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java
index f83f64f0..883b7d77 100644
--- a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java
+++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/BrowserTCIFactoryConfig.java
@@ -33,5 +33,5 @@ public interface BrowserTCIFactoryConfig
boolean deactivateCdpIfPossible();
- BrowserTCIFactory.BrowserConsoleLogLevel browserConsoleLogLevel();
+ BrowserTCIFactory.BrowserConsoleLogLevel minBrowserConsoleLogLevel();
}
diff --git a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java
index fa1ac71e..316952ac 100644
--- a/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java
+++ b/selenium/src/main/java/software/xdev/tci/selenium/factory/config/DefaultBrowserTCIFactoryConfig.java
@@ -30,12 +30,43 @@ public class DefaultBrowserTCIFactoryConfig extends DefaultConfig implements Bro
{
private static final Logger LOG = LoggerFactory.getLogger(DefaultBrowserTCIFactoryConfig.class);
- public static final String RECORD_MODE = "recordMode";
- public static final String RECORD_DIR = "recordDir";
- public static final String VNC_ENABLED = "vncEnabled";
- public static final String BIDI_ENABLED = "bidiEnabled";
- public static final String DEACTIVATE_CDP_IF_POSSIBLE = "deactivateCdpIfPossible";
- public static final String BROWSER_CONSOLE_LOG_LEVEL = "browserConsoleLogLevel";
+ /**
+ * @deprecated Use non legacy option instead
+ */
+ @Deprecated(since = "4.0.0")
+ public static final String LEGACY_RECORD_MODE = "recordMode";
+ /**
+ * @deprecated Use non legacy option instead
+ */
+ @Deprecated(since = "4.0.0")
+ public static final String LEGACY_RECORD_DIR = "recordDir";
+ /**
+ * @deprecated Use non legacy option instead
+ */
+ @Deprecated(since = "4.0.0")
+ public static final String LEGACY_VNC_ENABLED = "vncEnabled";
+ /**
+ * @deprecated Use non legacy option instead
+ */
+ @Deprecated(since = "4.0.0")
+ public static final String LEGACY_BIDI_ENABLED = "bidiEnabled";
+ /**
+ * @deprecated Use non legacy option instead
+ */
+ @Deprecated(since = "4.0.0")
+ public static final String LEGACY_DEACTIVATE_CDP_IF_POSSIBLE = "deactivateCdpIfPossible";
+ /**
+ * @deprecated Use non legacy option instead
+ */
+ @Deprecated(since = "4.0.0")
+ public static final String LEGACY_BROWSER_CONSOLE_LOG_LEVEL = "browserConsoleLogLevel";
+
+ public static final String RECORD_MODE = "record-mode";
+ public static final String RECORD_DIR = "record-dir";
+ public static final String VNC_ENABLED = "vnc-enabled";
+ public static final String BIDI_ENABLED = "bidi-enabled";
+ public static final String DEACTIVATE_CDP_IF_POSSIBLE = "deactivate-cdp-if-possible";
+ public static final String MIN_BROWSER_CONSOLE_LOG_LEVEL = "min-browser-console-log-level";
public static final String DEFAULT_RECORD_DIR = "target/records";
@@ -57,7 +88,10 @@ public BrowserWebDriverContainer.RecordingMode recordingMode()
{
if(this.systemRecordingMode == null)
{
- final String resolvedRecordMode = this.resolve(RECORD_MODE).orElse(null);
+ final String resolvedRecordMode = this.resolve(RECORD_MODE)
+ .or(() -> this.resolve(LEGACY_RECORD_MODE)
+ .map(v -> this.reportLegacyConfigOption(LEGACY_RECORD_MODE, RECORD_MODE, v)))
+ .orElse(null);
this.systemRecordingMode = Stream.of(BrowserWebDriverContainer.RecordingMode.values())
.filter(rm -> rm.toString().equals(resolvedRecordMode))
.findFirst()
@@ -72,7 +106,10 @@ public Path dirForRecords()
{
if(this.dirForRecords == null)
{
- this.dirForRecords = Path.of(this.resolve(RECORD_DIR).orElse(DEFAULT_RECORD_DIR));
+ this.dirForRecords = Path.of(this.resolve(RECORD_DIR)
+ .or(() -> this.resolve(LEGACY_RECORD_DIR)
+ .map(v -> this.reportLegacyConfigOption(LEGACY_RECORD_DIR, RECORD_DIR, v)))
+ .orElse(DEFAULT_RECORD_DIR));
final boolean wasCreated = this.dirForRecords.toFile().mkdirs();
LOG.info(
"Default directory for records='{}', created={}", this.dirForRecords.toAbsolutePath(),
@@ -87,7 +124,10 @@ public boolean vncEnabled()
{
if(this.vncEnabled == null)
{
- this.vncEnabled = this.resolveBool(VNC_ENABLED, false);
+ this.vncEnabled = this.resolveBool(VNC_ENABLED)
+ .or(() -> this.resolveBool(LEGACY_VNC_ENABLED)
+ .map(v -> this.reportLegacyConfigOption(LEGACY_VNC_ENABLED, VNC_ENABLED, v)))
+ .orElse(false);
LOG.info("VNC enabled={}", this.vncEnabled);
}
return this.vncEnabled;
@@ -98,7 +138,10 @@ public boolean bidiEnabled()
{
if(this.bidiEnabled == null)
{
- this.bidiEnabled = this.resolveBool(BIDI_ENABLED, true);
+ this.bidiEnabled = this.resolveBool(BIDI_ENABLED)
+ .or(() -> this.resolveBool(LEGACY_BIDI_ENABLED)
+ .map(v -> this.reportLegacyConfigOption(LEGACY_BIDI_ENABLED, BIDI_ENABLED, v)))
+ .orElse(true);
LOG.info("BiDi enabled={}", this.bidiEnabled);
}
return this.bidiEnabled;
@@ -116,11 +159,17 @@ public boolean deactivateCdpIfPossible()
}
@Override
- public BrowserTCIFactory.BrowserConsoleLogLevel browserConsoleLogLevel()
+ public BrowserTCIFactory.BrowserConsoleLogLevel minBrowserConsoleLogLevel()
{
if(this.browserConsoleLogLevel == null)
{
- this.browserConsoleLogLevel = this.resolve(BROWSER_CONSOLE_LOG_LEVEL)
+ this.browserConsoleLogLevel = this.resolve(MIN_BROWSER_CONSOLE_LOG_LEVEL)
+ .or(() -> this.resolve(LEGACY_BROWSER_CONSOLE_LOG_LEVEL)
+ .map(v -> this.reportLegacyConfigOption(
+ LEGACY_BROWSER_CONSOLE_LOG_LEVEL,
+ MIN_BROWSER_CONSOLE_LOG_LEVEL,
+ v))
+ )
.map(BrowserTCIFactory.BrowserConsoleLogLevel::valueOf)
.orElse(BrowserTCIFactory.BrowserConsoleLogLevel.ERROR);
LOG.info("BrowserConsoleLogLevel={}", this.browserConsoleLogLevel);
diff --git a/spring-dao-support/pom.xml b/spring-dao-support/pom.xml
index a5c9d8dc..421dd4ba 100644
--- a/spring-dao-support/pom.xml
+++ b/spring-dao-support/pom.xml
@@ -6,7 +6,7 @@
software.xdev.tci
spring-dao-support
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
jar
spring-dao-support
@@ -53,7 +53,7 @@
software.xdev.tci
db-jdbc
- 3.4.2-SNAPSHOT
+ 4.0.0-SNAPSHOT
@@ -78,7 +78,7 @@
org.junit.jupiter
junit-jupiter
- 6.1.0
+ 6.1.2
test
@@ -274,7 +274,7 @@
com.puppycrawl.tools
checkstyle
- 13.6.0
+ 13.8.0
@@ -312,12 +312,12 @@
net.sourceforge.pmd
pmd-core
- 7.25.0
+ 7.26.0
net.sourceforge.pmd
pmd-java
- 7.25.0
+ 7.26.0