This is an automated email from the ASF dual-hosted git repository.

Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git


The following commit(s) were added to refs/heads/main by this push:
     new 5ed6c8bf2ae CAMEL-25465: Speed up and harden the starter integration 
test harness
5ed6c8bf2ae is described below

commit 5ed6c8bf2aeb16a9f7573c5f1dfadd70f7ba6b53
Author: croway <[email protected]>
AuthorDate: Thu Oct 8 16:28:10 2026 +0200

    CAMEL-25465: Speed up and harden the starter integration test harness
    
    The two nested Maven JVMs per test were about three quarters of a
    starter test's run time. The archetype project is now generated once
    per build into a file-locked template under target/ and copied per
    test, and both nested invocations share one runner with -nsu, a 1 GB
    heap and captured output.
    
    The dependency version mismatch check could never fail: it only looked
    for one artifact with two versions, which Maven mediation rules out. It
    now anchors on the local repository via the camel-spring-boot-starter
    jar, fails when org.apache.camel or org.apache.camel.springboot
    artifacts disagree, and warns for other groups differing in major.minor.
    
    maven-core is replaced by maven-model so the Maven runtime no longer
    shadows the application under test on the parent classloader. The
    archetype config is resolved lazily, the extension registers in the
    class-level store, the fat-jar IT asserts every route, and the dead
    logback.xml and redundant junit-jupiter-api are removed.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
---
 .../CamelXmlWildcardRoutesIT.java                  |   9 +-
 tests/camel-itest-spring-boot/README.adoc          |  10 +-
 tests/camel-itest-spring-boot/pom.xml              |   8 +-
 .../common/AbstractSpringBootBaseTestSupport.java  | 137 ++++++++------
 .../common/ArchetypeGenerationExtension.java       | 206 ++++++++++++++++-----
 .../src/main/resources/logback.xml                 |  30 ---
 6 files changed, 248 insertions(+), 152 deletions(-)

diff --git 
a/tests/camel-itest-spring-boot-farjar/src/test/java/org/apache/camel/springfatjartests/CamelXmlWildcardRoutesIT.java
 
b/tests/camel-itest-spring-boot-farjar/src/test/java/org/apache/camel/springfatjartests/CamelXmlWildcardRoutesIT.java
index b43835fd8ed..72b7295935d 100644
--- 
a/tests/camel-itest-spring-boot-farjar/src/test/java/org/apache/camel/springfatjartests/CamelXmlWildcardRoutesIT.java
+++ 
b/tests/camel-itest-spring-boot-farjar/src/test/java/org/apache/camel/springfatjartests/CamelXmlWildcardRoutesIT.java
@@ -41,10 +41,11 @@ public class CamelXmlWildcardRoutesIT {
 
     @Test
     public void shouldDetectRoutes() {
-        // When
-        Route route = camelContext.getRoute("foobar-route");
-        // Then
-        assertNotNull(route);
+        // routes live in two directories of the 
camel-itest-resources-for-fatjar jar
+        for (String routeId : new String[] { "foobar-route", "abc-route", 
"xyz-route" }) {
+            Route route = camelContext.getRoute(routeId);
+            assertNotNull(route, "route not loaded from jar: " + routeId);
+        }
     }
 
 }
diff --git a/tests/camel-itest-spring-boot/README.adoc 
b/tests/camel-itest-spring-boot/README.adoc
index 3cbc8c1a4a5..2e9fc0f9c2b 100644
--- a/tests/camel-itest-spring-boot/README.adoc
+++ b/tests/camel-itest-spring-boot/README.adoc
@@ -6,8 +6,8 @@ Integration tests that verify each Camel Spring Boot starter 
can be compiled, st
 
 Each test class (e.g. `CamelActivemq6IT`) follows these steps:
 
-. **Archetype generation** -- The `ArchetypeGenerationExtension` JUnit 6 
extension generates a minimal Spring Boot project from 
`camel-archetype-spring-boot`. The generated project is then customized: 
test-scoped dependencies and the demo `camel-stream-starter` are removed, the 
starter under test is added, and any extra dependencies or 
`application.properties` entries from the test config are applied.
-. **Compilation** -- The generated project is compiled via `mvn compile 
dependency:build-classpath`, which also writes the full runtime classpath to a 
file for the next step.
+. **Archetype generation** -- The `ArchetypeGenerationExtension` JUnit 6 
extension generates a minimal Spring Boot project from 
`camel-archetype-spring-boot`. Because `archetype:generate` is a Maven JVM 
start per invocation, the project is generated once per build into 
`target/archetype-templates/<key>` (the key covers the archetype jar and the 
versions passed to it, concurrent forks are serialised with a file lock) and 
copied for every test. The copy is then customized: the artifactId i [...]
+. **Compilation** -- The generated project is compiled via `mvn compile 
dependency:build-classpath`, which also writes the full runtime classpath to a 
file for the next step. Both nested Maven invocations run with `-nsu` (the 
reactor build just installed every snapshot they need) and a 1 GB heap instead 
of the one in `.mvn/jvm.config`.
 . **Classloader isolation** -- A child `URLClassLoader` is created with the 
compiled classes and all resolved dependency JARs, parented by the test 
classloader. Only `camel-api` and `spring-context` live on the parent 
classloader so that assertion types (`CamelContext`, `Component`, `DataFormat`, 
`Language`, `CamelClusterService`) are shared.
 . **Spring Boot startup** -- `SpringApplication` is loaded from the child 
classloader and started via reflection. A `DefaultResourceLoader` pointing at 
the child classloader ensures the generated project's `application.properties` 
is picked up. By default, the embedded web server is disabled 
(`spring.main.web-application-type=none`). When `webRequired` is set to `true`, 
the web stack is kept and the server binds to a random port (`server.port=0`) 
to avoid conflicts during parallel execution.
 . **Assertions** -- Tests verify the `CamelContext` is running, 
components/dataformats/languages can be resolved, and no dependency version 
mismatches exist.
@@ -45,7 +45,7 @@ AbstractSpringBootBaseTestSupport        archetype generation 
via @RegisterExten
 Every subclass of `AbstractSpringBootBaseTestSupport` automatically inherits 
two `@Test` methods:
 
 * **`camelContextIsRunning`** -- Verifies the `CamelContext` bean exists and 
is started.
-* **`dependencyVersionMismatchTest`** -- Parses the resolved runtime classpath 
(Maven local repository JAR paths), groups artifacts by 
`groupId:artifactPrefix`, and fails if the same artifact appears with multiple 
versions that differ in major or minor (patch-only differences are reported as 
warnings). Warning reports are written to 
`target/failsafe-reports/<module>-version-mismatches.txt`.
+* **`dependencyVersionMismatchTest`** -- Parses the resolved runtime classpath 
(Maven local repository JAR paths), groups artifacts by 
`groupId:artifactPrefix`, and fails if artifacts of Camel's own groups 
(`org.apache.camel`, `org.apache.camel.springboot`) disagree on their version. 
For any other group, versions differing in major or minor are reported as 
warnings. Warning reports are written to 
`target/failsafe-reports/<module>-version-mismatches.txt`.
 
 == Configuration
 
@@ -132,6 +132,10 @@ mvn verify -pl tests/camel-itest-spring-boot 
-Dit.test=CamelActivemq6IT
 | `it.test`
 | (all)
 | Run specific test(s), e.g. `-Dit.test=CamelCoreIT,CamelFtpIT`
+
+| `mvn-command`
+| `./mvnw`
+| Maven command used for the nested invocations (archetype generation, 
compilation). With the Maven daemon installed, `-Dmvn-command=mvnd` cuts each 
nested invocation by roughly half to two thirds.
 |===
 
 == Parallelism
diff --git a/tests/camel-itest-spring-boot/pom.xml 
b/tests/camel-itest-spring-boot/pom.xml
index 545fb7e7d35..bbcfa6252ac 100644
--- a/tests/camel-itest-spring-boot/pom.xml
+++ b/tests/camel-itest-spring-boot/pom.xml
@@ -57,9 +57,11 @@
     </dependencyManagement>
 
     <dependencies>
+        <!-- only the pom model is needed; the whole Maven runtime (guava, 
guice, asm...) would otherwise
+             sit on the parent classloader of the application under test and 
shadow its own versions -->
         <dependency>
             <groupId>org.apache.maven</groupId>
-            <artifactId>maven-core</artifactId>
+            <artifactId>maven-model</artifactId>
             <version>${maven-version}</version>
         </dependency>
         <dependency>
@@ -89,10 +91,6 @@
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter</artifactId>
         </dependency>
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter-api</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.junit.platform</groupId>
             <artifactId>junit-platform-launcher</artifactId>
diff --git 
a/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/common/AbstractSpringBootBaseTestSupport.java
 
b/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/common/AbstractSpringBootBaseTestSupport.java
index b4dd6421177..2c6c46140c1 100644
--- 
a/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/common/AbstractSpringBootBaseTestSupport.java
+++ 
b/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/common/AbstractSpringBootBaseTestSupport.java
@@ -16,24 +16,27 @@
  */
 package org.apache.camel.itest.springboot.common;
 
-import ch.qos.logback.classic.LoggerContext;
 import org.apache.camel.CamelContext;
 import org.apache.camel.Component;
 import org.apache.camel.cluster.CamelClusterService;
 import org.apache.camel.spi.DataFormat;
 import org.apache.camel.spi.Language;
-import org.junit.jupiter.api.*;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.context.ConfigurableApplicationContext;
 
 import java.io.File;
+import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -47,9 +50,19 @@ public abstract class AbstractSpringBootBaseTestSupport {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(AbstractSpringBootBaseTestSupport.class);
     private static final String CLASSPATH_FILE = "target/classpath.txt";
+    private static final String STARTER_ARTIFACT_ID = 
"camel-spring-boot-starter";
+    /**
+     * Artifacts of these groups all come from a single release, so any 
version disagreement among them is
+     * a real mismatch, not a potential one.
+     */
+    private static final Set<String> CAMEL_GROUP_IDS = 
Set.of("org.apache.camel", "org.apache.camel.springboot");
+    /** artifacts versioned independently of the others sharing their groupId 
and prefix, by groupId:artifactId prefix */
+    private static final List<String> IGNORED_ARTIFACTS = 
List.of("io.netty:netty-tcnative");
 
+    // the supplier is resolved lazily by the extension, so a subclass may 
override getArchetypeConfig()
+    // using its own fields even though this field is initialised before them
     @RegisterExtension
-    ArchetypeGenerationExtension archetype = new 
ArchetypeGenerationExtension(getArchetypeConfig());
+    ArchetypeGenerationExtension archetype = new 
ArchetypeGenerationExtension(this::getArchetypeConfig);
 
     private List<String> classpathEntries;
 
@@ -73,32 +86,12 @@ public abstract class AbstractSpringBootBaseTestSupport {
 
     @BeforeAll
     void compileAndStartApp() throws Exception {
-        ((LoggerContext) 
LoggerFactory.getILoggerFactory()).setName(inferModuleName(getClass()));
-
         Path projectDir = archetype.getGeneratedProject().getProjectDir();
+        ArchetypeConfig config = archetype.getArchetypeConfig();
 
-        List<String> command = new ArrayList<>(List.of(
-                ArchetypeGenerationExtension.resolveMavenCommand(), "-q", 
"compile", "dependency:build-classpath",
+        ArchetypeGenerationExtension.runMaven(projectDir, "compile", 
"dependency:build-classpath",
                 "-DincludeScope=runtime",
-                "-Dmdep.outputFile=" + CLASSPATH_FILE,
-                "-B"));
-        String localRepo = System.getProperty("maven.repo.local");
-        if (localRepo != null) {
-            command.add("-Dmaven.repo.local=" + localRepo);
-        }
-
-        LOG.debug("running: {}", String.join(" ", command));
-
-        ProcessBuilder pb = new ProcessBuilder(command)
-                .directory(projectDir.toFile())
-                .redirectErrorStream(true);
-        Process process = pb.start();
-        String output = new String(process.getInputStream().readAllBytes());
-        int exitCode = process.waitFor();
-        if (exitCode != 0) {
-            throw new RuntimeException(String.join(" ", command) + " failed 
(exit code "
-                    + exitCode + ") for " + projectDir + ":" + 
System.lineSeparator() + output);
-        }
+                "-Dmdep.outputFile=" + CLASSPATH_FILE);
 
         Path classpathFile = projectDir.resolve(CLASSPATH_FILE);
         String cpContent = Files.readString(classpathFile).trim();
@@ -114,15 +107,12 @@ public abstract class AbstractSpringBootBaseTestSupport {
             urls.add(new File(entry).toURI().toURL());
         }
 
-        appClassLoader = new URLClassLoader(
-                urls.toArray(URL[]::new),
-                Thread.currentThread().getContextClassLoader());
-
         originalClassLoader = Thread.currentThread().getContextClassLoader();
+        appClassLoader = new URLClassLoader(urls.toArray(URL[]::new), 
originalClassLoader);
         Thread.currentThread().setContextClassLoader(appClassLoader);
 
         Class<?> springAppClass = 
appClassLoader.loadClass("org.springframework.boot.SpringApplication");
-        Class<?> appClass = 
appClassLoader.loadClass(getArchetypeConfig().getMainClassFqn());
+        Class<?> appClass = appClassLoader.loadClass(config.getMainClassFqn());
 
         Object springApp = springAppClass.getConstructor(Class[].class)
                 .newInstance((Object) new Class<?>[]{appClass});
@@ -142,7 +132,7 @@ public abstract class AbstractSpringBootBaseTestSupport {
         // Bridge CamelClusterService beans from Spring to the CamelContext 
service registry.
         // The child classloader prevents 
DefaultConfigurationConfigurer.afterConfigure()
         // from discovering these beans via the Camel registry.
-        if (getArchetypeConfig().isRegisterClusterServices()) {
+        if (config.isRegisterClusterServices()) {
             Map<String, CamelClusterService> clusterServices
                     = 
applicationContext.getBeansOfType(CamelClusterService.class);
             for (CamelClusterService css : clusterServices.values()) {
@@ -174,7 +164,9 @@ public abstract class AbstractSpringBootBaseTestSupport {
                 applicationContext.close();
             }
         } finally {
-            Thread.currentThread().setContextClassLoader(originalClassLoader);
+            if (originalClassLoader != null) {
+                
Thread.currentThread().setContextClassLoader(originalClassLoader);
+            }
             if (appClassLoader != null) {
                 appClassLoader.close();
             }
@@ -236,42 +228,49 @@ public abstract class AbstractSpringBootBaseTestSupport {
     }
 
     /**
-     * Parses the runtime classpath entries (Maven local repository JAR paths) 
and detects
-     * version mismatches among artifacts sharing the same groupId and 
artifact prefix.
+     * Parses the runtime classpath entries (Maven local repository JAR paths) 
and detects version
+     * mismatches among artifacts sharing the same groupId and artifact prefix.
      * <p>
      * Maven repository paths follow the layout:
      * {@code 
<repo>/<groupId-dirs>/<artifactId>/<version>/<artifactId>-<version>.jar}
+     * <p>
+     * Artifacts of Camel's own groups ({@link #CAMEL_GROUP_IDS}) must all 
have the same version, any
+     * disagreement fails the test. For other groups, versions differing in 
major or minor are reported
+     * as potential mismatches: logged as a warning and written to
+     * {@code target/failsafe-reports/<module>-version-mismatches.txt}.
      */
     protected void assertNoVersionMismatch() {
         Assertions.assertNotNull(classpathEntries, "Classpath not resolved 
yet");
 
         record Artifact(String groupId, String artifactId, String version) {}
 
+        Path localRepository = localRepository();
         List<Artifact> artifacts = new ArrayList<>();
         for (String entry : classpathEntries) {
-            Path path = Path.of(entry);
-            String fileName = path.getFileName().toString();
-            if (!fileName.endsWith(".jar")) {
+            Path path = Path.of(entry).toAbsolutePath().normalize();
+            if (!path.getFileName().toString().endsWith(".jar") || 
!path.startsWith(localRepository)) {
                 continue;
             }
+            // <groupId-dirs>/<artifactId>/<version>/<jar>
+            Path relative = localRepository.relativize(path);
+            int count = relative.getNameCount();
+            if (count < 4) {
+                continue;
+            }
+            String version = relative.getName(count - 2).toString();
+            String artifactId = relative.getName(count - 3).toString();
+            StringBuilder groupId = new StringBuilder();
+            for (int i = 0; i < count - 3; i++) {
+                if (i > 0) {
+                    groupId.append('.');
+                }
+                groupId.append(relative.getName(i));
+            }
 
-            Path versionDir = path.getParent();
-            if (versionDir == null) continue;
-            Path artifactDir = versionDir.getParent();
-            if (artifactDir == null) continue;
-
-            String version = versionDir.getFileName().toString();
-            String artifactId = artifactDir.getFileName().toString();
-
-            String fullPath = artifactDir.getParent() == null ? "" : 
artifactDir.getParent().toString();
-            int repoIdx = fullPath.indexOf("repository" + File.separator);
-            if (repoIdx < 0) continue;
-            String groupPath = fullPath.substring(repoIdx + 
"repository".length() + 1);
-            String groupId = groupPath.replace(File.separatorChar, '.');
-
-            artifacts.add(new Artifact(groupId, artifactId, version));
+            artifacts.add(new Artifact(groupId.toString(), artifactId, 
version));
             LOG.debug("Dependency: {}:{}:{}", groupId, artifactId, version);
         }
+        Assertions.assertFalse(artifacts.isEmpty(), "No artifacts from " + 
localRepository + " on the classpath");
 
         Map<String, Map<String, Set<String>>> status = new TreeMap<>();
         for (Artifact a : artifacts) {
@@ -281,6 +280,9 @@ public abstract class AbstractSpringBootBaseTestSupport {
             }
             String prefixId = a.groupId() + ":" + artifactPrefix;
             String identifier = a.groupId() + ":" + a.artifactId();
+            if (IGNORED_ARTIFACTS.stream().anyMatch(identifier::startsWith)) {
+                continue;
+            }
 
             status.computeIfAbsent(prefixId, k -> new TreeMap<>())
                     .computeIfAbsent(identifier, k -> new LinkedHashSet<>())
@@ -302,11 +304,10 @@ public abstract class AbstractSpringBootBaseTestSupport {
                 continue;
             }
 
-            boolean exactMismatch = artifactVersions.values().stream()
-                    .anyMatch(v -> v.size() > 1 && !differOnlyInPatch(v));
-            if (exactMismatch) {
+            String groupId = prefixId.substring(0, prefixId.indexOf(':'));
+            if (CAMEL_GROUP_IDS.contains(groupId)) {
                 mismatches.add(prefixId);
-            } else {
+            } else if (!differOnlyInPatch(allVersions)) {
                 potentialMismatches.add(prefixId);
             }
         }
@@ -345,9 +346,27 @@ public abstract class AbstractSpringBootBaseTestSupport {
                 "Library version mismatches found in runtime dependencies:" + 
System.lineSeparator() + message);
     }
 
+    /**
+     * Locates the Maven local repository from the classpath entry of {@code 
camel-spring-boot-starter},
+     * which every generated project depends on. This does not rely on the 
repository's location or name,
+     * which can be configured in settings.xml and is not visible from here.
+     */
+    private Path localRepository() {
+        String starterJar = STARTER_ARTIFACT_ID + "-" + 
System.getProperty("project-version") + ".jar";
+        for (String entry : classpathEntries) {
+            Path path = Path.of(entry).toAbsolutePath().normalize();
+            if (path.getFileName().toString().equals(starterJar)) {
+                // 
<repo>/org/apache/camel/springboot/camel-spring-boot-starter/<version>/<jar>
+                return path.getParent().getParent().getParent()
+                        .getParent().getParent().getParent().getParent();
+            }
+        }
+        throw new AssertionError(starterJar + " not found on the runtime 
classpath: " + classpathEntries);
+    }
+
     /**
      * Returns the major.minor prefix of a semantic version string.
-     * E.g. {@code "1.2.3"} -> {@code "1.2"}, {@code "4.23.0-SNAPSHOT"} -> 
{@code "4.19"}.
+     * E.g. {@code "1.2.3"} -> {@code "1.2"}, {@code "4.23.0-SNAPSHOT"} -> 
{@code "4.23"}.
      * Returns the full version if it has fewer than two dot-separated 
segments.
      */
     private static String majorMinor(String version) {
diff --git 
a/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/common/ArchetypeGenerationExtension.java
 
b/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/common/ArchetypeGenerationExtension.java
index a9461b24bb0..125c5a7f42a 100644
--- 
a/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/common/ArchetypeGenerationExtension.java
+++ 
b/tests/camel-itest-spring-boot/src/main/java/org/apache/camel/itest/springboot/common/ArchetypeGenerationExtension.java
@@ -28,28 +28,41 @@ import org.junit.jupiter.api.extension.ExtensionContext;
 import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
 import org.junit.jupiter.api.extension.ParameterContext;
 import org.junit.jupiter.api.extension.ParameterResolver;
+import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.Reader;
 import java.io.Writer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Properties;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
 
 public class ArchetypeGenerationExtension implements BeforeAllCallback, 
BeforeEachCallback, ParameterResolver {
 
+    private static final Logger LOG = 
LoggerFactory.getLogger(ArchetypeGenerationExtension.class);
     private static final Namespace NAMESPACE = 
Namespace.create(ArchetypeGenerationExtension.class);
 
     private static final String ARCHETYPE_GROUP_ID = 
"org.apache.camel.archetypes";
     private static final String ARCHETYPE_ARTIFACT_ID = 
"camel-archetype-spring-boot";
+    /** artifactId of the project generated once per build and copied for 
every test, see {@link #templateProject} */
+    private static final String TEMPLATE_ARTIFACT_ID = "archetype-template";
 
-    private final ArchetypeConfig archetypeConfig;
+    private final Supplier<ArchetypeConfig> configSupplier;
+    private ArchetypeConfig archetypeConfig;
     private GeneratedProject generatedProject;
 
     public ArchetypeGenerationExtension() {
@@ -57,7 +70,15 @@ public class ArchetypeGenerationExtension implements 
BeforeAllCallback, BeforeEa
     }
 
     public ArchetypeGenerationExtension(ArchetypeConfig archetypeConfig) {
-        this.archetypeConfig = archetypeConfig;
+        this(() -> archetypeConfig);
+    }
+
+    /**
+     * The supplier is only called when the project is first needed, so a test 
class can register the
+     * extension in a field initializer and still build its configuration from 
its own fields.
+     */
+    public ArchetypeGenerationExtension(Supplier<ArchetypeConfig> 
configSupplier) {
+        this.configSupplier = configSupplier;
     }
 
     @Override
@@ -67,7 +88,9 @@ public class ArchetypeGenerationExtension implements 
BeforeAllCallback, BeforeEa
 
     @Override
     public void beforeEach(ExtensionContext context) throws Exception {
-        ensureProjectCreated(context);
+        // only reached without a preceding beforeAll (PER_METHOD lifecycle); 
register the project in the
+        // class-level store, otherwise it would be deleted after the first 
test method and reused by the next
+        ensureProjectCreated(context.getParent().orElse(context));
     }
 
     private void ensureProjectCreated(ExtensionContext context) {
@@ -88,63 +111,128 @@ public class ArchetypeGenerationExtension implements 
BeforeAllCallback, BeforeEa
         return generatedProject;
     }
 
+    public ArchetypeConfig getArchetypeConfig() {
+        if (archetypeConfig == null) {
+            archetypeConfig = configSupplier.get();
+        }
+        return archetypeConfig;
+    }
+
+    public GeneratedProject getGeneratedProject() {
+        return generatedProject;
+    }
+
     private GeneratedProject createProject() {
         try {
-            ((LoggerContext) 
LoggerFactory.getILoggerFactory()).setName(archetypeConfig.artifactId);
+            ArchetypeConfig config = getArchetypeConfig();
+            ((LoggerContext) 
LoggerFactory.getILoggerFactory()).setName(config.getArtifactId());
             Path targetDir = 
Path.of(requireSystemProperty("project.build.directory"));
             Files.createDirectories(targetDir);
+
+            Path template = templateProject(targetDir, config);
             Path outputDir = Files.createTempDirectory(targetDir, 
"archetype-test-");
+            Path projectDir = outputDir.resolve(config.getArtifactId());
+            copyDirectory(template, projectDir);
+            customizeArchetype(projectDir, config);
+            return new GeneratedProject(outputDir, projectDir);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to generate archetype project", 
e);
+        }
+    }
 
-            String archetypeVersion = requireSystemProperty("project-version");
-            String camelVersion = requireSystemProperty("camel-version");
-            String springBootVersion = 
requireSystemProperty("spring-boot-version");
-            String mavenCompilerPluginVersion = 
requireSystemProperty("maven-compiler-plugin-version");
-            String mavenVersion = requireSystemProperty("maven-version");
+    /**
+     * Returns the project generated from the archetype, generating it on 
first use.
+     * <p>
+     * {@code archetype:generate} is a Maven JVM start per invocation and 
costs about as much as the rest of
+     * a starter test together, while its output only depends on the archetype 
and the versions passed to
+     * it. So the project is generated once per build under {@code 
target/archetype-templates/<key>} and
+     * copied for every test. The key includes the archetype jar's timestamp 
and size, so a rebuilt archetype
+     * is picked up without a clean. Concurrent failsafe forks are serialised 
with a file lock, and the
+     * template directory only appears once it is complete (atomic move).
+     */
+    private static Path templateProject(Path targetDir, ArchetypeConfig 
config) throws Exception {
+        String archetypeVersion = requireSystemProperty("project-version");
+        String camelVersion = requireSystemProperty("camel-version");
+        String springBootVersion = 
requireSystemProperty("spring-boot-version");
+        String mavenCompilerPluginVersion = 
requireSystemProperty("maven-compiler-plugin-version");
+        String mavenVersion = requireSystemProperty("maven-version");
+
+        File archetypeFile = resolveArchetypeJar(archetypeVersion);
+        if (!archetypeFile.exists()) {
+            throw new IllegalStateException("Archetype JAR not found at: " + 
archetypeFile
+                    + ". Run 'mvn install -pl 
archetypes/camel-archetype-spring-boot -am -DskipTests' first.");
+        }
 
-            File archetypeFile = resolveArchetypeJar(archetypeVersion);
-            if (!archetypeFile.exists()) {
-                throw new IllegalStateException("Archetype JAR not found at: " 
+ archetypeFile
-                        + ". Run 'mvn install -pl 
archetypes/camel-archetype-spring-boot -am -DskipTests' first.");
-            }
+        String key = 
Integer.toHexString(Objects.hash(archetypeFile.lastModified(), 
archetypeFile.length(),
+                config.getGroupId(), config.getVersion(), 
config.getPackageName(),
+                camelVersion, springBootVersion, mavenCompilerPluginVersion, 
mavenVersion));
+        Path templatesDir = targetDir.resolve("archetype-templates");
+        Path template = templatesDir.resolve(key);
+        if (Files.isDirectory(template)) {
+            return template;
+        }
 
-            List<String> command = new ArrayList<>(List.of(
-                    resolveMavenCommand(), "-q", "archetype:generate",
-                    "-B",
+        Files.createDirectories(templatesDir);
+        Path lockFile = templatesDir.resolve(key + ".lock");
+        try (FileChannel channel = FileChannel.open(lockFile, 
StandardOpenOption.CREATE, StandardOpenOption.WRITE);
+             FileLock lock = channel.lock()) {
+            if (Files.isDirectory(template)) {
+                // generated by another fork while waiting for the lock
+                return template;
+            }
+            LOG.info("Generating archetype template project in {}", template);
+            Path workDir = Files.createTempDirectory(templatesDir, key + "-");
+            runMaven(workDir, "archetype:generate",
                     "-DarchetypeGroupId=" + ARCHETYPE_GROUP_ID,
                     "-DarchetypeArtifactId=" + ARCHETYPE_ARTIFACT_ID,
                     "-DarchetypeVersion=" + archetypeVersion,
-                    "-DgroupId=" + archetypeConfig.groupId,
-                    "-DartifactId=" + archetypeConfig.artifactId,
-                    "-Dversion=" + archetypeConfig.version,
-                    "-Dpackage=" + archetypeConfig.packageName,
+                    "-DgroupId=" + config.getGroupId(),
+                    "-DartifactId=" + TEMPLATE_ARTIFACT_ID,
+                    "-Dversion=" + config.getVersion(),
+                    "-Dpackage=" + config.getPackageName(),
                     "-Dcamel-version=" + camelVersion,
                     "-Dspring-boot-version=" + springBootVersion,
                     "-Dmaven-compiler-plugin-version=" + 
mavenCompilerPluginVersion,
-                    "-Dmaven-version=" + mavenVersion));
-
-            String localRepo = System.getProperty("maven.repo.local");
-            if (localRepo != null) {
-                command.add("-Dmaven.repo.local=" + localRepo);
-            }
-
-            ProcessBuilder pb = new ProcessBuilder(command)
-                    .directory(outputDir.toFile())
-                    .redirectOutput(ProcessBuilder.Redirect.DISCARD)
-                    .redirectError(ProcessBuilder.Redirect.DISCARD);
-            Process process = pb.start();
-            int exitCode = process.waitFor();
-
-            if (exitCode != 0) {
-                throw new RuntimeException("Archetype generation failed (exit 
code "
-                        + exitCode + ")");
-            }
+                    "-Dmaven-version=" + mavenVersion);
+            Files.move(workDir.resolve(TEMPLATE_ARTIFACT_ID), template, 
StandardCopyOption.ATOMIC_MOVE);
+            FileUtil.removeDir(workDir.toFile());
+        }
+        return template;
+    }
 
-            Path projectDir = outputDir.resolve(archetypeConfig.artifactId);
-            customizeArchetype(projectDir, archetypeConfig);
-            return new GeneratedProject(outputDir, projectDir);
-        } catch (Exception e) {
-            throw new RuntimeException("Failed to generate archetype project", 
e);
+    /**
+     * Runs Maven in {@code workDir} and returns its output, failing with the 
output on a non-zero exit.
+     * <p>
+     * The invocation is quiet, in batch mode and skips snapshot update checks 
({@code -nsu}): every snapshot
+     * the generated project needs was just installed by the reactor build, 
and the remote metadata check
+     * costs up to a second per invocation. Missing artifacts are still 
downloaded.
+     */
+    static String runMaven(Path workDir, String... args) throws IOException, 
InterruptedException {
+        List<String> command = new ArrayList<>();
+        command.add(resolveMavenCommand());
+        command.addAll(List.of("-q", "-B", "-nsu"));
+        command.addAll(List.of(args));
+        String localRepo = System.getProperty("maven.repo.local");
+        if (localRepo != null) {
+            command.add("-Dmaven.repo.local=" + localRepo);
+        }
+        LOG.debug("running: {}", String.join(" ", command));
+
+        ProcessBuilder pb = new ProcessBuilder(command)
+                .directory(workDir.toFile())
+                .redirectErrorStream(true);
+        // The generated project compiles a single class: a small heap keeps 
the parallel forks from each
+        // reserving the heap configured in .mvn/jvm.config, which the child 
inherits. A -Xmx in the
+        // caller's MAVEN_OPTS comes last and therefore wins.
+        pb.environment().merge("MAVEN_OPTS", "-Xmx1g", (user, ours) -> ours + 
" " + user);
+        Process process = pb.start();
+        String output = new String(process.getInputStream().readAllBytes());
+        int exitCode = process.waitFor();
+        if (exitCode != 0) {
+            throw new RuntimeException(String.join(" ", command) + " failed 
(exit code "
+                    + exitCode + ") in " + workDir + ":" + 
System.lineSeparator() + output);
         }
+        return output;
     }
 
     private static String requireSystemProperty(String key) {
@@ -164,7 +252,7 @@ public class ArchetypeGenerationExtension implements 
BeforeAllCallback, BeforeEa
         return command;
     }
 
-    private File resolveArchetypeJar(String version) {
+    private static File resolveArchetypeJar(String version) {
         String localRepo = System.getProperty("maven.repo.local");
         if (localRepo == null) {
             localRepo = System.getProperty("user.home") + File.separator + 
".m2" + File.separator + "repository";
@@ -173,8 +261,17 @@ public class ArchetypeGenerationExtension implements 
BeforeAllCallback, BeforeEa
                 , ARCHETYPE_ARTIFACT_ID, version, ARCHETYPE_ARTIFACT_ID + "-" 
+ version + ".jar").toFile();
     }
 
-    public GeneratedProject getGeneratedProject() {
-        return generatedProject;
+    private static void copyDirectory(Path source, Path target) throws 
IOException {
+        try (Stream<Path> paths = Files.walk(source)) {
+            for (Path path : (Iterable<Path>) paths::iterator) {
+                Path destination = 
target.resolve(source.relativize(path).toString());
+                if (Files.isDirectory(path)) {
+                    Files.createDirectories(destination);
+                } else {
+                    Files.copy(path, destination, 
StandardCopyOption.COPY_ATTRIBUTES);
+                }
+            }
+        }
     }
 
     /**
@@ -190,10 +287,13 @@ public class ArchetypeGenerationExtension implements 
BeforeAllCallback, BeforeEa
 
         Path mainJavaDir = projectDir.resolve("src/main/java");
         if (Files.exists(mainJavaDir)) {
-            Files.walk(mainJavaDir)
-                    .filter(p -> p.toString().endsWith(".java"))
-                    .filter(p -> 
!p.getFileName().toString().equals(mainClassFile))
-                    .forEach(p -> p.toFile().delete());
+            try (Stream<Path> paths = Files.walk(mainJavaDir)) {
+                for (Path path : (Iterable<Path>) paths::iterator) {
+                    if (path.toString().endsWith(".java") && 
!path.getFileName().toString().equals(mainClassFile)) {
+                        Files.delete(path);
+                    }
+                }
+            }
         }
 
         Path pomFile = projectDir.resolve("pom.xml");
@@ -203,7 +303,11 @@ public class ArchetypeGenerationExtension implements 
BeforeAllCallback, BeforeEa
             model = reader.read(r);
         }
 
+        // the project was generated once with a placeholder artifactId, see 
templateProject
+        model.setArtifactId(config.getArtifactId());
+
         model.getDependencies().removeIf(d -> "test".equals(d.getScope()));
+        // only used by the sample route deleted above
         model.getDependencies().removeIf(d -> 
"camel-stream-starter".equals(d.getArtifactId()));
 
         if (!config.isWebRequired()) {
diff --git a/tests/camel-itest-spring-boot/src/main/resources/logback.xml 
b/tests/camel-itest-spring-boot/src/main/resources/logback.xml
deleted file mode 100644
index 87d03b87c6d..00000000000
--- a/tests/camel-itest-spring-boot/src/main/resources/logback.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-
-    Licensed to the Apache Software Foundation (ASF) under one or more
-    contributor license agreements.  See the NOTICE file distributed with
-    this work for additional information regarding copyright ownership.
-    The ASF licenses this file to You 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.
-
--->
-<configuration>
-    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
-        <encoder>
-            <pattern>[%-5level] %msg%n</pattern>
-        </encoder>
-    </appender>
-
-    <root level="INFO">
-        <appender-ref ref="STDOUT"/>
-    </root>
-</configuration>

Reply via email to