Skip to content

Build the Rust extension in CI and fail loudly without it (#258) - #269

Merged
leynos merged 9 commits into
mainfrom
ci-build-rust-extension
Aug 4, 2026
Merged

Build the Rust extension in CI and fail loudly without it (#258)#269
leynos merged 9 commits into
mainfrom
ci-build-rust-extension

Conversation

@leynos

@leynos leynos commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Summary

The modules covering the Python/Rust boundary are gated on cuprum._rust_backend_native being importable, and no CI job built it. The test jobs reach the suite through make build, which is only uv sync --group dev, so those modules have skipped on every run since they were written — a green build was indistinguishable from one that never exercised the boundary.

Closes #258.

What changed

make develop is now the single definition of the extension build, used by contributors and CI alike so the two cannot drift. It runs ensurepip before maturin develop, the ordering the benchmark-ratchet job already proved necessary on a fresh virtual environment.

CUPRUM_REQUIRE_RUST_EXTENSION turns a missing extension into a failure rather than a skip. The check is session-level in conftest.py rather than per-module, because the gated modules gate three different ways — fixture, module-level guard, availability probe — and a new module should not be able to opt out of the requirement by skipping differently.

A dedicated extension-tests job builds the extension and runs the gated modules with the guard set.

Why a separate job, not typecheck-test

This is forced, not stylistic, and it is the main thing to review.

With the extension present, cuprum/unittests/test_pipeline.py::test_pipeline_run_sync_failure_semantics trips the file-descriptor close race tracked by #124 (roadmap 8.1.1, still unchecked) and aborts the interpreter:

Fatal Python error: Aborted
  File ".../cuprum/_streams_rs.py", line 92 in rust_pump_stream
  File ".../concurrent/futures/thread.py", line 59 in run

Measured 6 aborts in 8 runs. Installing the extension for the general test run would therefore break CI outright. Until #124 lands, the general run must stay extension-free; widening this job is the natural follow-up.

Results

All six gated modules pass under the new job — 70 tests, none skipped:

test_rust_streams.py, test_rust_streams_boundary_property.py, test_rust_extension.py, test_rust_splice.py, test_rust_errno.py, and the extension-dependent test_backend.py cases.

That includes the four TestRustConsumeStream replacement scenarios, which remain the end-to-end regression coverage for #105.

Guard behaviour verified both ways: without the extension the run fails with an actionable message; with it built, everything passes.

Validation

make check-fmt, make lint, make typecheck, make test all pass in a clean (extension-free) environment; mbake validate Makefile and markdownlint clean; cs delta reports no issues.

Note

Stacked on #268, which is stacked on #241. #268 fixes the one gated test that would otherwise fail this job.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary

  • Build the Rust extension through the shared make develop target.
  • Fail explicitly when CUPRUM_REQUIRE_RUST_EXTENSION=1 is set and the extension is unavailable.
  • Run 70 extension-gated tests in the dedicated extension-tests CI job.
  • Keep extension tests out of the general job to avoid issue #124.
  • Add Windows-target Rust linting through make lint-windows.
  • Validate Makefile and CI wiring with extension build contract tests.
  • Document the workflow and gated modules in the developers’ guide.
  • Address issue #258 by preventing silent test skips.
  • Validate formatting, linting, type checking, Makefile rules, tests, and Markdown.

Walkthrough

Add Windows-target Rust linting. Add make develop and make test-extension. Add a pytest guard that fails when required Rust extension tests cannot load. Add a CI job that builds the extension before running selected tests.

Changes

Rust extension CI coverage

Layer / File(s) Summary
Windows-target Rust linting
Makefile, .github/workflows/ci.yml, rust/cuprum-rust/src/io_utils/mod.rs
Install the Windows Rust target and run cross-target Clippy with the configured PyO3 ABI. Compile the write retry helper only on Unix.
Extension setup and availability guard
Makefile, tests/helpers/extension_requirement.py, conftest.py, cuprum/unittests/test_extension_requirement_guard.py
Build the native extension with maturin develop. Run extension-gated tests with CUPRUM_REQUIRE_RUST_EXTENSION=1. Raise pytest.UsageError when the required extension is unavailable. Test failing and silent cases.
CI workflow and build contract validation
.github/workflows/ci.yml, cuprum/unittests/test_extension_build_contract.py, cuprum/unittests/__snapshots__/test_maturin_build.ambr
Build the extension before gated tests. Route benchmark builds through make develop. Validate Makefile targets, extension test declarations, CI ordering, and packaged test entries.

Possibly related issues

  • leynos/netsuke#518: Adds related Windows-target CI and Makefile lint coverage.

Possibly related PRs

Suggested labels: Issue

Suggested reviewers: codescene-access

Poem

Build the extension now.
Run boundary tests and show how.
Make pytest fail when imports break.
Keep skipped coverage awake.
Let CI verify every link.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Module-Level Documentation ❌ Error conftest.py has a module docstring for shared fixtures only, but the module also defines the session-level pytest_configure extension guard. Update the conftest.py module docstring to describe both shared fixtures and the CUPRUM_REQUIRE_RUST_EXTENSION guard, including its link to the helper module and gated tests.
Out of Scope Changes check ⚠️ Warning The Windows linting configuration and related documentation exceed the linked issue [#258] scope. Move Windows cross-target linting and unrelated documentation changes to a separate pull request, or link an issue that requires them.
Testing (Compile-Time / Ui) ⚠️ Warning PR adds #[cfg(unix)] compile-time directives to rust/cuprum-rust/src/io_utils/mod.rs but lacks a traditional trybuild test. Whilst make lint-windows (Clippy on Windows target) provides static... Add trybuild UI test cases (under tests/ui/) that verify the #[cfg(unix)] and #[cfg(windows)] conditional paths compile correctly, with expected error messages where branches should fail, rather than relying solely on Clippy linting.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes building the Rust extension in CI and includes the linked issue reference (#258).
Description check ✅ Passed The description directly explains the CI extension build, failure guard, dedicated job, and validation results.
Linked Issues check ✅ Passed The PR addresses issue [#258] by building the extension, enforcing the requirement guard, and running the gated boundary tests in CI.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Testing (Overall) ✅ Passed Tests are substantive and rigorous. test_extension_requirement_guard.py covers the decision logic with a full parametric matrix and tests hook invocation; test_extension_build_contract.py reads...
User-Facing Documentation ✅ Passed The changes affect CI, test enforcement, and contributor build workflows, not user-facing library behaviour; the existing users' guide already documents optional Rust extensions.
Developer Documentation ✅ Passed All new build requirements and internal APIs have comprehensive documentation. The developers guide contains detailed "Building the extension for tests" section (120 lines) documenting make develop...
Testing (Unit And Behavioural) ✅ Passed The PR adds meaningful unit tests (test_extension_requirement_guard.py) covering the guard logic, edge cases, and hook behaviour; contract tests (test_extension_build_contract.py) verifying Makefil...
Testing (Property / Proof) ✅ Passed The PR introduces deterministic invariants (guard decision logic, build ordering, module declarations) with finite input spaces and single correct sequences; none span ranges of inputs or states re...
Unit Architecture ✅ Passed Architectural boundaries are preserved. Query functions are pure; fallibility is explicit in API signatures; environment access occurs only at session-level boundary; side-effects isolated to pytes...
Domain Architecture ✅ Passed Domain logic remains segregated from test infrastructure. The PR adds CUPRUM_REQUIRE_RUST_EXTENSION exclusively to conftest.py and tests/helpers/extension_requirement.py—both test-only modules. N...
Observability ✅ Passed Pass this check: the PR changes CI and development test execution, not production operations; the guard and Windows target provide actionable CI-visible failure messages without secrets or unbounde...
Security And Privacy ✅ Passed The PR introduces CUPRUM_REQUIRE_RUST_EXTENSION as a non-secret feature flag to gate tests. No credentials, authentication bypasses, secrets, injection risks, unsafe deserialization, or over-broa...
Performance And Resource Use ✅ Passed All subprocess, file I/O, and pattern matching operations are bounded and cached. Pytest hook runs once per session with O(1) operations. No algorithmic regressions or unbounded resource growth det...
Concurrency And State ✅ Passed The extension build changes introduce no shared mutable state, concurrency issues, or ordering hazards. The pytest_configure hook (session-level) reads immutable environment variables and extension...
Architectural Complexity And Maintainability ✅ Passed Architectural complexity is proportional to the problem. The PR introduces one justified concept (Rust extension requirement guard), eliminates drift with a single build definition, avoids circular...
Rust Compiler Lint Integrity ✅ Passed PR adds documentation only to existing record_read_retry and record_write_retry functions in rust/cuprum-rust/src/io_utils/mod.rs. No lint suppressions, clones, or artificial references added...
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci-build-rust-extension

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

@sourcery-ai

sourcery-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR ensures the Python/Rust boundary tests are actually built and exercised in CI by introducing a shared make develop target to build the native extension, adding a session-level guard that fails when the extension is missing but required, and wiring a dedicated CI job that runs only the extension-gated test modules; it also updates the developer documentation to describe this flow and enumerate the gated modules.

Sequence diagram for extension-gated CI tests with native build and session guard

sequenceDiagram
    actor GitHubActions
    participant CIWorkflow as extension-tests_job
    participant Makefile
    participant PythonEnv
    participant Pytest
    participant RustBackend as _rust_backend

    GitHubActions->>CIWorkflow: start job extension-tests
    CIWorkflow->>CIWorkflow: cargo fetch --locked
    CIWorkflow->>Makefile: make develop
    Makefile->>PythonEnv: uv run python -m ensurepip --upgrade
    Makefile->>PythonEnv: uv run maturin develop --manifest-path cuprum-rust/Cargo.toml
    CIWorkflow->>Pytest: uv run pytest -v (CUPRUM_REQUIRE_RUST_EXTENSION=1)

    Pytest->>Pytest: pytest_configure(config)
    Pytest->>RustBackend: _rust_backend.is_available()

    alt [extension available]
        RustBackend-->>Pytest: True
        Pytest->>Pytest: continue session
        Pytest->>Pytest: run extension-gated modules
    else [extension missing]
        RustBackend-->>Pytest: False
        Pytest->>Pytest: raise pytest.UsageError
        Pytest-->>GitHubActions: job fails (missing cuprum._rust_backend_native)
    end
Loading

File-Level Changes

Change Details Files
Add a shared build target that compiles the PyO3 extension into the dev virtual environment for both local development and CI.
  • Introduce a develop Makefile target that depends on build and then runs ensurepip followed by maturin develop against the Rust project manifest.
  • Document make develop as the canonical way to build the extension for tests in the developer guide, including the rationale for the ensurepip ordering and CI parity.
Makefile
docs/developers-guide.md
Fail loudly when the native extension is required but unavailable, using a single session-level pytest hook controlled by an environment variable.
  • Add a CUPRUM_REQUIRE_RUST_EXTENSION environment-variable gate in conftest.py via pytest_configure that raises pytest.UsageError if the extension is not importable.
  • Explain in documentation how to enable the guard (CUPRUM_REQUIRE_RUST_EXTENSION=1) and why the check is session-level to cover all gating styles across modules.
conftest.py
docs/developers-guide.md
Introduce a dedicated CI job that builds the native extension and runs only the extension-gated test modules under the strict guard.
  • Add an extension-tests job to the CI workflow that installs uv, fetches Cargo dependencies, runs make develop, and executes pytest over the six extension-gated modules.
  • Set CUPRUM_REQUIRE_RUST_EXTENSION=1 for the new job so missing extension builds fail instead of silently skipping tests, and document the interaction with the known test_pipeline.py descriptor race (Rust: fold split-first-iteration splice duplication into one loop; fix drain_reader EINTR handling #124).
  • Extend the developer guide with a table enumerating the extension-gated modules and what each covers.
.github/workflows/ci.yml
docs/developers-guide.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-access[bot]

This comment was marked as outdated.

@leynos leynos changed the title Run the extension-gated tests in CI, and fail if they cannot (#258) Build the Rust extension in CI and fail loudly without it (#258) Jul 31, 2026
@leynos
leynos marked this pull request as ready for review August 1, 2026 02:07

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d26c765464

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added the Issue label Aug 1, 2026

@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: 4

🤖 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/ci.yml:
- Around line 148-152: Update the “Check out repository” and “Install uv”
workflow steps to use immutable 40-character commit SHAs instead of the current
version tags. Add persist-credentials: false to the checkout step’s
configuration.

In `@conftest.py`:
- Around line 48-54: Update the UsageError message in the native backend
validation block to describe the Rust extension as unavailable rather than
saying it could not be imported, covering both import failures and
_rust_backend.is_available() returning False while preserving the existing
guidance.
- Around line 29-42: The pytest_configure docstring currently lacks the required
NumPy-style sections. Update pytest_configure to document config under
Parameters with its pytest.Config type, document pytest.UsageError under Raises,
and move the existing rationale into a Notes section while preserving its
content.

In `@Makefile`:
- Around line 65-73: Shorten the develop target recipe by removing its lengthy
rationale comments, retaining only one concise comment and the two existing
commands: ensurepip and maturin develop. Move the removed explanation to
docs/developers-guide.md, preserving the rationale for dependency syncing,
extension compilation, identical local/CI invocation, and pip availability.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 660c298d-6401-4918-a3d7-538f3747ccff

📥 Commits

Reviewing files that changed from the base of the PR and between 6434c99 and d26c765.

📒 Files selected for processing (4)
  • .github/workflows/ci.yml
  • Makefile
  • conftest.py
  • docs/developers-guide.md
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)

Comment thread .github/workflows/ci.yml Outdated
Comment thread conftest.py
Comment thread conftest.py Outdated
Comment thread Makefile Outdated
@lodyai
lodyai Bot force-pushed the ci-build-rust-extension branch from d26c765 to 804a45f Compare August 1, 2026 12:34
codescene-access[bot]

This comment was marked as outdated.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 4

🤖 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 `@cuprum/unittests/test_extension_requirement_guard.py`:
- Line 78: Update the assertion checking that message is not None to include a
descriptive diagnostic message, following the required assert condition,
"message" format; do not leave it as a bare assertion.
- Around line 133-147: Update the _SilentCase dataclass decorator to enable
slots while preserving its frozen behavior by adding slots=True to the existing
dataclass configuration.

In `@docs/developers-guide.md`:
- Around line 1447-1451: Update the developer guide command following make
develop so it does not present the full make test suite as the extension test
command. Replace it with the selected test command used by extension-tests, or
reference a dedicated safe Make target, while preserving the documented
test_pipeline.py behavior described in Lines 1462-1468.

In `@tests/helpers/extension_requirement.py`:
- Line 16: Update the module containing REQUIRE_EXTENSION_ENV by importing
typing as typ and annotating REQUIRE_EXTENSION_ENV with typ.Final[str],
preserving its existing environment-variable value.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 81457415-5684-4e49-bf4c-e9307b0f34ee

📥 Commits

Reviewing files that changed from the base of the PR and between 3a0ac56 and 804a45f.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • Makefile
  • conftest.py
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_extension_requirement_guard.py
  • docs/developers-guide.md
  • tests/helpers/extension_requirement.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)

Comment thread cuprum/unittests/test_extension_requirement_guard.py Outdated
Comment thread cuprum/unittests/test_extension_requirement_guard.py Outdated
Comment thread docs/developers-guide.md Outdated
Comment thread tests/helpers/extension_requirement.py Outdated
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@buzzybee-df12

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

lodyai Bot pushed a commit that referenced this pull request Aug 3, 2026
The Windows case added in 85bc50c derived its expectations from the very
exception it was testing: it read `winerror` off the raised `OSError`,
built a second `OSError` from that number and the raised `strerror`, and
compared the two. That pins CPython's own derivation, which was never in
doubt, and says nothing about the value the extension supplied. A Rust
side that hard-coded a plausible Win32 code, or reported one taken from
some other failure, would have satisfied every assertion. The pre-merge
review was right to call it circular.

Obtain the expected code from outside the boundary instead. The test now
issues the same failing `ReadFile` through `ctypes` — a Windows stream
handle is a `std::fs::File`, whose `read` is `ReadFile`, so this is the
same system call on the same handle — and reads the code back from
`GetLastError`. `ctypes.WinError` then turns that code into the `OSError`
CPython itself would raise, supplying the expected `errno`, `strerror`,
and subclass. Nothing the extension returns feeds an expectation any
more, and the `[WinError N]` guard names the code rather than accepting
any digits. `use_last_error` keeps the interpreter from overwriting
`GetLastError` before it is read, and explicit `argtypes` stop a
pointer-sized `HANDLE` being truncated to a C int.

Give the POSIX arm an oracle too. `strerror` was only asserted truthy;
it is now compared against `os.strerror(EBADF)`, which CPython looks up
independently of the exception.

Cover the fallback arm, which nothing exercised. `io_error_to_py_err`
hands an `io::Error` carrying no `raw_os_error` to PyO3 unchanged, and
that arm is live in production: the write paths in `io_utils` raise
`ErrorKind::WriteZero` for a write that makes no progress, and such an
error has no number to preserve. Asserting on the built `PyErr` would
need a live interpreter, and the crate is compiled with
`pyo3/extension-module`, so no `cargo test` binary can link one — a probe
fails at link time on undefined `PyExc_*` symbols. Split the decision out
into `raw_os_error_parts`, which is pure, and test that directly for both
`io::Error` representations.

Mutation-verified: making the helper report code 0 instead of no code
fails all three synthesized cases.

Verified the POSIX assertions against a built extension (`maturin
develop`): four pass, the Windows case skips. The Windows arm stays
unexecuted, as no job runs the Python suite on Windows.

No CI job is added here. POSIX extension-gated execution is #269's
`extension-tests` job, and native Windows runtime coverage is
#277; this branch does not touch `ci.yml`, and adding a job
would conflict with #269 on rebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

leynos and others added 6 commits August 4, 2026 03:31
The modules covering the Python/Rust boundary are gated on
`cuprum._rust_backend_native` being importable, and no CI job built it:
the test jobs reach the suite through `make build`, which is only
`uv sync --group dev`. They have skipped on every run since they were
written, so a green build was indistinguishable from one that never
exercised the boundary at all.

Add `make develop` as the single definition of that build, used by both
contributors and CI, so the two cannot drift. It runs `ensurepip` before
`maturin develop`, the ordering the benchmark job already proved
necessary on a fresh virtual environment.

Add a `CUPRUM_REQUIRE_RUST_EXTENSION` guard in `conftest.py` so a missing
extension fails the run instead of skipping. The check is session-level
rather than per-module because the gated modules gate three different
ways — fixture, module-level guard, availability probe — and a new module
should not be able to opt out of the requirement by skipping differently.

Run the gated modules in a dedicated `extension-tests` job rather than
installing the extension for `typecheck-test`. That is forced, not
stylistic: with the extension present, `test_pipeline.py` trips the
descriptor close race tracked by `#124` (roadmap 8.1.1) and aborts the
interpreter in roughly three runs in four. Until that lands, the general
test run must stay extension-free. Widening the job is the follow-up.

All six gated modules pass under the new job — 70 tests, none skipped —
including the four `TestRustConsumeStream` replacement scenarios that
remain the end-to-end regression coverage for `#105`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Five review findings on the extension-test job, none of which changed the
#124 constraint that shaped it.

The guard had no test at all, so deleting it would have left the job
green — the exact failure it exists to prevent. Move the decision into
`tests/helpers/extension_requirement.py` and cover it there: a root
conftest is shadowed by the per-package one and cannot be imported by
name from a test module, which is why this could not simply be tested in
place. The hook is reached through the plugin manager so the raise
itself is covered, not just the message. Mutation-verified: disabling
the raise fails that test.

Also correct the message. `is_available()` can report false without an
`ImportError`, so "could not be imported" would misdescribe that case.

Add the three extension-gated behavioural modules. Without an extension
four of their scenarios skip — confirmed with `pytest -rs` — so they
were never boundary coverage in the ordinary test jobs either, which is
the same reason the unit modules are listed. All 88 selected tests pass
with the extension present, none skipped.

Pin `actions/checkout` and `astral-sh/setup-uv` to the SHAs the rest of
the workflow already uses, rather than the mutable tags this job was
added with. Pin the Rust toolchain and set up Python for the same
reason: the job compiled the extension against whatever the runner image
shipped.

Move the `develop` rationale above the target so its body fits
checkmake's five-line limit, and give `pytest_configure` a NumPy-style
docstring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five review findings on the extension-test job.

The guide told contributors to run
`CUPRUM_REQUIRE_RUST_EXTENSION=1 make test`, and then explained three
paragraphs later that the extension makes the full suite abort the
interpreter. Following the documented command would have done exactly
that.

Fix it where it starts. The module list moves into the Makefile as
`EXTENSION_TEST_TARGETS`, behind a `test-extension` target that sets the
guard; CI and the guide both call the target, so the list has one
definition instead of living inline in the workflow and being restated
in prose. Verified both ways: without the extension the target exits 2
naming `make develop`, and with it 88 tests pass, none skipped.

Add `persist-credentials: false` to the extension-tests checkout. This
job compiles and runs repository code, so the token must not be left in
`.git/config` where that code can reach it. The other job that runs
built code already does this.

Annotate `REQUIRE_EXTENSION_ENV` as `typ.Final[str]`, give `_SilentCase`
`slots=True`, and add the missing message to a bare assertion.

Three further findings were already resolved in the previous round and
needed no change: the actions are SHA-pinned, `pytest_configure` has its
NumPy sections, and the `develop` recipe is two lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The original commit here added a `windows-cross-check` CI job on the
premise that nothing compiled the `cfg(windows)` arm of
`os_error_to_py_err` on a pull request, because `build-wheels.yml` is
`workflow_call` only. That premise is wrong: `ci.yml` calls
`build-wheels.yml` unconditionally, so the `windows-2022` matrix entry
builds the extension natively for `x86_64-pc-windows-msvc` on every pull
request. Run `30699964270` shows that job green on a pull request event.

Drop the job rather than pay for a second, weaker compile of the same
code — a plain `cargo check` with no `-D warnings` — and record the real
mechanism in the developers' guide instead. Keep the local
cross-compilation recipe: checking the Windows arm without a Windows
machine is still useful before pushing.

While here, bring the POSIX bullet up to date. `#258` is no longer "in
flight": `test_rust_errno.py` is one of the `EXTENSION_TEST_TARGETS`, so
the `extension-tests` job runs it with `CUPRUM_REQUIRE_RUST_EXTENSION=1`
and fails when the extension was never built.

Gate the two `EINTR` retry counters on `cfg(unix)`. Only the Unix read
and write paths retry on `EINTR`, so both are dead code on Windows and
the wheel build compiles them there for nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
checkmake counts the comment block above `develop` as part of the target
body and flagged it at eight lines against a limit of five. The rationale
belongs in prose anyway, so move it into "Building the extension for
tests" and leave the recipe pointing there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit gated the two `EINTR` retry counters on `cfg(unix)`
because they are dead code on Windows. Nothing in CI would have caught
that gate being wrong. The Windows wheel build compiles the arm on every
pull request, but it is a plain `maturin build`, so it runs without
`-D warnings`: it catches a Windows arm that fails to compile, and only
that. A misplaced `#[cfg(unix)]` leaves dead code behind, which is a
warning, and the build stays green.

Add `make lint-windows`, which runs Clippy for `x86_64-pc-windows-msvc`
with the same `--all-targets --all-features -- -D warnings` the host
gate uses, and call it from the `lint-test` job after `make lint`.
Clippy type-checks without linking, so the target standard library is
enough and no Windows runner is involved. PyO3 cannot probe an
interpreter for the target, so the Makefile states the ABI version.

Verified non-vacuous by mutation. Dropping `#[cfg(unix)]` from
`record_write_retry` fails the new target with `error: function
record_write_retry is never used`; a type error inside the
`#[cfg(windows)]` arm of `write_all_windows` fails it with `E0308`.
Host `cargo clippy -- -D warnings` passes with that same type error in
place, which is the gap being closed.

Keep it out of `make lint`. The target hard-fails when the Windows
standard library is absent rather than skipping quietly, and making
every contributor install a second standard library before any lint run
is a poor trade for a check this narrow. CI installs the target, which
is where the check has to hold.

This also answers the review's suggestion of a trybuild case, which
cannot cover this: trybuild compiles its fixtures for the host target,
so on a Linux runner it only ever sees the `cfg(unix)` arm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lodyai
lodyai Bot force-pushed the ci-build-rust-extension branch from 02b3b56 to c0e572e Compare August 4, 2026 01:36
codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@pandalump

Copy link
Copy Markdown
Collaborator

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

leynos and others added 2 commits August 4, 2026 15:27
`make develop` was described as the single definition of the extension
build, but `ci.yml`'s `benchmark-ratchet` job still ran the target's exact
three-step sequence inline — `make build`, `ensurepip --upgrade`, then
`maturin develop --manifest-path …` — with `--release` bolted on. Two
copies of the same sequence drift, and the copy the ratchet owns is the
one nobody would notice going stale.

Add `MATURIN_DEVELOP_FLAGS` to the `develop` target and have
`benchmark-ratchet` pass `--release` through it. The flag is the only
thing that job needed differently, so it is now the only thing it states.
It stays empty by default: a debug build is what contributors and the
`extension-tests` job want.

The variable resolves identically from the benchmark job's `pushd
"${workspace}"`, because the Makefile's `UV_ENV` sets the same relative
`UV_CACHE_DIR` and `UV_TOOL_DIR` the inline commands did, against the
same working directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The guard's own tests covered the decision and the `pytest_configure`
hook, but nothing covered the wiring that makes the guard fire at all.
Delete `CUPRUM_REQUIRE_RUST_EXTENSION=1` from the `test-extension`
recipe, drop a module from `EXTENSION_TEST_TARGETS`, or remove the
`make develop` step from the `extension-tests` job, and the suite stayed
green — which left this branch's central claim asserted by nothing.

Add `test_extension_build_contract.py`, which reads the wiring back. It
reads the Makefile through `make --dry-run` rather than by parsing it:
the expanded recipe is the command line CI actually runs, so the guard
variable and the module list are asserted about the same string.

`EXTENSION_TEST_TARGETS` gets two independent checks, because neither
implies the other. A scan derives the gated modules from the suite —
those requesting the `rust_streams` fixture, skipping with the shared
"Rust extension is not installed" reason, or naming
`cuprum._rust_backend_native` — and requires each to be declared; that
is what would notice a *new* gated module being forgotten, which a
hard-coded copy of today's list never would. It covers nine of the ten
entries today with no false positives. The tenth,
`test_extension_requirement_guard.py`, does not gate at all, so it is
named explicitly with the reason it belongs in the job anyway. A third
test fails when any signal stops matching, so a renamed fixture empties
the scan loudly rather than quietly.

The module lives under `cuprum/unittests/` rather than extending
`tests/test_workflow_contract.py`: `PYTEST_TARGETS` globs only
`cuprum/unittests/test_*.py` and `tests/behaviour/test_*.py`, so a
contract test in `tests/` is never run by `make test` and would not gate
anything. It sits beside `test_maturin_pins.py` and
`test_maturin_toolchain.py`, which parse repository config the same way.

Every assertion was checked against the mutation it claims to catch:
removing the guard variable, deleting either a derived or the companion
module, removing the `make develop` step, dropping `--release` from the
benchmark job, breaking the Makefile's flag forwarding, and restoring
the inline `maturin develop` all fail the suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@leynos

leynos commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Both pre-merge findings addressed in 98c6394.

Testing (Overall) — ERROR

Correct: nothing asserted the Makefile or CI wiring. Added
cuprum/unittests/test_extension_build_contract.py (11 tests).

Where it lives. Not an extension of tests/test_workflow_contract.py, and for a
concrete reason: PYTEST_TARGETS globs only cuprum/unittests/test_*.py and
tests/behaviour/test_[a-z]*.py, so tests/test_workflow_contract.py is not run by
make test
at all. A contract test added there would not gate anything. The new module
sits beside test_maturin_pins.py / test_maturin_toolchain.py, which parse repository
config the same way and do run. (Flagging separately: the existing workflow-contract module
is currently ungated by make test — worth a follow-up.)

How the Makefile is read. Through make --dry-run, not by parsing the file. The
expanded recipe line is the command line CI actually runs, so the guard variable and the
module list are asserted about the same string rather than about nearby text.

What the EXTENSION_TEST_TARGETS test pins. Two independent properties, since neither
implies the other:

  1. Derived lower bound (no forgotten module). A scan over all test modules identifies the
    extension-gated ones by three signals — requesting the root rust_streams fixture,
    skipping with the shared "Rust extension is not installed" reason, or naming
    cuprum._rust_backend_native — and requires each to be a declared target. This covers
    9 of the 10 current entries with zero false positives, and is what would notice a
    new gated module being forgotten. It is a textual lower bound: a module gating through
    some other idiom goes unnoticed, and I say so in the module docstring rather than
    claiming completeness.
  2. Companion pin (no derivable reason). The tenth entry,
    test_extension_requirement_guard.py, does not gate at all — running it inside the
    guarded job is what proves the guard stays silent when the extension is present. No
    scan can derive that, so it is named explicitly alongside its reason.

A third test fails when any signal stops matching anything, so a renamed fixture empties the
scan loudly instead of quietly making check 1 vacuous.

Architectural Complexity — WARNING

Also correct, and the diagnosis was exact: benchmark-ratchet ran make develop's three-step
sequence inline with --release bolted on. Added MATURIN_DEVELOP_FLAGS to the develop
target; benchmark-ratchet now runs make develop MATURIN_DEVELOP_FLAGS=--release.

On UV_CACHE_DIR/UV_TOOL_DIR: they are interchangeable here. The Makefile's
UV_ENV is byte-identical (UV_CACHE_DIR=.uv-cache UV_TOOL_DIR=.uv-tools), and both the
inline commands and make resolve those relative paths against the same working directory,
since the call happens after pushd "${workspace}". UV_RUN_ENV additionally prefixes
PATH with $HOME/.local/bin, which is where astral-sh/setup-uv installs uv — a
superset, not a conflict. So the two paths genuinely share one definition.

Non-vacuity

Every assertion was checked against the mutation it claims to catch. Each was applied, the
suite run, and the tree restored.

# Mutation Result Caught by
1 Remove CUPRUM_REQUIRE_RUST_EXTENSION=1 from the test-extension recipe 4 failed ..._recipe_sets_the_guard_variable (+3 downstream)
2a Delete a derived module (test_rust_streams.py) from EXTENSION_TEST_TARGETS 1 failed ..._every_extension_gated_module_is_a_declared_target
2b Delete the companion (test_extension_requirement_guard.py) 1 failed ..._always_run_companions_stay_declared
3 Remove the make develop step from extension-tests 1 failed ..._builds_the_extension_before_running_the_gated_tests
4a Drop --release from benchmark-ratchet 1 failed ..._benchmark_job_builds_through_the_develop_target
4b Stop forwarding $(MATURIN_DEVELOP_FLAGS) in the Makefile 1 failed ..._develop_target_forwards_the_release_flag
4c Restore the inline maturin develop --release in ci.yml 2 failed ..._no_ci_step_invokes_maturin_develop_directly (+1)

Baseline before and after every mutation: 11 passed.

Gates

make check-fmt, make lint, make typecheck, make test, make markdownlint,
make nixie, mbake validate Makefile, and this branch's make lint-windows all green,
run sequentially before each of the two commits. cs delta origin/main HEAD reports no
issues. make test: 816 passed, 52 skipped (the extension-gated skips are expected — the
extension was deliberately not built in this worktree). The maturin wheel-manifest snapshot
was regenerated for the new file.

codescene-access[bot]

This comment was marked as outdated.

codescene-access[bot]

This comment was marked as outdated.

@leynos

leynos commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following failed checks now been resolved?

If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures.

Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already. (Treat o11y, code safety, documentation and validation coverage as in scope).

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Guard tests substantively cover the decision and hook, but no tests verify Makefile or CI wiring, so removing the target's required environment or gated module list would still pass. Add Makefile/CI contract or integration tests that assert make develop ordering, test-extension sets the guard, and all required modules remain in the job.
Architectural Complexity And Maintainability ⚠️ Warning The new make develop path does not provide the claimed single extension-build definition: ci.yml still directly runs maturin develop --release in benchmark-ratchet. Parameterize develop for release flags and make benchmark-ratchet call that target, so all extension builds share one maintained command path.

@coderabbitai

This comment was marked as resolved.

@leynos

leynos commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
✅ Action performed

Reviews resumed.

@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

🤖 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 `@cuprum/unittests/test_extension_build_contract.py`:
- Around line 165-183: Replace typ.Any in _workflow() and _job_steps() with
shared TypedDict models (or a recursive YAML value alias) representing the
workflow, jobs, job steps, and step objects, including the string-keyed run
field. Keep the existing isinstance validation, then use cast or structured
narrowing to return the typed values and reuse these shapes in _run_scripts()
and _step_index_running(); run make typecheck to verify the annotations.
- Around line 77-95: Update the environment filter in _dry_run to exclude
MAKEOVERRIDES alongside the existing Make variables, ensuring nested make runs
use repository defaults; also replace typ.Any with object in the return
annotations of _workflow and _job_steps to satisfy strict typing while
preserving runtime YAML validation.

In `@docs/developers-guide.md`:
- Around line 1653-1659: Update the CI build scope sentence in the documented
extension-build section to exclude the separate wheel job, stating that every CI
development and test extension build uses this target while preserving the
surrounding benchmark-ratchet and MATURIN_DEVELOP_FLAGS guidance.
🪄 Autofix

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 68b4f7c4-1b3c-41e7-8cd3-6160a8679427

📥 Commits

Reviewing files that changed from the base of the PR and between c0e572e and 98c6394.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • Makefile
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_extension_build_contract.py
  • docs/developers-guide.md
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/shared-actions (auto-detected)
  • leynos/pylint-pypy-shim (auto-detected)
  • leynos/whitaker (auto-detected)

Comment thread cuprum/unittests/test_extension_build_contract.py
Comment thread cuprum/unittests/test_extension_build_contract.py Outdated
Comment thread docs/developers-guide.md Outdated
`make` exports each of its command-line variable overrides into every
recipe's environment under that variable's own name, and a `?=`
assignment yields to a name already in the environment. The contract
module shells out to `make --dry-run` and stripped only `MAKEFLAGS`,
`MAKELEVEL` and `MFLAGS`, so the override arrived by the other route and
the nested `make` reported the caller's configuration as the
repository's. Measured, on GNU Make 4.4.1: run as
`make … EXTENSION_TEST_TARGETS=test_extension_requirement_guard.py` the
module went 11 passed to 1 failed, and as
`make … MATURIN_DEVELOP_FLAGS=--release` the debug-default assertion
failed. Bare `pytest` passed in both cases — the contract held or broke
according to how the suite was invoked.

Scrub every name the Makefile declares with `?=`, read back from the
Makefile so the set cannot go stale as variables come and go, and pin
the behaviour with a test that plants an override and requires it not to
reach the nested `make`. `MAKEOVERRIDES` is not the route: GNU Make does
not place it in a recipe's environment at all, so filtering it changes
nothing.

Split the CI half of the contract into `test_extension_ci_contract.py`,
along the seam of what each module reads — the Makefile, or `ci.yml`.
The split is what makes room under the 400-line cap, and the new module
declares the workflow shapes it reads (jobs, steps, and a step's `run:`)
in place of `typing.Any`, so a misspelled key is a type error rather
than a `None` that quietly satisfies the assertion above it. Step
*values* stay `object`: they come from a file this suite does not
control, so they are narrowed where they are read.

Correct the guide's claim that every extension build in CI goes through
`make develop`. The wheel build does not: `build-wheels.yml` runs
`maturin build` for a distributable artefact, never installing into a
virtual environment. What is true is that every CI job which installs
the extension and then runs against it goes through the target —
`extension-tests` and `benchmark-ratchet`, and no others.

All seven mutations this contract exists to catch still fail: dropping
the guard variable, dropping either a derived or the companion module
from `EXTENSION_TEST_TARGETS`, removing the `make develop` step,
dropping `--release` from the benchmark job, breaking the flag
forwarding, and restoring the inline `maturin develop`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codescene-access[bot]

This comment was marked as outdated.

@codescene-access codescene-access 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.

No quality gates enabled for this code.

@leynos

leynos commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following failed checks now been resolved?

If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures.

Do not treat warnings as optional or aspirational. Where a change is out of scope for this PR, propose a GitHub issue unless one exists already. (Treat o11y, code safety, documentation and validation coverage as in scope).

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Guard tests substantively cover the decision and hook, but no tests verify Makefile or CI wiring, so removing the target's required environment or gated module list would still pass. Add Makefile/CI contract or integration tests that assert make develop ordering, test-extension sets the guard, and all required modules remain in the job.
Architectural Complexity And Maintainability ⚠️ Warning The new make develop path does not provide the claimed single extension-build definition: ci.yml still directly runs maturin develop --release in benchmark-ratchet. Parameterize develop for release flags and make benchmark-ratchet call that target, so all extension builds share one maintained command path.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== PR branch HEAD =='
git rev-parse --short HEAD

printf '%s\n' '== Relevant Makefile and CI definitions =='
rg -n -C 5 \
  -e '^(MATURIN_DEVELOP_FLAGS|EXTENSION_TEST_TARGETS)\s*[:?+]?=' \
  -e '^(develop|test-extension):' \
  -e '\bmake\s+develop\b' \
  -e '\bmaturin\s+develop\b' \
  -e 'CUPRUM_REQUIRE_RUST_EXTENSION' \
  Makefile .github/workflows/ci.yml

contract_test="$(fd -t f '^test_extension_build_contract\.py$' cuprum)"
printf '\n== Contract test outline ==\n'
ast-grep outline "$contract_test" --items all

printf '\n== Contract test assertions and relevant helpers ==\n'
rg -n -C 4 \
  -e 'test_extension_recipe_sets_the_guard_variable' \
  -e 'test_every_extension_gated_module_is_a_declared_target' \
  -e 'test_always_run_companions_stay_declared' \
  -e 'test_extension_job_builds_the_extension_before_running_the_gated_tests' \
  -e 'test_benchmark_job_builds_through_the_develop_target' \
  -e 'test_develop_target_forwards_the_release_flag' \
  -e 'test_no_ci_step_invokes_maturin_develop_directly' \
  "$contract_test"

Length of output: 11258


🏁 Script executed:

#!/bin/bash
set -euo pipefail

contract_test="cuprum/unittests/test_extension_build_contract.py"

printf '%s\n' '== Extension-tests workflow commands =='
rg -n -C 12 \
  -e '^  extension-tests:' \
  -e 'run: make (develop|test-extension)' \
  .github/workflows/ci.yml

printf '%s\n' '== Remaining contract test =='
sed -n '285,390p' "$contract_test"

printf '%s\n' '== CI workflow references from contract test =='
rg -n -C 6 \
  -e 'ci\.yml|workflow|extension-tests|make test-extension|make develop|maturin develop' \
  "$contract_test"

Length of output: 11487


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ci_contract="$(fd -t f '^test_extension_ci_contract\.py$' cuprum)"
printf '%s\n' '== CI contract test outline =='
ast-grep outline "$ci_contract" --items all

printf '%s\n' '== CI contract test =='
cat -n "$ci_contract"

printf '%s\n' '== Collection path =='
rg -n -C 5 \
  -e '^PYTEST_TARGETS\s*[:?+]?=' \
  -e 'cuprum/unittests/test_\*\.py' \
  Makefile

printf '%s\n' '== Direct CI maturin develop invocations =='
rg -n -C 3 '\bmaturin\s+develop\b' .github/workflows/ci.yml

Length of output: 9244


@leynos: Mark both findings as resolved.

  • Testing (Overall): Resolved. test-extension sets CUPRUM_REQUIRE_RUST_EXTENSION=1 and uses EXTENSION_TEST_TARGETS. cuprum/unittests/test_extension_build_contract.py verifies the guard, declared targets, extension-gated modules, companion target, and develop ordering. cuprum/unittests/test_extension_ci_contract.py verifies that extension-tests runs make develop before make test-extension. Both test modules are collected by PYTEST_TARGETS.

  • Architectural Complexity And Maintainability: Resolved. Makefile forwards MATURIN_DEVELOP_FLAGS to maturin develop. benchmark-ratchet runs make develop MATURIN_DEVELOP_FLAGS=--release. The CI contract test rejects every direct maturin develop invocation in .github/workflows/ci.yml.

Do not create a follow-up issue. Do not create an AI agent prompt. No remaining work is required for these findings.

Static inspection verified PR HEAD 3a6825e. The sandbox did not execute the test suite.

You are interacting with an AI system.

@leynos
leynos merged commit ccc7b5a into main Aug 4, 2026
21 checks passed
@leynos
leynos deleted the ci-build-rust-extension branch August 4, 2026 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extension-gated Python tests never run in CI: no test job builds the Rust extension

3 participants