diff --git a/quest/m0/ci/README.md b/quest/m0/ci/README.md index a9de7b9f6d9..09d87707907 100644 --- a/quest/m0/ci/README.md +++ b/quest/m0/ci/README.md @@ -140,4 +140,3 @@ The historical samples locate candidates; they are not controlled benchmarks. - [Release credentials off main](/quest/m0/ci/publish-release-only.md) - `publish` admits only `release` and release tags once its `workflow_run` publishers run on their tag - [Biome checks every file it can parse](/quest/m0/ci/biome-whole-repo.md) - Biome runs over the whole repository and main is clean under it - [workspace.test.py ignores the caller's gate filter](/quest/m0/ci/workspace-test-env.md) - the dispatcher test passes under any `just gate` module filter -- [PRs build the feature shapes they can break](/quest/m0/ci/pr-feature-builds.md) - a PR touching a crate's source runs that crate's isolated feature builds diff --git a/quest/m0/ci/pr-feature-builds.md b/quest/m0/ci/pr-feature-builds.md deleted file mode 100644 index eff9176572e..00000000000 --- a/quest/m0/ci/pr-feature-builds.md +++ /dev/null @@ -1,18 +0,0 @@ -# [S] PRs build the feature shapes they can break - -## Goal - -A PR that narrows which features compile a function's callers fails the merge gate when it leaves that function unused in some isolated feature build, instead of breaking Nightly's `rs features` job after it lands. - -## Plan - -Found by the agent on [moq-dev/moq#7380](https://github.com/moq-dev/moq/pull/7380): only Nightly runs every isolated `moq-tokio` feature build (`just rs tokio-features --all`), plus PRs that change feature definitions. [moq-dev/moq#7120](https://github.com/moq-dev/moq/pull/7120) fixed one unused helper and missed `Addr::with_url`, because nothing on the PR built `--features webrtc` alone. - -Decided 2026-10-30 (maintainer, follow-up planning after the /quest-afk run). Select the isolated builds for a PR from the crates its diff touches, not only from changes to feature tables: a diff touching `rs/moq-tokio/src` runs moq-tokio's isolated shapes. Keep the gate's time budget from [the five-minute quest](/quest/m0/ci/five-minutes.md): if every shape is too slow, run them through `cargo check` (not tests), and only for crates whose source changed. Measure the added wall time on the queue runner and record it in the PR. - -Public API: none. Wire: none. - -## Related - -- [Gate each diff in under five minutes](/quest/m0/ci/five-minutes.md) - the time budget this must fit in -- [Unify dependency features](/quest/m0/ci/hakari.md) - shares the feature-shape plumbing diff --git a/rs/justfile b/rs/justfile index 150ceee0dd4..4e496569407 100644 --- a/rs/justfile +++ b/rs/justfile @@ -294,8 +294,8 @@ quic-features *args: fi bash sh/local.sh cargo clippy --locked -p moq-quic "${targets[@]}" --no-default-features -- -D warnings -# Compile media boundaries (--all isolates every mode) and prove expensive -# native/render dependencies are absent when their features are excluded. +# Compile media boundaries; --package CRATE isolates its modes, --all every mode. +# Prove expensive native/render dependencies are absent when excluded. [positional-arguments] media-features *args: sh/rs/media-features.sh "$@" diff --git a/sh/rs/media-features.sh b/sh/rs/media-features.sh index bda7d4b3a8c..54fff9a75cd 100755 --- a/sh/rs/media-features.sh +++ b/sh/rs/media-features.sh @@ -6,19 +6,31 @@ if [[ "${GITHUB_ACTIONS:-}" != true && "${LOCAL_SUPERVISED:-}" != 1 ]]; then fi all=0 +isolated=() if [[ $# == 1 && $1 == --all ]]; then all=1 -elif [[ $# != 0 ]]; then - echo 'usage: just rs media-features [--all]' >&2 - exit 2 + isolated=(moq-video moq-audio moq-transcode) +else + while (($#)); do + if [[ $# -lt 2 || $1 != --package || ! $2 =~ ^moq-(video|audio|transcode)$ ]]; then + echo 'usage: just rs media-features [--all | --package CRATE ...]' >&2 + exit 2 + fi + isolated+=("$2") + shift 2 + done fi targets=(--tests) if ((all)); then targets=(--all-targets); fi +isolates() { + [[ " ${isolated[*]} " == *" $1 "* ]] +} + check() { echo "media features: cargo check $*" - cargo check --locked "${targets[@]}" "$@" + CARGO_BUILD_WARNINGS=deny cargo check --locked "${targets[@]}" "$@" } # Resolved for Linux, the platform every asserted dependency targets: moq-nvenc @@ -55,13 +67,13 @@ for crate in openh264 openh264-sys2 wgpu cpal; do done cargo nextest run --locked -p moq-video --no-default-features -if ((all)); then check -p moq-video --no-default-features --features openh264; fi +if isolates moq-video; then check -p moq-video --no-default-features --features openh264; fi software=$(tree -p moq-video --no-default-features --features openh264) require_crate "$software" openh264 "moq-video software-only" require_crate "$software" openh264-sys2 "moq-video software-only" forbid_crate "$software" wgpu "moq-video software-only" -if ((all)); then check -p moq-video --no-default-features --features nvidia; fi +if isolates moq-video; then check -p moq-video --no-default-features --features nvidia; fi native=$(tree -p moq-video --no-default-features --features nvidia) require_crate "$native" moq-nvenc "moq-video native-only" for crate in openh264 openh264-sys2 wgpu; do @@ -79,7 +91,7 @@ for crate in openh264 openh264-sys2 wgpu; do done cargo nextest run --locked -p moq-video --no-default-features --features vpx -E 'test(/::vpx::/)' -if ((all)); then check -p moq-video --no-default-features --features render; fi +if isolates moq-video; then check -p moq-video --no-default-features --features render; fi render=$(tree -p moq-video --no-default-features --features render) require_crate "$render" wgpu "moq-video rendering" for crate in openh264 openh264-sys2; do @@ -92,12 +104,12 @@ for crate in openh264 openh264-sys2 wgpu moq-nvenc; do forbid_crate "$transcode_minimal" "$crate" "moq-transcode minimal" done -if ((all)); then check -p moq-transcode --no-default-features --features openh264; fi +if isolates moq-transcode; then check -p moq-transcode --no-default-features --features openh264; fi transcode_software=$(tree -p moq-transcode --no-default-features --features openh264) require_crate "$transcode_software" openh264 "moq-transcode software-only" forbid_crate "$transcode_software" wgpu "moq-transcode software-only" -if ((all)); then check -p moq-transcode --no-default-features --features nvidia; fi +if isolates moq-transcode; then check -p moq-transcode --no-default-features --features nvidia; fi transcode_native=$(tree -p moq-transcode --no-default-features --features nvidia) require_crate "$transcode_native" moq-nvenc "moq-transcode native-only" for crate in openh264 openh264-sys2 wgpu; do @@ -108,7 +120,7 @@ check -p moq-audio --no-default-features --features pipewire audio_host=$(tree -p moq-audio --no-default-features --features pipewire) forbid_crate "$audio_host" cpal "moq-audio host-only" -if ((all)); then check -p moq-audio --no-default-features --features capture,pipewire; fi +if isolates moq-audio; then check -p moq-audio --no-default-features --features capture,pipewire; fi audio_capture=$(tree -p moq-audio --no-default-features --features capture,pipewire) require_crate "$audio_capture" cpal "moq-audio PipeWire capture" diff --git a/sh/rs/select.sh b/sh/rs/select.sh index c9cb5b8452b..5209519d102 100755 --- a/sh/rs/select.sh +++ b/sh/rs/select.sh @@ -140,6 +140,13 @@ shapes_want() { [[ "$shape_packages" == ALL ]] || grep -qwE "$1" <<<"$shape_names" } +# True when the diff edits the crate's own compiled code or build definition, +# so its isolated permutations run too. A dependency edit keeps only its +# consumers' boundary checks; shared build inputs select every owner's. +changed() { + [[ "$packages" == ALL ]] || grep -qE "^rs/$1/(src/|tests/|examples/|Cargo\\.toml$|build\\.rs$)" "$list" +} + case "$packages" in "") echo "rs: no crates affected; skipping." @@ -183,8 +190,18 @@ case "$action" in if wants '(kio|moq-net)'; then just rs loom-check; fi if shapes_want moq-net; then just rs net-features; fi if shapes_want moq-quic; then just rs quic-features; fi - if shapes_want moq-tokio; then just rs tokio-features; fi - if shapes_want '(moq-video|moq-audio|moq-transcode)'; then just rs media-features; fi + if shapes_want moq-tokio; then + isolated=() + if changed moq-tokio; then isolated=(--all); fi + just rs tokio-features ${isolated[@]+"${isolated[@]}"} + fi + if shapes_want '(moq-video|moq-audio|moq-transcode)'; then + isolated=() + for crate in moq-video moq-audio moq-transcode; do + if changed "$crate"; then isolated+=(--package "$crate"); fi + done + just rs media-features ${isolated[@]+"${isolated[@]}"} + fi # moq-wasm's crate root is `#![cfg(target_arch = "wasm32")]`, so only a # wasm32 pass checks it. moq-mux, moq-ffi, and moq-clock ride along in that pass, and # nothing depends on moq-ffi, so each has to reach it on its own. diff --git a/sh/rs/select.test.py b/sh/rs/select.test.py index 05c586f513f..57b3eca47e5 100644 --- a/sh/rs/select.test.py +++ b/sh/rs/select.test.py @@ -75,16 +75,37 @@ def test_queue_checks_selected_feature_shapes(self): self.assertIn("rs feature-shapes --workspace --exclude moq-net-fuzz --exclude moq-quic-fuzz", calls) def test_boundary_checks_follow_reverse_dependencies(self): - self.assertIn("rs tokio-features", self.selected("rs/moq-tokio/src/lib.rs")) + self.assertIn("rs tokio-features", self.selected("rs/moq-sock/src/lib.rs")) self.assertIn("rs net-features", self.selected("rs/moq-net/src/lib.rs")) self.assertIn("rs quic-features", self.selected("rs/moq-quic/src/lib.rs")) - self.assertIn("rs media-features", self.selected("rs/moq-video/src/lib.rs")) + self.assertIn("rs media-features", self.selected("rs/moq-mux/src/lib.rs")) calls = self.selected("rs/kio/src/lib.rs") for owner in ("tokio", "net", "quic", "media"): self.assertIn(f"rs {owner}-features", calls) calls = self.selected("rs/moq-stats/benches/producer_tick.rs") self.assertFalse(any(call.endswith("-features") for call in calls), calls) + def test_isolated_shapes_follow_their_own_source_and_manifest(self): + for scope in (None, "changed"): + for action in ("check", "check-test", "lint"): + for owner in ("moq-tokio", "moq-video", "moq-audio", "moq-transcode"): + for source in ("src/added.rs", "tests/added.rs", "examples/added.rs", "Cargo.toml", "build.rs"): + with self.subTest(scope=scope, action=action, owner=owner, source=source): + calls = self.selected(f"rs/{owner}/{source}", action=action, feature_scope=scope) + expected = ("rs tokio-features --all" if owner == "moq-tokio" + else f"rs media-features --package {owner}") + self.assertIn(expected, calls) + calls = self.selected("rs/moq-video/src/added.rs", "rs/moq-audio/src/added.rs") + self.assertIn("rs media-features --package moq-video --package moq-audio", calls) + # Shared build inputs isolate every owner, still without Nightly's + # bench and example targets. + calls = self.selected("Cargo.lock") + self.assertIn("rs tokio-features --all", calls) + self.assertIn("rs media-features --package moq-video --package moq-audio --package moq-transcode", calls) + for path in ("rs/moq-video/benches/added.rs", "rs/moq-tokio/README.md"): + calls = self.selected(path) + self.assertFalse(any("-features --" in call for call in calls), calls) + def test_local_feature_contracts_skip_dependents(self): # `just check` lints feature shapes for the changed crates alone; their # dependents keep the default-feature lint and the landing worker's shapes. @@ -97,12 +118,14 @@ def test_local_feature_contracts_skip_dependents(self): calls = self.selected("rs/kio/src/lib.rs", feature_scope="changed") self.assertTrue(any("/rs/moq-net#" in call for call in calls if call.startswith("rs lint ")), calls) self.assertIn("rs loom-check", calls) - self.assertFalse(any(call.endswith("-features") for call in calls), calls) - for path, owner in (("rs/moq-net/src/lib.rs", "net"), ("rs/moq-tokio/src/lib.rs", "tokio"), - ("rs/moq-quic/src/lib.rs", "quic"), ("rs/moq-video/src/lib.rs", "media")): + self.assertFalse(any("-features" in call for call in calls), calls) + for path, expected in (("rs/moq-net/src/lib.rs", "rs net-features"), + ("rs/moq-tokio/src/lib.rs", "rs tokio-features --all"), + ("rs/moq-quic/src/lib.rs", "rs quic-features"), + ("rs/moq-video/src/lib.rs", "rs media-features --package moq-video")): with self.subTest(path=path): calls = self.selected(path, feature_scope="changed") - self.assertEqual([call for call in calls if call.endswith("-features")], [f"rs {owner}-features"]) + self.assertEqual([call for call in calls if "-features" in call], [expected]) # Inputs every crate shares still select the workspace. self.assertIn("rs feature-shapes --workspace --exclude moq-net-fuzz --exclude moq-quic-fuzz", self.selected("Cargo.lock", action="feature-shapes", feature_scope="changed")) @@ -291,7 +314,7 @@ def trace(self, owner, *args, net_features="", expected_returncode=0): result = subprocess.run(["bash", f"sh/rs/{owner}-features.sh", *args], cwd=ROOT, env=env, capture_output=True, text=True) self.assertEqual(result.returncode, expected_returncode, result.stdout + result.stderr) - return calls.read_text().splitlines() + return calls.read_text().splitlines() if calls.exists() else [] def test_net_keeps_model_and_browser_contracts(self): boundary = self.trace("net") @@ -331,6 +354,26 @@ def test_media_keeps_dependency_contracts_and_vpx_tests(self): self.assertEqual([call for call in boundary if not call.startswith("check ")], [call for call in exhaustive if not call.startswith("check ")]) + def test_media_isolates_only_the_edited_owners(self): + boundary = self.trace("media") + exhaustive = self.trace("media", "--all") + for owners in (("moq-video",), ("moq-transcode",), ("moq-audio",), + ("moq-video", "moq-audio")): + with self.subTest(owners=owners): + calls = self.trace("media", *(arg for owner in owners for arg in ("--package", owner))) + # Keep all dependency contracts and tests, adding only the + # edited owners' checks without Nightly's bench/example targets. + expected = [call.replace(" --all-targets ", " --tests ") for call in exhaustive + if not call.startswith("check ") or + call.replace(" --all-targets ", " --tests ") in boundary or + any(f" -p {owner} " in call for owner in owners)] + self.assertEqual(calls, expected) + + def test_media_rejects_unsupported_selections(self): + for args in (("--package",), ("--package", "moq-net"), ("--all", "--package", "moq-video")): + with self.subTest(args=args): + self.assertEqual(self.trace("media", *args, expected_returncode=2), []) + CARGO_STUB = r"""#!/usr/bin/env bash printf '%s\n' "$*" >> "$FEATURE_CALLS" diff --git a/sh/rs/tokio-features.sh b/sh/rs/tokio-features.sh index 2845816a04e..c066a613503 100755 --- a/sh/rs/tokio-features.sh +++ b/sh/rs/tokio-features.sh @@ -34,7 +34,7 @@ if leaked=$(grep -E '^(moq-net|reqwest|notify|quinn[a-z-]*|noq[a-z-]*|web-transp sort -u <<<"$leaked" >&2 exit 1 fi -# Only a feature-owner edit needs every isolated permutation on a PR. Shared +# Source and manifest edits select every isolated permutation on a PR. Shared # dependencies still compile at both extremes; Nightly runs every permutation. if ((all)); then cargo clippy --locked -p moq-tokio --no-default-features --features quic,aws-lc-rs -- -D warnings