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>