This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-kit.git
commit 17f91c1e9412f622c0e6036757657d395e0fa090 Author: Jinal Sarvaiya <[email protected]> AuthorDate: Wed Nov 11 20:04:08 2026 +0000 review feedback --- .../camel/kit/CamelKitCommandParityTest.java | 27 +++++++- .../camel/kit/service/BaselineTestResults.java | 6 +- .../org/apache/camel/kit/service/InitService.java | 5 +- .../main/resources/skills/camel-execute/SKILL.md | 14 ++-- .../main/resources/skills/camel-validate/SKILL.md | 20 +++--- .../camel/kit/service/BaselineTestResultsTest.java | 6 +- .../apache/camel/kit/graph/ProjectDiscovery.java | 73 +++++++++++++------ .../apache/camel/kit/graph/parser/PomParser.java | 18 +++-- .../camel/kit/graph/ProjectDiscoveryTest.java | 81 ++++++++++++++++++++-- .../camel/kit/graph/parser/PomParserTest.java | 7 +- 10 files changed, 200 insertions(+), 57 deletions(-) diff --git a/kit/camel-jbang-plugin-kit/src/test/java/org/apache/camel/kit/CamelKitCommandParityTest.java b/kit/camel-jbang-plugin-kit/src/test/java/org/apache/camel/kit/CamelKitCommandParityTest.java index db707b90..0ee04d92 100644 --- a/kit/camel-jbang-plugin-kit/src/test/java/org/apache/camel/kit/CamelKitCommandParityTest.java +++ b/kit/camel-jbang-plugin-kit/src/test/java/org/apache/camel/kit/CamelKitCommandParityTest.java @@ -121,7 +121,18 @@ class CamelKitCommandParityTest { // --record-baseline: the project's own wrapper is run and the result is recorded for (String name : new String[]{"standalone-baseline", "plugin-baseline"}) { - Path dir = existingProject(name, "#!/bin/sh\ntouch mvnw-was-run\nexit 0\n"); + // The wrapper must produce fresh Surefire reports for the baseline to be recorded as "passed" + Path dir = existingProject(name, """ + #!/bin/sh + touch mvnw-was-run + mkdir -p target/surefire-reports + cat > target/surefire-reports/TEST-com.example.RouteTest.xml <<'XML' + <testsuite name="com.example.RouteTest" tests="1"> + <testcase classname="com.example.RouteTest" name="testRoute"/> + </testsuite> + XML + exit 0 + """); int exit = name.startsWith("plugin") ? plugin.execute("kit", "init", dir.toString(), "--ai", "bob2", "--silent", "--no-fetch", "--force", "--record-baseline") @@ -132,6 +143,20 @@ class CamelKitCommandParityTest { assertEquals("passed", loadConfig(dir).getProperty("project.baselineTestStatus"), name); } + // --record-baseline with exit 0 but no reports: recorded as not_run + for (String name : new String[]{"standalone-no-reports", "plugin-no-reports"}) { + Path dir = existingProject(name, "#!/bin/sh\ntouch mvnw-was-run\nexit 0\n"); + int exit = name.startsWith("plugin") + ? plugin.execute("kit", "init", dir.toString(), "--ai", "bob2", "--silent", "--no-fetch", "--force", + "--record-baseline") + : standalone.execute("init", dir.toString(), "--ai", "bob2", "--silent", "--no-fetch", "--force", + "--record-baseline"); + assertEquals(0, exit, name); + assertTrue(Files.exists(dir.resolve("mvnw-was-run")), name + ": --record-baseline must run the wrapper"); + assertEquals("not_run", loadConfig(dir).getProperty("project.baselineTestStatus"), + name + ": exit 0 without fresh reports must record not_run"); + } + // without --replace-wrapper the project's wrapper is kept; with it, it is replaced for (String name : new String[]{"standalone-keep", "plugin-keep"}) { Path dir = existingProject(name, "#!/bin/sh\n# ORIGINAL\n"); diff --git a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/BaselineTestResults.java b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/BaselineTestResults.java index 79f7e13b..ee47171d 100644 --- a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/BaselineTestResults.java +++ b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/BaselineTestResults.java @@ -158,9 +158,12 @@ final class BaselineTestResults { * @param reason why, for a human or the validation report * @param revision the source revision the results were recorded against (the Git HEAD, or {@code unknown}) * @param dirty whether the working tree had uncommitted changes when the tests ran + * @param complete whether all test reports were successfully parsed; false if any were malformed or unreadable * @param tests per-test outcomes; empty when none were recorded */ - static String toJson(String status, String reason, String revision, boolean dirty, Map<String, Outcome> tests) + static String toJson( + String status, String reason, String revision, boolean dirty, boolean complete, + Map<String, Outcome> tests) throws IOException { ObjectNode root = MAPPER.createObjectNode(); root.put("status", status); @@ -168,6 +171,7 @@ final class BaselineTestResults { root.put("timestamp", Instant.now().toString()); root.put("revision", revision); root.put("dirty", dirty); + root.put("complete", complete); root.put("testCount", tests.size()); ObjectNode testNode = root.putObject("tests"); tests.forEach((id, outcome) -> { diff --git a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/InitService.java b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/InitService.java index c9992d60..65134512 100644 --- a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/InitService.java +++ b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/service/InitService.java @@ -930,7 +930,7 @@ public class InitService { } writeBaselineFile(projectRoot, baselineResultsFile, BaselineTestResults.toJson(status, reason, sourceRevision(projectRoot), - sourceTreeDirty(projectRoot), tests)); + sourceTreeDirty(projectRoot), collectionResult.complete(), tests)); config.setProperty("project.baselineTestStatus", status); } catch (Exception e) { writeBaselineStatus(baselineResultsFile, "not_run", "Could not execute tests: " + e.getMessage(), @@ -998,9 +998,10 @@ public class InitService { */ private void writeBaselineStatus(Path file, String status, String reason, Path projectRoot) { try { + // Error statuses (not_run, timeout) have no collection, so complete=true (no parse failures) writeBaselineFile(projectRoot, file, BaselineTestResults.toJson(status, reason, sourceRevision(projectRoot), - sourceTreeDirty(projectRoot), java.util.Map.of())); + sourceTreeDirty(projectRoot), true, java.util.Map.of())); } catch (Exception e) { // Best-effort - ignore write failures } diff --git a/kit/camel-kit-core/src/main/resources/skills/camel-execute/SKILL.md b/kit/camel-kit-core/src/main/resources/skills/camel-execute/SKILL.md index b4f16154..a81cfece 100644 --- a/kit/camel-kit-core/src/main/resources/skills/camel-execute/SKILL.md +++ b/kit/camel-kit-core/src/main/resources/skills/camel-execute/SKILL.md @@ -261,16 +261,18 @@ Read `.camel-kit/config.properties`: 6. **Capture the test baseline BEFORE the first edit (existing projects only):** - Read `.camel-kit/baseline-test-results.json`. If `status` is `passed` or `failed`, `tests` is not empty, `dirty` - is `false`, and `revision` equals the current `git rev-parse HEAD`, the baseline already exists. Do not touch it. + is `false`, `complete` is `true`, and `revision` equals the current `git rev-parse HEAD`, the baseline already + exists. Do not touch it. - Otherwise nothing has been edited yet, so this is the last moment a baseline can exist. Note the current timestamp (to reject stale reports), run the project's tests with its own wrapper (`./mvnw test -B`), then write `.camel-kit/baseline-test-results.json` in the schema that `camel-validate` Step 1 documents: `status`, `reason`, `timestamp`, `revision` (`git rev-parse HEAD`), `dirty` (`true` if `git status --porcelain --untracked-files=no` - is not empty), and every test under `tests` keyed `class#method` with `passed`, `failed`, `error` or `skipped`, - read from `target/surefire-reports/TEST-*.xml` and `target/failsafe-reports/TEST-*.xml` **modified after the - noted timestamp**. Reports older than the test invocation are stale and must be ignored. If the tests exit 0 but - produce no fresh reports, set `status` to `not_run` with reason "Tests did not run (no reports produced)" rather - than claiming they passed. + is not empty), `complete` (`true` if all reports parsed successfully, `false` if any were malformed), and every + test under `tests` keyed `[module:]class#method` (module prefix only for non-root modules) with `passed`, `failed`, + `error` or `skipped`, read from `target/surefire-reports/TEST-*.xml` and `target/failsafe-reports/TEST-*.xml` + **modified after the noted timestamp**. Reports older than the test invocation are stale and must be ignored. If + the tests exit 0 but produce no fresh reports, set `status` to `not_run` with reason "Tests did not run (no reports + produced)" rather than claiming they passed. If any report cannot be parsed, set `complete` to `false`. - If the tests cannot be run (no wrapper, build broken, timeout), write the file with `status` `not_run` or `timeout` and the reason, and tell the user that baseline preservation will be reported as UNVERIFIED. Do not proceed as if a baseline existed. diff --git a/kit/camel-kit-core/src/main/resources/skills/camel-validate/SKILL.md b/kit/camel-kit-core/src/main/resources/skills/camel-validate/SKILL.md index 03f16e76..b6b99013 100644 --- a/kit/camel-kit-core/src/main/resources/skills/camel-validate/SKILL.md +++ b/kit/camel-kit-core/src/main/resources/skills/camel-validate/SKILL.md @@ -101,22 +101,24 @@ The baseline is `.camel-kit/baseline-test-results.json`. It is written before an "timestamp": "2026-01-01T00:00:00Z", "revision": "<git HEAD the tests ran against, or unknown>", "dirty": false, + "complete": true, "testCount": 2, "tests": { "com.example.ExistingTest#testA": {"status": "passed"}, - "com.example.ExistingTest#testB": {"status": "failed", "reason": "..."} + "module-a:com.example.SharedTest#sameTest": {"status": "failed", "reason": "..."} }, - "nonPassing": ["com.example.ExistingTest#testB"] + "nonPassing": ["module-a:com.example.SharedTest#sameTest"] } ``` -`tests` maps `class#method` to `passed`, `failed`, `error` or `skipped`. Read the file with the file-reading tool; do not -derive anything from counts. +`tests` maps `[module:]class#method` to `passed`, `failed`, `error` or `skipped`. Module prefix is only present for +non-root modules. Read the file with the file-reading tool; do not derive anything from counts. The baseline is usable only when ALL of these hold: - `status` is `passed` or `failed` and `tests` is not empty. (`not_run`, `timeout`, a missing file, or an empty `tests` means there is no per-test evidence.) +- `complete` is `true`. If `complete` is `false`, some test reports could not be parsed, so the evidence is incomplete. - `revision` is a Git revision (not `unknown`) and `dirty` is `false` (no tracked file had uncommitted changes), so the results describe the project as it was before this pipeline. If `git merge-base --is-ancestor <revision> HEAD` fails, the baseline belongs to different code. @@ -226,9 +228,10 @@ Run the project's tests again with the project's own wrapper, so the current res Then read the current per-test outcomes from the Surefire/Failsafe XML reports (`target/surefire-reports/TEST-*.xml`, `target/failsafe-reports/TEST-*.xml`, including module directories). Delete stale reports first if they predate this run. -Do not run a subset of the baseline tests and do not rely on console totals. +Do not run a subset of the baseline tests and do not rely on console totals. Build test IDs using the same module-qualified +format as the baseline: `[module:]class#method` (module prefix only for non-root modules). -If the baseline is usable (Step 1), compare **by test identity** (`class#method`), never by totals: +If the baseline is usable (Step 1), compare **by test identity** (`[module:]class#method`), never by totals: | Baseline | Current | Verdict | |----------|---------|---------| @@ -250,6 +253,7 @@ comparison above reports that as a regression. | Test | Baseline | Current | Verdict | |------|----------|---------|---------| | com.example.FooTest#a | passed | failed | REGRESSION | +| module-b:com.example.SharedTest#sameTest | passed | passed | OK | ``` **Verdict:** @@ -275,8 +279,8 @@ The validation report for existing projects MUST have this structure: **New Routes:** N routes **Baseline Test Results (per-test comparison):** -- Baseline revision: <revision> (dirty: false) -- Baseline tests compared: N; regressions: N (list each `class#method`); pre-existing failures: N; fixed: N +- Baseline revision: <revision> (dirty: false, complete: true) +- Baseline tests compared: N; regressions: N (list each `[module:]class#method`); pre-existing failures: N; fixed: N - Regressions: YES/NO/UNVERIFIED --- diff --git a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/BaselineTestResultsTest.java b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/BaselineTestResultsTest.java index 64fcb09b..8001f5a1 100644 --- a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/BaselineTestResultsTest.java +++ b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/BaselineTestResultsTest.java @@ -209,11 +209,12 @@ class BaselineTestResultsTest { tests.put("com.example.A#ok", new BaselineTestResults.Outcome("passed", "")); tests.put("com.example.A#bad", new BaselineTestResults.Outcome("failed", "nope")); - JsonNode json = MAPPER.readTree(BaselineTestResults.toJson("failed", "reason", "abc123", true, tests)); + JsonNode json = MAPPER.readTree(BaselineTestResults.toJson("failed", "reason", "abc123", true, true, tests)); assertEquals("failed", json.get("status").asText()); assertEquals("abc123", json.get("revision").asText()); assertTrue(json.get("dirty").asBoolean()); + assertTrue(json.get("complete").asBoolean(), "all reports parsed successfully"); assertEquals(2, json.get("testCount").asInt()); assertEquals("passed", json.at("/tests/com.example.A#ok/status").asText()); assertEquals("nope", json.at("/tests/com.example.A#bad/reason").asText()); @@ -224,9 +225,10 @@ class BaselineTestResultsTest { @Test void statusOnlyBaselinesHaveTheSameShapeWithAnEmptyTestList() throws Exception { JsonNode json = MAPPER.readTree( - BaselineTestResults.toJson("not_run", "not run", "unknown", true, Map.of())); + BaselineTestResults.toJson("not_run", "not run", "unknown", true, true, Map.of())); assertEquals("not_run", json.get("status").asText()); + assertTrue(json.get("complete").asBoolean(), "no collection means no parse failures"); assertEquals(0, json.get("testCount").asInt()); assertTrue(json.get("tests").isObject()); assertEquals(0, json.get("tests").size()); diff --git a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/ProjectDiscovery.java b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/ProjectDiscovery.java index e4e7655d..c408b679 100644 --- a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/ProjectDiscovery.java +++ b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/ProjectDiscovery.java @@ -131,10 +131,12 @@ public final class ProjectDiscovery { } private static boolean isCamelBom(String groupId, String artifactId) { - // Only Camel core BOMs (org.apache.camel group) represent Camel core versions. - // Camel Quarkus extension BOMs (org.apache.camel.quarkus group) have independent versioning. - return artifactId != null && artifactId.startsWith("camel-") && artifactId.endsWith("-bom") - && "org.apache.camel".equals(groupId); + // Camel core BOMs (org.apache.camel and org.apache.camel.springboot groups) represent Camel core versions. + // Camel Quarkus extension BOMs (org.apache.camel.quarkus group) have independent versioning and are excluded. + if (artifactId == null || !artifactId.startsWith("camel-") || !artifactId.endsWith("-bom")) { + return false; + } + return "org.apache.camel".equals(groupId) || "org.apache.camel.springboot".equals(groupId); } private static void addVersion(Map<String, Set<String>> versions, String version, String source) { @@ -184,37 +186,68 @@ public final class ProjectDiscovery { /** * The Spring Boot framework version: an imported {@code spring-boot-dependencies}, otherwise the * {@code spring-boot-starter-parent} parent. This is a different artifact from the Camel BOM and is kept apart so - * the two are never mixed up. + * the two are never mixed up. Returns null if modules disagree on the version. */ private static String detectSpringBootVersion(ProjectGraph graph) { - String fromBom = firstManaged(graph, "spring-boot-dependencies"); - if (fromBom != null) { - return fromBom; + Map<String, Set<String>> bomVersions = collectManagedVersions(graph, "spring-boot-dependencies"); + if (!bomVersions.isEmpty()) { + // If modules agree on the BOM version, use it; if they conflict, return null + return bomVersions.size() == 1 ? bomVersions.keySet().iterator().next() : null; } - for (GraphNode parent : graph.findByType(NodeType.CONFIG_PROPERTY)) { - if ("parent".equals(parent.properties().get("kind")) - && "spring-boot-starter-parent".equals(parent.properties().get("artifactId"))) { - String version = resolve(graph, parent.properties().get("version")); - if (version != null && !version.contains("${") && !"null".equals(version)) { - return version; - } - } + Map<String, Set<String>> parentVersions = collectParentVersions(graph, "spring-boot-starter-parent"); + if (!parentVersions.isEmpty()) { + // If modules agree on the parent version, use it; if they conflict, return null + return parentVersions.size() == 1 ? parentVersions.keySet().iterator().next() : null; } return null; } - private static String firstManaged(ProjectGraph graph, String... artifactIds) { + /** + * Collects all distinct versions of the given managed artifacts across all modules, with their module provenance. + * Returns a map of version → set of sources (e.g., "4.8.0" → {"module-a/pom.xml"}). + */ + private static Map<String, Set<String>> collectManagedVersions(ProjectGraph graph, String... artifactIds) { List<String> wanted = List.of(artifactIds); + Map<String, Set<String>> versions = new TreeMap<>(); for (GraphNode managed : graph.findByType(NodeType.CONFIG_PROPERTY)) { if ("managed".equals(managed.properties().get("kind")) && wanted.contains(managed.properties().get("artifactId"))) { String version = resolve(graph, managed.properties().get("version")); - if (version != null && !"managed".equals(version) && !version.contains("${")) { - return version; + String file = managed.properties().get("file"); + if (version != null && !"managed".equals(version) && !version.contains("${") && file != null) { + versions.computeIfAbsent(version, k -> new TreeSet<>()).add(file); } } } - return null; + return versions; + } + + /** + * Collects all distinct versions of the given parent artifacts across all modules, with their module provenance. + * Returns a map of version → set of sources. + */ + private static Map<String, Set<String>> collectParentVersions(ProjectGraph graph, String artifactId) { + Map<String, Set<String>> versions = new TreeMap<>(); + for (GraphNode parent : graph.findByType(NodeType.CONFIG_PROPERTY)) { + if ("parent".equals(parent.properties().get("kind")) + && artifactId.equals(parent.properties().get("artifactId"))) { + String version = resolve(graph, parent.properties().get("version")); + String file = parent.properties().get("file"); + if (version != null && !version.contains("${") && !"null".equals(version) && file != null) { + versions.computeIfAbsent(version, k -> new TreeSet<>()).add(file); + } + } + } + return versions; + } + + private static String firstManaged(ProjectGraph graph, String... artifactIds) { + Map<String, Set<String>> versions = collectManagedVersions(graph, artifactIds); + if (versions.isEmpty()) { + return null; + } + // If modules agree, return the single version; if they conflict, return null + return versions.size() == 1 ? versions.keySet().iterator().next() : null; } private static Set<DslType> detectDslTypes(ProjectGraph graph) { diff --git a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java index 031f22a1..a1c8c505 100644 --- a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java +++ b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PomParser.java @@ -74,13 +74,15 @@ public class PomParser implements GraphParser { // Version evidence from the parent and from dependencyManagement (imported BOMs). These carry the platform // version (Spring Boot, Quarkus platform, Camel BOM) that dependencies without a version rely on. They are - // recorded as separate nodes so they never masquerade as a dependency of the project. Versions are resolved + // recorded with module provenance in their IDs to prevent cross-module overwriting. Versions are resolved // against the declaring POM's own properties to prevent cross-module property collision. String pomRelative = projectRoot.relativize(pomFile).toString(); if (model.getParent() != null) { String parentVersion = resolveProperty(model.getParent().getVersion(), model); + // Include module provenance in node ID to preserve per-module parent versions graph.addNode(new GraphNode( - "maven-parent:" + model.getParent().getGroupId() + ":" + model.getParent().getArtifactId(), + "maven-parent:" + pomRelative + ":" + model.getParent().getGroupId() + ":" + + model.getParent().getArtifactId(), NodeType.CONFIG_PROPERTY, Map.of("kind", "parent", "groupId", model.getParent().getGroupId(), @@ -93,8 +95,9 @@ public class PomParser implements GraphParser { for (Dependency managed : management.getDependencies()) { String rawVersion = managed.getVersion(); String resolvedVersion = rawVersion != null ? resolveProperty(rawVersion, model) : "managed"; + // Include module provenance in node ID to preserve per-module managed versions graph.addNode(new GraphNode( - "maven-managed:" + managed.getGroupId() + ":" + managed.getArtifactId(), + "maven-managed:" + pomRelative + ":" + managed.getGroupId() + ":" + managed.getArtifactId(), NodeType.CONFIG_PROPERTY, Map.of("kind", "managed", "groupId", managed.getGroupId(), @@ -148,11 +151,12 @@ public class PomParser implements GraphParser { DependencyManagement management = model.getDependencyManagement(); if (management != null) { for (Dependency managed : management.getDependencies()) { - // Only Camel core BOMs (org.apache.camel group) represent Camel core versions. + // Camel core BOMs (org.apache.camel and org.apache.camel.springboot groups) represent Camel core versions. // Camel Quarkus extension BOMs (org.apache.camel.quarkus group) have independent versioning. - if ("pom".equals(managed.getType()) && "org.apache.camel".equals(managed.getGroupId()) - && managed.getArtifactId() != null - && managed.getArtifactId().startsWith("camel-") && managed.getArtifactId().endsWith("-bom")) { + if ("pom".equals(managed.getType()) && managed.getArtifactId() != null + && managed.getArtifactId().startsWith("camel-") && managed.getArtifactId().endsWith("-bom") + && ("org.apache.camel".equals(managed.getGroupId()) + || "org.apache.camel.springboot".equals(managed.getGroupId()))) { addCamelVersionEvidence(graph, pomRelative, "imported BOM " + managed.getArtifactId(), managed.getVersion(), model); } diff --git a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/ProjectDiscoveryTest.java b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/ProjectDiscoveryTest.java index 309cd8fa..74c674ea 100644 --- a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/ProjectDiscoveryTest.java +++ b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/ProjectDiscoveryTest.java @@ -507,12 +507,17 @@ class ProjectDiscoveryTest { } private static GraphNode managed(String artifactId, String version) { + return managed(artifactId, version, "pom.xml"); + } + + private static GraphNode managed(String artifactId, String version, String file) { + String groupId = artifactId.startsWith("camel-") + ? "org.apache.camel.springboot" + : "org.springframework.boot"; return new GraphNode( - "maven-managed:org.example:" + artifactId, NodeType.CONFIG_PROPERTY, - Map.of("kind", "managed", "groupId", artifactId.startsWith("camel-") - ? "org.apache.camel.springboot" - : "org.springframework.boot", "artifactId", artifactId, "version", version, "type", "pom", - "scope", "import")); + "maven-managed:" + file + ":" + groupId + ":" + artifactId, NodeType.CONFIG_PROPERTY, + Map.of("kind", "managed", "groupId", groupId, "artifactId", artifactId, "version", version, "type", + "pom", "scope", "import", "file", file)); } private static GraphNode springBootStarter(String file) { @@ -549,9 +554,9 @@ class ProjectDiscoveryTest { ProjectGraph graph = new ProjectGraph(); graph.addNode(springBootStarter("pom.xml")); graph.addNode(new GraphNode( - "maven-parent:org.springframework.boot:spring-boot-starter-parent", + "maven-parent:pom.xml:org.springframework.boot:spring-boot-starter-parent", NodeType.CONFIG_PROPERTY, Map.of("kind", "parent", "groupId", "org.springframework.boot", - "artifactId", "spring-boot-starter-parent", "version", "3.2.1"))); + "artifactId", "spring-boot-starter-parent", "version", "3.2.1", "file", "pom.xml"))); assertEquals("3.2.1", ProjectDiscovery.discover(graph).springBootVersion()); } @@ -683,6 +688,7 @@ class ProjectDiscoveryTest { "groupId", "org.apache.camel.quarkus", "artifactId", "camel-quarkus-bom", "version", "3.15.0", + "type", "pom", "file", "pom.xml"))); ProjectDiscovery.DiscoveryResult result = ProjectDiscovery.discover(graph); @@ -706,6 +712,7 @@ class ProjectDiscoveryTest { "groupId", "org.apache.camel.quarkus", "artifactId", "camel-quarkus-bom", "version", "3.15.0", + "type", "pom", "file", "pom.xml"))); graph.addNode(new GraphNode( "maven:org.apache.camel.quarkus:camel-quarkus-core", @@ -722,4 +729,64 @@ class ProjectDiscoveryTest { assertTrue(result.allCamelVersions().isEmpty(), "no Camel core version evidence should be present"); } + + @Test + void camelSpringBootBomProvidesCamelCoreVersion() { + // org.apache.camel.springboot:camel-spring-boot-bom version represents Camel core version + ProjectGraph graph = new ProjectGraph(); + graph.addNode(new GraphNode( + "maven-managed:org.apache.camel.springboot:camel-spring-boot-bom", + NodeType.CONFIG_PROPERTY, + Map.of( + "kind", "managed", + "groupId", "org.apache.camel.springboot", + "artifactId", "camel-spring-boot-bom", + "version", "4.8.0", + "type", "pom", + "file", "pom.xml"))); + + ProjectDiscovery.DiscoveryResult result = ProjectDiscovery.discover(graph); + + assertEquals("4.8.0", result.camelVersion(), + "Camel Spring Boot BOM version should be recognized as Camel core version"); + assertTrue(result.allCamelVersions().contains("4.8.0")); + } + + @Test + void detectsSpringBootVersionConflictAcrossModules() { + // Two modules with different Spring Boot versions should cause conflict detection + ProjectGraph graph = new ProjectGraph(); + graph.addNode(springBootStarter("module-a/pom.xml")); + graph.addNode(springBootStarter("module-b/pom.xml")); + + // Module-a uses Spring Boot 3.3.4 + graph.addNode(new GraphNode( + "maven-managed:module-a/pom.xml:org.springframework.boot:spring-boot-dependencies", + NodeType.CONFIG_PROPERTY, + Map.of("kind", "managed", + "groupId", "org.springframework.boot", + "artifactId", "spring-boot-dependencies", + "version", "3.3.4", + "type", "pom", + "scope", "import", + "file", "module-a/pom.xml"))); + + // Module-b uses Spring Boot 3.4.0 + graph.addNode(new GraphNode( + "maven-managed:module-b/pom.xml:org.springframework.boot:spring-boot-dependencies", + NodeType.CONFIG_PROPERTY, + Map.of("kind", "managed", + "groupId", "org.springframework.boot", + "artifactId", "spring-boot-dependencies", + "version", "3.4.0", + "type", "pom", + "scope", "import", + "file", "module-b/pom.xml"))); + + ProjectDiscovery.DiscoveryResult result = ProjectDiscovery.discover(graph); + + assertNull(result.springBootVersion(), + "Conflicting Spring Boot versions across modules should not be resolved"); + assertEquals("spring-boot", result.runtime()); + } } diff --git a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/PomParserTest.java b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/PomParserTest.java index 9bb04725..eec94226 100644 --- a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/PomParserTest.java +++ b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/PomParserTest.java @@ -85,11 +85,11 @@ class PomParserTest { new PomParser().parse(root, local); - var parent = local.getNode("maven-parent:org.springframework.boot:spring-boot-starter-parent"); + var parent = local.getNode("maven-parent:pom.xml:org.springframework.boot:spring-boot-starter-parent"); assertNotNull(parent); assertEquals("parent", parent.properties().get("kind")); assertEquals("3.3.5", parent.properties().get("version")); - var bom = local.getNode("maven-managed:org.apache.camel.springboot:camel-spring-boot-bom"); + var bom = local.getNode("maven-managed:pom.xml:org.apache.camel.springboot:camel-spring-boot-bom"); assertNotNull(bom); assertEquals("managed", bom.properties().get("kind")); assertEquals("4.8.0", bom.properties().get("version"), @@ -143,7 +143,8 @@ class PomParserTest { ProjectGraph reactor = new ProjectGraph(); new PomParser().parse(root, reactor); - var bom = reactor.getNode("maven-managed:org.apache.camel.springboot:camel-spring-boot-bom"); + var bom = reactor + .getNode("maven-managed:module-a/pom.xml:org.apache.camel.springboot:camel-spring-boot-bom"); assertNotNull(bom, "Camel Spring Boot BOM should be recorded"); assertEquals("4.8.0", bom.properties().get("version"), "BOM version should be resolved from module-a's bom.version property, not module-b's");
