Skip to content
Merged
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
23 changes: 14 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ callback into application code on the back of that VM being available.
- **Bodies live once.** `commonTest`'s `Common*Tests` classes hold the test bodies;
`nativeTest`, `jvmTest` and `androidHostTest` are thin classes that construct the runner and
delegate one `@Test` per method. Every facade is held to identical behaviour, so a body added to
a runner must be added to all **three** delegators — 216 shared bodies run in each. The Android ones
a runner must be added to all **three** delegators — 227 shared bodies run in each. The Android ones
additionally carry `@RunWith(RobolectricTestRunner::class)` and `@Config(sdk = [34])`. Note what
that does *not* buy: the shared bodies touch no Android API, so a delegator that omits the runner
still passes — measured, not assumed. It is there so the class is an Android unit test rather
Expand All @@ -209,13 +209,13 @@ callback into application code on the back of that VM being available.
one) would need. Keep it on new delegators; nothing will fail loudly if you don't.
(`BinaryReaderTests` is the one exception on the shared side: pure-Kotlin decoder tests with no
facade, so they carry `@Test` directly in `commonTest` and are picked up by every compilation.
The 216 is 210 delegated bodies plus its 6.)
- **The counts are not symmetric, and the asymmetry is deliberate.** 216 shared bodies per facade,
so 432 across the two tested facades (`jvmTest`, `macosArm64Test`), and `androidHostTest` runs
those 216 **plus 6 Android-only tests** — `AndroidMulticastLockTests`, the one class in that
The 227 is 221 delegated bodies plus its 6.)
- **The counts are not symmetric, and the asymmetry is deliberate.** 227 shared bodies per facade,
so 454 across the two tested facades (`jvmTest`, `macosArm64Test`), and `androidHostTest` runs
those 227 **plus 6 Android-only tests** — `AndroidMulticastLockTests`, the one class in that
source set that is not a delegator, because `Iroh4kAndroid.multicastLock` is `androidMain` code
over `WifiManager` and there is no other facade to hold to the same behaviour. 222 on Android,
654 host tests in all. Something that exists only on Android belongs in a class of its own there,
over `WifiManager` and there is no other facade to hold to the same behaviour. 233 on Android,
687 host tests in all. Something that exists only on Android belongs in a class of its own there,
with a KDoc saying why it is not a delegator; do not invent a `Common*Tests` body that only one
facade can run, and do not "restore symmetry" by deleting the class.
- **`androidDeviceTest` is deliberately not a fourth delegator.** It runs on a device or emulator
Expand All @@ -229,7 +229,7 @@ callback into application code on the back of that VM being available.
shared bodies at all — Kotlin turns a suspend lambda inside ``fun `a name with spaces`()`` into a
class whose name contains spaces, which DEX rejects below version 040, i.e. below `minSdk 35`.
That is why its methods are named without backticks and why the compilation is left out of the
`test` source-set tree. Everything these tests found was invisible to all 654 host tests: a
`test` source-set tree. Everything these tests found was invisible to all 687 host tests: a
process-aborting missing init, and a missing `INTERNET` permission.
- **Robolectric gives each test class its own sandbox classloader**, so under `androidHostTest`
every class loads its *own copy* of the host `libiroh4k.so` — the loader in `androidMain`
Expand Down Expand Up @@ -314,7 +314,12 @@ Other build facts worth knowing:
`cargo-<triple>`, `cinteropFfi<Target>` and `compileKotlin<Target>`. Verify with `--dry-run` if
in doubt. `compileKotlin<Target>` on its own is a narrower gate when you only want the compile.
- `core.rs`'s `IROH_VERSION` is kept in sync with `Cargo.toml` **by hand** — there is no build-time
way to read a dependency's version. Bumping iroh means editing both.
way to read a dependency's version. Bumping iroh means editing both. It also means re-reading the
guard thresholds in upstream's `endpoint/quic.rs`: `TransportConfig.kt`'s KDoc hardcodes four of
them — values below 9 ignored for `maxConcurrentMultipathPaths`, `defaultPathMaxIdleTimeout`
clamped to 15 seconds, `defaultPathKeepAliveInterval` above 5 seconds ignored, and values below 8
ignored for `maxRemoteNatTraversalAddresses` — read out of private code with no test pinning them,
so a version bump can silently falsify all four without anything here noticing.
- A new `extern "C"` export reaches Kotlin through cbindgen (`build.rs` regenerates
`target/iroh4k.h`); cbindgen only emits a type that appears in an exported signature, which is
what the `auto_generated_for_struct_*` no-ops in `ffi.rs` are for.
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ strategy.

iroh4k is that different strategy: one Rust crate built twice — a `staticlib` linked into
Kotlin/Native through cinterop, and a `cdylib` reached from the JVM and Android through hand-written
JNI. Both facades call the same core through the same codec, and the same 216 shared test bodies run
JNI. Both facades call the same core through the same codec, and the same 227 shared test bodies run
against each.

Writing the binding by hand rather than generating it also changed three things. They are trade-offs,
Expand Down Expand Up @@ -289,7 +289,7 @@ as is. Without `android` in `-Ptargets`, the Android Gradle plugin is never appl
Version `0.1.0`, the first release, targeting **iroh 1.0.3**. Dual licensed Apache-2.0 or MIT.

The transport itself — endpoints, connections, streams, datagrams, the router — is covered by the
test suite on every change. Four things around it are in different states, and the difference is
test suite on every change. Five things around it are in different states, and the difference is
worth seeing rather than lumping them together. [`STATUS.md`](STATUS.md) has the evidence for each.

**Verified by hand, not in CI.** mDNS discovery works: measured across two hosts on different Wi-Fi
Expand All @@ -302,8 +302,17 @@ the point of binding, but no pkarr relay and no DNS zone was ever stood up. Like
domain is tested only against its failure path; the successful round trips to services.iroh.computer
are not covered.

**Not implemented.** No 0-RTT and no per-connection transport configuration. Both are upstream
features that have a place waiting for them.
**Every tag round-trips; rarely shown to reach the wire.** Per-connection transport configuration —
an endpoint's default, one outgoing connection's, one incoming connection's — covers all twenty-nine
of iroh's QUIC transport knobs. The suite proves the value types, the encoding, that every one of the
twenty-nine top-level fields and both nested records decodes back correctly when all of them are set
at once, and that a bind, a connect and an accept each succeed with a configuration set. For
twenty-eight of the twenty-nine, that round trip into iroh's own config object is all that is shown —
confirming more needs traffic analysis the suite does not do. The exception is
`datagramReceiveBufferSize`, whose effect the other end can observe directly.

**Not implemented.** No 0-RTT. `Endpoint.startConnect` maps onto iroh's `connect_with_opts`, and
0-RTT is upstream's option there, waiting for a place in it.

**Known and pinned.** An IPv6 zone id does not survive an `EndpointTicket`, because upstream's
encoding has nowhere to put one. A test holds that behaviour in place so it cannot regress quietly.
Expand Down
43 changes: 35 additions & 8 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Android — are held to identical behaviour by the same shared test bodies.

| Target | What is actually run |
| --- | --- |
| `macosArm64`, `jvm` | 432 test bodies, 216 per facade, on every change |
| `android` (AAR) | The same 216 shared bodies under Robolectric, plus 6 Android-only tests for `Iroh4kAndroid.multicastLock`, which exists on no other target — 222 in all. Plus 6 instrumented tests, the only ones that exercise the packaged `.so` |
| `macosArm64`, `jvm` | 454 test bodies, 227 per facade, on every change |
| `android` (AAR) | The same 227 shared bodies under Robolectric, plus 6 Android-only tests for `Iroh4kAndroid.multicastLock`, which exists on no other target — 233 in all. Plus 6 instrumented tests, the only ones that exercise the packaged `.so` |
| `iosArm64`, `iosSimulatorArm64` | Compiles and links; cinterop verified in CI |
| `linuxX64` | Test suite configured in CI on `ubuntu-latest`; not verified locally |
| `linuxArm64`, `mingwX64` | Cross-compiled and assembled in CI; never executed |
Expand Down Expand Up @@ -85,12 +85,39 @@ postcard encoding of a `SocketAddr` has nowhere to put one. So an address that g
`EndpointTicket` comes back unzoned. This is upstream's encoding, pinned by a test rather than
hidden — do not rely on a zone surviving a round trip.

### No 0-RTT, and no per-connection transport configuration

`Endpoint.startConnect` maps onto iroh's `connect_with_opts` with default options; when 0-RTT and a
per-accepted-connection `ServerConfig` arrive they belong there. `Endpoint.watchNetworkChange()` is
also derived from address changes rather than from iroh's own net-report watcher, which is unstable
upstream — read its documentation before relying on it.
### No 0-RTT

`Endpoint.startConnect` maps onto iroh's `connect_with_opts`; 0-RTT is upstream's option there and
this binding does not expose it yet. `Endpoint.watchNetworkChange()` is also derived from address
changes rather than from iroh's own net-report watcher, which is unstable upstream — read its
documentation before relying on it.

### Per-connection transport configuration: every tag round-trips, rarely shown to reach the wire

`EndpointConfig.transportConfig`, `Endpoint.startConnect`'s `transportConfig` parameter and
`Incoming.acceptWith`'s `transportConfig` parameter cover all twenty-nine of iroh's QUIC transport
knobs as one sparse tagged payload shared by `TransportConfig.kt` and `transport.rs`. The suite covers
the value types — every field absent by default, equality and `hashCode` sensitive to what was
actually set, the ordinals for `CongestionController` pinned against the Rust wire contract — the
encoding for every kind the codec carries (a duration, a boolean, a float, an ordinal, both nested
records), and that an endpoint binds, a connection is made with `startConnect`'s `transportConfig`,
and one is accepted with `acceptWith`'s, each with a configuration set. One body,
`every transport configuration tag round-trips through the codec` in `CommonEndpointTests`, sets all
twenty-nine top-level fields and every field of both nested records at once and binds successfully —
the only way that bind can succeed is if all thirty-six tag numbers involved agree between
`TransportConfig.kt` and `transport.rs` and every reader consumes exactly what its writer produced, so
that body is what actually establishes that every tag round-trips, not only the handful the other
bodies happen to set.

What round-tripping through the codec does not show, for twenty-eight of the twenty-nine knobs, is
that the value changed anything beyond landing in iroh's own config object. Confirming more than that
generally needs traffic analysis this suite does not do. The one exception is
`datagramReceiveBufferSize`: setting it to zero makes the *other end's* `maxDatagramSize()` report `0`
instead of iroh's default — transport parameters travel inside the QUIC handshake, so the peer
observing a different value is evidence the setting reached the wire, not just the local config
object. That is demonstrated in both directions — the dialling side setting it in `startConnect`, the
accepting side setting it in `acceptWith` — because it is the one setting a hermetic loopback test can
observe without capturing packets.

### No logger of its own

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,20 @@ class AndroidConnectionTests {
@Test
fun `incoming and local addresses are values`() =
runner.`incoming and local addresses are values`()

@Test
fun `a connection made with datagrams refused reports no datagram size`() =
runner.`a connection made with datagrams refused reports no datagram size`()

@Test
fun `an incoming connection can be accepted with its own transport configuration`() =
runner.`an incoming connection can be accepted with its own transport configuration`()

@Test
fun `acceptWith with no overlapping ALPN fails loudly rather than silently`() =
runner.`acceptWith with no overlapping ALPN fails loudly rather than silently`()

@Test
fun `acceptWith refuses an empty ALPN list without touching the incoming connection`() =
runner.`acceptWith refuses an empty ALPN list without touching the incoming connection`()
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,32 @@ class AndroidEndpointTests {
@Test
fun `PublishedAddrs ordinals match the Rust wire contract`() =
runner.`PublishedAddrs ordinals match the Rust wire contract`()

@Test
fun `transport config is a value and says nothing by default`() =
runner.`transport config is a value and says nothing by default`()

@Test
fun `CongestionController ordinals match the Rust wire contract`() =
runner.`CongestionController ordinals match the Rust wire contract`()

@Test
fun `overriddenBy merges with the override winning and nested records replaced wholesale`() =
runner.`overriddenBy merges with the override winning and nested records replaced wholesale`()

@Test
fun `an endpoint binds with a transport configuration`() =
runner.`an endpoint binds with a transport configuration`()

@Test
fun `a transport value upstream ignores is passed through rather than refused`() =
runner.`a transport value upstream ignores is passed through rather than refused`()

@Test
fun `the largest transport duration a caller can express is accepted and a negative one is refused`() =
runner.`the largest transport duration a caller can express is accepted and a negative one is refused`()

@Test
fun `every transport configuration tag round-trips through the codec`() =
runner.`every transport configuration tag round-trips through the codec`()
}
Loading