From f1657fe9ebbedf4036487b5a4ec28a4657049645 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 30 Jun 2026 15:54:59 -0400 Subject: [PATCH 1/2] CAMEL-23863: Fix invalid YAML in rest-api jbang example and add example-loading test The bundled rest-api example placed a `steps:` key under `rest:` with no `to:` targets. That is not valid REST DSL: verbs (get/post/put/delete) must be direct children of `rest`, each carrying a `to:` target. As a result `camel run --example=rest-api` failed during route-model construction with "Error constructing YAML node id: rest". Correct the verbs to be direct children of `rest`, routing GET /api/hello and GET /api/hello/{name} to direct:hello / direct:hello-name (the two routes the example already defines). Add ExampleRoutesLoadTest, a parameterized test that loads every bundled example under camel-jbang-core through the routes loader, guarding all examples against this class of regression. A camel.example.Greeter test fixture mirrors the runtime-compiled bean so the routes/beans.yaml example can be parsed. Co-Authored-By: Claude Opus 4.8 --- .../examples/rest-api/rest-api.camel.yaml | 12 ++- .../src/test/java/camel/example/Greeter.java | 41 ++++++++ .../core/common/ExampleRoutesLoadTest.java | 93 +++++++++++++++++++ 3 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/camel/example/Greeter.java create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml index d070e85497275..65232bd3ca634 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml @@ -1,10 +1,12 @@ - rest: path: /api - steps: - - get: - path: /hello - - get: - path: "/hello/{name}" + get: + - path: /hello + to: + uri: direct:hello + - path: "/hello/{name}" + to: + uri: direct:hello-name - route: id: hello from: diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/camel/example/Greeter.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/camel/example/Greeter.java new file mode 100644 index 0000000000000..5eadb64ae844f --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/camel/example/Greeter.java @@ -0,0 +1,41 @@ +/* + * 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. + */ +package camel.example; + +import org.apache.camel.Exchange; +import org.apache.camel.Processor; + +/** + * Test fixture mirroring {@code examples/routes/Greeter.java}, which the CLI compiles at runtime. It is needed on the + * test classpath so {@code examples/routes/beans.yaml} can resolve its bean type while being parsed by + * {@link org.apache.camel.dsl.jbang.core.common.ExampleRoutesLoadTest}. + */ +public class Greeter implements Processor { + + private String message; + + public void setMessage(String message) { + this.message = message; + } + + @Override + public void process(Exchange exchange) throws Exception { + String body = exchange.getIn().getBody(String.class); + exchange.getIn().setBody(message + " " + body); + } + +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java new file mode 100644 index 0000000000000..b5c51b2351562 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java @@ -0,0 +1,93 @@ +/* + * 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. + */ +package org.apache.camel.dsl.jbang.core.common; + +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; +import java.util.stream.Stream; + +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.spi.Resource; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResourceHelper; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that every bundled example route resource loads and parses through the Camel routes loader without error. + * + * The example files ship as run-ready resources for {@code camel run --example=...}. A malformed file (for instance a + * misplaced YAML key that the DSL cannot construct) is only discovered when the loader builds the route model, so this + * test loads each one through the same path the CLI uses. The context is intentionally not started: that keeps the test + * focused on parsing and avoids instantiating example beans or opening external endpoints (JMS, Kafka, SQL, ...). + */ +class ExampleRoutesLoadTest { + + private static final Path EXAMPLES_DIR = Path.of("src/main/resources/examples"); + + static Stream exampleRouteFiles() throws Exception { + try (Stream files = Files.walk(EXAMPLES_DIR)) { + return files.filter(Files::isRegularFile) + .filter(p -> { + String name = p.getFileName().toString(); + return name.endsWith(".yaml") || name.endsWith(".yml"); + }) + .sorted() + .toList() + .stream(); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("exampleRouteFiles") + void shouldLoadAndParseExample(Path file) throws Exception { + try (DefaultCamelContext context = new DefaultCamelContext()) { + // mirror the CLI: make the example's own application.properties available so that property + // placeholders bound eagerly at parse time (e.g. bean properties in beans.yaml) can be resolved + loadExampleProperties(context, file); + + Resource resource = ResourceHelper.resolveResource(context, "file:" + file); + + assertDoesNotThrow(() -> PluginHelper.getRoutesLoader(context).loadRoutes(resource), + "Failed to load and parse example: " + file); + + // a parsed example must yield either routes or a beans definition; an empty model means the + // loader silently accepted content it did not understand + int routeCount = context.getRouteDefinitions().size(); + boolean beansFile = Files.readString(file).contains("beans:"); + assertTrue(routeCount > 0 || beansFile, + "Example parsed to an empty model (no routes and no beans): " + file); + } + } + + private static void loadExampleProperties(DefaultCamelContext context, Path file) throws Exception { + Path properties = file.resolveSibling("application.properties"); + if (!Files.exists(properties)) { + return; + } + Properties props = new Properties(); + try (InputStream is = Files.newInputStream(properties)) { + props.load(is); + } + context.getPropertiesComponent().setInitialProperties(props); + } +} From 075d7439be2185bfe3066946982de7323e681b88 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 30 Jun 2026 16:39:02 -0400 Subject: [PATCH 2/2] CAMEL-23863: Strengthen ExampleRoutesLoadTest assertions and example discovery Address review feedback on the example-loading test: - Replace the `beans:` substring heuristic with a model-aware check that also counts REST definitions, route templates and registered beans, so a tolerated-but-empty example fails and a REST/template-only example does not spuriously fail. - Resolve the examples from the classpath instead of a CWD-relative path, so the test runs identically under Surefire and IDEs; use a relativized display name for each parameterized case. - Correct the javadoc: example beans are instantiated during pre-parse (only endpoint wiring is deferred to context start), and document the Greeter fixture sync requirement. Co-Authored-By: Claude Opus 4.8 --- .../src/test/java/camel/example/Greeter.java | 10 ++-- .../core/common/ExampleRoutesLoadTest.java | 47 ++++++++++++++----- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/camel/example/Greeter.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/camel/example/Greeter.java index 5eadb64ae844f..ae48efcb5fd7c 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/camel/example/Greeter.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/camel/example/Greeter.java @@ -20,9 +20,13 @@ import org.apache.camel.Processor; /** - * Test fixture mirroring {@code examples/routes/Greeter.java}, which the CLI compiles at runtime. It is needed on the - * test classpath so {@code examples/routes/beans.yaml} can resolve its bean type while being parsed by - * {@link org.apache.camel.dsl.jbang.core.common.ExampleRoutesLoadTest}. + * Test fixture mirroring {@code src/main/resources/examples/routes/Greeter.java}, which the CLI compiles at runtime. A + * compiled copy is needed on the test classpath so that {@code examples/routes/beans.yaml} can instantiate its + * {@code greeter} bean while {@link org.apache.camel.dsl.jbang.core.common.ExampleRoutesLoadTest} pre-parses it. + * + * Keep this in sync with the example source. Only the type and its {@code message} property are load-bearing for the + * test: a missing property fails bean binding loudly, whereas the {@link #process} behavior is never exercised (the + * test does not start the context), so behavioral drift would go unnoticed. */ public class Greeter implements Processor { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java index b5c51b2351562..abaa20f65836d 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleRoutesLoadTest.java @@ -17,19 +17,24 @@ package org.apache.camel.dsl.jbang.core.common; import java.io.InputStream; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.Properties; import java.util.stream.Stream; import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.model.Model; import org.apache.camel.spi.Resource; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; +import org.junit.jupiter.api.Named; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -37,21 +42,30 @@ * * The example files ship as run-ready resources for {@code camel run --example=...}. A malformed file (for instance a * misplaced YAML key that the DSL cannot construct) is only discovered when the loader builds the route model, so this - * test loads each one through the same path the CLI uses. The context is intentionally not started: that keeps the test - * focused on parsing and avoids instantiating example beans or opening external endpoints (JMS, Kafka, SQL, ...). + * test loads each one through the same loader the CLI uses. The context is intentionally not started, but note this is + * still a parse-time test: example beans are instantiated while the loader pre-parses (which is why the {@code Greeter} + * fixture and the example's {@code application.properties} are required), whereas endpoints and their producers and + * consumers are only created at start. Not starting therefore avoids opening external endpoints (JMS, Kafka, SQL, ...) + * but also means endpoint wiring is not exercised. */ class ExampleRoutesLoadTest { - private static final Path EXAMPLES_DIR = Path.of("src/main/resources/examples"); + static Stream exampleRouteFiles() throws Exception { + // resolve the examples from the classpath (target/classes/examples) so the test does not depend on the working + // directory being the module base, which differs between Maven Surefire and IDE run configurations + URL examplesUrl = ExampleRoutesLoadTest.class.getClassLoader().getResource("examples"); + assertNotNull(examplesUrl, "examples resources not found on the test classpath"); + Path examplesDir = Path.of(examplesUrl.toURI()); - static Stream exampleRouteFiles() throws Exception { - try (Stream files = Files.walk(EXAMPLES_DIR)) { + try (Stream files = Files.walk(examplesDir)) { return files.filter(Files::isRegularFile) .filter(p -> { String name = p.getFileName().toString(); return name.endsWith(".yaml") || name.endsWith(".yml"); }) .sorted() + // use the path relative to the examples root as a stable, readable test display name + .map(p -> Arguments.of(Named.of(examplesDir.relativize(p).toString(), p))) .toList() .stream(); } @@ -61,8 +75,10 @@ static Stream exampleRouteFiles() throws Exception { @MethodSource("exampleRouteFiles") void shouldLoadAndParseExample(Path file) throws Exception { try (DefaultCamelContext context = new DefaultCamelContext()) { - // mirror the CLI: make the example's own application.properties available so that property - // placeholders bound eagerly at parse time (e.g. bean properties in beans.yaml) can be resolved + // make the example's own application.properties available so that property placeholders bound eagerly while + // the loader pre-parses (e.g. bean properties in beans.yaml) can be resolved; this reproduces the effect of + // the CLI loading the example properties, through setInitialProperties rather than the CLI's + // properties-location mechanism loadExampleProperties(context, file); Resource resource = ResourceHelper.resolveResource(context, "file:" + file); @@ -70,12 +86,17 @@ void shouldLoadAndParseExample(Path file) throws Exception { assertDoesNotThrow(() -> PluginHelper.getRoutesLoader(context).loadRoutes(resource), "Failed to load and parse example: " + file); - // a parsed example must yield either routes or a beans definition; an empty model means the - // loader silently accepted content it did not understand - int routeCount = context.getRouteDefinitions().size(); - boolean beansFile = Files.readString(file).contains("beans:"); - assertTrue(routeCount > 0 || beansFile, - "Example parsed to an empty model (no routes and no beans): " + file); + // a parsed example must contribute something the loader understood: a route, a REST definition, a route + // template or a bean. An otherwise-empty model means the loader silently accepted content it did not + // understand, so inspect the parsed model rather than the raw text (which would pass on a tolerated-but- + // empty file that merely contains a recognised keyword) + Model model = context.getCamelContextExtension().getContextPlugin(Model.class); + int modelElements = model.getRouteDefinitions().size() + + model.getRestDefinitions().size() + + model.getRouteTemplateDefinitions().size() + + model.getCustomBeans().size(); + assertTrue(modelElements > 0, + "Example parsed to an empty model (no routes, rests, templates or beans): " + file); } }