Skip to content

ci: run the integration suites a PR's diff names (#5960)#6338

Open
proggeramlug wants to merge 2 commits into
mainfrom
fix/5960-ci-integration-suite-gate
Open

ci: run the integration suites a PR's diff names (#5960)#6338
proggeramlug wants to merge 2 commits into
mainfrom
fix/5960-ci-integration-suite-gate

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #5960 (suggestion 3, plus a scoped version of suggestion 2).

The hole

The per-PR cargo-test gate runs --lib --bins. That means no crates/*/tests/*.rs integration suite has ever executed on a pull request — not perry's 163, not perry-hir's 30, not perry-codegen's 17. The suites appear in the scope listing (their crate is in scope), they are compiled, and they are never run.

The sharp consequence, in the issue's words: a PR's own new acceptance suite cannot fail its own CI. #5938 landed with capture_rereg_renamed_class.rs — the suite the PR itself added — red at the very commit that introduced it, through green required checks.

What this adds

A new PR-only e2e-scoped job plus scripts/ci_e2e_scope.py, which maps a diff to the suites it names and runs exactly those:

changed path runs
crates/<pkg>/tests/<suite>.rs cargo test -p <pkg> --test <suite>
crates/<pkg>/tests/<suite>/<file>.rs (module dir, e.g. native_proof_regressions/) the parent suite
crates/<pkg>/tests/common/…, tests/fixtures/… (no sibling <name>.rs) every suite in that crate
anything else (incl. plain src/ changes) nothing

Details: deleted suites are skipped (the target is gone); cross-host UI crates are excluded (shares EXCLUDED with ci_test_scope.py); the selection is capped at 12 so a mass rename of suites can't turn one PR into a multi-hour run (overflow is reported and left to the nightly); each suite gets a 25-min timeout so one hung perry compile can't eat the budget and hide the other suites' results; libperry_{runtime,stdlib}.a are built up front when a perry/perry-stdlib suite is selected, because cargo test never builds the staticlib crate-type and the PERRY_NO_AUTO_OPTIMIZE=1 suites link those archives directly.

--self-test covers the mapping table above and runs as the first line of the job.

Cost

The scope step parses crates/<dir>/Cargo.toml directly instead of shelling out to cargo metadata, specifically so it can run before any toolchain is installed:

  • PR names no integration suite (the large majority — e.g. fix(codegen): scalar replacement ignored own-method writes and prototype mutation (#5872) #6324's diff maps to zero): checkout + one Python call. ~20–30s, no toolchain, no cache restore, no build. Every later step is if: steps.scope.outputs.suites != ''.
  • PR adds or edits a suite: it pays a build, but in a parallel job, and cargo-test (50–60 min warm) remains the longer pole. Added wall-clock on the PR's critical path: ~0 for most PRs; roughly 0–30 min in the worst case of a suite-heavy diff whose e2e build outruns cargo-test.

The job always produces a check run on PRs (it exits early rather than being if-skipped on the diff), so it is safe to add to the required-checks list — worth doing, otherwise the gate is advisory.

What this would and would not have caught

Honest accounting, since the point is to close a specific hole and not to claim more:

Concurrency (issue suggestion 2)

The starvation vector described in the issue is no longer live: pushes to main don't trigger this workflow, so there is no main-push run to be cancelled by a merge train, and PR runs live under refs/pull/N/merge (their own group). The residual vector is narrow but real — a manual workflow_dispatch on main shares refs/heads/main with the cron and would cancel a nightly mid-flight (the last nightlies ran 1h0m–1h53m).

So: keep cancel-in-progress for PR pushes (that's where the minutes are) and turn it off everywhere else, with the event in the group key.

concurrency:
  group: test-${{ github.event_name }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

Two lines, no behaviour change for PRs, and the nightly + release-tag runs can no longer be cancelled. Given the nightly is now formally the only backstop for the #6037 class, it must always reach a conclusion.

Notes

  • Nightly lint is also red, on the Address-classification audit step — unrelated to this change, but it means the nightly is red on two independent counts right now.
  • Suggestion 1 (merge-train discipline) is process; suggestion 4 (nightly backstop) already exists.

Validation

Summary by CodeRabbit

  • CI Improvements
    • Updated pull request CI cancellation behavior to cancel only superseded runs for the same event/ref.
    • Added an e2e-scoped job to run only the end-to-end suites affected by pull request changes.
    • CI now avoids unnecessary toolchain/cache/build work when no suites are in scope, evicts stale archives, conditionally builds required artifacts, and applies per-suite timeouts.
  • Testing
    • Introduced an automated suite-selection tool that determines which integration suites to run based on changed files, with built-in self-validation.

The per-PR `cargo-test` gate runs `--lib --bins` only, so no `tests/*.rs`
integration suite has ever executed on a pull request — including the PR's
own new acceptance suite. #5938 landed with `capture_rereg_renamed_class.rs`
red at the very commit that introduced it, through green required checks.

New `e2e-scoped` job (PR-only) + `scripts/ci_e2e_scope.py`: map the changed
file list to the suites it names and run exactly those.

  crates/<pkg>/tests/<suite>.rs      -> cargo test -p <pkg> --test <suite>
  crates/<pkg>/tests/<suite>/mod.rs  -> the parent suite
  crates/<pkg>/tests/common/...      -> every suite in that crate (capped)

Deleted suites are skipped, cross-host UI crates are excluded (shared
`EXCLUDED` with ci_test_scope), and the selection is capped at 12 so a mass
rename can't turn one PR into a multi-hour run. Manifest names are parsed
straight out of Cargo.toml, not `cargo metadata`, so the scope step needs no
toolchain: a PR that names no suite finishes in ~20-30s and installs nothing.
When a suite IS named, the build runs in parallel with cargo-test (the longer
pole), so added wall-clock on the critical path is ~0 for most PRs.

Also: the concurrency group now keys on the event and only cancels in-progress
runs for `pull_request`. The nightly full run is the only backstop for a
regression in a suite the diff doesn't name (#6037 class), so it must always
reach a conclusion; keeping it cancellable by a manual dispatch on main was a
loose end. Superseded PR pushes still cancel, which is where the minutes are.

Self-test: `python3 scripts/ci_e2e_scope.py --self-test` (runs in the job).
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5453c56e-52ce-4869-b669-9ef57c85708b

📥 Commits

Reviewing files that changed from the base of the PR and between aa21bef and 469c8bb.

📒 Files selected for processing (1)
  • .github/workflows/test.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/test.yml

📝 Walkthrough

Walkthrough

The PR adds diff-based integration-suite selection and a dedicated pull-request E2E job. It also updates workflow concurrency behavior and documents that scoped acceptance suites run outside the cargo-test lib/bin gate.

Changes

Scoped E2E CI

Layer / File(s) Summary
Diff-based suite selection
scripts/ci_e2e_scope.py
Maps changed test paths to available suites, applies exclusions and a default cap, and includes self-tests for selection behavior.
Scoped E2E job execution
.github/workflows/test.yml
Adds a pull-request-only job that selects suites, conditionally prepares tooling and artifacts, and runs each suite with timeout and aggregate status tracking.
Workflow concurrency policy
.github/workflows/test.yml
Keys concurrency by event and ref, and only cancels in-progress pull-request runs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PullRequest
  participant GitHubActions
  participant ScopeScript
  participant Cargo
  PullRequest->>GitHubActions: provide changed paths
  GitHubActions->>ScopeScript: compute scoped suites
  ScopeScript-->>GitHubActions: return package-suite pairs
  GitHubActions->>Cargo: prepare and run selected tests
  Cargo-->>GitHubActions: return aggregate suite status
Loading

Possibly related issues

  • PerryTS/perry issue 5960 — The PR implements pull-request execution of changed integration suites and updates concurrency behavior for main-branch runs.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: running integration suites based on the PR diff.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/5960-ci-integration-suite-gate

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
.github/workflows/test.yml (2)

515-543: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Toolchain/sccache/cache/eviction setup is duplicated verbatim between cargo-test and e2e-scoped.

Steps at Lines 515-543 (install toolchain, install sccache, cache sccache objects, rust-cache, evict stale auto-opt archives) mirror the existing cargo-test job's steps almost line-for-line. Given the eviction step exists specifically to avoid a repeat of a hard-to-diagnose bug (#5892), duplicating it across two jobs risks the fix drifting out of sync if it's ever updated in only one place. A composite action (.github/actions/setup-perry-build/action.yml) would let both jobs share this logic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 515 - 543, Extract the duplicated
Rust build setup steps—Install Rust toolchain, Install sccache, Cache sccache
objects, rust-cache, and Evict stale auto-opt archives—into a composite action
at .github/actions/setup-perry-build/action.yml. Update both the cargo-test and
e2e-scoped jobs to invoke that action with equivalent conditions and inputs,
preserving the existing cache keys, save behavior, and eviction command so the
shared setup cannot drift.

519-536: 🔒 Security & Privacy | 🔵 Trivial

Cache-restore steps flagged for cache-poisoning risk (sccache-action, actions/cache, Swatinem/rust-cache).

zizmor flags all three as always-restoring/enabled-by-default caches. This is the same pattern already used by the cargo-test job (unchanged), so it's an inherited risk rather than a new regression — but since it's now duplicated into a second job that also compiles PR-controlled code, it's worth revisiting as a pair: e.g. confirm save-if scoping is sufficient (this job's save-if: ${{ github.ref == 'refs/heads/main' }} is trivially always false here since the job only runs on pull_request, so it only ever restores — which is arguably the safe outcome, just worth being intentional about rather than incidental).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yml around lines 519 - 536, Make the cache
configuration for Install sccache, Cache sccache objects, and
Swatinem/rust-cache explicitly safe for pull_request runs by preventing
restoration of caches for untrusted PR-controlled code, rather than relying only
on save-if. Preserve cache use for trusted main-branch builds where appropriate,
and apply the same intentional restore policy consistently across all three
steps.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Around line 489-490: Update the actions/checkout step in the affected test job
to set persist-credentials to false. Apply the same checkout hardening to the
existing cargo-test job checkout as well, without changing the build or test
steps.
- Around line 476-578: Increase the e2e-scoped job’s timeout-minutes to cover
the maximum serial runtime of all suites allowed by DEFAULT_CAP, using the
1500-second per-suite timeout plus sufficient setup and build buffer. Keep the
per-suite timeout and failure reporting in “Run scoped integration suites”
unchanged, and ensure the job-level timeout remains synchronized with those
limits.

In `@scripts/ci_e2e_scope.py`:
- Around line 78-123: The suite discovery in _suites_of currently misses Cargo
integration-test targets using tests/<name>/main.rs, causing changes there to
select all suites while omitting <name>. Update _suites_of to include
directory-style targets whose tests/<name>/main.rs exists, and extend _self_test
to verify discovery and selection of that suite without changing existing
top-level-file or shared-helper behavior.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Around line 515-543: Extract the duplicated Rust build setup steps—Install
Rust toolchain, Install sccache, Cache sccache objects, rust-cache, and Evict
stale auto-opt archives—into a composite action at
.github/actions/setup-perry-build/action.yml. Update both the cargo-test and
e2e-scoped jobs to invoke that action with equivalent conditions and inputs,
preserving the existing cache keys, save behavior, and eviction command so the
shared setup cannot drift.
- Around line 519-536: Make the cache configuration for Install sccache, Cache
sccache objects, and Swatinem/rust-cache explicitly safe for pull_request runs
by preventing restoration of caches for untrusted PR-controlled code, rather
than relying only on save-if. Preserve cache use for trusted main-branch builds
where appropriate, and apply the same intentional restore policy consistently
across all three steps.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1d696092-7ab3-405b-95e8-b3d10dab8a50

📥 Commits

Reviewing files that changed from the base of the PR and between 735e894 and aa21bef.

📒 Files selected for processing (2)
  • .github/workflows/test.yml
  • scripts/ci_e2e_scope.py

Comment thread .github/workflows/test.yml
Comment thread .github/workflows/test.yml
Comment thread scripts/ci_e2e_scope.py
Comment on lines +78 to +123
def _suites_of(root: str, crate_dir: str):
"""Every `tests/*.rs` integration target of a crate, by suite name."""
tests_dir = os.path.join(root, "crates", crate_dir, "tests")
try:
entries = os.listdir(tests_dir)
except OSError:
return []
return sorted(
e[:-3]
for e in entries
if e.endswith(".rs") and os.path.isfile(os.path.join(tests_dir, e))
)


def select(changed, root: str):
"""-> sorted list of (package, suite) named by the changed paths."""
selected = set()
for path in changed:
m = _TESTS_PATH.match(path.strip())
if not m:
continue
crate_dir, rest = m.group(1), m.group(2)
pkg = _package_name(root, crate_dir)
if pkg is None or pkg in EXCLUDED:
continue

if "/" not in rest:
if not rest.endswith(".rs"):
continue
suite = rest[:-3]
# Skip deletions: the target no longer exists.
if os.path.isfile(os.path.join(root, "crates", crate_dir, "tests", rest)):
selected.add((pkg, suite))
continue

head = rest.split("/", 1)[0]
sibling = os.path.join(root, "crates", crate_dir, "tests", head + ".rs")
if os.path.isfile(sibling):
# Module directory of `<head>.rs`.
selected.add((pkg, head))
else:
# Shared helper / fixture dir (`common/`, `fixtures/`): any suite in
# the crate can depend on it.
for suite in _suites_of(root, crate_dir):
selected.add((pkg, suite))
return sorted(selected)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby tests without executing repo code.
git ls-files scripts/ci_e2e_scope.py
wc -l scripts/ci_e2e_scope.py
sed -n '1,220p' scripts/ci_e2e_scope.py

# Look for self-tests or references to the suite selection logic.
rg -n "_self_test|select\\(|_suites_of\\(|tests/<name>/main\\.rs|main\\.rs" scripts tests .github -g '!**/node_modules/**'

Repository: PerryTS/perry

Length of output: 12214


🌐 Web query:

Cargo Book integration tests tests/<name>/main.rs standalone target directory-style main.rs

💡 Result:

In Cargo, integration tests are defined in files located within the tests/ directory at the root of your project [1][2][3]. When you run cargo test, Cargo automatically discovers these files and compiles each one as a separate, standalone crate that links against your library's public API [1][4][3]. Regarding the specific structure tests//main.rs, while the standard convention is to place integration tests directly in the tests/ directory (e.g., tests/my_test.rs), Cargo also supports using subdirectories for complex integration tests [4][5]. If you create a file at tests//main.rs, Cargo will treat it as a distinct integration test target [1][4]. Key details about this structure include: 1. Standalone Compilation: Each file in the tests/ directory (including those in subdirectories like tests//main.rs) is compiled as an independent crate [1][4][3]. Because these act as separate crates, they can only access the public items of your library [1][4][6]. 2. Directory-Style Integration Tests: Placing a main.rs inside a subdirectory (e.g., tests/my_integration_test/main.rs) is a valid way to organize tests that require their own local modules or supporting files [5][6]. This allows you to keep supporting code within that subdirectory, preventing it from being incorrectly treated as a separate, standalone integration test by Cargo [5][6][3]. 3. Target Directory: Cargo typically builds all targets in a central target/ directory [7][1]. The term standalone target directory usually refers to the isolation mechanism where each integration test is compiled independently, or sometimes to specific build patterns used in advanced CI scenarios or distribution packaging to avoid workspace-level interference [8][9][3]. It is important to note that integration tests require a library crate (src/lib.rs) to function effectively [4][3]. If your project is a pure binary crate (only src/main.rs), it does not expose a public API for other crates to use, which is why integration tests are generally not supported for projects lacking a library component [4][3].

Citations:


🌐 Web query:

site:doc.rust-lang.org cargo integration tests subdirectory main.rs tests/<name>/main.rs

💡 Result:

Yes, Cargo supports organizing integration tests into subdirectories within the tests directory. If an integration test consists of multiple source files, you can place a main.rs file (along with any necessary additional modules) inside a subdirectory of the tests directory, such as tests//main.rs [1]. In this structure, Cargo automatically treats the directory name () as the name of the integration test target [1]. When you run cargo test, Cargo compiles the files in this directory as an individual crate [1][2]. This approach allows you to organize complex integration tests into multiple files while maintaining the standard Cargo testing workflow [1].

Citations:


Handle tests/<name>/main.rs as a suite target

tests/<name>/main.rs is a valid Cargo integration-test target named after the directory, but _suites_of() only sees top-level tests/*.rs files. A change under that layout currently falls into the shared-dir branch, selects every suite in the crate, and still omits <name> itself. Add the directory-style target to _suites_of() and cover it in _self_test().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci_e2e_scope.py` around lines 78 - 123, The suite discovery in
_suites_of currently misses Cargo integration-test targets using
tests/<name>/main.rs, causing changes there to select all suites while omitting
<name>. Update _suites_of to include directory-style targets whose
tests/<name>/main.rs exists, and extend _self_test to verify discovery and
selection of that suite without changing existing top-level-file or
shared-helper behavior.

The job capped out at 90 minutes while allowing up to 12 suites (DEFAULT_CAP)
each bounded by `timeout 1500` (25 min) — a 300-minute worst case. A
legitimately-slow selection would hit the job wall clock and GitHub would
cancel the whole job, losing every suite's result behind an uninformative
"cancelled" instead of the per-suite ::error:: — reintroducing, one level up,
exactly the failure mode the per-suite timeout exists to prevent.

Derived the job timeout from the two bounds it has to cover (12 x 25 + 30 for
toolchain/cache/staticlib build = 330) and documented the invariant so the
three numbers can't drift apart. It's a backstop, not a budget: each suite is
independently bounded and the common case selects zero suites and exits in
~20-30s.

Also drop credential persistence from this job's checkout — it compiles
third-party crates (build scripts run) and nothing in it pushes back to the
repo.

Found by review.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI: PR cargo-test never executes crates/perry integration suites; main-push full run starves under merge trains (documented near-miss)

1 participant