This directory holds tests that validate the C++ port's behaviour against the Python Reticulum reference implementation. Two flavours, in increasing scope:
Four C++ sender executables, one per interop scenario. Each is a CMake target of the same name and is configured at compile time with its own UDP port pair so the scenarios can coexist on a single host:
| CMake target | UDP local | UDP remote | Scenario |
|---|---|---|---|
packet_interop_sender |
14253 | 14252 | DATA packets to a Destination, bidirectional |
link_interop_sender |
14255 | 14254 | Link establish + two-pass teardown |
request_interop_sender |
14257 | 14256 | Link::request("/echo", ...) single-packet RPC |
resource_interop_sender |
14243 | 14242 | Full Resource transfer over a Link (1024-byte payload) |
The four C++ interop senders are built by the top-level CMake project. Configure and build from the repository root:
Configure:
cmake -S . -B build
Build all interop senders:
cmake --build build -j --target packet_interop_sender link_interop_sender request_interop_sender resource_interop_sender
Build a single sender:
cmake --build build -j --target packet_interop_sender
After building, point the existing driver scripts at the binaries in
build/test_interop/ (the scripts default to the PlatformIO output path —
adjust PIO_BUILD_DIR or the binary path inside the script as needed):
bash test_interop/run_all.sh
bash test_interop/run_packet_exchange.sh
Skip building interop senders entirely when configuring the parent project:
cmake -S . -B build -DRNS_BUILD_INTEROP=OFF
test_interop/run_all.sh builds each C++ test app and runs all four
interop scenarios sequentially, reporting per-test PASS/FAIL and an
aggregated exit status. Run from the repo root:
bash test_interop/run_all.sh
Each scenario also has its own driver script if you only want to run one:
| Test | Driver script | What it covers |
|---|---|---|
| resource | test_interop/run_resource_transfer.sh |
Full Resource transfer over a Link (1024-byte payload) |
| packet | test_interop/run_packet_exchange.sh |
DATA packets to a Destination, bidirectional, 256-byte patterns |
| link | test_interop/run_link_lifecycle.sh |
Link establish + two-pass teardown (initiator and destination) |
| request | test_interop/run_request_response.sh |
Link::request("/echo", ...) single-packet RPC (200-byte payload) |
All four currently PASS against Python RNS 1.2.9 at /Users/chad/python/Reticulum-latest.
The C++ unit test test/test_resource_advertisement includes a
PYTHON_GOLDEN_HEX byte sequence captured from Python 1.2.9
RNS.Resource.ResourceAdvertisement.pack() for a known set of input fields.
The test:
- Unpacks Python's bytes via C++
ResourceAdvertisement::unpackand asserts every field round-trips correctly. - Packs the same input fields via C++
ResourceAdvertisement::pack, unpacks via C++ again, and compares byte-for-byte against the Python golden. If the bytes match exactly we have byte-equal wire interop; if they only match by field-equivalence we have spec-compliant but not byte-equal interop (msgpack permits multiple valid integer encodings of the same value).
encode_advertisement.py regenerates the golden bytes from Python so the
constants in the C++ test can be updated whenever the Python reference's
wire format changes.
A driver that:
- Spawns
python_resource_receiver.pyon UDP port 14242 (avoids the common RNS desktop-app conflict on 4242). - Spawns the C++ sender binary built from
test_interop/resource_interop_sender/on UDP port 14243, configured to send to 127.0.0.1:14242. - Watches both stdouts and exits 0 iff both report SUCCESS.
Wire path on each side:
- Python: listens on
127.0.0.1:14242, sends to127.0.0.1:14243. - C++: listens on
127.0.0.1:14243, sends to127.0.0.1:14242.
Point-to-point loopback is used instead of 255.255.255.255 broadcast
because macOS doesn't reliably loop limited-broadcast traffic between two
processes on the same host. The example's platformio.ini sets
DEFAULT_UDP_LOCAL_HOST, DEFAULT_UDP_REMOTE_HOST,
DEFAULT_UDP_LOCAL_PORT and DEFAULT_UDP_REMOTE_PORT build flags;
examples/common/udp_interface/UDPInterface.h has been extended with
matching #ifndef guards so these are overridable per-example without
touching the library code.
Architectural caveats:
- microReticulum uses a global static
RNS::Transport. A two-instance C++ loopback inside the same process is not possible — this is why the test is structured as two OS processes communicating over UDP.
# Build the C++ sender once
cd test_interop/resource_interop_sender
pio run -e native17
cd ../..
# Run the interop driver
bash test_interop/run_resource_transfer.sh
Expected last line: [driver] PASS with both processes exiting 0.
The interop test exposed several pre-existing bugs as well as ones in the new Resource implementation:
Link::ready_for_new_resource()— boolean returned the inverse of what it should (true when an outgoing resource already existed, blocking new ones). Fixed insrc/Link.cpp.Link::link_closed()— iterated_incoming_resources/_outgoing_resourceswhileResource::cancel()erases from those same sets, invalidating the iterator and crashing. Now snapshots first.Link::receive()PROOF / RESOURCE_PRF dispatch — same iterator invalidation pattern whenvalidate_proof()callsresource_concluded()which erases. Now snapshots first.Resourcehash & expected-proof input order — Python computesfull_hash(data || random_hash)andfull_hash(data || hash); the C++ port was computingfull_hash(random_hash || data || ...). Receiver computed the right thing locally but the advertised hash didn't match, failing the integrity check. Fixed insrc/Resource.cpp.UDPInterface.hmacros —DEFAULT_UDP_PORTetc. lacked#ifndefguards, so build-flag overrides were silently ignored.
Additional pre-existing bugs uncovered by the broader interop suite
(test_interop/run_all.sh):
RequestReceiptData::_linkdeclared as bareLink _link;instead ofLink _link = {Type::NONE};. The default Link constructor with no destination and no owner generates_prvbut leaves_sig_prvnull, then dereferences_sig_prv->public_key()→ SIGSEGV. Every call toLink::request()constructs aRequestReceiptcontaining aRequestReceiptData, so this crashed the RPC path entirely. Fixed insrc/LinkData.h.- Embedded
Linkmembers inRequestReceiptDatarequired by the fix above are now self-consistent across all four interop scenarios.