Add cross-platform mDNS integration test (Docker Compose + Python) - #19
ZayanKhan-12 wants to merge 4 commits into
Conversation
Re-implements the integration test that was dropped in youtube#6, using Docker Compose for orchestration and Python for the runner. Docker, because mDNS discovery is unreliable on a GitHub Actions runner's host network. Putting both agents on a user-defined bridge network gives them a Linux bridge of their own, where multicast behaves predictably. Python and Compose, because the two together behave identically on Linux, macOS and Windows. The runner imports only the standard library, so there is no pip install step on a fresh checkout. The receiver name is generated per run (openscreen-ci-<random>) and the test asserts that name appears both in the sender's discovery output and on the Display Name line of the agent-info response. Checking the name rather than just the exit status is what distinguishes real mDNS discovery from a direct-connection fallback or a stale receiver left by a concurrent run -- app-sender selects services[0], so "it exited 0" alone is not evidence that this run's receiver was the one found. Details worth noting: * Both agents use mDNS port 5454 rather than 5353, so the test never contends with the host's own responder (Bonjour, Avahi). * The receiver has a healthcheck and the sender waits for service_healthy. depends_on alone waits only for the container to start, not for the service to be advertised. app-receiver publishes its mDNS service before binding the QUIC socket, so a listening UDP socket on the app port implies both are ready. * `up --exit-code-from sender` implies --abort-on-container-exit, so the run ends when the sender finishes and its status becomes the test's status. The receiver would otherwise run forever. * Both containers set NO_COLOR, since the binaries colourise through the `colored` crate and the test asserts on those strings. The assertion logic is a pure function so that --self-test can exercise it, including the negative cases, without Docker. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two jobs:
* integration-test runs the full Compose stack on ubuntu-latest. No
docker-in-docker setup is needed -- the GitHub-hosted Ubuntu runner
already exposes a working Docker daemon, and the containers reach
each other over their own bridge network rather than the runner's
host network, which is what makes mDNS work here.
* runner-self-test runs `run.py --self-test` on Linux, macOS and
Windows.
The end-to-end job is Linux-only by necessity: GitHub's hosted macOS
runners ship no Docker daemon, and its Windows runners can run Windows
containers but not the Linux containers this stack builds. Rather than
claim cross-platform support and test it on one platform, the self-test
job exercises the runner's own portability -- argument handling, path
handling, log assertions -- on all three.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records what is not obvious from reading the tree: which crates exist to keep tests off the real network, that app-receiver publishes mDNS before binding QUIC (the integration test's healthcheck depends on that ordering), that the binaries' println markers are asserted on by the test, and that Cargo.lock is gitignored so builds track Rust stable rather than a pinned toolchain. Also the debugging notes that cost time to rediscover: the sender sleeps rather than polls for discovery, and it selects services[0]. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The integration test runner is the first Python in this repository, so __pycache__/ was not ignored yet. Also drops a .pyc that was committed by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
Merges the branch proposed upstream as youtube#19. Integration test, Build & Test, and runner self-test on Linux/macOS/Windows all green.
There was a problem hiding this comment.
Code Review
This pull request introduces a cross-platform integration test suite for the Open Screen Protocol implementation, including a Dockerfile, a Docker Compose configuration, a Python test runner, and comprehensive documentation. The review feedback points out a compatibility issue in the Python runner where the --no-log-prefix flag is used with docker compose logs, which might fail on older Docker Compose versions, and suggests programmatically stripping the prefix instead.
| def service_logs(compose: list[str], env: dict[str, str], service: str) -> str: | ||
| """Return one service's logs with Compose's ``service |`` prefix stripped.""" | ||
| result = subprocess.run( | ||
| [*compose, "logs", "--no-log-prefix", service], | ||
| env=env, | ||
| cwd=HERE, | ||
| capture_output=True, | ||
| text=True, | ||
| errors="replace", | ||
| check=False, | ||
| ) | ||
| return result.stdout + result.stderr |
There was a problem hiding this comment.
The --no-log-prefix option was introduced in Docker Compose v2.20.0. Using it directly will cause the integration test to fail on environments running older versions of Docker Compose v2, or those using Docker Compose v1 (docker-compose), which do not support this flag.
To maximize compatibility across different environments and platforms, we can retrieve the logs without this flag and strip the service | prefix programmatically in Python.
| def service_logs(compose: list[str], env: dict[str, str], service: str) -> str: | |
| """Return one service's logs with Compose's ``service |`` prefix stripped.""" | |
| result = subprocess.run( | |
| [*compose, "logs", "--no-log-prefix", service], | |
| env=env, | |
| cwd=HERE, | |
| capture_output=True, | |
| text=True, | |
| errors="replace", | |
| check=False, | |
| ) | |
| return result.stdout + result.stderr | |
| def service_logs(compose: list[str], env: dict[str, str], service: str) -> str: | |
| """Return one service's logs with Compose's ``service |`` prefix stripped.""" | |
| result = subprocess.run( | |
| [*compose, "logs", service], | |
| env=env, | |
| cwd=HERE, | |
| capture_output=True, | |
| text=True, | |
| errors="replace", | |
| check=False, | |
| ) | |
| raw_logs = result.stdout + result.stderr | |
| stripped_lines = [] | |
| for line in raw_logs.splitlines(): | |
| if " | " in line: | |
| stripped_lines.append(line.split(" | ", 1)[1]) | |
| else: | |
| stripped_lines.append(line) | |
| return "\n".join(stripped_lines) |
Summary
Re-implements the integration test dropped in #6, as a Docker Compose stack driven by a Python runner.
Closes #7.
The two reasons the old
integration-test.shwas removed are addressed directly:pip installstep on a fresh Windows or macOS checkout.It works. From the CI run on this branch:
Found 1 receiver(s)is worth noting: the bridge network plus the non-standard mDNS port means the test sees exactly its own receiver and nothing else.What it asserts, and why not just the exit code
app-senderselectsservices[0], so "the sender exited 0" is not by itself evidence that this run's receiver was the one discovered — a stale container or a concurrent run could satisfy it.So the receiver name is generated fresh per run (
openscreen-ci-<random>) and the test requires that name to appear:Display Name:line of the agent-info response — only reachable after QUIC, TLS, SPAKE2 and the application exchange all succeed.Plus the sender exiting 0 and the receiver reporting an authenticated client. Both container logs are printed on failure before teardown.
Design notes
mDNS port 5454, not 5353. So the test never contends with the host's own responder — macOS always has Bonjour, and many Linux desktops run Avahi.
A healthcheck, not a sleep.
depends_onalone waits for the receiver's container to start, not for it to be advertising, which is exactly the kind of race that makes an integration test flaky.app-receiverpublishes its mDNS service before binding the QUIC socket, so a listening UDP socket on the app port means both are ready; the sender waits onservice_healthy. (This ordering is now written down inCLAUDE.md, since reordering those two steps would silently break the healthcheck.)up --exit-code-from senderimplies--abort-on-container-exit, so the run ends when the sender finishes and its status becomes the test's status — the receiver would otherwise run forever.NO_COLOR=1in both containers, since the binaries colourise through thecoloredcrate and the test asserts on those strings.No docker-in-docker. The issue floated it, but GitHub's hosted Ubuntu runner already exposes a working Docker daemon. What was actually needed was for the containers to talk over their own bridge rather than the runner's host network. Dropping DinD keeps the workflow considerably simpler.
Rust stable, not a pinned version, in the Dockerfile —
Cargo.lockis.gitignored here, so dependencies resolve fresh on every build and a pinned older toolchain would drift out of date. This matchesdtolnay/rust-toolchain@stablein the existing jobs.Cross-platform, and how that's kept honest
The runner and the Compose stack behave identically on Linux, macOS and Windows for local development.
In CI the end-to-end job is Linux-only by necessity: GitHub's hosted macOS runners ship no Docker daemon, and its Windows runners can run Windows containers but not the Linux containers this stack builds from.
Rather than claim cross-platform support and test it on one platform, the assertion logic is a pure function and a
runner-self-testjob runsrun.py --self-teston all three platforms — covering argument handling, path handling and the log assertions, including the negative cases (stale receiver name, missing success marker, unauthenticated receiver, wrong agent-info name). That job is green on ubuntu, macos and windows.Usage
Compose defaults are baked into
docker-compose.yml, sodocker compose upin that directory also works for poking at the stack by hand. Seetests/integration/README.md.Also included
CLAUDE.md, per the repo's request for one — the mDNS-before-QUIC ordering, that the binaries'println!markers are asserted on (and are listed as constants in one place), thatCargo.lockis gitignored so builds track stable, and the debugging notes that otherwise cost time to rediscover:--discovery-timeoutis a flat sleep rather than a poll, and the sender takesservices[0].__pycache__/added to.gitignore, since this is the repo's first Python.Pre-existing CI failure, not from this PR
The Lint / Pre-commit job is red on this branch, and it is red before it too. This PR changes 0 Rust files; the clippy errors are in
openscreen-crypto-rustcrypto/src/lib.rsandopenscreen-network/src/state_machine/*.rs, and those exact lines are present unchanged onmain:Clippy 1.98 tightened
map_unwrap_oranduninlined_format_args; combined with-D warningsand an unpinned stable toolchain, previously-clean code now fails. Deliberately left alone here to keep this PR to one concern — happy to send a separate PR for it.Everything else is green: Build & Test, mDNS discovery (Docker Compose), and Runner self-test on ubuntu, macos and windows.
🤖 Generated with Claude Code