From 8f50825d05cf94231765ec0a2805aaf25cdd2ba1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 21:38:43 -0700 Subject: [PATCH 1/4] quest: plan follow-ups from the 2026-10-25 quest-complete run Co-Authored-By: Claude Opus 5.5 --- quest/m0/nightly/README.md | 1 + quest/m0/nightly/competing-cubic.md | 17 +++++++++++++++++ quest/m1/api-audit/README.md | 1 + quest/m1/api-audit/dial-credential.md | 17 +++++++++++++++++ quest/m1/drop-set.md | 9 +++++++++ quest/m1/js-api-reference.md | 4 ++++ quest/m1/pure-crypto/README.md | 1 + quest/m1/pure-crypto/graviola-cpu-check.md | 16 ++++++++++++++++ quest/m1/test-flakes-2/README.md | 2 ++ quest/m1/test-flakes-2/gst-plugin-load.md | 17 +++++++++++++++++ quest/m1/test-flakes-2/swapped-js-opus.md | 16 ++++++++++++++++ quest/m1/ts-reliability/README.md | 2 +- quest/m1/ts-reliability/export-si-order.md | 9 ++++++--- quest/m1/unbounded-groups/byte-holes.md | 7 ++++++- quest/m2/README.md | 1 + quest/m2/e2ee/README.md | 4 ++++ quest/m2/mpeg2-open-gop-export.md | 18 ++++++++++++++++++ quest/m2/p2p/README.md | 4 ++++ 18 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 quest/m0/nightly/competing-cubic.md create mode 100644 quest/m1/api-audit/dial-credential.md create mode 100644 quest/m1/pure-crypto/graviola-cpu-check.md create mode 100644 quest/m1/test-flakes-2/gst-plugin-load.md create mode 100644 quest/m1/test-flakes-2/swapped-js-opus.md create mode 100644 quest/m2/mpeg2-open-gop-export.md diff --git a/quest/m0/nightly/README.md b/quest/m0/nightly/README.md index 7e0d9638066..c76d9fb0929 100644 --- a/quest/m0/nightly/README.md +++ b/quest/m0/nightly/README.md @@ -37,3 +37,4 @@ Public API: none. Wire: none. - [OBS audio-candidate test](/quest/m0/nightly/obs-audio-candidate.md) - the failed audio candidate check passes under TSan and load, fixed at its reproduced cause - [moq-net builds without async](/quest/m0/nightly/net-async-free.md) - the no-default-features lane stops pulling `async` back in - [Feature shapes per crate](/quest/m0/nightly/hls-poll-closed-gate.md) - moq-hls `Broadcaster::poll_closed` is gated on `server`, and the gate checks each crate's feature shapes in isolation +- [Probe against Cubic](/quest/m0/nightly/competing-cubic.md) - the `competing_cubic` probe test stops resending a stream after its reset diff --git a/quest/m0/nightly/competing-cubic.md b/quest/m0/nightly/competing-cubic.md new file mode 100644 index 00000000000..d1dea8a1a24 --- /dev/null +++ b/quest/m0/nightly/competing-cubic.md @@ -0,0 +1,17 @@ +# [S] Probe against Cubic passes + +## Goal + +`tests::probe::competing_cubic` in `rs/moq-quic/src/tests/probe.rs`, run by +Nightly through `just rs quic-sim`, passes on `main`, fixed at its cause. + +## Plan + +[moq-dev/moq#6719](https://github.com/moq-dev/moq/pull/6719) found it failing +the same way on `main` and on its branch: a probe resends a stream after that +stream was reset. A reset stream's data is never retransmitted, so find which +path still queues it (probe padding, a loss-recovery retransmit, or a stale +send queue entry) and stop it there. The other probe tests in the release +suite pass, so the fix must keep them passing. + +Public API: none. Wire: none. diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index f1e5976e555..70464eaa864 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -80,6 +80,7 @@ Rejected in planning: - [One lifecycle vocabulary](/quest/m1/api-audit/lifecycle.md) - finish, abort, closed, and close mean one thing on every handle, and Rust and JS observers fire alike - [moq-net names say one thing](/quest/m1/api-audit/net-names.md) - demand says `wanted`, colliding stats and transport names are renamed, and Rust and JS info lookups mirror - [Request handlers are named requests](/quest/m1/api-audit/requests.md) - `dynamic()` and `Dynamic` become `requests()` and `Requests` at every layer +- [One meaning for Credential](/quest/m1/api-audit/dial-credential.md) - the internal dial type gets its own name in JS and moq-tokio, and the `Presented` alias goes ## Related diff --git a/quest/m1/api-audit/dial-credential.md b/quest/m1/api-audit/dial-credential.md new file mode 100644 index 00000000000..13bb972b7fc --- /dev/null +++ b/quest/m1/api-audit/dial-credential.md @@ -0,0 +1,17 @@ +# [XS] One meaning for Credential + +## Goal + +`Credential` means only the public `auth::Credential`. The internal type +for what one dial presents, in `js/net/src/connection/tokens.ts` and moq-tokio's +`connect/token.rs`, gets its own name, and the `Presented` import alias in +`tokens.ts` goes away. + +## Plan + +[moq-dev/moq#6751](https://github.com/moq-dev/moq/pull/6751) renamed the +public `Auth` to `Credential`, which collides with these internal types. +Pick a name by role (such as `Presentation`) and use it in both languages. +Both types are internal, so nothing public changes. + +Public API: none. Wire: none. diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 0d273d2d834..ff9f5b5b063 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -236,6 +236,15 @@ run array once for the track and once per sink (`track.ts:914`). The replacement keeps the per-cursor walk incremental in both languages, with a benchmark swept over runs and readers. +### Idle START unreport test + +[moq-dev/moq#6717](https://github.com/moq-dev/moq/pull/6717) made the JS lite +publisher unreport stale skips below an idle SUBSCRIBE_START, so a later +widening can re-offer them as Rust does, but landed it without a test: the +case needs an update to arrive between START and the first group. Cover it +deterministically, through the track hooks rather than wire timing, when the +drop runs are reworked here. + ## Required - [A widened max age re-offers cached groups](/quest/m1/resolved-demand.md) - the re-offer path this extends to reported groups diff --git a/quest/m1/js-api-reference.md b/quest/m1/js-api-reference.md index 3f1643554e8..6dda392612e 100644 --- a/quest/m1/js-api-reference.md +++ b/quest/m1/js-api-reference.md @@ -23,6 +23,10 @@ Decided 2026-10-23 (maintainer interview): matches the guides beside it. Nothing generated is committed. - JSR keeps publishing its own reference; this does not replace it. - Rust is a non-goal: docs.rs covers it. +- The link resolver (`nameableReference` from + [moq-dev/moq#6794](https://github.com/moq-dev/moq/pull/6794)) gets fixtures + for a hidden and an exported declaration sharing a name, and for chains of + aliases. ## Required diff --git a/quest/m1/pure-crypto/README.md b/quest/m1/pure-crypto/README.md index de948835495..a730ed081c1 100644 --- a/quest/m1/pure-crypto/README.md +++ b/quest/m1/pure-crypto/README.md @@ -38,3 +38,4 @@ picks a random one, and jsonwebtoken 11 already takes a custom - [ACME on graviola](/quest/m1/pure-crypto/acme-upstream.md) - graviola builds turn ACME back on once rustls-acme gains a provider hook or instant-acme 0.9 is released - [rustls-graviola's QUIC keys](/quest/m1/pure-crypto/graviola-quic.md) - a rustls-graviola release ships QUIC keys, and moq-quic's adapter is deleted - [The next graviola release](/quest/m1/pure-crypto/graviola-release.md) - moq pins a graviola with ECDSA key generation and faster ChaCha20-Poly1305, drops RustCrypto's `p256`, and re-measures the ChaCha20 gap +- [graviola's own CPU check](/quest/m1/pure-crypto/graviola-cpu-check.md) - waiting on ctz/graviola#227; the relay then calls graviola's check instead of mirroring its list diff --git a/quest/m1/pure-crypto/graviola-cpu-check.md b/quest/m1/pure-crypto/graviola-cpu-check.md new file mode 100644 index 00000000000..c504cde51f9 --- /dev/null +++ b/quest/m1/pure-crypto/graviola-cpu-check.md @@ -0,0 +1,16 @@ +# [XS] graviola's own CPU check + +## Goal + +Once a graviola release ships a public CPU feature check +([ctz/graviola#227](https://github.com/ctz/graviola/issues/227)), moq-relay +calls it at startup instead of mirroring graviola's mandatory feature list. +Check the issue and graviola's changelog; until it ships, this quest waits. + +## Plan + +[moq-relay on graviola](/quest/m1/pure-crypto/relay-graviola.md) mirrors the +list because no public check exists. If the check ships before that quest +lands, that quest uses it and this one is deleted. + +Public API: none. Wire: none. diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index a7cdb7669ac..af1f03de45b 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -46,3 +46,5 @@ Public API: none. Wire: none. - [moq-uring allocations under load](/quest/m1/test-flakes-2/uring-alloc-load.md) - the steady-state send test and MEMLOCK users pass in a parallel check - [IETF22 fallback](/quest/m1/test-flakes-2/ietf22-fallback.md) - an IETF22 browser viewer follows a fallback within the media lane's bound, keeping its session and its audio - [Echo report test on mocked time](/quest/m1/test-flakes-2/export-echo-time.md) - the moq-cli echo report test stops taking 35 s of wall time +- [Swapped Rust to JS Opus](/quest/m1/test-flakes-2/swapped-js-opus.md) - the `rust-swapped -> js` interop cell stops timing out under load +- [Interop loads moq-gst](/quest/m1/test-flakes-2/gst-plugin-load.md) - the interop gst cells always find the local plugin diff --git a/quest/m1/test-flakes-2/gst-plugin-load.md b/quest/m1/test-flakes-2/gst-plugin-load.md new file mode 100644 index 00000000000..a5d6fb382bb --- /dev/null +++ b/quest/m1/test-flakes-2/gst-plugin-load.md @@ -0,0 +1,17 @@ +# [S] Interop loads the local moq-gst plugin + +## Goal + +The interop `gst` cells find the locally built moq-gst plugin on every run, +instead of sometimes failing with "moqsrc not exposed" (the check near +`test/interop/interop.sh:780`), fixed at its cause. + +## Plan + +Seen in [moq-dev/moq#6745](https://github.com/moq-dev/moq/pull/6745)'s +`just test interop --all` runs: one of two runs failed to load the plugin, and +the other passed. Find whether the plugin build, the registry cache +(`GST_REGISTRY`), or `GST_PLUGIN_PATH` races between cells, and make the +harness load the plugin deterministically. + +Public API: none. Wire: none. diff --git a/quest/m1/test-flakes-2/swapped-js-opus.md b/quest/m1/test-flakes-2/swapped-js-opus.md new file mode 100644 index 00000000000..ef0eb042a2c --- /dev/null +++ b/quest/m1/test-flakes-2/swapped-js-opus.md @@ -0,0 +1,16 @@ +# [S] Swapped Rust publisher to JS Opus in interop + +## Goal + +The `rust-swapped -> js` interop cell stops timing out on its Opus check +under local load, fixed at its cause. + +## Plan + +Seen in [moq-dev/moq#6745](https://github.com/moq-dev/moq/pull/6745)'s +`just test interop --all` runs: one of two runs timed out on this cell, and +the other passed. Reproduce under a loaded machine and find whether the JS +subscriber never receives the Opus track, receives it late, or decodes it and +misses the check. + +Public API: none. Wire: none. diff --git a/quest/m1/ts-reliability/README.md b/quest/m1/ts-reliability/README.md index 09f5351f16c..e57b61e455f 100644 --- a/quest/m1/ts-reliability/README.md +++ b/quest/m1/ts-reliability/README.md @@ -100,7 +100,7 @@ catalog linger (#5305). - [PAT changes](/quest/m1/ts-reliability/pat-lifecycle.md) - a revised PAT never leaves a broadcast meaning another program - [TS workflow passes](/quest/m1/ts-reliability/validation-green.md) - the scheduled TS lanes pass on main again, with no lane loosened -- [Export SI order](/quest/m1/ts-reliability/export-si-order.md) - a TS export sends SI after its PMT and a PCR interval, so a hop keeps first emissions +- [Export SI order](/quest/m1/ts-reliability/export-si-order.md) - a TS export sends SI and media after its PMT and a PCR interval, so a hop keeps first emissions ## Related diff --git a/quest/m1/ts-reliability/export-si-order.md b/quest/m1/ts-reliability/export-si-order.md index 2c952959ee9..0e6ddefcfd8 100644 --- a/quest/m1/ts-reliability/export-si-order.md +++ b/quest/m1/ts-reliability/export-si-order.md @@ -1,9 +1,9 @@ -# [S] TS export sends SI after its PMT and PCR +# [S] TS export leads with its PMT and PCR ## Goal -A TS export holds its first SI tables until the PMT and one PCR interval are -out, so an export-to-import hop keeps each table's first emission instead of +A TS export holds its first SI tables and media until the PMT and one PCR +interval are out, so an export-to-import hop keeps each table's first emission instead of dropping it until the table repeats. ## Plan @@ -16,6 +16,9 @@ here is what `si.rs` needs to place a section: one forward PCR step of at most `MAX_INTERVAL`, not the importer's two-second catalog window. Test it with an export-to-import round trip that sees every SI table's first emission. +The first media PES also waits for that PCR, so a reader of our own export +never sees media it cannot place. + Not planned (2026-10-25, maintainer): bounding SI aging against a source that claims a huge mux rate through valid PCR steps. That needs a program-clock floor per input byte, a new contract. diff --git a/quest/m1/unbounded-groups/byte-holes.md b/quest/m1/unbounded-groups/byte-holes.md index 40516ecb018..85e843e3650 100644 --- a/quest/m1/unbounded-groups/byte-holes.md +++ b/quest/m1/unbounded-groups/byte-holes.md @@ -31,10 +31,15 @@ See the [epic](/quest/m1/unbounded-groups/README.md) for the decisions. - Update `drafts/draft-lcurley-moq-lite.md` and run `just drafts check`. Also drop the 32 MiB interop cap in `drafts/draft-lcurley-moq-e2ee.md` if it no longer follows from a model limit. +- Rust readers get a chunked payload read, so a frame larger than memory + never has to be collected whole. [moq-dev/moq#6628](https://github.com/moq-dev/moq/pull/6628) + kept its segments internal; the JS half is in + [JS](/quest/m1/unbounded-groups/js.md). - Test a frame several times the pool size through a relay chain: a late joiner reads it whole, and memory stays near the pool size. -Public API: `Position` gains a byte field, and `MAX_CACHE_BYTES` is removed. +Public API: `Position` gains a byte field, frames gain a chunked read, and +`MAX_CACHE_BYTES` is removed. Wire: a byte offset on the next lite version's range ends and group header. ## Required diff --git a/quest/m2/README.md b/quest/m2/README.md index a7bac8f3b3c..2866b68ed51 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -237,3 +237,4 @@ lite-07; bitrate enforcement is not part of this release. - [No MOQ_* knobs outside the binaries](/quest/m2/env-knobs.md) - test, bench, and library knobs leave the `MOQ_*` prefix - [Clean ALSA playback onset](/quest/m2/audio-alsa-onset.md) - no onset restarts or clicks over PulseAudio - [Interop audit](/quest/m2/audit-interop.md) - our interop runner entries match what we ship, and every red interop cell with a moq.dev side is fixed, reported, or noted as down or unsupported +- [Open-GOP MPEG-2 export](/quest/m2/mpeg2-open-gop-export.md) - a TS export resuming in an open GOP trims the B-pictures that reference the previous one diff --git a/quest/m2/e2ee/README.md b/quest/m2/e2ee/README.md index 0ca06bc4194..6fa23b6d5ae 100644 --- a/quest/m2/e2ee/README.md +++ b/quest/m2/e2ee/README.md @@ -57,6 +57,10 @@ Rejected: The broadcast wrappers merged in [#5876](https://github.com/moq-dev/moq/pull/5876) with `reserve_track`, which still serves declared names; [rust-publish](/quest/m2/e2ee/rust-publish.md) needs nothing more (maintainer, 2026-10-19). +Before `@moq/e2ee` is first published, add its entrypoints to the API +Extractor declaration checks, which analyze only published packages since +[moq-dev/moq#6794](https://github.com/moq-dev/moq/pull/6794). + ## Required - [Undeclared track names](/quest/m2/e2ee/undeclared-names.md) - an encrypted diff --git a/quest/m2/mpeg2-open-gop-export.md b/quest/m2/mpeg2-open-gop-export.md new file mode 100644 index 00000000000..bc2c62e2655 --- /dev/null +++ b/quest/m2/mpeg2-open-gop-export.md @@ -0,0 +1,18 @@ +# [S] Open-GOP MPEG-2 export trims broken-link pictures + +## Goal + +When a TS export starts or resumes at an MPEG-2 sequence header in an open +GOP, it drops or flags the leading B-pictures that reference the previous +GOP, as it already does for codecs whose frames carry presentation time. + +## Plan + +Found while landing [moq-dev/moq#6252](https://github.com/moq-dev/moq/pull/6252): +whole-PES MPEG-2 frames carry their placement time, not their presentation +time, so the leading-picture check cannot apply. Parse the picture coding type +and temporal reference, or the GOP header's `closed_gop` and `broken_link` +flags, to find them. Low priority: decoders usually skip broken-link +B-pictures. + +Public API: none. Wire: none. diff --git a/quest/m2/p2p/README.md b/quest/m2/p2p/README.md index fcd23b43442..66e17539243 100644 --- a/quest/m2/p2p/README.md +++ b/quest/m2/p2p/README.md @@ -150,6 +150,10 @@ Backend choice does not change the policy that watching tabs never re-serve. - Symmetric NATs on both ends fail ICE without TURN. By design the relay keeps serving. +Before `@moq/p2p` is first published, add its entrypoints to the API +Extractor declaration checks, which analyze only published packages since +[moq-dev/moq#6794](https://github.com/moq-dev/moq/pull/6794). + ## Required - [Peer grants](/quest/m2/p2p/peer-grant.md) - the relay issues a key-bound, asymmetrically signed grant a browser can verify; HS256 keys issue none From 96f0cabed4d1a5e485fac0f24ed7ad3e57f9c435 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 21:48:13 -0700 Subject: [PATCH 2/4] quest: move the C follow-ups to c-retire and moq-audio's grant Co-Authored-By: Claude Opus 5.5 --- ...848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md | 5 +++++ quest/m2/release/c-retire.md | 3 +++ 2 files changed, 8 insertions(+) diff --git a/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md b/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md index f63fd9a454d..c227daf271f 100644 --- a/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md +++ b/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md @@ -57,6 +57,11 @@ grant; `Options::bandwidth` documents that reservation before video sees a bit and audio is squeezed only once the link cannot carry audio alone. Worth doing for that tail, not worth blocking on. +- moq-ffi's audio encode takes an options record shaped like the video one + ([moq-dev/moq#6781](https://github.com/moq-dev/moq/pull/6781) left + `moq_audio_encode_options` out), so bindings pass the bandwidth policy the + same way for both. The generated C picks it up from moq-ffi. + Decided 2026-10-08: moved to m2, since that tail is the only gain. Tests: retain the shared `Control` tests; an Opus Producer whose diff --git a/quest/m2/release/c-retire.md b/quest/m2/release/c-retire.md index 1c02587a92f..a3824fd6c4d 100644 --- a/quest/m2/release/c-retire.md +++ b/quest/m2/release/c-retire.md @@ -13,6 +13,9 @@ served it are deleted once the generated `moq-c` publishes. The generated - Decided 2026-10-20 (maintainer): no final hand-written notice release, because the hand-written `moq-c` never published to a registry; the generated package is the first `moq-c` users install. +- `rs/moq-c/native-libs/` serves the generated package too, and + `cpp/moq/cmake/moq-ffi.cmake` serves both C++ and C. Move them to a neutral + home shared by both packages before deleting the crate. - The code-free `rs/libmoq` stub left under the old name is [Retire the libmoq stub](/quest/m2/release/libmoq-retire.md)'s, separately. From e54354870971604356baee1b061057f0a0f5729e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 22:55:01 -0700 Subject: [PATCH 3/4] chore: claim gst plugin discovery quest Co-Authored-By: Codex From f4fbe6793ce8f3f73c122d5466b480b3022348b8 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 23:00:50 -0700 Subject: [PATCH 4/4] fix(interop): consume plugin inspection output before checking factories Reuse the packaging smoke probe for interop discovery and retain load diagnostics. Avoid early-closing producer pipes under pipefail, cover large inspection output and failure cases, and retire the completed quest. Co-Authored-By: Codex --- quest/m1/test-flakes-2/README.md | 1 - quest/m1/test-flakes-2/gst-plugin-load.md | 17 ------ rs/moq-gst/smoke.sh | 8 +-- rs/moq-gst/smoke.test.py | 65 +++++++++++++++++++++++ test/interop/interop.sh | 11 ++-- test/justfile | 1 + 6 files changed, 75 insertions(+), 28 deletions(-) delete mode 100644 quest/m1/test-flakes-2/gst-plugin-load.md create mode 100644 rs/moq-gst/smoke.test.py diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index af1f03de45b..42345632430 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -47,4 +47,3 @@ Public API: none. Wire: none. - [IETF22 fallback](/quest/m1/test-flakes-2/ietf22-fallback.md) - an IETF22 browser viewer follows a fallback within the media lane's bound, keeping its session and its audio - [Echo report test on mocked time](/quest/m1/test-flakes-2/export-echo-time.md) - the moq-cli echo report test stops taking 35 s of wall time - [Swapped Rust to JS Opus](/quest/m1/test-flakes-2/swapped-js-opus.md) - the `rust-swapped -> js` interop cell stops timing out under load -- [Interop loads moq-gst](/quest/m1/test-flakes-2/gst-plugin-load.md) - the interop gst cells always find the local plugin diff --git a/quest/m1/test-flakes-2/gst-plugin-load.md b/quest/m1/test-flakes-2/gst-plugin-load.md deleted file mode 100644 index a5d6fb382bb..00000000000 --- a/quest/m1/test-flakes-2/gst-plugin-load.md +++ /dev/null @@ -1,17 +0,0 @@ -# [S] Interop loads the local moq-gst plugin - -## Goal - -The interop `gst` cells find the locally built moq-gst plugin on every run, -instead of sometimes failing with "moqsrc not exposed" (the check near -`test/interop/interop.sh:780`), fixed at its cause. - -## Plan - -Seen in [moq-dev/moq#6745](https://github.com/moq-dev/moq/pull/6745)'s -`just test interop --all` runs: one of two runs failed to load the plugin, and -the other passed. Find whether the plugin build, the registry cache -(`GST_REGISTRY`), or `GST_PLUGIN_PATH` races between cells, and make the -harness load the plugin deterministically. - -Public API: none. Wire: none. diff --git a/rs/moq-gst/smoke.sh b/rs/moq-gst/smoke.sh index edc62f37bbb..6741c3d8a37 100755 --- a/rs/moq-gst/smoke.sh +++ b/rs/moq-gst/smoke.sh @@ -31,7 +31,7 @@ if ! command -v gst-inspect-1.0 >/dev/null 2>&1; then exit 0 fi -echo "Smoke testing against $(gst-inspect-1.0 --version | head -1)..." +echo "Smoke testing against $(gst-inspect-1.0 --version | sed -n '1p')..." # Isolate discovery to $plugin_dir: # - GST_PLUGIN_PATH_1_0 alone is additive, so a system-installed moq @@ -51,8 +51,10 @@ out="$( GST_REGISTRY_1_0="$tmp_registry" \ gst-inspect-1.0 moq )" -if ! echo "$out" | grep -qE '^[[:space:]]+moqsink:' || - ! echo "$out" | grep -qE '^[[:space:]]+moqsrc:'; then +# Read captured output without a producer pipe: grep -q can close it early, +# making a successful inspection fail with SIGPIPE under pipefail. +if ! grep -qE '^[[:space:]]+moqsink:' <<<"$out" || + ! grep -qE '^[[:space:]]+moqsrc:' <<<"$out"; then echo "Error: gst-inspect-1.0 didn't find moqsink/moqsrc in the plugin." >&2 echo "$out" >&2 exit 1 diff --git a/rs/moq-gst/smoke.test.py b/rs/moq-gst/smoke.test.py new file mode 100644 index 00000000000..e0369af6e62 --- /dev/null +++ b/rs/moq-gst/smoke.test.py @@ -0,0 +1,65 @@ +"""Plugin discovery consumes inspection output and checks both factories.""" + +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +SMOKE = Path(__file__).with_name("smoke.sh") + + +class SmokeTest(unittest.TestCase): + def inspect(self, mode): + with tempfile.TemporaryDirectory() as directory: + scratch = Path(directory) + inspect = scratch / "gst-inspect-1.0" + inspect.write_text('''#!/usr/bin/env python3 +import os +import signal +import sys + +signal.signal(signal.SIGPIPE, signal.SIG_DFL) +if sys.argv[1] == "--version": + print("gst-inspect-1.0 test", flush=True) +else: + assert sys.argv[1] == "moq" + assert os.environ["GST_PLUGIN_PATH_1_0"] == os.environ["PLUGIN_DIR"] + assert os.environ["GST_PLUGIN_SYSTEM_PATH_1_0"] == "" + assert os.environ["GST_REGISTRY_1_0"] != os.environ["INHERITED_REGISTRY"] + print(" moqsink: MoQ Sink", flush=True) + if os.environ["INSPECT_MODE"] != "missing": + print(" moqsrc: MoQ Src", flush=True) +# A reader must allow the writer to finish after finding its first match. +for _ in range(16): + sys.stdout.write("x" * 4096 + "\\n") + sys.stdout.flush() +if sys.argv[1] == "moq" and os.environ["INSPECT_MODE"] == "error": + sys.exit(1) +''') + inspect.chmod(0o755) + env = dict(os.environ, PATH=f"{scratch}:{os.environ['PATH']}", BASH_ENV=os.devnull, + PLUGIN_DIR=str(scratch), INSPECT_MODE=mode, + GST_PLUGIN_PATH_1_0="inherited", GST_PLUGIN_SYSTEM_PATH_1_0="inherited", + GST_REGISTRY_1_0=str(scratch / "inherited-registry"), + INHERITED_REGISTRY=str(scratch / "inherited-registry")) + return subprocess.run(["bash", str(SMOKE), str(scratch)], env=env, + text=True, capture_output=True) + + def test_large_inspection_completes(self): + result = self.inspect("complete") + self.assertEqual(result.returncode, 0, result.stderr[:1000]) + self.assertIn("Smoke test passed", result.stdout) + + def test_missing_factory_is_refused(self): + result = self.inspect("missing") + self.assertNotEqual(result.returncode, 0) + self.assertIn("didn't find moqsink/moqsrc", result.stderr) + + def test_inspection_failure_is_refused_even_with_both_factories(self): + self.assertNotEqual(self.inspect("error").returncode, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/interop/interop.sh b/test/interop/interop.sh index 04680dcf871..1743d2d6a2c 100755 --- a/test/interop/interop.sh +++ b/test/interop/interop.sh @@ -771,13 +771,10 @@ prepare_gst() { return fi GST_PLUGIN_DIR="$TARGET_BASE/$PROFILE" - # gst-inspect exits 0 even when the .so fails to load, so grep for the - # factory. Isolate discovery to our dir + a temp registry so a system-wide moq - # plugin can't shadow it (mirrors rs/moq-gst/smoke.sh). - if ! GST_PLUGIN_PATH_1_0="$GST_PLUGIN_DIR" GST_PLUGIN_SYSTEM_PATH_1_0="" \ - GST_REGISTRY_1_0="$HARNESS_RUN/gst-registry.bin" \ - gst-inspect-1.0 moq 2>/dev/null | grep -qE '^[[:space:]]+moqsrc:'; then + # Use the packaging smoke check's isolated discovery and factory assertions. + if ! "$WORKSPACE/rs/moq-gst/smoke.sh" "$GST_PLUGIN_DIR" >"$HARNESS_RUN/gst-inspect.log" 2>&1; then mark_broken gst "moqsrc not exposed (plugin failed to load against this GStreamer)" + sed 's/^/ /' "$HARNESS_RUN/gst-inspect.log" >&2 || true return fi # The audio check decodes through these, from the system's plugins. @@ -1114,7 +1111,7 @@ run_native() { local out out=$( (cd "$NATIVE_DIR" && "$@") 2>&1) || true printf '%s\n' "$out" >&2 - printf '%s\n' "$out" | grep -q '^received ' + grep -q '^received ' <<<"$out" } # Write a track name and the group the compat client's live subscription saw on it to diff --git a/test/justfile b/test/justfile index b71f700909b..e0a35035d65 100644 --- a/test/justfile +++ b/test/justfile @@ -42,6 +42,7 @@ default: harness: ./lib/harness.test.sh ./interop/relay-log.test.sh + python3 ../rs/moq-gst/smoke.test.py python3 interop/report.test.py python3 interop/tone.test.py cd .. && bun test {{ replace(harness_compat_tests, "\n", " ") }}