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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,9 @@ jobs:
if: ${{ !cancelled() }}
run: ./scripts/check_changeset_fragment.sh --self-test

- name: Cargo test scope regression tests
run: python3 -m unittest discover -s tests -p 'test_ci_test_scope.py' -v

- name: Docs catalog toolchain tests
run: python3 docs/scripts/test_i18n_toolchain.py

Expand Down
8 changes: 8 additions & 0 deletions changelog.d/11424-stdlib-ci-fanout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Runtime changes now select the `perry-stdlib` unit tests in per-PR CI, covering
the GC, arena, closure and event-loop interactions previously skipped by the
reverse-dependency selector (#11423). Extension shims remain outside dependency
fan-out unless directly changed. Required lint coverage checks the selector’s
runtime, direct-change, unrelated, metadata-only and full-workspace cases.
Library-target detection now recognizes explicit Cargo crate types such as
`rlib`, so the selected stdlib suite actually runs with `--lib --bins` instead
of being skipped by `--bins`.
6 changes: 6 additions & 0 deletions docs/src/testing/ci-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ Within the **pr** tier the changed-file list narrows the plan further:
- **deps** (a lockfile, manifest, `deny.toml`, `package.json`, `.claude/`, `skills/`,
…) → additionally the `security-audit` reusable workflow.

Within `cargo-test`, runtime changes also select `perry-stdlib` unit tests:
those tests exercise runtime GC, arenas, closures, promises and the event loop.
The `perry-ext-*` crates remain excluded from dependency fan-out unless directly
changed. `scripts/ci_test_scope.py` owns this policy, with regression tests in
`lint`.

`e2e-scoped` runs integration suites selected by the diff, but its
`SUITE_EXCLUSIONS` check is deliberately not scoped: whenever that list is
nonempty, every core PR reruns each excluded exact test and fails if it no
Expand Down
21 changes: 11 additions & 10 deletions scripts/ci_test_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,13 @@ def _runtime_link_augment(seeds):
def _is_fanout_leaf(name):
"""Crates we never fan *into* when a dependency changes.

`perry-ext-*` and `perry-stdlib` are runtime FFI shims whose UNIT tests are
self-contained pure-Rust logic — they do not exercise perry-runtime internals,
so a perry-runtime change need not re-run them per-PR (the nightly full run +
perry's integration tests cover that interaction). Excluding them from the
reverse-dep fan-out keeps a foundational change from selecting ~40 crates, and
avoids perry-runtime feature-unification rebuilds. A direct change *to* one of
these crates still selects it (it starts as a seed).
`perry-ext-*` shims stay out of dependency fan-out to bound per-PR test
links and feature-unification rebuilds; direct changes still select them.
`perry-stdlib` MUST participate: its unit tests exercise runtime GC, arena,
closure, promise and event-loop behavior. Excluding it hid runtime-caused
failures until an unrelated stdlib change selected the suite (#11423).
"""
return name == "perry-stdlib" or name.startswith("perry-ext-")
return name.startswith("perry-ext-")


def _reverse_dep_closure(md, seeds):
Expand Down Expand Up @@ -133,16 +131,19 @@ def _reverse_dep_closure(md, seeds):


def _has_lib_mode() -> int:
"""Exit 0 if any package named on stdin has a `lib` target, else exit 1.
"""Exit 0 if any package named on stdin has a library target, else exit 1.

`cargo test --lib` errors ("no library targets") when *no* selected package
has a library — e.g. a perry-only diff selects just the bin-only `perry`
crate. The fast per-PR path uses this to choose `--lib --bins` vs `--bins`.
"""
names = set(sys.stdin.read().split())
md = _load_metadata()
# Cargo reports an explicit crate-type as the kind (e.g. stdlib's rlib),
# rather than always spelling library targets "lib".
library_kinds = {"lib", "rlib", "staticlib", "dylib", "cdylib", "proc-macro"}
has = any(
any("lib" in t["kind"] for t in p["targets"])
any(library_kinds.intersection(t["kind"]) for t in p["targets"])
for p in md["packages"]
if p["name"] in names
)
Expand Down
91 changes: 91 additions & 0 deletions tests/test_ci_test_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Regression coverage for the per-PR cargo-test package selection."""
import contextlib
import io
from pathlib import Path
import sys
import unittest
from unittest.mock import patch

SCRIPTS = Path(__file__).resolve().parents[1] / "scripts"
sys.path.insert(0, str(SCRIPTS))
import ci_test_scope as scope


def package(name, *dependencies):
return {
"name": name,
"manifest_path": f"/workspace/crates/{name}/Cargo.toml",
"dependencies": [{"name": dep} for dep in dependencies],
}


class CargoTestScopeTests(unittest.TestCase):
def setUp(self):
self.metadata = {"packages": [
package("perry-runtime"),
package("perry-stdlib", "perry-runtime"),
package("perry-stdlib-static", "perry-stdlib"),
package("perry-ffi", "perry-runtime"),
package("perry", "perry-ffi"),
package("perry-ext-net", "perry-runtime", "perry-stdlib"),
package("perry-parser"),
]}

def selected(self, *paths, full=False):
output = io.StringIO()
argv = ["ci_test_scope.py"] + (["--full"] if full else [])
with patch.object(scope, "_load_metadata", return_value=self.metadata), \
patch.object(sys, "argv", argv), \
patch.object(sys, "stdin", io.StringIO("\n".join(paths))), \
contextlib.redirect_stdout(output):
self.assertEqual(scope.main(), 0)
return set(output.getvalue().splitlines())

def test_library_target_detection(self):
for kind in ("lib", "rlib", "staticlib", "dylib", "cdylib", "proc-macro",
"bin", "test", "example", "bench", "custom-build"):
with self.subTest(kind=kind):
md = {"packages": [{"name": "subject", "targets": [{"kind": [kind]}]}]}
with patch.object(scope, "_load_metadata", return_value=md), \
patch.object(sys, "stdin", io.StringIO("subject\n")):
expected = 0 if kind in {
"lib", "rlib", "staticlib", "dylib", "cdylib", "proc-macro"
} else 1
self.assertEqual(scope._has_lib_mode(), expected)

def test_runtime_changes_run_stdlib_tests(self):
for path in ("arena/block.rs", "gc/roots.rs", "closure/mod.rs",
"event_pump.rs", "promise.rs"):
with self.subTest(path=path):
selected = self.selected(f"crates/perry-runtime/src/{path}")
self.assertIn("perry-stdlib", selected)
self.assertIn("perry-runtime", selected)
self.assertIn("perry", selected)
self.assertNotIn("perry-ext-net", selected)
self.assertNotIn("perry-parser", selected)

def test_stdlib_direct_change_selects_stdlib_and_driver(self):
selected = self.selected("crates/perry-stdlib/src/common/async_bridge.rs")
self.assertIn("perry-stdlib", selected)
self.assertIn("perry", selected)
self.assertNotIn("perry-ext-net", selected)

def test_extension_direct_change_is_still_selected(self):
self.assertEqual(self.selected("crates/perry-ext-net/src/lib.rs"),
{"perry-ext-net", "perry"})

def test_unrelated_change_does_not_run_stdlib(self):
self.assertEqual(self.selected("crates/perry-parser/src/lib.rs"),
{"perry-parser"})

def test_metadata_only_selects_nothing(self):
self.assertEqual(self.selected("Cargo.toml", "Cargo.lock", "CLAUDE.md"), set())

def test_full_and_infra_still_include_extensions(self):
expected = {p["name"] for p in self.metadata["packages"]} - scope.EXCLUDED
self.assertEqual(self.selected(full=True), expected)
self.assertEqual(self.selected("scripts/ci_test_scope.py"), expected)


if __name__ == "__main__":
unittest.main()
Loading