CB-521: make the AMQP contract test runnable locally and in CI
This commit is contained in:
@@ -53,3 +53,55 @@ jobs:
|
||||
grep -qE "Failures: [1-9]|Errors: [1-9]" "$f" && { echo "===== $f ====="; cat "$f"; }
|
||||
done
|
||||
exit 0
|
||||
|
||||
# CB-521 — actually run the AMQP contract test in CI, against a REAL broker. The broker is a
|
||||
# RabbitMQ SERVICE CONTAINER, not Testcontainers-with-Docker: the runner image has no Docker, so
|
||||
# AmqpReplyInboxContractTest reads AMQP_URI (set below to the service's network alias) and binds
|
||||
# straight to it — no Docker, no skipped tests. This separation (build job hermetic and
|
||||
# Docker-free; contract job broker-provided) is deliberate — see the default-excludes/contract
|
||||
# profiles in bridged/pom.xml. `setup-java` provides the JDK only; Maven is installed separately,
|
||||
# exactly as in the build job above.
|
||||
contract:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
rabbitmq:
|
||||
image: rabbitmq:3.13 # same AMQP 0-9-1 engine the local Testcontainers fixture uses
|
||||
env:
|
||||
RABBITMQ_DEFAULT_USER: guest
|
||||
RABBITMQ_DEFAULT_PASS: guest
|
||||
ports:
|
||||
- 5672:5672
|
||||
env:
|
||||
# Service containers are reachable from the job by their network alias on their internal port.
|
||||
AMQP_URI: amqp://guest:guest@rabbitmq:5672
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25'
|
||||
cache: maven
|
||||
|
||||
- name: Install Maven
|
||||
run: |
|
||||
apt-get update && apt-get install -y --no-install-recommends maven
|
||||
mvn -version
|
||||
|
||||
# The `contract` profile clears the default-excludes group, so the @Tag("contract") AMQP test
|
||||
# runs against the RabbitMQ service container (AMQP_URI). Pinned to the one contract test to
|
||||
# avoid re-running the unit suite already covered by the `build` job.
|
||||
- name: Contract tests
|
||||
working-directory: bridged
|
||||
run: mvn -B -Pcontract test -Dtest=AmqpReplyInboxContractTest
|
||||
|
||||
- name: Failing test output
|
||||
if: failure()
|
||||
working-directory: bridged
|
||||
run: |
|
||||
for f in target/surefire-reports/*.txt; do
|
||||
[ -f "$f" ] || continue
|
||||
grep -qE "Failures: [1-9]|Errors: [1-9]" "$f" && { echo "===== $f ====="; cat "$f"; }
|
||||
done
|
||||
exit 0
|
||||
|
||||
@@ -236,6 +236,26 @@
|
||||
<profile>
|
||||
<id>contract</id>
|
||||
<properties><excludedGroups/></properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<!-- Docker-engine compat (see "Running the contract tests" in
|
||||
docs/CB-307-Reliable-Delivery.md): Testcontainers 1.20.4's docker-java
|
||||
client defaults to Docker API 1.32 when no version is set, but modern
|
||||
engines (OrbStack on this dev host, min 1.40) reject that as too old —
|
||||
which surfaces as "Could not find a valid Docker environment". Pinning
|
||||
api.version=1.43 works on OrbStack and Docker 24+, and is overridable
|
||||
per-host via -Dapi.version. Only active under -Pcontract, so the
|
||||
default hermetic build never sets it. -->
|
||||
<systemPropertyVariables>
|
||||
<api.version>1.43</api.version>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.ltms.bridged.msg;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.RabbitMQContainer;
|
||||
@@ -19,19 +20,46 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
* excluded from {@code mvn test}/{@code mvn clean install} (which stay hermetic and need no Docker);
|
||||
* run it with Docker present via {@code mvn test -Pcontract}.
|
||||
*
|
||||
* <p>Two broker modes:
|
||||
* <ul>
|
||||
* <li><b>Locally</b> ({@code AMQP_URI} unset): Testcontainers spins a RabbitMQ container. Requires
|
||||
* a working Docker engine; see the "Running the contract tests" note in
|
||||
* {@code docs/CB-307-Reliable-Delivery.md} for the {@code api.version} engine-compat pin.</li>
|
||||
* <li><b>In CI</b> ({@code AMQP_URI} set): a RabbitMQ service container provisions the broker and
|
||||
* {@code AMQP_URI} points at it, so the contract job needs <em>no</em> Docker on the runner.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>It proves the port contract on genuine infrastructure: eventual visibility of a published reply,
|
||||
* ack removal, msgId dedup, and — the reason Stage 2 exists — cross-restart durability: an unacked
|
||||
* reply survives closing the inbox and is redelivered to a fresh connection.
|
||||
*/
|
||||
@Tag("contract")
|
||||
@Testcontainers
|
||||
// disabledWithoutDocker=false on purpose: on the CI path (AMQP_URI set) no container is started and
|
||||
// the class must still RUN against the external broker even though the runner has no Docker — a
|
||||
// disabled-without-docker check would silently skip the whole contract suite there.
|
||||
@Testcontainers(disabledWithoutDocker = false)
|
||||
class AmqpReplyInboxContractTest {
|
||||
|
||||
@Container
|
||||
static final RabbitMQContainer BROKER =
|
||||
// When a broker is provisioned out-of-band (CI service container), AMQP_URI takes us straight to
|
||||
// it and we never touch Testcontainers. Unset locally → Testcontainers starts the container below.
|
||||
private static final String EXTERNAL_URI = System.getenv("AMQP_URI");
|
||||
|
||||
private static final RabbitMQContainer BROKER =
|
||||
new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.13-management"));
|
||||
|
||||
// No @Container on BROKER: the JUnit 5 extension would force-start it even when AMQP_URI is set.
|
||||
// Start it manually only on the local (no-external-broker) path; Ryuk reaps it on JVM exit.
|
||||
@BeforeAll
|
||||
static void startBrokerUnlessExternal() {
|
||||
if (EXTERNAL_URI == null) {
|
||||
BROKER.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static String uri() {
|
||||
if (EXTERNAL_URI != null) {
|
||||
return EXTERNAL_URI;
|
||||
}
|
||||
// guest/guest against the mapped AMQP port. No trailing slash: an empty path is vhost "",
|
||||
// which does not exist — omitting it selects the default vhost "/".
|
||||
return "amqp://guest:guest@" + BROKER.getHost() + ":" + BROKER.getAmqpPort();
|
||||
|
||||
@@ -162,3 +162,40 @@ Stage-2 AMQP adapter: absent → in-memory, present → AMQP).
|
||||
Commit on your feature branch and reply with: the commit SHA, the surefire total (run/failures/errors), a
|
||||
one-line note on the drain-surface decision (§2.4) you shipped, and confirmation that `.mcp.json`/`wiki/`
|
||||
were untouched. The primary re-gates and integrates.
|
||||
|
||||
## 8. Running the contract tests (CB-521)
|
||||
|
||||
`AmqpReplyInboxContractTest` is the real-broker proof of the `ReplyInbox` port (eventual visibility, ack
|
||||
removal, msgId dedup, cross-restart redelivery). It is `@Tag("contract")`, so the default
|
||||
`mvn test` / `mvn clean install` **skip it** — that hermetic, Docker-free default is deliberate and
|
||||
untouched. Run it explicitly when Docker (or a broker) is available:
|
||||
|
||||
```bash
|
||||
cd bridged
|
||||
mvn -Pcontract test -Dtest=AmqpReplyInboxContractTest # local: spins a RabbitMQ Testcontainers fixture
|
||||
```
|
||||
|
||||
### Two broker modes
|
||||
|
||||
| Mode | Trigger | Broker | Needs Docker? |
|
||||
|---|---|---|---|
|
||||
| Local | `AMQP_URI` unset | Testcontainers starts `rabbitmq:3.13-management` | Yes |
|
||||
| CI / external | `AMQP_URI` set | the broker at that URI (CI RabbitMQ service container) | **No** — binds straight to the URI, never touches Testcontainers |
|
||||
|
||||
In CI the broker is provided as a RabbitMQ **service container** and `AMQP_URI` points at it, so the
|
||||
contract job runs the same assertions with no Docker on the runner and no skipped test
|
||||
(see `.gitea/workflows/ci.yml` → `contract`). The `build` job stays hermetic and Docker-free — keep
|
||||
that separation.
|
||||
|
||||
### Docker-engine discovery (why the contract profile pins `api.version`)
|
||||
|
||||
Out of the box, Testcontainers 1.20.4's docker-java client defaults to Docker API **1.32** when no
|
||||
version is requested. Modern engines reject that as too old — on this host's OrbStack (`min API 1.40`)
|
||||
testcontainers fails with *"Could not find a valid Docker environment … client version 1.32 is too
|
||||
old"* even though the `docker` CLI works (the CLI negotiates a newer API).
|
||||
|
||||
The `contract` Maven profile sets `api.version=1.43` in surefire, which works on OrbStack and Docker
|
||||
24+, and is overridable per host: `mvn -Pcontract -Dapi.version=1.54 test …`. It only applies under
|
||||
`-Pcontract`, so the default build is unaffected. If your engine differs, set `-Dapi.version` to a
|
||||
version ≥ your engine's minimum API (e.g. `docker version` shows `API version`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user