Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,79 @@ jobs:
name: lcov
path: lcov.info
retention-days: 30

# ---------------------------------------------------------------------------
# interop-harness — deterministic, vendor-free tests of the interop gate
# harness itself (result classifier, exact-PID cleanup, shell lint). Runs
# everywhere and blocks merges; no DDS stack required. See tests/interop/ci/.
# ---------------------------------------------------------------------------
interop-harness:
name: interop harness self-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install pytest
run: python -m pip install --user pytest
- name: Result classifier unit tests
run: python -m pytest tests/interop/ci/tests/test_interop_result.py -q
- name: Exact-PID cleanup contract test
run: bash tests/interop/ci/tests/test_cleanup.sh
- name: Shell lint the runner and libs
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends shellcheck
shellcheck -S warning tests/interop/ci/run_vendor.sh \
tests/interop/ci/lib.sh \
tests/interop/ci/install_cyclone.sh \
tests/interop/ci/install_fastdds.sh \
tests/interop/ci/tests/test_cleanup.sh

# ---------------------------------------------------------------------------
# interop — REQUIRED cross-vendor gate: typed `Robot` samples exchanged over
# live DDS/RTPS between ZeroDDS and each open-source vendor, BOTH directions.
# SPDP discovery alone is never a pass. Separate matrix cell per vendor with
# fail-fast: false so one vendor's logs survive the other's failure. Vendor
# versions are pinned (see the install_* scripts). ubuntu-24.04 is pinned so
# the apt-provided CycloneDDS version is deterministic.
# ---------------------------------------------------------------------------
interop:
name: interop (${{ matrix.vendor }})
needs: [fmt, clippy, interop-harness]
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
vendor: [cyclone, fastdds]
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# Cache the built vendor SDK prefix, keyed on the pinned versions in the
# install scripts. The first run builds from source (bounded parallelism
# to fit the runner); later runs restore and skip the build.
- name: Cache vendor SDK (${{ matrix.vendor }})
uses: actions/cache@v4
with:
path: |
~/cyclonedds-install
~/fastdds-install
key: interop-${{ matrix.vendor }}-${{ runner.os }}-${{ hashFiles(format('tests/interop/ci/install_{0}.sh', matrix.vendor)) }}
- name: Install vendor (${{ matrix.vendor }})
run: bash tests/interop/ci/install_${{ matrix.vendor }}.sh
- name: Run interop gate (${{ matrix.vendor }})
run: |
mkdir -p interop-artifacts
bash tests/interop/ci/run_vendor.sh ${{ matrix.vendor }} interop-artifacts
- name: Upload interop artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: interop-${{ matrix.vendor }}
path: interop-artifacts
retention-days: 30
11 changes: 9 additions & 2 deletions interop/cyclone-xtypes-27/reader/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ fn main() {
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(12);
// Domain is configurable (ZERODDS_DOMAIN) so the #28 interop CI gate can
// give each vendor/direction cell a unique domain and avoid cross-job
// multicast contamination. Defaults to 100 (the #27/#29 matrix domain).
let domain: i32 = std::env::var("ZERODDS_DOMAIN")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(100);
let f = DomainParticipantFactory::instance();
let p = f
.create_participant(100, DomainParticipantQos::default())
.create_participant(domain, DomainParticipantQos::default())
.unwrap();
let t = p
.create_topic::<Robot>("robot", TopicQos::default())
Expand All @@ -34,7 +41,7 @@ fn main() {
let r = s
.create_datareader::<Robot>(&t, DataReaderQos::default())
.expect("reader");
eprintln!("[zerodds reader] domain=100 topic=robot window={secs}s");
eprintln!("[zerodds reader] domain={domain} topic=robot window={secs}s");

let start = std::time::Instant::now();
let mut matched = 0usize;
Expand Down
9 changes: 7 additions & 2 deletions interop/cyclone-xtypes-27/reader/src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@ fn main() {
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(20);
// Configurable domain (ZERODDS_DOMAIN), default 100 — see reader main.rs.
let domain: i32 = std::env::var("ZERODDS_DOMAIN")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(100);
let f = DomainParticipantFactory::instance();
let p = f
.create_participant(100, DomainParticipantQos::default())
.create_participant(domain, DomainParticipantQos::default())
.unwrap();
let t = p
.create_topic::<Robot>("robot", TopicQos::default())
Expand All @@ -26,7 +31,7 @@ fn main() {
let w = pubr
.create_datawriter::<Robot>(&t, DataWriterQos::default())
.expect("writer");
eprintln!("[zerodds writer] domain=100 topic=robot window={secs}s");
eprintln!("[zerodds writer] domain={domain} topic=robot window={secs}s");
let start = std::time::Instant::now();
let mut c: u32 = 0;
while start.elapsed().as_secs() < secs {
Expand Down
3 changes: 2 additions & 1 deletion interop/cyclone-xtypes-27/writers/cyclone_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Counts successfully received samples on topic `robot`, domain 100, and prints
`CYCLONE_RESULT samples=<n>`.
"""
import os
import sys
import time
from dataclasses import dataclass
Expand All @@ -33,7 +34,7 @@ class RobotType(IdlStruct, typename="Robot"):
id: uint32 = 0
label: uint32 = 0

dp = DomainParticipant(domain_id=100)
dp = DomainParticipant(domain_id=int(os.environ.get("ZERODDS_DOMAIN", "100")))
tp = Topic(dp, "robot", RobotType)
r = DataReader(Subscriber(dp), tp)
print(f"[cyclone reader] ext={ext}", flush=True)
Expand Down
3 changes: 2 additions & 1 deletion interop/cyclone-xtypes-27/writers/cyclone_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
generator default); representation is set via the DataRepresentation QoS.
Writes topic `robot` on domain 100.
"""
import os
import sys
import time
from dataclasses import dataclass
Expand Down Expand Up @@ -39,7 +40,7 @@ class RobotType(IdlStruct, typename="Robot"):
use_cdrv0_representation=(rep == "xcdr1"),
use_xcdrv2_representation=(rep == "xcdr2"),
))
dp = DomainParticipant(domain_id=100)
dp = DomainParticipant(domain_id=int(os.environ.get("ZERODDS_DOMAIN", "100")))
tp = Topic(dp, "robot", RobotType)
w = DataWriter(Publisher(dp), tp, qos=qos)
print(f"[cyclone writer] ext={ext} rep={rep}", flush=True)
Expand Down
88 changes: 88 additions & 0 deletions tests/interop/ci/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Cross-vendor interop CI gate (issue #28)

A small, deterministic, **required** public-CI gate that exchanges typed
`Robot` samples over live DDS/RTPS between ZeroDDS and the two widely used
open-source DDS implementations — Eclipse **CycloneDDS** and eProsima
**Fast DDS** — in **both directions**. Endpoint matching *and* actual decoded
sample delivery are asserted; SPDP participant discovery alone is never a pass.

## Required matrix

| Vendor | Direction | Assertion |
|---|---|---|
| CycloneDDS | vendor writer → ZeroDDS reader | matched; samples > 0; decode errors = 0 |
| CycloneDDS | ZeroDDS writer → vendor reader | matched; samples > 0 |
| Fast DDS | vendor writer → ZeroDDS reader | matched; samples > 0; decode errors = 0 |
| Fast DDS | ZeroDDS writer → vendor reader | matched; samples > 0 |

The CycloneDDS cells also retain the issue #29 XTypes regression semantics: a
compatible final/XCDR1 case, a compatible appendable/XCDR2 case, the
final-vs-appendable XCDR2 **negative** case (which must surface a *visible*
decode error, not a silent timeout), and the `--cyclone` compatible-generation
case.

## Pieces

| File | Role |
|---|---|
| `interop_result.py` | Pure result classifier: counts → `PASS` / `EXPECTED_NEGATIVE` / `PRODUCT_FAIL` / `TIMEOUT` / `SETUP_FAIL`. |
| `run_vendor.sh` | Vendor-agnostic runner: orchestrates every cell, unique domain per cell, exact-PID cleanup, bounded timeouts, machine-readable `<vendor>-result.json` + human summary. |
| `lib.sh` | Shared exact-PID process tracking/cleanup (unit tested). |
| `install_cyclone.sh` / `install_fastdds.sh` | Pinned vendor installation for the hosted Ubuntu runner. |
| `fastdds/` | Fast DDS `Robot` pub/sub client (`fastdds_robot`), type generated by Fast-DDS-Gen from `fastdds/robot.idl`. |
| `tests/` | Harness self-tests (classifier, cleanup) runnable on any host. |

The ZeroDDS reader/writer and the CycloneDDS Python client are reused from
`interop/cyclone-xtypes-27/` (issue #29), parameterised by the `ZERODDS_DOMAIN`
environment variable so each cell gets an isolated domain.

## One-command local reproduction (Linux)

Build the ZeroDDS `Robot` reader/writer once (the runner rebuilds per
extensibility as needed):

```bash
( cd interop/cyclone-xtypes-27/reader && cargo build )
```

### CycloneDDS

```bash
# Cyclone C library + Python binding (pinned):
tests/interop/ci/install_cyclone.sh
# Run both directions + the #29 XTypes cases:
tests/interop/ci/run_vendor.sh cyclone /tmp/interop-cyclone
```

### Fast DDS

```bash
# Fast DDS + Fast-DDS-Gen (pinned) and build the client:
tests/interop/ci/install_fastdds.sh
# Run both directions:
tests/interop/ci/run_vendor.sh fastdds /tmp/interop-fastdds
```

Each run writes `<vendor>-result.json` and per-cell logs to the output
directory and exits non-zero unless every cell is green (`PASS` or
`EXPECTED_NEGATIVE`).

## Harness self-tests (no DDS stack required)

```bash
python -m pytest tests/interop/ci/tests/test_interop_result.py -q
bash tests/interop/ci/tests/test_cleanup.sh
```

## Pinned versions

Vendor versions are pinned in the `install_*` scripts (overridable via the
`*_VERSION` environment variables) and must never float to `latest` in the
required job. CycloneDDS is pinned by the `ubuntu-24.04` runner image's apt
archive plus a matching `cyclonedds` Python release; Fast DDS and Fast-DDS-Gen
are pinned to official versioned GitHub releases.

## Non-goals

RTI Connext (licensed), the full pairwise vendor×vendor matrix, DDS Security,
SHM, TCP, TSN, WAN, soak and latency benchmarking. See issue #28.
50 changes: 50 additions & 0 deletions tests/interop/ci/fastdds/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 ZeroDDS Contributors
#
# Builds `fastdds_robot`, the Fast DDS interop client for the #28 CI gate.
# The Robot type support is generated from robot.idl by Fast-DDS-Gen at
# configure time, then compiled against the installed Fast DDS SDK.
#
# Requires: an installed Fast DDS (find_package(fastdds)) + fastcdr, and the
# `fastddsgen` code generator on PATH (or passed via -DFASTDDSGEN=/path).
cmake_minimum_required(VERSION 3.16)
project(fastdds_robot CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(fastcdr REQUIRED)
find_package(fastdds REQUIRED)

# ---- code generation (configure time, so the sources exist for the build) --
find_program(FASTDDSGEN NAMES fastddsgen fast-dds-gen)
if(NOT FASTDDSGEN)
message(FATAL_ERROR "fastddsgen not found; install Fast-DDS-Gen or pass -DFASTDDSGEN=/path/to/fastddsgen")
endif()

set(GEN_DIR "${CMAKE_CURRENT_BINARY_DIR}/gen")
file(MAKE_DIRECTORY "${GEN_DIR}")
execute_process(
COMMAND "${FASTDDSGEN}" -replace -d "${GEN_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/robot.idl"
RESULT_VARIABLE GEN_RC
OUTPUT_VARIABLE GEN_OUT
ERROR_VARIABLE GEN_ERR)
if(NOT GEN_RC EQUAL 0)
message(FATAL_ERROR "fastddsgen failed (${GEN_RC}):\n${GEN_OUT}\n${GEN_ERR}")
endif()

file(GLOB GEN_SRCS "${GEN_DIR}/*.cxx")
if(GEN_SRCS STREQUAL "")
message(FATAL_ERROR "fastddsgen produced no .cxx sources in ${GEN_DIR}")
endif()

# ---- executable ------------------------------------------------------------
add_executable(fastdds_robot fastdds_robot.cpp ${GEN_SRCS})
target_include_directories(fastdds_robot PRIVATE "${GEN_DIR}")
target_link_libraries(fastdds_robot PRIVATE fastdds fastcdr)

# Surface the Fast DDS version to the binary's `version` command if the SDK
# exports it (best-effort; harmless when absent).
if(DEFINED fastdds_VERSION)
target_compile_definitions(fastdds_robot PRIVATE FASTDDS_VERSION_STR="${fastdds_VERSION}")
endif()
Loading