This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 7941461d6afb CAMEL-26765: camel-chaos - see through stub endpoints:
suggestions for stubbed remote endpoints, and kept consumers (#29458)
7941461d6afb is described below
commit 7941461d6afb65e350970187b1465beff64d4614
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 9 11:27:35 2026 +0100
CAMEL-26765: camel-chaos - see through stub endpoints: suggestions for
stubbed remote endpoints, and kept consumers (#29458)
* CAMEL-26765: camel-chaos - see through stub endpoints: suggestions for
stubbed remote endpoints, and kept consumers
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/chaos.adoc | 26 ++++-
components/camel-chaos/pom.xml | 6 +
components/camel-chaos/src/main/docs/chaos.adoc | 26 ++++-
.../apache/camel/component/chaos/ChaosAdvisor.java | 4 +-
.../camel/component/chaos/ChaosConsumers.java | 39 ++++---
.../apache/camel/component/chaos/ChaosManager.java | 8 ++
.../apache/camel/component/chaos/ChaosReport.java | 12 +-
.../apache/camel/component/chaos/ChaosStubs.java | 99 ++++++++++++++++
.../camel/component/chaos/ChaosStubsTest.java | 125 +++++++++++++++++++++
.../org/apache/camel/component/chaos/remotemq.json | 10 ++
.../services/org/apache/camel/component/remotemq | 19 ++++
.../dsl/jbang/core/commands/process/ListChaos.java | 11 +-
.../jbang/core/commands/process/ListChaosTest.java | 15 +++
.../dsl/jbang/core/commands/tui/ChaosInfo.java | 5 +-
.../dsl/jbang/core/commands/tui/ChaosTab.java | 6 +-
.../core/commands/tui/ChaosTabRenderTest.java | 4 +-
16 files changed, 390 insertions(+), 25 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
index 62a335f5edf5..8d6bd7a724ae 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc
@@ -266,6 +266,28 @@ In JSON the report has `tried`: per `route` its `kinds`
(`kind`, `rules`, `fired
(`tried`): `camel cmd chaos-suggest` ends its line with `- tried`, and the
what to try list of the Camel TUI (key `w`)
shows both.
+=== With stubs
+
+`camel run --stub=remote` serves the remote components with the stub
component, a queue in memory, so the integration
+runs without the systems behind it, and `camel cmd send` feeds a stubbed
consumer:
+
+[source,bash]
+----
+camel run * --stub=remote --chaos
+camel cmd send --endpoint=kafka:refunds --body=file:samples/refund.json
+----
+
+The rules match a stub by its URI (`endpoint=kafka:orders`, `route=refunds`),
and chaos counts a stub as the component
+it stands in for:
+
+* What to try takes a stub as remote when the component is. It reads that from
the JSON of the component, which
+`camel run` downloads with the component for a stub too.
+* A stubbed consumer keeps a failed message by the options of its URI, as the
component would:
+`kafka:refunds?breakOnFirstError=true` keeps it. Options set on the component
do not count, as a stub does not take
+them. The stub does not read the message again, though, so the report says per
route that its consumer is a stub
+(`stubbedConsumer` in the JSON, `refunds (stub of kafka)` in `camel get chaos
--report`, and `refunds (stub)` on the
+chaos screen of the Camel TUI): a kept message is then not lost, though
nothing reads it again while the stub runs.
+
=== From an AI agent (MCP)
The Camel MCP server (`camel mcp`, see xref:manual::camel-jbang-mcp.adoc[Camel
MCP server]) has the same operations
@@ -762,7 +784,9 @@ per exchange too.
`acknowledgementModeName=CLIENT_ACKNOWLEDGE` (the endpoint option; the number
set with `setAcknowledgementMode` in Java
is not seen) and `asyncConsumer=false`; `kafka` with `breakOnFirstError=true`.
The options are read through the
configurer of the endpoint, so options set on the component count too. The
component is known by the scheme of the
-endpoint URI.
+endpoint URI. A stub that stands in for one of them keeps it by the options of
its URI (see <<_with_stubs,With stubs>>).
+* `stubbedConsumer`: the component the consumer of the route is a stub of
(`camel run --stub`), such as `kafka`; left
+out when it is not a stub. A stub does not read a kept message again.
* `handled`: the completed exchanges after the error handler handled a failure
(the dead letter channel, or
`onException` with `handled(true)`). A failure handled in a route called with
`direct` counts as handled in the
calling routes too.
diff --git a/components/camel-chaos/pom.xml b/components/camel-chaos/pom.xml
index 3a2851d18b34..7409abbbb018 100644
--- a/components/camel-chaos/pom.xml
+++ b/components/camel-chaos/pom.xml
@@ -72,6 +72,12 @@
<artifactId>camel-file</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <!-- a stub that stands in for another component, as camel run
with stubs serves it -->
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-stub</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<!-- the deterministic simulation test that runs chaos rules in
virtual time (dst package) -->
<groupId>org.apache.camel</groupId>
diff --git a/components/camel-chaos/src/main/docs/chaos.adoc
b/components/camel-chaos/src/main/docs/chaos.adoc
index 62a335f5edf5..8d6bd7a724ae 100644
--- a/components/camel-chaos/src/main/docs/chaos.adoc
+++ b/components/camel-chaos/src/main/docs/chaos.adoc
@@ -266,6 +266,28 @@ In JSON the report has `tried`: per `route` its `kinds`
(`kind`, `rules`, `fired
(`tried`): `camel cmd chaos-suggest` ends its line with `- tried`, and the
what to try list of the Camel TUI (key `w`)
shows both.
+=== With stubs
+
+`camel run --stub=remote` serves the remote components with the stub
component, a queue in memory, so the integration
+runs without the systems behind it, and `camel cmd send` feeds a stubbed
consumer:
+
+[source,bash]
+----
+camel run * --stub=remote --chaos
+camel cmd send --endpoint=kafka:refunds --body=file:samples/refund.json
+----
+
+The rules match a stub by its URI (`endpoint=kafka:orders`, `route=refunds`),
and chaos counts a stub as the component
+it stands in for:
+
+* What to try takes a stub as remote when the component is. It reads that from
the JSON of the component, which
+`camel run` downloads with the component for a stub too.
+* A stubbed consumer keeps a failed message by the options of its URI, as the
component would:
+`kafka:refunds?breakOnFirstError=true` keeps it. Options set on the component
do not count, as a stub does not take
+them. The stub does not read the message again, though, so the report says per
route that its consumer is a stub
+(`stubbedConsumer` in the JSON, `refunds (stub of kafka)` in `camel get chaos
--report`, and `refunds (stub)` on the
+chaos screen of the Camel TUI): a kept message is then not lost, though
nothing reads it again while the stub runs.
+
=== From an AI agent (MCP)
The Camel MCP server (`camel mcp`, see xref:manual::camel-jbang-mcp.adoc[Camel
MCP server]) has the same operations
@@ -762,7 +784,9 @@ per exchange too.
`acknowledgementModeName=CLIENT_ACKNOWLEDGE` (the endpoint option; the number
set with `setAcknowledgementMode` in Java
is not seen) and `asyncConsumer=false`; `kafka` with `breakOnFirstError=true`.
The options are read through the
configurer of the endpoint, so options set on the component count too. The
component is known by the scheme of the
-endpoint URI.
+endpoint URI. A stub that stands in for one of them keeps it by the options of
its URI (see <<_with_stubs,With stubs>>).
+* `stubbedConsumer`: the component the consumer of the route is a stub of
(`camel run --stub`), such as `kafka`; left
+out when it is not a stub. A stub does not read a kept message again.
* `handled`: the completed exchanges after the error handler handled a failure
(the dead letter channel, or
`onException` with `handled(true)`). A failure handled in a route called with
`direct` counts as handled in the
calling routes too.
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
index 92e99753bb9d..1fa1abd6d935 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosAdvisor.java
@@ -293,7 +293,7 @@ final class ChaosAdvisor {
85));
continue;
}
- if (!to.isRemote()) {
+ if (!ChaosStubs.isRemote(to)) {
continue;
}
if (LOCAL_FILES.contains(scheme)) {
@@ -340,7 +340,7 @@ final class ChaosAdvisor {
}
// the input of the route
- if (from != null && from.isRemote()) {
+ if (from != null && ChaosStubs.isRemote(from)) {
if (route.getConsumer() instanceof ScheduledPollConsumer) {
add(answer, recipes, "poll-error", id, Kind.CONSUMER,
Map.of("endpoint", uri(from)),
"route " + id + " polls " + uri(from)
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosConsumers.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosConsumers.java
index 06503dc654f9..55ab7b42b3fd 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosConsumers.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosConsumers.java
@@ -16,6 +16,8 @@
*/
package org.apache.camel.component.chaos;
+import java.util.function.Function;
+
import org.apache.camel.Component;
import org.apache.camel.Endpoint;
import org.apache.camel.spi.PropertyConfigurer;
@@ -33,7 +35,9 @@ import org.apache.camel.spi.PropertyConfigurerGetter;
* <li>{@code kafka}: with {@code breakOnFirstError=true}.</li>
* </ul>
* The component is known by the scheme of the endpoint, so a component added
under another name counts as any other.
- * Any other consumer, or an endpoint without a configurer, does not keep it.
+ * Any other consumer, or an endpoint without a configurer, does not keep it.
A stub that stands in for one of these
+ * components ({@code camel run --stub}) keeps it by the options of its URI,
as the component would, though the stub
+ * does not read the message again (see {@link ChaosStubs}).
*/
final class ChaosConsumers {
@@ -59,38 +63,45 @@ final class ChaosConsumers {
if (endpoint == null) {
return false;
}
- PropertyConfigurerGetter options = options(endpoint);
- if (options == null) {
+ Function<String, Object> option = option(endpoint);
+ if (option == null) {
return false;
}
try {
- return keepsFailed(endpoint, options);
+ return keepsFailed(endpoint, option);
} catch (RuntimeException e) {
// an option that cannot be read: the report says "failed"
return false;
}
}
- private static boolean keepsFailed(Endpoint endpoint,
PropertyConfigurerGetter options) {
+ private static boolean keepsFailed(Endpoint endpoint, Function<String,
Object> option) {
return switch (ChaosPatterns.scheme(endpoint.getEndpointUri())) {
- case "file", "ftp", "sftp", "ftps" ->
options.getOptionValue(endpoint, "moveFailed", true) == null;
- case "jms", "activemq", "activemq6", "amqp" ->
isTrue(options.getOptionValue(endpoint, "transacted", true))
- || isClientAcknowledge(endpoint, options)
- && !isTrue(options.getOptionValue(endpoint,
"asyncConsumer", true));
- case "kafka" -> isTrue(options.getOptionValue(endpoint,
"breakOnFirstError", true));
+ case "file", "ftp", "sftp", "ftps" -> option.apply("moveFailed")
== null;
+ case "jms", "activemq", "activemq6", "amqp" ->
isTrue(option.apply("transacted"))
+ || isClientAcknowledge(option) &&
!isTrue(option.apply("asyncConsumer"));
+ case "kafka" -> isTrue(option.apply("breakOnFirstError"));
default -> false;
};
}
// acknowledgementModeName is the endpoint option (acknowledgementMode,
the number, is not one)
- private static boolean isClientAcknowledge(Endpoint endpoint,
PropertyConfigurerGetter options) {
- return
"CLIENT_ACKNOWLEDGE".equals(text(options.getOptionValue(endpoint,
"acknowledgementModeName", true)));
+ private static boolean isClientAcknowledge(Function<String, Object>
option) {
+ return
"CLIENT_ACKNOWLEDGE".equals(text(option.apply("acknowledgementModeName")));
}
- private static PropertyConfigurerGetter options(Endpoint endpoint) {
+ /**
+ * How the options of the endpoint are read: through the configurer of its
component, or from the URI of a stub, as
+ * the stub component does not configure them; null when they cannot be
read.
+ */
+ private static Function<String, Object> option(Endpoint endpoint) {
+ if (ChaosStubs.stubbedScheme(endpoint) != null) {
+ return ChaosStubs.uriOptions(endpoint)::get;
+ }
Component component = endpoint.getComponent();
PropertyConfigurer configurer = component != null ?
component.getEndpointPropertyConfigurer() : null;
- return configurer instanceof PropertyConfigurerGetter getter ? getter
: null;
+ return configurer instanceof PropertyConfigurerGetter getter
+ ? name -> getter.getOptionValue(endpoint, name, true) : null;
}
private static boolean isTrue(Object value) {
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
index 2ae52bfad6e9..0e1c01964d7e 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java
@@ -726,6 +726,10 @@ public final class ChaosManager {
void routeDone(Route route, Exchange exchange) {
String routeId = route.getRouteId();
RouteCounts counts = counts(routeId);
+ if (!counts.consumerChecked) {
+ counts.stubbedConsumer =
ChaosStubs.stubbedScheme(route.getEndpoint());
+ counts.consumerChecked = true;
+ }
ChaosExchangeState.Outcome outcome = outcome(route, exchange);
ChaosExchangeState.RouteState state =
ChaosExchangeState.of(exchange).route(routeId);
ChaosExchangeState.Outcome previous = state.counts == counts ?
state.outcome : ChaosExchangeState.Outcome.ENTERED;
@@ -1089,6 +1093,10 @@ public final class ChaosManager {
final LongAdder failedKept = new LongAdder();
final LongAdder handled = new LongAdder();
final LongAdder redeliveries = new LongAdder();
+ // the component the consumer of the route is a stub of (camel run
--stub), once a route was done with an
+ // exchange: a stub does not read a kept message again
+ volatile String stubbedConsumer;
+ volatile boolean consumerChecked;
void count(ChaosExchangeState.Outcome outcome) {
add(outcome, 1);
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
index a2dfd381f5ea..5371d6c484b6 100644
---
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosReport.java
@@ -267,8 +267,11 @@ public final class ChaosReport {
}
for (RouteEntry r : routes) {
sb.append("\n ").append(r.route).append(": completed
").append(r.completed).append(", failed ")
- .append(r.failed).append(" (kept by the consumer
").append(r.failedKept).append("), handled ")
- .append(r.handled).append(", redeliveries
").append(r.redeliveries);
+ .append(r.failed).append(" (kept by the consumer
").append(r.failedKept);
+ if (r.stubbedConsumer != null) {
+ sb.append(", a stub of ").append(r.stubbedConsumer).append("
that does not read them again");
+ }
+ sb.append("), handled ").append(r.handled).append(", redeliveries
").append(r.redeliveries);
}
if (!tried.isEmpty()) {
sb.append("\n tried, per route:");
@@ -456,6 +459,9 @@ public final class ChaosReport {
jo.put("completed", r.completed);
jo.put("failed", r.failed);
jo.put("failedKeptByConsumer", r.failedKept);
+ if (r.stubbedConsumer != null) {
+ jo.put("stubbedConsumer", r.stubbedConsumer);
+ }
jo.put("handled", r.handled);
jo.put("redeliveries", r.redeliveries);
routeArray.add(jo);
@@ -860,6 +866,7 @@ public final class ChaosReport {
final long failedKept;
final long handled;
final long redeliveries;
+ final String stubbedConsumer;
RouteEntry(String route, RouteCounts counts) {
this.route = route;
@@ -868,6 +875,7 @@ public final class ChaosReport {
failedKept = counts.failedKept.sum();
handled = counts.handled.sum();
redeliveries = counts.redeliveries.sum();
+ stubbedConsumer = counts.stubbedConsumer;
}
}
}
diff --git
a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosStubs.java
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosStubs.java
new file mode 100644
index 000000000000..fd55cd915cad
--- /dev/null
+++
b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosStubs.java
@@ -0,0 +1,99 @@
+/*
+ * 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.component.chaos;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CatalogCamelContext;
+import org.apache.camel.Component;
+import org.apache.camel.Endpoint;
+import org.apache.camel.util.URISupport;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * A stub that stands in for another component: {@code camel run --stub}
serves a URI such as {@code kafka:refunds} with
+ * the stub component, a queue in memory, so the integration runs without the
system behind it. Chaos counts such an
+ * endpoint as the component it stands in for: remote when that component is
remote (from the JSON of the component,
+ * found when its JAR is on the classpath, as {@code camel run} downloads it
for a stub too), and with the options of
+ * its URI, which the stub component does not configure. The stub component is
known by its class name, as camel-core
+ * knows it, so camel-chaos does not depend on camel-stub.
+ */
+final class ChaosStubs {
+
+ private static final String STUB_COMPONENT = "StubComponent";
+
+ private ChaosStubs() {
+ }
+
+ /**
+ * The scheme of the component a stub stands in for ({@code kafka} for
{@code kafka://refunds}), or null when the
+ * endpoint is not a stub, or is a plain {@code stub:} endpoint.
+ */
+ static String stubbedScheme(Endpoint endpoint) {
+ Component component = endpoint != null ? endpoint.getComponent() :
null;
+ // NOTE: the stub component is not available at compilation time
+ if (component == null ||
!STUB_COMPONENT.equals(component.getClass().getSimpleName())) { // NOSONAR
+ return null;
+ }
+ String scheme = ChaosPatterns.scheme(endpoint.getEndpointUri());
+ return "stub".equals(scheme) ? null : scheme;
+ }
+
+ /** Whether the endpoint is remote: a stub is when the component it stands
in for is. */
+ static boolean isRemote(Endpoint endpoint) {
+ if (endpoint.isRemote()) {
+ return true;
+ }
+ String scheme = stubbedScheme(endpoint);
+ return scheme != null && isRemoteComponent(endpoint.getCamelContext(),
scheme);
+ }
+
+ /** Whether the component of the scheme is remote, from its JSON; false
when it cannot be found. */
+ static boolean isRemoteComponent(CamelContext context, String scheme) {
+ if (!(context instanceof CatalogCamelContext catalog)) {
+ return false;
+ }
+ try {
+ String json = catalog.getComponentParameterJsonSchema(scheme);
+ if (json == null) {
+ return false;
+ }
+ JsonObject component = ((JsonObject)
Jsoner.deserialize(json)).getMap("component");
+ return component != null &&
component.getBooleanOrDefault("remote", false);
+ } catch (Exception e) {
+ // the JSON of a component that cannot be read: the stub is not
taken as remote
+ return false;
+ }
+ }
+
+ /** The options in the URI of a stub, by name in any case; empty when the
URI cannot be read. */
+ static Map<String, Object> uriOptions(Endpoint endpoint) {
+ Map<String, Object> answer = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+ String query = URISupport.extractQuery(endpoint.getEndpointUri());
+ if (query != null) {
+ try {
+ answer.putAll(URISupport.parseQuery(query));
+ } catch (Exception e) {
+ // a query that cannot be read has no options
+ }
+ }
+ return answer;
+ }
+}
diff --git
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosStubsTest.java
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosStubsTest.java
new file mode 100644
index 000000000000..57d8353dbeb7
--- /dev/null
+++
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosStubsTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.component.chaos;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.stub.StubComponent;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+/**
+ * Chaos sees through a stub that stands in for another component, as {@code
camel run --stub=remote} serves
+ * {@code kafka:} with the stub component (CAMEL-26765): the suggestions take
it as remote when the component is, and a
+ * consumer keeps a failed message by the options of its URI, which the report
marks as kept by a stub.
+ */
+class ChaosStubsTest extends ChaosTestSupport {
+
+ private static final String REFUNDS =
"kafka:refunds?brokers=localhost:9092&breakOnFirstError=true";
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ // remotemq is remote by its JSON (in the test resources); kafka has
no JSON here, as its JAR is not there
+ context.addComponent("remotemq", shadow());
+ context.addComponent("kafka", shadow());
+ return context;
+ }
+
+ private static StubComponent shadow() {
+ StubComponent stub = new StubComponent();
+ stub.setShadow(true);
+ return stub;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("remotemq:in").routeId("mq").to("remotemq:out");
+ from(REFUNDS).routeId("refunds").to("mock:refunds");
+
from("kafka:payments?brokers=localhost:9092").routeId("payments").to("mock:payments");
+ from("direct:plain").routeId("plain").to("stub:audit");
+ }
+ };
+ }
+
+ private static Map<String, String> rules(List<ChaosAdvisor.Suggestion>
suggestions) {
+ Map<String, String> answer = new LinkedHashMap<>();
+ suggestions.forEach(s -> answer.put(s.route() + " " + s.recipe() + " "
+ s.kind().key, s.rule()));
+ return answer;
+ }
+
+ @Test
+ void aStubOfARemoteComponentIsRemote() {
+
assertThat(ChaosStubs.stubbedScheme(context.getEndpoint("remotemq:in"))).isEqualTo("remotemq");
+
assertThat(ChaosStubs.isRemote(context.getEndpoint("remotemq:in"))).isTrue();
+ // a component whose JSON is not on the classpath is not taken as
remote
+
assertThat(ChaosStubs.isRemote(context.getEndpoint("kafka:payments?brokers=localhost:9092"))).isFalse();
+ // a plain stub: endpoint stands in for nothing
+
assertThat(ChaosStubs.stubbedScheme(context.getEndpoint("stub:audit"))).isNull();
+
assertThat(ChaosStubs.isRemote(context.getEndpoint("stub:audit"))).isFalse();
+
assertThat(ChaosStubs.stubbedScheme(context.getEndpoint("mock:refunds"))).isNull();
+ }
+
+ @Test
+ void theSuggestionsSeeTheStubbedRemoteEndpoints() {
+ Map<String, String> rules = rules(ChaosAdvisor.suggest(context));
+ assertThat(rules)
+ .containsEntry("mq route-error consumer", "route=mq error
rate=0.05 until=10m")
+ .containsEntry("mq connection-refused errors",
+ "endpoint=remotemq://out error
exception=java.net.ConnectException rate=0.25 until=10m");
+ // the stub of audit stands in for nothing, so there is nothing remote
to try in route plain
+ assertThat(rules.keySet()).noneMatch(k -> k.startsWith("plain "));
+ }
+
+ @Test
+ void aStubOfKafkaKeepsByTheOptionsOfItsUri() throws Exception {
+
assertThat(ChaosConsumers.keepsFailed(context.getEndpoint(REFUNDS))).isTrue();
+
assertThat(ChaosConsumers.keepsFailed(context.getEndpoint("kafka:payments?brokers=localhost:9092"))).isFalse();
+
+ chaos().addRule("route=refunds error until=2");
+ chaos().addRule("route=payments error until=1");
+ template.sendBody(REFUNDS, "R-1");
+ template.sendBody(REFUNDS, "R-2");
+ template.sendBody("kafka:payments?brokers=localhost:9092", "P-1");
+ await().atMost(10, TimeUnit.SECONDS)
+ .until(() -> routeCount("refunds", "failed") == 2 &&
routeCount("payments", "failed") == 1);
+
+ // kept by kafka (breakOnFirstError=true), but the stub does not read
them again: the report says so
+
assertThat(routeReport("refunds").getLong("failedKeptByConsumer")).isEqualTo(2);
+
assertThat(routeReport("refunds").getString("stubbedConsumer")).isEqualTo("kafka");
+
assertThat(routeReport("payments").getLong("failedKeptByConsumer")).isZero();
+
assertThat(routeReport("payments").getString("stubbedConsumer")).isEqualTo("kafka");
+ assertThat(chaos().report().toText())
+ .contains("refunds: completed 0, failed 2 (kept by the
consumer 2, a stub of kafka that does not read"
+ + " them again)");
+
+ // a route whose consumer is not a stub has no mark
+ template.sendBody("direct:plain", "A-1");
+ await().atMost(10, TimeUnit.SECONDS).until(() -> routeCount("plain",
"completed") == 1);
+ assertThat(routeReport("plain").getString("stubbedConsumer")).isNull();
+ }
+}
diff --git
a/components/camel-chaos/src/test/resources/META-INF/org/apache/camel/component/chaos/remotemq.json
b/components/camel-chaos/src/test/resources/META-INF/org/apache/camel/component/chaos/remotemq.json
new file mode 100644
index 000000000000..d3b7f1890d94
--- /dev/null
+++
b/components/camel-chaos/src/test/resources/META-INF/org/apache/camel/component/chaos/remotemq.json
@@ -0,0 +1,10 @@
+{
+ "component": {
+ "kind": "component",
+ "name": "remotemq",
+ "title": "Remote MQ (ChaosStubsTest)",
+ "scheme": "remotemq",
+ "syntax": "remotemq:queue",
+ "remote": true
+ }
+}
diff --git
a/components/camel-chaos/src/test/resources/META-INF/services/org/apache/camel/component/remotemq
b/components/camel-chaos/src/test/resources/META-INF/services/org/apache/camel/component/remotemq
new file mode 100644
index 000000000000..c445612d7ec6
--- /dev/null
+++
b/components/camel-chaos/src/test/resources/META-INF/services/org/apache/camel/component/remotemq
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
+# a remote component that ChaosStubsTest serves with a stub, as camel run
--stub=remote does: its JSON says it is remote
+class=org.apache.camel.component.chaos.TestPollComponent
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
index bf321715801a..4a95b0894029 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaos.java
@@ -268,7 +268,7 @@ public class ListChaos extends ProcessWatchCommand {
if (routes != null && !routes.isEmpty()) {
List<JsonObject> list =
routes.stream().map(JsonObject.class::cast).toList();
printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, list,
List.of(
- new
Column().header("ROUTE").dataAlign(HorizontalAlign.LEFT).with(r ->
r.getString("route")),
+ new
Column().header("ROUTE").dataAlign(HorizontalAlign.LEFT).with(ListChaos::routeName),
new
Column().header("COMPLETED").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
.with(r -> String.valueOf(r.get("completed"))),
new
Column().header("FAILED").headerAlign(HorizontalAlign.RIGHT).dataAlign(HorizontalAlign.RIGHT)
@@ -280,6 +280,9 @@ public class ListChaos extends ProcessWatchCommand {
new
Column().header("REDELIVERIES").headerAlign(HorizontalAlign.RIGHT)
.dataAlign(HorizontalAlign.RIGHT)
.with(r ->
String.valueOf(r.get("redeliveries"))))));
+ if (list.stream().anyMatch(r -> r.getString("stubbedConsumer") !=
null)) {
+ printer().println(" a stub does not read a kept message
again, as the component it stands in for would");
+ }
}
JsonArray recent = report.getCollection("recentFaults");
if (recent != null && !recent.isEmpty()) {
@@ -418,6 +421,12 @@ public class ListChaos extends ProcessWatchCommand {
return sj.toString();
}
+ /** The route of the report, and the component its consumer is a stub of
(camel run --stub). */
+ static String routeName(JsonObject route) {
+ String stub = route.getString("stubbedConsumer");
+ return route.getString("route") + (stub != null ? " (stub of " + stub
+ ")" : "");
+ }
+
/** The rule, and below it the rule in plain words (an integration before
Camel 4.24 has none). */
static String ruleText(JsonObject rule) {
String text = rule.getString("rule");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
index 56be4ec4d512..1bb84ce6b76c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/process/ListChaosTest.java
@@ -169,6 +169,21 @@ class ListChaosTest extends ProcessCommandTestSupport {
assertThat(out).contains("replaced").contains("rate=0.1").contains("orders:100
unmet (97 completed)");
assertThat(out).contains("COMPLETED").contains("REDELIVERIES");
assertThat(lineOf(out, "orders
")).containsPattern("97\\s+6\\s+2\\s+1\\s+4");
+ assertThat(out).doesNotContain("stub");
+ }
+
+ @Test
+ void aRouteWhoseConsumerIsAStub() throws Exception {
+ JsonObject chaos = chaosOn();
+ JsonObject route = (JsonObject) ((JsonArray) ((JsonObject)
chaos.get("report")).get("routes")).get(0);
+ route.put("stubbedConsumer", "kafka");
+ writeStatus(chaos);
+
+ assertThat(run("--report")).isZero();
+
+ String out = printer.getStdout();
+ assertThat(lineOf(out, "orders (stub of
kafka)")).containsPattern("97\\s+6\\s+2\\s+1\\s+4");
+ assertThat(out).contains("a stub does not read a kept message again,
as the component it stands in for would");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
index d9a41f94d876..e9e9e63cef69 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosInfo.java
@@ -249,7 +249,8 @@ final class ChaosInfo {
private static final List<String> OPTIONS = List.of("at", "in", "when",
"rate", "until", "from", "expect", "within");
/** The counts of a route in the report. */
- record RouteCounts(String route, long completed, long failed, long
failedKept, long handled, long redeliveries) {
+ record RouteCounts(String route, long completed, long failed, long
failedKept, long handled, long redeliveries,
+ String stubbedConsumer) {
}
/** The kinds of fault tried in a route, and those the suggestions offer
that no rule tried yet. */
@@ -333,7 +334,7 @@ final class ChaosInfo {
info.routes.add(new RouteCounts(
r.getString("route"), num(r.get("completed"),
0), num(r.get("failed"), 0),
num(r.get("failedKeptByConsumer"), 0),
num(r.get("handled"), 0),
- num(r.get("redeliveries"), 0)));
+ num(r.get("redeliveries"), 0),
r.getString("stubbedConsumer")));
}
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
index 2cc5cef96a17..ad9c093807e6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTab.java
@@ -646,8 +646,12 @@ class ChaosTab extends AbstractTableTab {
private static void renderRoutes(Frame frame, Rect area, ChaosInfo chaos) {
List<Row> rows = new ArrayList<>();
for (ChaosInfo.RouteCounts c : chaos.routes) {
+ // a consumer that is a stub (camel run --stub) does not read a
kept message again; the panel is narrow, so
+ // the component it stands in for is left to the report
+ Span name = Span.styled(c.route() != null ? c.route() : "",
Style.EMPTY.fg(Theme.accent()));
+ Line route = c.stubbedConsumer() != null ? Line.from(name,
Span.styled(" (stub)", Theme.muted())) : Line.from(name);
rows.add(Row.from(
- Cell.from(Span.styled(c.route() != null ? c.route() : "",
Style.EMPTY.fg(Theme.accent()))),
+ Cell.from(route),
rightCell(String.valueOf(c.completed()), 6),
rightCell(String.valueOf(c.failed()), 5, c.failed() > 0 ?
Theme.error() : Style.EMPTY),
rightCell(String.valueOf(c.failedKept()), 5),
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTabRenderTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTabRenderTest.java
index be424cf4fe3f..cfa741608e6d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTabRenderTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ChaosTabRenderTest.java
@@ -73,7 +73,7 @@ class ChaosTabRenderTest {
{"route": "orders", "completed": 97, "failed": 0,
"failedKeptByConsumer": 0, "handled": 2,
"redeliveries": 44},
{"route": "stock", "completed": 141, "failed": 0,
"failedKeptByConsumer": 0, "handled": 0,
- "redeliveries": 0}
+ "redeliveries": 0, "stubbedConsumer": "kafka"}
],
"recentFaults": [
{"timestamp": 1000, "ago": 2000, "rule": "r1", "version": 1,
"fault": "error status 503",
@@ -132,6 +132,8 @@ class ChaosTabRenderTest {
assertThat(screen).contains("round 1 of 3").contains("route
poller").contains("down, back in 8s");
// the routes
assertThat(screen).contains("ROUTE").contains("orders").contains("141");
+ // a route whose consumer is a stub (camel run --stub) says so
+ assertThat(screen).contains("stock (stub)");
}
@Test