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 e12401164dac CAMEL-26757: camel-chaos - test that chaos step rules and 
the exchange sanitizer work together (#29527)
e12401164dac is described below

commit e12401164dac3a49235f4a1350d3f8db81f2f8c3
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Nov 10 13:29:14 2026 +0530

    CAMEL-26757: camel-chaos - test that chaos step rules and the exchange 
sanitizer work together (#29527)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 components/camel-chaos/pom.xml                     |   6 +
 .../component/chaos/ChaosWithSanitizerTest.java    | 182 +++++++++++++++++++++
 2 files changed, 188 insertions(+)

diff --git a/components/camel-chaos/pom.xml b/components/camel-chaos/pom.xml
index 7409abbbb018..e2140b6e9068 100644
--- a/components/camel-chaos/pom.xml
+++ b/components/camel-chaos/pom.xml
@@ -102,6 +102,12 @@
             <artifactId>camel-resilience4j</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <!-- chaos and the exchange sanitizer together on the same routes 
-->
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-sanitizer</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git 
a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosWithSanitizerTest.java
 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosWithSanitizerTest.java
new file mode 100644
index 000000000000..ca4960f8fa87
--- /dev/null
+++ 
b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosWithSanitizerTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.AsyncCallback;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.sanitizer.Sanitizer;
+import org.apache.camel.sanitizer.Violation;
+import org.apache.camel.sanitizer.junit.CamelSanitizer;
+import org.apache.camel.sanitizer.junit.ExpectSanitizerViolation;
+import org.apache.camel.support.AsyncProcessorSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Chaos and the exchange sanitizer together (CAMEL-26714): real chaos rules 
fail, delay and duplicate the steps and
+ * sends of the routes, and the sanitizer, on for the same contexts, reports 
only real breaks of the contract of
+ * AsyncProcessor. A step that a chaos rule fails is not a call of its 
processor, so the sanitizer neither checks nor
+ * reports it; a processor that does break the contract is still reported, 
once, also when chaos acts on its step.
+ */
+@CamelSanitizer
+class ChaosWithSanitizerTest extends ChaosTestSupport {
+
+    private final AtomicInteger work = new AtomicInteger();
+    private final AtomicInteger plain = new AtomicInteger();
+    private final AtomicInteger thrown = new AtomicInteger();
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:retry").routeId("retry")
+                        
.errorHandler(defaultErrorHandler().maximumRedeliveries(2).redeliveryDelay(0).logStackTrace(false))
+                        .process(e -> work.incrementAndGet()).id("work")
+                        .to("mock:retry").id("toRetry");
+
+                from("direct:plain").routeId("plain")
+                        .errorHandler(noErrorHandler())
+                        .process(e -> plain.incrementAndGet()).id("unhandled");
+
+                from("direct:broken").routeId("broken")
+                        
.errorHandler(defaultErrorHandler().maximumRedeliveries(1).redeliveryDelay(0).logStackTrace(false))
+                        .process(new AsyncProcessorSupport() {
+                            @Override
+                            public boolean process(Exchange exchange, 
AsyncCallback callback) {
+                                thrown.incrementAndGet();
+                                throw new IllegalStateException("Forced by the 
test");
+                            }
+                        }).id("throws");
+
+                
from("direct:twice").routeId("twice").to("direct:receiver").id("toReceiver");
+                
from("direct:receiver").routeId("receiver").to("mock:received").id("toReceived");
+                
from("direct:twiceToMock").routeId("twiceToMock").to("mock:duplicated").id("toMock");
+            }
+        };
+    }
+
+    private Sanitizer sanitizer() {
+        Sanitizer sanitizer = Sanitizer.get(context);
+        assertThat(sanitizer).as("the sanitizer is installed").isNotNull();
+        return sanitizer;
+    }
+
+    @Test
+    void aStepThatChaosFailsIsNotACallOfItsProcessor() throws Exception {
+        // the error handler redelivers the attempt that the rule failed, and 
that one calls the processor
+        String id = chaos().addRule("step=work error until=1");
+        getMockEndpoint("mock:retry").expectedMessageCount(1);
+
+        Exchange out = template.send("direct:retry", e -> 
e.getMessage().setBody("x"));
+
+        assertThat(out.getException()).isNull();
+        MockEndpoint.assertIsSatisfied(context);
+        assertThat(fired(id, "step")).isEqualTo(1);
+        assertThat(work.get()).isEqualTo(1);
+        // checked: the redelivered call of work and the send to mock:retry; 
not the attempt that chaos failed
+        assertThat(sanitizer().getCallCount()).isEqualTo(2);
+        assertThat(sanitizer().getViolations()).isEmpty();
+    }
+
+    @Test
+    void aStepThatChaosFailsWithoutAnErrorHandler() {
+        // no error handler: the channel of the step asks the chaos hook
+        String id = chaos().addRule("step=unhandled error");
+
+        Exchange out = template.send("direct:plain", e -> 
e.getMessage().setBody("x"));
+
+        assertThat(out.getException()).isInstanceOf(ChaosException.class);
+        assertThat(fired(id, "step")).isEqualTo(1);
+        assertThat(plain.get()).isZero();
+        assertThat(sanitizer().getCallCount()).isZero();
+        assertThat(sanitizer().getViolations()).isEmpty();
+    }
+
+    @Test
+    void aDelayedStepIsOneCall() throws Exception {
+        String id = chaos().addRule("step=work delay duration=5ms");
+        getMockEndpoint("mock:retry").expectedMessageCount(1);
+
+        template.sendBody("direct:retry", "x");
+
+        MockEndpoint.assertIsSatisfied(context);
+        assertThat(fired(id, "step")).isEqualTo(1);
+        assertThat(work.get()).isEqualTo(1);
+        assertThat(sanitizer().getCallCount()).isEqualTo(2);
+        assertThat(sanitizer().getViolations()).isEmpty();
+    }
+
+    @Test
+    void aSendThatChaosDuplicatesCallsBackOnce() throws Exception {
+        // the producer of direct:receiver is called twice within one call of 
the step toReceiver, whose callback is
+        // called once; the first send completes asynchronously (it routes 
through receiver), so the thread that
+        // completes it makes the second send
+        String id = chaos().addRule("endpoint=direct:receiver duplicate");
+        getMockEndpoint("mock:received").expectedBodiesReceived("x", "x");
+
+        Exchange out = template.send("direct:twice", e -> 
e.getMessage().setBody("x"));
+
+        assertThat(out.getException()).isNull();
+        MockEndpoint.assertIsSatisfied(context);
+        assertThat(fired(id, "send")).isEqualTo(1);
+        // checked: toReceiver once, and toReceived once for each of the two 
sends
+        assertThat(sanitizer().getCallCount()).isEqualTo(3);
+        assertThat(sanitizer().getViolations()).isEmpty();
+    }
+
+    @Test
+    void aSendThatChaosDuplicatesAndThatCompletesSynchronouslyCallsBackOnce() 
throws Exception {
+        // the mock producer completes each send synchronously, so the thread 
of the step makes the second send
+        String id = chaos().addRule("endpoint=mock:duplicated duplicate");
+        getMockEndpoint("mock:duplicated").expectedBodiesReceived("x", "x");
+
+        Exchange out = template.send("direct:twiceToMock", e -> 
e.getMessage().setBody("x"));
+
+        assertThat(out.getException()).isNull();
+        MockEndpoint.assertIsSatisfied(context);
+        assertThat(fired(id, "send")).isEqualTo(1);
+        assertThat(sanitizer().getCallCount()).isEqualTo(1);
+        assertThat(sanitizer().getViolations()).isEmpty();
+    }
+
+    @Test
+    @ExpectSanitizerViolation(value = Violation.THREW_BEFORE_CALLBACK, step = 
"throws")
+    void aRealBreakIsStillReportedOnceWhenChaosFailsTheFirstAttempt() {
+        // the rule fails the first attempt; the redelivery calls the 
processor, which throws instead of calling its
+        // callback
+        String id = chaos().addRule("step=throws error until=1");
+
+        Exchange out = template.send("direct:broken", e -> 
e.getMessage().setBody("x"));
+
+        
assertThat(out.getException()).isInstanceOf(IllegalStateException.class).hasMessage("Forced
 by the test");
+        assertThat(fired(id, "step")).isEqualTo(1);
+        assertThat(thrown.get()).isEqualTo(1);
+        assertThat(sanitizer().getCallCount()).isEqualTo(1);
+        
assertThat(sanitizer().getViolationCount(Violation.THREW_BEFORE_CALLBACK)).isEqualTo(1);
+        assertThat(sanitizer().getViolations()).singleElement().satisfies(v -> 
{
+            assertThat(v.getStepId()).isEqualTo("throws");
+            assertThat(v.getRouteId()).isEqualTo("broken");
+            assertThat(v.getText()).contains("java.lang.IllegalStateException: 
Forced by the test");
+        });
+    }
+}

Reply via email to