Skip to content

Property tests for subprocess timeout & fail-fast reducers (#75) - #245

Open
leynos wants to merge 2 commits into
mainfrom
python-subprocess-timeout-tests
Open

Property tests for subprocess timeout & fail-fast reducers (#75)#245
leynos wants to merge 2 commits into
mainfrom
python-subprocess-timeout-tests

Conversation

@leynos

@leynos leynos commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

The subprocess timeout handler and the pipeline fail-fast cleanup are temporal and branch-heavy, with a risk of inconsistent timeout payloads, non-idempotent cleanup, and double-termination scheduling — and no isolated seam for their decisions (#75). This extracts two pure reducers and property-tests them without process state or a clock.

Seams

_resolve_timeout_payload(exc, _TimeoutFallback) (cuprum._subprocess_timeout) — the timeout-payload seam:

  • a _SubprocessTimeoutError carries a payload captured on the stream-timeout path, used verbatim;
  • a bare TimeoutError is resolved from a _TimeoutFallback (configured timeout, captured stdout/stderr, injected exit-time clock reading);
  • either branch yields a concrete timeout, so downstream TimeoutExpired reporting is consistent.

_handle_subprocess_timeout now calls it and keeps the exit-event emit and the raise.

_stages_to_terminate(failure_index, done) (cuprum._process_lifecycle) — the fail-fast selection: every stage except the failed one and any already-finished stage, each at most once. _terminate_pipeline_remaining_stages drives it (keeping the strict-length zip for scheduling).

Tests

cuprum/unittests/test_subprocess_timeout_reducers.py (Hypothesis) proves the three #75 goals:

  • Timeout payload consistency — the carried payload wins and never consults the fallback; a bare timeout resolves exactly from the fallback (timeout/clock/stdout/stderr); a missing configured timeout is an internal invariant error.
  • No double-termination scheduling — the selection is exactly the running, non-failed stages, unique and ordered, never the failure index.
  • Idempotent cleanup — once the selected stages settle, a second pass selects nothing.

Validation

Full gates green: make check-fmt, make lint (ruff, interrogate 100%, pylint 10.00/10), make test (825 passed / 58 skipped incl. existing timeout & pipeline behaviour suites; Rust nextest 57/57). Wheel-manifest snapshot regenerated.

Closes #75

🤖 Generated with Claude Code

Summary by Sourcery

Extract pure reducers for subprocess timeout payload resolution and fail-fast pipeline termination, and property-test their behaviour for consistency and idempotence.

New Features:

  • Introduce a _TimeoutFallback data structure and _resolve_timeout_payload reducer to unify subprocess timeout payload resolution.
  • Introduce a _stages_to_terminate reducer to select pipeline stages for termination after fail-fast.

Enhancements:

  • Refactor _handle_subprocess_timeout to delegate payload resolution to a pure reducer for consistent timeout reporting.
  • Refactor _terminate_pipeline_remaining_stages to use a precomputed termination target set, avoiding double-scheduling and ensuring idempotent cleanup.

Tests:

  • Add Hypothesis-based property tests for subprocess timeout payload resolution and fail-fast termination selection reducers.

The subprocess timeout and pipeline fail-fast paths are temporal and
branch-heavy, with no isolated seam for their decisions. Extract two pure
reducers so they can be property-tested without process state or a clock.

_subprocess_timeout.py: _resolve_timeout_payload(exc, _TimeoutFallback)
is the timeout-payload seam. A _SubprocessTimeoutError carries a payload
that is used verbatim; a bare TimeoutError is resolved from a
_TimeoutFallback (configured timeout, captured stdout/stderr, injected
exit-time clock). Either branch yields a concrete timeout, so downstream
TimeoutExpired reporting is consistent. _handle_subprocess_timeout now
calls it and keeps the event-emit and raise side effects.

_process_lifecycle.py: _stages_to_terminate(failure_index, done) selects
which stages get a termination task — every stage except the failed one
and any already-finished stage, each at most once.
_terminate_pipeline_remaining_stages drives it (keeping the strict-length
zip for scheduling).

Add cuprum/unittests/test_subprocess_timeout_reducers.py with Hypothesis
property tests proving: timeout-payload consistency (carried payload wins
and ignores the fallback; bare timeouts resolve from the fallback; a
missing configured timeout is an invariant error), the fail-fast
selection is exactly the running non-failed stages with no double
scheduling, and cleanup is idempotent (a second pass over settled stages
selects nothing).

Regenerate the maturin wheel-manifest snapshot for the new test file.

Closes #75

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

@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

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 76e264c7-e9c4-423e-9137-de278da7efb6

📥 Commits

Reviewing files that changed from the base of the PR and between 302858c and 2d30f10.

📒 Files selected for processing (4)
  • cuprum/_process_lifecycle.py
  • cuprum/_subprocess_timeout.py
  • cuprum/unittests/__snapshots__/test_maturin_build.ambr
  • cuprum/unittests/test_subprocess_timeout_reducers.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch python-subprocess-timeout-tests

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

@sourcery-ai

sourcery-ai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Extracts two pure reducer helpers for subprocess timeout payload resolution and pipeline fail-fast termination selection, updates the timeout handler and termination logic to use them, and adds Hypothesis-based property tests to validate timeout payload consistency, no double-termination, and idempotent cleanup.

Sequence diagram for updated subprocess timeout handling

sequenceDiagram
    participant Caller
    participant _handle_subprocess_timeout
    participant _resolve_timeout_payload
    participant _emit_exit_event
    participant _raise_timeout_expired
    participant _get_exit_code

    Caller->>_handle_subprocess_timeout: exc, ctx
    _handle_subprocess_timeout->>_resolve_timeout_payload: exc, _TimeoutFallback
    _resolve_timeout_payload-->>_handle_subprocess_timeout: _SubprocessTimeoutDetails payload
    _handle_subprocess_timeout->>_get_exit_code: ctx.process
    _get_exit_code-->>_handle_subprocess_timeout: exit_code
    _handle_subprocess_timeout->>_emit_exit_event: _SubprocessExitEvent
    _handle_subprocess_timeout->>_raise_timeout_expired: _TimeoutContext, exc
    _raise_timeout_expired-->>Caller: TimeoutExpired (exception)
Loading

Sequence diagram for updated fail-fast pipeline termination selection

sequenceDiagram
    participant Caller
    participant _terminate_pipeline_remaining_stages
    participant _stages_to_terminate
    participant asyncio_create_task as asyncio.create_task
    participant _terminate_process_via_wait_task
    participant asyncio_gather as asyncio.gather

    Caller->>_terminate_pipeline_remaining_stages: processes, wait_tasks, failure_index, cancel_grace
    _terminate_pipeline_remaining_stages->>_stages_to_terminate: failure_index, [wait_task.done()]
    _stages_to_terminate-->>_terminate_pipeline_remaining_stages: list[int] targets
    loop for each idx in targets
        _terminate_pipeline_remaining_stages->>asyncio_create_task: _terminate_process_via_wait_task(process, wait_task, cancel_grace)
        asyncio_create_task->>_terminate_process_via_wait_task: process, wait_task, cancel_grace
    end
    _terminate_pipeline_remaining_stages->>asyncio_gather: termination_tasks
    asyncio_gather-->>Caller: termination completed
Loading

File-Level Changes

Change Details Files
Introduce a pure resolver for subprocess timeout payloads and refactor timeout handling to use it.
  • Add the frozen dataclass _TimeoutFallback to hold configured timeout, captured stdout/stderr, and an injected exit-time clock value used for bare TimeoutError resolution.
  • Implement _resolve_timeout_payload to map either a _SubprocessTimeoutError (using its carried payload) or a bare TimeoutError (using _TimeoutFallback plus _require_timeout) into a _SubprocessTimeoutDetails instance.
  • Refactor _handle_subprocess_timeout to delegate payload computation to _resolve_timeout_payload, using a _TimeoutFallback built from the execution context and time.perf_counter(), then emit the exit event and raise TimeoutExpired using the resolved payload fields.
  • Export _TimeoutFallback and _resolve_timeout_payload in the module’s all list.
cuprum/_subprocess_timeout.py
Introduce a pure fail-fast stage selection helper and adjust pipeline termination logic to use it.
  • Add _stages_to_terminate, which returns indices of stages to terminate after fail-fast by filtering out the failed stage and any stages already marked done, ensuring each selected index appears at most once.
  • Refactor _terminate_pipeline_remaining_stages to compute a target index set from _stages_to_terminate and then create termination tasks only for those indices while preserving the strict zip over processes and wait_tasks.
  • Preserve idempotent cleanup semantics by basing selection solely on the done flags snapshot and never rescheduling already-finished stages.
cuprum/_process_lifecycle.py
Add property-based tests for the new reducers and update snapshot metadata.
  • Create cuprum/unittests/test_subprocess_timeout_reducers.py with Hypothesis tests that prove: carried _SubprocessTimeoutError payloads are used verbatim and ignore fallbacks, bare TimeoutError payloads are built solely from _TimeoutFallback and require a configured timeout, and fail-fast selection via _stages_to_terminate returns exactly the running, non-failed stages, uniquely and in order, and is idempotent over settled stages.
  • Regenerate or adjust the maturin build snapshot file to account for the new tests or distribution metadata changes.
cuprum/unittests/test_subprocess_timeout_reducers.py
cuprum/unittests/__snapshots__/test_maturin_build.ambr

Assessment against linked issues

Issue Objective Addressed Explanation
#75 Introduce a testable seam for subprocess timeout handling (around _handle_subprocess_timeout) and add property-based (Hypothesis) tests to verify timeout payload consistency.
#75 Introduce a testable seam for pipeline fail-fast termination (around _terminate_pipeline_remaining_stages) and add property-based (Hypothesis) tests to verify idempotent cleanup and no double-termination scheduling.
#75 Apply CrossHair verification to the extracted transition data / reducers involved in timeout and fail-fast lifecycle handling. The PR adds pure reducers (_resolve_timeout_payload, _stages_to_terminate) and Hypothesis property tests for them, but there is no use of CrossHair or any static/semantic verification tooling in the changes.

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.

codescene-access[bot]

This comment was marked as outdated.

test_resolve_uses_carried_payload_for_subprocess_timeout_error took 7
@given arguments (threshold 4). Draw the captured details from a
_timeout_details composite strategy (one argument) and use a fixed,
distinctive fallback: configured_timeout=None would raise if the carried
branch ever consulted it, so no raise plus == details proves the fallback
is never touched. Code health 10.0.

Co-Authored-By: Claude Opus 4.8 (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.

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.

Property-based tests for _handle_subprocess_timeout and _terminate_pipeline_remaining_stages

1 participant