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 84fee66c7630 CAMEL-25821: camel-test-dst - more pitfalls for agents
writing worlds (#28138)
84fee66c7630 is described below
commit 84fee66c763004c8e35ead5cdabe81dbe8c1e4c7
Author: allthingssecurity <[email protected]>
AuthorDate: Tue Oct 20 18:37:41 2026 +0530
CAMEL-25821: camel-test-dst - more pitfalls for agents writing worlds
(#28138)
The guide gets what came up when writing worlds for IMAP, JMS, NATS
JetStream and Azure Service Bus: a consumer task that never returns,
fakes that answer at once, Proxy fakes and undeclared checked
exceptions, objects of an earlier connection, an excuse per kind of
operator stop, a synchronous consumer over an asynchronous step,
test-infra modules in -pl, running a world alone in a module with
parallel tests, and locks that do not exclude on the one stack.
Follow-ups:
- camel-test-dst - say the later worlds add to the list, and how an empty
answer fails the seed
Closes #28138
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/catalog/docs/test-dst.adoc | 37 ++++++++++++++++++++--
.../camel-test-dst/src/main/docs/test-dst.adoc | 37 ++++++++++++++++++++--
2 files changed, 68 insertions(+), 6 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/test-dst.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/test-dst.adoc
index 583fe99f6dc5..7a001ec87504 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/test-dst.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/test-dst.adoc
@@ -292,7 +292,11 @@ Agents wrote nine worlds with this guide. These mistakes
came up:
`dst.currentTimeMillis()`), not 0. A Service Bus fake with lock times in
1970 made the lock renewer renew on every run,
so the world could not fail. The self-check (fail on the code before the
fix) caught it.
* *No end to the run.* A consumer that loops forever needs an unwind at the
end of the run, or the run never ends:
- cancel its task through the task registry, or stop the route at the end of
the drain phase.
+ cancel its task through the task registry, or stop the route at the end of
the drain phase. A consumer task that
+ never returns (a pull loop) sits above the test thread on the one stack, so
a `dst.runFor` or `dst.runUntil` in the
+ test body does not return while it loops, and the seed fails on the step
budget. Make the phases of the run (clear
+ the faults, drain, end) scheduled tasks, and end the run by having the fake
end the loop (close the subscription),
+ then `dst.runUntil(() -> closed, "subscription closed")`.
* *A fake that blocks under a lock.* A call of the fake made while the
consumer holds a lock must not block: with the
one-stack rule, the tasks nested above it run while the lock is held.
* *A violation without its excuse.* Every invariant needs the record of which
injected faults excuse a violation of it.
@@ -305,6 +309,27 @@ Agents wrote nine worlds with this guide. These mistakes
came up:
CAMEL-25737 (`visibilityTimeout=1`) and CAMEL-25741 (aws2-ddbstream, found
with a fake DynamoDB Streams world) were
found.
+Later worlds (IMAP, JMS, NATS JetStream, Azure Service Bus) showed more:
+
+* *An empty answer at once.* A fake that answers at once with nothing (no
message, no responder) lets a consumer with
+ its own receive loop (JMS, a NATS fetch) spin without virtual time moving;
the seed fails on the step budget or the
+ watchdog instead of reaching its end. Give every call of the fake a
round-trip latency.
+* *A proxy that throws what the interface does not declare.* A
`java.lang.reflect.Proxy` keeps the fake of a wide
+ interface (JMS) small. A checked exception that the interface method does
not declare comes out of the proxy as
+ `UndeclaredThrowableException`; do what the library does instead (for an
`InterruptedException` in a fetch: set the
+ interrupt flag again and return).
+* *Only the protocol.* Model what the client library does with the objects of
an earlier connection, not only what
+ the server does: after a reconnect, a message or handle from before may
still work, fail, or now refer to something
+ else. Read the library (its source, or its byte code) for that.
+* *One excuse for every stop.* Each kind of operator stop needs its own excuse
record: a consumer stopped through JMX
+ and a route stop that hits the shutdown timeout leave different work
unfinished (a lock renewer cancelled under
+ exchanges still in flight, for example).
+* *A synchronous consumer over an asynchronous step.* With
`DstCamel.asyncStep` in the route, a consumer that calls
+ the route synchronously waits on a latch of
`org.apache.camel.impl.engine.DefaultAsyncProcessorAwaitManager`: add
+ it to `alsoRewrite`, or the watchdog fails the seed. Let the asynchronous
exchanges in flight finish before
+ `dst.dropPending()`, which drops their answers too, so that the shutdown
does not wait for exchanges that cannot
+ complete.
+
=== Checklist
* [ ] The client is swapped through an endpoint option or a static mock, at a
synchronous level.
@@ -323,7 +348,8 @@ Agents wrote nine worlds with this guide. These mistakes
came up:
The worlds are tests of their components, in a `dst` package. Build the
harness in the same reactor as the component
(`-pl` both), so that the component's tests see it.
`-Dsurefire.rerunFailingTestsCount=0` stops surefire from running
-each failing seed again.
+each failing seed again. A component whose tests depend on a
`camel-test-infra-*` module (camel-nats,
+camel-azure-storage-queue) needs that module in `-pl` too when it is not in
the local repository.
[source,bash]
----
@@ -367,7 +393,12 @@ seed of the in-JVM replay test of the Event Hubs world,
default 1).
in the JVM sees it. A world must therefore not go into a module that runs
its tests in parallel
(`camel.surefire.parallel=true`, as camel-jms and camel-sql do): the other
tests in that surefire fork would read the
virtual clock while a run is active. The harness enforces this: a second run
while one is open fails, and a DST test
- refuses to start when JUnit runs tests in parallel.
+ refuses to start when JUnit runs tests in parallel. A local world in such a
module (camel-sjms, for example) can
+ still be run alone, with `-Dtest=<the world>
-Dcamel.surefire.parallel=false`, but it cannot be committed there:
+ the module's own build would fail it.
+* Locks do not exclude: every task runs on the test thread, so a
`ReentrantLock` or `synchronized` block held by a
+ blocked task is entered again by the tasks nested above it. A critical
section is atomic only because the fake does
+ not block inside it, and races between two code paths that each hold a lock
cannot be explored.
* The rewriting stays for the rest of the surefire fork: the classes of the
`@DstInstrument` packages (the component's
main classes, and its test classes in the same package) stay rewritten for
every test that runs after the DST test,
the same as the `alsoRewrite` entries. Outside a run they read the real
clock and sleep for real, so the other tests
diff --git a/components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
b/components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
index 583fe99f6dc5..7a001ec87504 100644
--- a/components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
+++ b/components/camel-test/camel-test-dst/src/main/docs/test-dst.adoc
@@ -292,7 +292,11 @@ Agents wrote nine worlds with this guide. These mistakes
came up:
`dst.currentTimeMillis()`), not 0. A Service Bus fake with lock times in
1970 made the lock renewer renew on every run,
so the world could not fail. The self-check (fail on the code before the
fix) caught it.
* *No end to the run.* A consumer that loops forever needs an unwind at the
end of the run, or the run never ends:
- cancel its task through the task registry, or stop the route at the end of
the drain phase.
+ cancel its task through the task registry, or stop the route at the end of
the drain phase. A consumer task that
+ never returns (a pull loop) sits above the test thread on the one stack, so
a `dst.runFor` or `dst.runUntil` in the
+ test body does not return while it loops, and the seed fails on the step
budget. Make the phases of the run (clear
+ the faults, drain, end) scheduled tasks, and end the run by having the fake
end the loop (close the subscription),
+ then `dst.runUntil(() -> closed, "subscription closed")`.
* *A fake that blocks under a lock.* A call of the fake made while the
consumer holds a lock must not block: with the
one-stack rule, the tasks nested above it run while the lock is held.
* *A violation without its excuse.* Every invariant needs the record of which
injected faults excuse a violation of it.
@@ -305,6 +309,27 @@ Agents wrote nine worlds with this guide. These mistakes
came up:
CAMEL-25737 (`visibilityTimeout=1`) and CAMEL-25741 (aws2-ddbstream, found
with a fake DynamoDB Streams world) were
found.
+Later worlds (IMAP, JMS, NATS JetStream, Azure Service Bus) showed more:
+
+* *An empty answer at once.* A fake that answers at once with nothing (no
message, no responder) lets a consumer with
+ its own receive loop (JMS, a NATS fetch) spin without virtual time moving;
the seed fails on the step budget or the
+ watchdog instead of reaching its end. Give every call of the fake a
round-trip latency.
+* *A proxy that throws what the interface does not declare.* A
`java.lang.reflect.Proxy` keeps the fake of a wide
+ interface (JMS) small. A checked exception that the interface method does
not declare comes out of the proxy as
+ `UndeclaredThrowableException`; do what the library does instead (for an
`InterruptedException` in a fetch: set the
+ interrupt flag again and return).
+* *Only the protocol.* Model what the client library does with the objects of
an earlier connection, not only what
+ the server does: after a reconnect, a message or handle from before may
still work, fail, or now refer to something
+ else. Read the library (its source, or its byte code) for that.
+* *One excuse for every stop.* Each kind of operator stop needs its own excuse
record: a consumer stopped through JMX
+ and a route stop that hits the shutdown timeout leave different work
unfinished (a lock renewer cancelled under
+ exchanges still in flight, for example).
+* *A synchronous consumer over an asynchronous step.* With
`DstCamel.asyncStep` in the route, a consumer that calls
+ the route synchronously waits on a latch of
`org.apache.camel.impl.engine.DefaultAsyncProcessorAwaitManager`: add
+ it to `alsoRewrite`, or the watchdog fails the seed. Let the asynchronous
exchanges in flight finish before
+ `dst.dropPending()`, which drops their answers too, so that the shutdown
does not wait for exchanges that cannot
+ complete.
+
=== Checklist
* [ ] The client is swapped through an endpoint option or a static mock, at a
synchronous level.
@@ -323,7 +348,8 @@ Agents wrote nine worlds with this guide. These mistakes
came up:
The worlds are tests of their components, in a `dst` package. Build the
harness in the same reactor as the component
(`-pl` both), so that the component's tests see it.
`-Dsurefire.rerunFailingTestsCount=0` stops surefire from running
-each failing seed again.
+each failing seed again. A component whose tests depend on a
`camel-test-infra-*` module (camel-nats,
+camel-azure-storage-queue) needs that module in `-pl` too when it is not in
the local repository.
[source,bash]
----
@@ -367,7 +393,12 @@ seed of the in-JVM replay test of the Event Hubs world,
default 1).
in the JVM sees it. A world must therefore not go into a module that runs
its tests in parallel
(`camel.surefire.parallel=true`, as camel-jms and camel-sql do): the other
tests in that surefire fork would read the
virtual clock while a run is active. The harness enforces this: a second run
while one is open fails, and a DST test
- refuses to start when JUnit runs tests in parallel.
+ refuses to start when JUnit runs tests in parallel. A local world in such a
module (camel-sjms, for example) can
+ still be run alone, with `-Dtest=<the world>
-Dcamel.surefire.parallel=false`, but it cannot be committed there:
+ the module's own build would fail it.
+* Locks do not exclude: every task runs on the test thread, so a
`ReentrantLock` or `synchronized` block held by a
+ blocked task is entered again by the tasks nested above it. A critical
section is atomic only because the fake does
+ not block inside it, and races between two code paths that each hold a lock
cannot be explored.
* The rewriting stays for the rest of the surefire fork: the classes of the
`@DstInstrument` packages (the component's
main classes, and its test classes in the same package) stay rewritten for
every test that runs after the DST test,
the same as the `alsoRewrite` entries. Outside a run they read the real
clock and sleep for real, so the other tests