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 6fd79ed88bcb4eba408162807ff6874659349275 Author: Jinal Sarvaiya <[email protected]> AuthorDate: Wed Nov 11 16:08:41 2026 +0000 YAML profile consumers, inventory newlines, per-module version evidence Signed-off-by: Jinal Sarvaiya <[email protected]> --- .../org/apache/camel/kit/service/InitService.java | 14 +++-- .../kit/generator/ResourceConsistencyTest.java | 10 ++++ .../apache/camel/kit/service/InitServiceTest.java | 35 +++++++++++++ .../apache/camel/kit/graph/ProjectDiscovery.java | 7 +++ .../camel/kit/graph/parser/ConfigParser.java | 25 ++------- .../apache/camel/kit/graph/parser/PomParser.java | 50 ++++++++++++++++++ .../kit/graph/parser/PropertyBindingParser.java | 14 ++++- .../camel/kit/graph/ProjectDiscoveryTest.java | 60 ++++++++++++++++++++++ .../camel/kit/graph/parser/ConfigParserTest.java | 54 +++++++++++++++++++ 9 files changed, 242 insertions(+), 27 deletions(-) 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 d4211d29..0c6e72c6 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 @@ -1011,7 +1011,7 @@ public class InitService { if (!testFiles.isEmpty()) { Path baselineFile = camelKitDir.resolve("baseline-tests.txt"); - writeBaselineFile(projectRoot, baselineFile, String.join("\n", testFiles.stream().sorted().toList())); + writeBaselineFile(projectRoot, baselineFile, inventoryText(testFiles.stream().sorted().toList())); } } catch (Exception e) { // Non-fatal - baseline recording is best-effort @@ -1043,7 +1043,7 @@ public class InitService { }) .sorted() .toList(); - writeBaselineFile(projectRoot, baselineFile, String.join("\n", relativePaths)); + writeBaselineFile(projectRoot, baselineFile, inventoryText(relativePaths)); } catch (Exception e) { // Non-fatal - baseline recording is best-effort } @@ -1089,9 +1089,17 @@ public class InitService { AtomicFileWriter.write(file, content); } + /** + * One record per line, every line (including the last) terminated by a line feed, so shell consumers such as + * {@code while read} see the final entry. An empty inventory is an empty file. + */ + static String inventoryText(List<String> lines) { + return lines.isEmpty() ? "" : String.join("\n", lines) + "\n"; + } + private void writeBaselineLines(Path file, List<String> lines, Path projectRoot) { try { - writeBaselineFile(projectRoot, file, String.join("\n", lines)); + writeBaselineFile(projectRoot, file, inventoryText(lines)); } catch (Exception e) { // Non-fatal - baseline recording is best-effort } diff --git a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/ResourceConsistencyTest.java b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/ResourceConsistencyTest.java index ed3ec554..0d80b03f 100644 --- a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/ResourceConsistencyTest.java +++ b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/ResourceConsistencyTest.java @@ -1552,6 +1552,16 @@ class ResourceConsistencyTest { assertFalse(validate.contains("done < .camel-kit/baseline-tests.txt"), "camel-validate must not derive test classes from file names and count lines"); assertFalse(validate.contains("| Tests Run |"), "camel-validate must not compare totals"); + // every outcome transition has a verdict: a swapped failing test (same totals), errors, removed and skipped tests + for (String row : List.of("| passed | failed or error | **REGRESSION** |", + "| passed | missing (test removed, renamed, or not run) | **REGRESSION**", + "| passed | skipped | **REGRESSION**", + "| failed or error | failed or error | Pre-existing failure", + "| failed or error | passed | Fixed")) { + assertTrue(validate.contains(row), "camel-validate verdict table lost the row: " + row); + } + assertTrue(validate.contains("one test fixed and another broken has the same totals"), + "a swap in failing test identity with unchanged totals must be called out as a regression"); assertTrue(validate.contains("DO NOT run the tests and call that the baseline")); // execute captures the baseline before the first edit; plan makes sure that happens diff --git a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/InitServiceTest.java b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/InitServiceTest.java index c0cd416d..06eacf76 100644 --- a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/InitServiceTest.java +++ b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/service/InitServiceTest.java @@ -1584,6 +1584,41 @@ class InitServiceTest { "Custom skill files should be preserved exactly, not overwritten on reinit"); } + /** + * Producer-to-consumer: the shipped camel-validate instructions read the inventories line by line, so a final entry + * without a line feed is silently skipped. Runs the real producer, then a literal {@code while read} loop. + */ + @Test + void baselineInventoriesAreNewlineTerminatedSoShellConsumersSeeEveryEntry() throws Exception { + Path targetDir = tempDir.resolve("inventory-newline"); + Files.createDirectories(targetDir.resolve("src/main/resources/camel")); + Files.createDirectories(targetDir.resolve("src/test/java/com/example")); + Files.writeString(targetDir.resolve("pom.xml"), minimalCamelPom("4.8.0")); + Files.writeString(targetDir.resolve("src/main/resources/camel/only-route.yaml"), + "- route:\n id: r\n from:\n uri: timer:t\n steps:\n - log: hi\n"); + Files.writeString(targetDir.resolve("src/test/java/com/example/OnlyTest.java"), + "package com.example;\nclass OnlyTest {\n}\n"); + + new InitService().initialize( + requestForExistingProject(targetDir, "claude", InitProgress.noop(), InitReporter.noop())); + + for (String inventory : List.of("baseline-routes.txt", "baseline-tests.txt")) { + Path file = targetDir.resolve(".camel-kit/" + inventory); + String content = Files.readString(file); + assertTrue(content.endsWith("\n"), inventory + " must end with a line feed: [" + content + "]"); + + Process loop = new ProcessBuilder( + "sh", "-c", + "n=0; while IFS= read -r entry; do n=$((n+1)); done < " + file + "; echo $n") + .redirectErrorStream(true).start(); + String counted + = new String(loop.getInputStream().readAllBytes(), java.nio.charset.StandardCharsets.UTF_8).trim(); + loop.waitFor(); + assertEquals("1", counted, inventory + ": the shell loop must see the single entry"); + } + assertEquals("", InitService.inventoryText(List.of()), "an empty inventory is an empty file"); + } + @Test void reinitRefreshesSharedSkillResourcesAndKeepsUserOwnedFiles() throws Exception { Path targetDir = tempDir.resolve("shared-refresh"); 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 56c1229a..f1cf4ce9 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 @@ -120,6 +120,13 @@ public final class ProjectDiscovery { "imported BOM " + managed.properties().get("artifactId")); } } + // Per-POM declarations: in a reactor the nodes above collapse to one module's value, these do not. + for (GraphNode evidence : graph.findByType(NodeType.CONFIG_PROPERTY)) { + if ("camel-version-evidence".equals(evidence.properties().get("kind"))) { + addVersion(versions, evidence.properties().get("version"), + evidence.properties().get("source") + " in " + evidence.properties().get("file")); + } + } return versions; } diff --git a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java index 5690bdab..5bdbc2cd 100644 --- a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java +++ b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/ConfigParser.java @@ -68,11 +68,11 @@ public class ConfigParser implements GraphParser { JsonNode document = (JsonNode) documents.nextValue(); Map<String, String> flattened = new LinkedHashMap<>(); - // Check for document-level profile (spring.config.activate.on-profile) - String documentProfile = extractDocumentProfile(document); - flatten("", document, flattened); + // Document-level profile (spring.config.activate.on-profile), nested or written as a flat dotted key + String documentProfile = flattened.get("spring.config.activate.on-profile"); + // Determine effective profile (document > filename) String effectiveProfile = documentProfile != null ? documentProfile : filenameProfile; @@ -100,25 +100,6 @@ public class ConfigParser implements GraphParser { } } - /** - * Extract profile from document content (spring.config.activate.on-profile). - */ - private String extractDocumentProfile(JsonNode document) { - if (document.has("spring")) { - JsonNode spring = document.get("spring"); - if (spring.has("config")) { - JsonNode config = spring.get("config"); - if (config.has("activate")) { - JsonNode activate = config.get("activate"); - if (activate.has("on-profile")) { - return activate.get("on-profile").asText(); - } - } - } - } - return null; - } - private static void flatten(String prefix, JsonNode node, Map<String, String> out) { if (node == null || node.isNull() || node.isMissingNode()) { return; 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 3068013a..d8687768 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 @@ -103,6 +103,8 @@ public class PomParser implements GraphParser { } } + recordCamelVersionEvidence(model, pomRelative, graph); + for (Dependency dep : model.getDependencies()) { String depId = "maven:" + dep.getGroupId() + ":" + dep.getArtifactId(); String depVersion = dep.getVersion() != null ? dep.getVersion() : "managed"; @@ -123,4 +125,52 @@ public class PomParser implements GraphParser { throw new RuntimeException("Failed to parse POM: " + pomFile, e); } } + + /** + * The graph keys properties and artifacts by name, so in a reactor the declarations of several modules collapse + * into one node and only the last survives. Camel version declarations are therefore also recorded once per POM + * under {@code maven-camel-version:<pom>:<source>}, with the property references resolved against that POM's own + * properties. Discovery uses them to notice modules that disagree. + */ + private void recordCamelVersionEvidence(Model model, String pomRelative, ProjectGraph graph) { + String camelVersionProperty + = model.getProperties() == null ? null : model.getProperties().getProperty("camel.version"); + addCamelVersionEvidence(graph, pomRelative, "property camel.version", camelVersionProperty, model); + for (Dependency dep : model.getDependencies()) { + if ("org.apache.camel".equals(dep.getGroupId())) { + addCamelVersionEvidence(graph, pomRelative, "dependency " + dep.getArtifactId(), dep.getVersion(), + model); + } + } + DependencyManagement management = model.getDependencyManagement(); + if (management != null) { + for (Dependency managed : management.getDependencies()) { + if ("pom".equals(managed.getType()) && managed.getGroupId() != null + && managed.getGroupId().startsWith("org.apache.camel") + && managed.getArtifactId().startsWith("camel-") && managed.getArtifactId().endsWith("-bom")) { + addCamelVersionEvidence(graph, pomRelative, "imported BOM " + managed.getArtifactId(), + managed.getVersion(), model); + } + } + } + } + + private void addCamelVersionEvidence( + ProjectGraph graph, String pomRelative, String source, String rawVersion, Model model) { + String version = resolveProperty(rawVersion, model); + if (version == null || version.isBlank() || version.contains("${")) { + return; + } + graph.addNode(new GraphNode( + "maven-camel-version:" + pomRelative + ":" + source, NodeType.CONFIG_PROPERTY, + Map.of("kind", "camel-version-evidence", "version", version, "source", source, "file", pomRelative))); + } + + private static String resolveProperty(String value, Model model) { + if (value == null || !value.startsWith("${") || !value.endsWith("}") || model.getProperties() == null) { + return value; + } + String resolved = model.getProperties().getProperty(value.substring(2, value.length() - 1)); + return resolved == null ? value : resolved; + } } diff --git a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PropertyBindingParser.java b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PropertyBindingParser.java index 3fb7f179..2036d098 100644 --- a/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PropertyBindingParser.java +++ b/kit/camel-kit-graph/src/main/java/org/apache/camel/kit/graph/parser/PropertyBindingParser.java @@ -1,6 +1,7 @@ package org.apache.camel.kit.graph.parser; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -182,9 +183,18 @@ public class PropertyBindingParser { Matcher matcher = PLACEHOLDER_PATTERN.matcher(uri); while (matcher.find()) { String key = matcher.group(1); + // The default value ("config:<key>") and every profile-specific variant ("config:<key>:profile=<name>") + // configure the endpoint; a key defined only in a profile document is still linked. String configNodeId = "config:" + key; - if (graph.hasNode(configNodeId)) { - graph.addEdge(new GraphEdge(configNodeId, node.id(), EdgeType.CONFIGURES, Map.of())); + for (String candidate : List.of(configNodeId)) { + if (graph.hasNode(candidate)) { + graph.addEdge(new GraphEdge(candidate, node.id(), EdgeType.CONFIGURES, Map.of())); + } + } + for (GraphNode config : graph.findByType(NodeType.CONFIG_PROPERTY)) { + if (config.id().startsWith(configNodeId + ":profile=")) { + graph.addEdge(new GraphEdge(config.id(), node.id(), EdgeType.CONFIGURES, Map.of())); + } } } } 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 bdc8dd2e..7384e0f9 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 @@ -605,4 +605,64 @@ class ProjectDiscoveryTest { assertTrue(result.coverageGaps().stream().anyMatch(g -> g.contains("Conflicting Camel versions") && g.contains("4.8.0") && g.contains("4.10.0"))); } + + private static void module(java.nio.file.Path root, String name, String camelVersion) throws java.io.IOException { + java.nio.file.Path dir = java.nio.file.Files.createDirectories(root.resolve(name)); + java.nio.file.Files.writeString(dir.resolve("pom.xml"), String.format(java.util.Locale.ROOT, """ + <project xmlns="http://maven.apache.org/POM/4.0.0"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.example</groupId> + <artifactId>%s</artifactId> + <version>1.0.0</version> + <properties><camel.version>%s</camel.version></properties> + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-main</artifactId> + <version>${camel.version}</version> + </dependency> + </dependencies> + </project> + """, name, camelVersion)); + } + + @Test + void modulesDeclaringDifferentCamelVersionsAreAConflictNotTheLastModulesVersion( + @org.junit.jupiter.api.io.TempDir java.nio.file.Path root) + throws Exception { + java.nio.file.Files.writeString(root.resolve("pom.xml"), """ + <project xmlns="http://maven.apache.org/POM/4.0.0"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.example</groupId> + <artifactId>parent</artifactId> + <version>1.0.0</version> + <packaging>pom</packaging> + <modules><module>a</module><module>b</module></modules> + </project> + """); + module(root, "a", "4.8.0"); + module(root, "b", "4.10.0"); + ProjectGraph reactor = new ProjectGraph(); + + new org.apache.camel.kit.graph.parser.PomParser().parse(root, reactor); + ProjectDiscovery.DiscoveryResult result = ProjectDiscovery.discover(reactor); + + assertNull(result.camelVersion(), "one module's version must not be bound to the whole reactor"); + assertTrue(result.hasConflict()); + assertTrue(result.allCamelVersions().containsAll(java.util.Set.of("4.8.0", "4.10.0"))); + assertTrue(result.coverageGaps().stream().anyMatch(g -> g.contains("Conflicting Camel versions") + && g.contains("a/pom.xml") && g.contains("b/pom.xml")), result.coverageGaps().toString()); + } + + @Test + void modulesThatAgreeOnTheCamelVersionStayLocked(@org.junit.jupiter.api.io.TempDir java.nio.file.Path root) + throws Exception { + module(root, "a", "4.8.0"); + module(root, "b", "4.8.0"); + ProjectGraph reactor = new ProjectGraph(); + + new org.apache.camel.kit.graph.parser.PomParser().parse(root, reactor); + + assertEquals("4.8.0", ProjectDiscovery.discover(reactor).camelVersion()); + } } diff --git a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ConfigParserTest.java b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ConfigParserTest.java index a6625d2b..0b563c8b 100644 --- a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ConfigParserTest.java +++ b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ConfigParserTest.java @@ -3,7 +3,9 @@ package org.apache.camel.kit.graph.parser; import java.nio.file.Files; import java.nio.file.Path; +import org.apache.camel.kit.graph.GraphBuilder; import org.apache.camel.kit.graph.ProjectGraph; +import org.apache.camel.kit.graph.model.EdgeType; import org.apache.camel.kit.graph.model.NodeType; import org.junit.jupiter.api.BeforeEach; @@ -127,4 +129,56 @@ class ConfigParserTest { assertEquals("prod:9092", prod.properties().get("value")); assertEquals("prod", prod.properties().get("profile")); } + + @Test + void flatDottedActivationKeyIsRecognisedAsTheDocumentProfile(@TempDir Path root) throws Exception { + Path res = Files.createDirectories(root.resolve("src/main/resources")); + Files.writeString(res.resolve("application.yml"), """ + port: 8080 + --- + spring.config.activate.on-profile: prod + port: 9090 + """); + ProjectGraph yamlGraph = new ProjectGraph(); + + new ConfigParser().parse(root, yamlGraph); + + assertEquals("8080", yamlGraph.getNode("config:port").properties().get("value")); + var prod = yamlGraph.getNode("config:port:profile=prod"); + assertNotNull(prod, "a flat dotted activation key must scope the document too"); + assertEquals("9090", prod.properties().get("value")); + assertEquals("prod", prod.properties().get("profile")); + } + + /** Connected consumer: a YAML property referenced by a route placeholder must still configure the endpoint. */ + @Test + void yamlPropertiesStillConfigureTheRouteEndpointsThatReferenceThem(@TempDir Path root) throws Exception { + Path res = Files.createDirectories(root.resolve("src/main/resources/camel")); + Files.writeString(res.getParent().resolve("application.yml"), """ + timer: + name: tick + --- + spring.config.activate.on-profile: prod + kafka: + topic: prod-topic + """); + Files.writeString(res.resolve("routes.yaml"), """ + - route: + id: ticker + from: + uri: "timer:{{timer.name}}" + steps: + - to: + uri: "kafka:{{kafka.topic}}" + """); + + ProjectGraph built = new GraphBuilder().build(root); + + assertTrue(built.getEdges().stream().anyMatch(e -> e.type() == EdgeType.CONFIGURES + && e.from().equals("config:timer.name") && e.to().startsWith("endpoint:timer")), + "a default YAML property must keep its CONFIGURES edge"); + assertTrue(built.getEdges().stream().anyMatch(e -> e.type() == EdgeType.CONFIGURES + && e.from().equals("config:kafka.topic:profile=prod") && e.to().startsWith("endpoint:kafka")), + "a property defined only in a profile document must still be linked"); + } }
