Skip to content

Implement fail-fast pipeline with failure metadata and internals - #15

Merged
leynos merged 7 commits into
mainfrom
terragon/implement-pipeline-failure-policy-vr9prp
Dec 20, 2025
Merged

Implement fail-fast pipeline with failure metadata and internals#15
leynos merged 7 commits into
mainfrom
terragon/implement-pipeline-failure-policy-vr9prp

Conversation

@leynos

@leynos leynos commented Dec 19, 2025

Copy link
Copy Markdown
Owner

Summary

  • Implement fail-fast policy: on the first non-zero exit, downstream stages are terminated after a grace period and the failing stage is surfaced via PipelineResult.failure and PipelineResult.failure_index.
  • Centralize internal coordination for per-stage waiting, exit code collection, and graceful termination into new internal modules cuprum._pipeline_internals.py and cuprum/_streams.py to improve maintainability.
  • Extend PipelineResult with failure_index: int | None and a new failure property to expose the failing stage.
  • Add internal types _PipelineWaitResult and _PipelineWaitState to map per-stage outcomes to the overall result.
  • Provide helpers _terminate_pipeline_remaining_stages and _terminate_process_via_wait_task for graceful termination.
  • Expose internal streaming helpers for capturing and echoing subprocess I/O with clean shutdown paths.
  • Update tests and docs to reflect the new behavior and API surface.

Changes

  • cuprum/sh.py
    • Extend PipelineResult with failure_index and a new failure property.
    • Wire the pipeline runner to propagate the failure index from the wait/watcher logic into the final PipelineResult.
    • Add internal types _PipelineWaitResult and _PipelineWaitState to coordinate waiting on processes and mapping results to stages. (Inherited from internal module; not defined here.)
    • Implement fail-fast semantics:
      • On first non-zero exit, terminate remaining stages after a grace period.
      • Record the index of the failing stage and expose it via PipelineResult.failure_index and PipelineResult.failure.
    • Add helper _terminate_pipeline_remaining_stages and _terminate_process_via_wait_task to perform graceful termination.
  • cuprum/_pipeline_internals.py (new)
    • Centralises internal coordination for per-stage waiting, exit code collection, and graceful termination.
    • Introduces _MIN_PIPELINE_STAGES, internal pipeline run/config handling, and fail-fast termination helpers.
  • cuprum/_streams.py (new)
    • Introduces internal streaming helpers for capturing and echoing subprocess I/O with clean shutdown paths.
  • cuprum/_testing.py (new)
    • Exposes internal helpers to tests to validate tricky edge cases (process/pipe coordination, stream handling, etc.).
  • cuprum/unittests/test_pipeline.py
    • Unit tests updated to validate fail-fast behavior, including result.failure, result.failure_index, and downstream termination signals.
    • Behavioural tests expanded to cover fail-fast middle and final-stage failure scenarios.
    • Feature tests updated to reflect fail-fast semantics.
  • Tests (general)
    • Behavioural and feature tests updated to exercise new fail-fast semantics and failure metadata on PipelineResult.
  • Documentation
    • docs/cuprum-design.md: document PipelineResult.failure and failure_index, and describe fail-fast policy.
    • docs/users-guide.md: mention new fail-fast semantics and how to access the failing stage via result.failure / result.failure_index.
    • docs/roadmap.md: reflect that fail-fast policy is implemented.

API Changes

  • PipelineResult now includes:
    • failure_index: int | None – index of the stage that triggered fail-fast termination (or None if all stages succeeded).
    • failure: CommandResult | None – the failing stage's result when applicable.

Testing Plan

  • Run unit tests: pytest -k pipeline.
  • Run behavioural tests: ensure scenarios for fail-fast middle and final stage failures pass.
  • Validate that per-stage exit codes are preserved and that downstream stages are terminated when appropriate.

Migration / Compatibility

  • Additive API changes; existing users can ignore new fields unless they rely on per-stage failure information.
  • No breaking changes to existing public interfaces beyond the new metadata exposure.

Generated by Terry

📎 Task: https://www.terragonlabs.com/task/229b58af-8c49-496f-b1a3-092aa8746018

…lure

- Pipeline stages now fail fast: when any stage exits non-zero, remaining stages are terminated.
- `PipelineResult` includes `failure_index` and `failure` properties identifying the failing stage.
- Internal `_wait_for_pipeline` captures failure index, terminates downstream stages, and returns detailed exit metadata.
- Updated tests and behavior scenarios to cover fail-fast semantics and failure stage attribution.
- Documentation updated to describe fail-fast pipeline behavior, termination policy, and failure metadata exposure.

This improves pipeline robustness and diagnostic capability by aborting on the first error and surfacing the failing stage explicitly.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@sourcery-ai

sourcery-ai Bot commented Dec 19, 2025

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors pipeline execution internals by extracting stream and pipeline coordination logic into new internal modules, implements fail-fast semantics in _wait_for_pipeline that track the first failing stage and terminate remaining stages, and threads this information through to PipelineResult via new failure_index and failure accessors, with tests and docs updated accordingly.

Sequence diagram for fail-fast pipeline execution and termination

sequenceDiagram
    actor Client
    participant sh as cuprum_sh
    participant internals as _pipeline_internals
    participant proc as asyncio_subprocess
    participant streams as _streams

    Client->>sh: pipeline.run(capture, echo, context)
    sh->>internals: _run_pipeline(parts, capture, echo, context)

    internals->>internals: _prepare_pipeline_config()
    internals->>internals: _run_before_hooks() per SafeCmd

    internals->>proc: _spawn_pipeline_processes(parts, config)
    proc-->>internals: processes, stderr_tasks, stdout_task

    internals->>internals: _create_pipe_tasks(processes)
    internals->>internals: _flatten_stream_tasks(stderr_tasks, stdout_task)

    internals->>internals: _wait_for_pipeline(processes, pipe_tasks, stream_tasks, cancel_grace)
    internals->>internals: _PipelineWaitState.from_processes(processes)

    loop wait for stages
        internals->>proc: process.wait() as Task
        proc-->>internals: exit_code
        internals->>internals: _process_completed_task(task, state, processes, cancel_grace)
        alt first non_zero exit
            internals->>internals: set state.failure_index
            internals->>internals: _terminate_pipeline_remaining_stages(processes, wait_tasks, failure_index, cancel_grace)
            internals->>internals: _terminate_process_via_wait_task() for each remaining stage
            internals->>proc: process.terminate()/kill()
            proc-->>internals: wait_task completes
        end
    end

    internals->>internals: _finalize_pipeline_wait(pipe_tasks, pipe_results, caught)
    internals-->>internals: _PipelineWaitResult(exit_codes, failure_index)

    internals->>streams: _consume_stream() for stderr/stdout
    streams-->>internals: captured text

    internals->>internals: build CommandResult per stage
    internals->>internals: _run_pipeline_after_hooks()

    internals-->>sh: PipelineResult(stages, failure_index)
    sh-->>Client: PipelineResult
    Client->>Client: inspect result.failure and result.failure_index
Loading

Class diagram for pipeline coordination and fail-fast types

classDiagram
    class CommandResult {
        +str program
        +tuple argv
        +int exit_code
        +int pid
        +str stdout
        +str stderr
    }

    class PipelineResult {
        +tuple~CommandResult~ stages
        +int failure_index
        +CommandResult final()
        +CommandResult failure()
        +bool ok()
    }

    class ExecutionContext {
        +float cancel_grace
        +str encoding
        +str errors
        +IO stdout_sink
        +IO stderr_sink
    }

    class SafeCmd {
        +str program
        +tuple argv
        +tuple argv_with_program
    }

    class _StreamConfig {
        +bool capture_output
        +bool echo_output
        +IO sink
        +str encoding
        +str errors
    }

    class _PipelineRunConfig {
        +ExecutionContext ctx
        +bool capture
        +bool echo
        +IO stdout_sink
        +IO stderr_sink
        +bool capture_or_echo
        +_StreamConfig stream_config
    }

    class _PipelineWaitResult {
        +list~int~ exit_codes
        +int failure_index
    }

    class _PipelineWaitState {
        +list~Task~ wait_tasks
        +dict~Task,int~ task_to_index
        +list~int~ exit_codes
        +int failure_index
        +from_processes(processes)
    }

    PipelineResult "*" --> "*" CommandResult : stages
    _PipelineRunConfig --> ExecutionContext : ctx
    _PipelineRunConfig --> _StreamConfig : creates
    _PipelineWaitResult --> CommandResult : indexes into
    _PipelineWaitState --> _PipelineWaitResult : builds
    SafeCmd --> ExecutionContext : validated_by
Loading

File-Level Changes

Change Details Files
Extend PipelineResult to expose the failing stage and wire pipeline-running code to consume new fail-fast wait results.
  • Add failure_index field and failure property to PipelineResult to indicate which stage triggered fail-fast termination.
  • Update _run_pipeline to use _PipelineWaitResult.exit_codes and .failure_index when building CommandResult objects and the final PipelineResult.
  • Ensure existing accessors like final and ok continue to behave consistently with the new failure metadata.
cuprum/sh.py
docs/cuprum-design.md
docs/users-guide.md
Extract pipeline coordination (spawning, waiting, fail-fast termination, hooks) into a new internal module and change _wait_for_pipeline to enforce fail-fast semantics.
  • Move pipeline config, spawning, env merge, before/after hooks, and termination helpers from sh.py into cuprum._pipeline_internals with minimal behavioural change aside from fail-fast.
  • Redefine _wait_for_pipeline to wait on per-process tasks, record exit codes into a _PipelineWaitState, and on first non-zero exit terminate remaining stages via _terminate_pipeline_remaining_stages and _terminate_process_via_wait_task.
  • Introduce _PipelineWaitResult and _PipelineWaitState types to encapsulate exit codes and failure_index while retaining pipe/stream cleanup and error propagation semantics.
cuprum/_pipeline_internals.py
cuprum/sh.py
Extract subprocess I/O streaming helpers into a dedicated internal streams module and reuse them from pipeline internals.
  • Move _StreamConfig, _consume_stream, _pump_stream, _close_stream_writer, and _write_chunk into cuprum._streams, preserving their behaviour.
  • Update pipeline code to import _StreamConfig and stream helpers from cuprum._streams instead of defining them in sh.py.
  • Keep _READ_SIZE as a shared constant in cuprum._streams used by all consumers.
cuprum/_streams.py
cuprum/sh.py
cuprum/_pipeline_internals.py
Add and extend tests to cover fail-fast behaviour and new failure metadata for various failing-stage positions.
  • Add unit tests around _wait_for_pipeline using _StubPipelineWaitProcess to verify early, middle, and last-stage failures set failure_index correctly and terminate only non-failing stages.
  • Extend existing PipelineResult tests to assert failure and failure_index for simple two-stage failing pipelines.
  • Expand behavioural and feature tests to use three-stage pipelines exercising failing first, middle, and final stages, and to assert both per-stage exit codes and failure/failure_index semantics.
cuprum/unittests/test_pipeline.py
tests/behaviour/test_pipeline_execution.py
tests/features/pipeline_execution.feature
Update design and user documentation to describe fail-fast policy and the new failure metadata surface.
  • Document PipelineResult.failure_index and failure accessors in the design doc and describe that pipelines terminate remaining stages when a stage exits non-zero.
  • Clarify user-facing docs to call out fail-fast behaviour and how to inspect the failing stage from the result object.
  • Mark the roadmap item for defining/implementing fail-fast policy as complete.
  • Align documentation examples and narrative with the new three-stage fail-fast scenarios used in tests.
docs/cuprum-design.md
docs/users-guide.md
docs/roadmap.md

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

@coderabbitai

coderabbitai Bot commented Dec 19, 2025

Copy link
Copy Markdown

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Refactor pipeline runtime into two new internal modules: _pipeline_internals.py (multi-stage orchestration with fail-fast, spawning, waiting, termination and hooks) and _streams.py (subprocess stream pumping and consumption). Add failure_index and failure to PipelineResult. Extend tests and docs to exercise and document fail-fast behaviour across early, middle and final-stage failures.

Changes

Cohort / File(s) Summary
Internal pipeline orchestration module
cuprum/_pipeline_internals.py
Add comprehensive pipeline orchestration: before/after hook handling, env merging, graceful termination helpers, run config dataclasses, stage stream config, process spawning, pipe wiring, stream capture tasks, concurrent wait machinery (_PipelineWaitState, _PipelineWaitResult), fail-fast termination of downstream stages, cleanup and result collation. Many functions and dataclasses added.
Internal stream utilities
cuprum/_streams.py
Add stream helpers: _StreamConfig dataclass, _consume_stream, _pump_stream, _write_to_stream_writer, _close_stream_writer, _write_chunk, chunked read size constant and robust writers/decoders for subprocess I/O.
Public API refactor / delegation
cuprum/sh.py
Remove local pipeline runtime code and delegate to new internals. Extend PipelineResult with `failure_index: int
Test helpers & re-exports
cuprum/_testing.py
Add test-only module that re-exports selected internals from _pipeline_internals and _streams, exposing them for unit tests via a consolidated __all__.
Unit tests
cuprum/unittests/test_pipeline.py
Expose _PipelineWaitResult and _wait_for_pipeline for tests; add _StubPipelineWaitProcess and multiple tests exercising fail-fast termination semantics and failure index reporting.
Behaviour tests
tests/behaviour/test_pipeline_execution.py
Expand pipeline scenarios from two to three stages. Add tests for middle-stage fail-fast and final-stage failure, adjust helpers and assertions to validate failure indices and per-stage exit codes.
Feature specs
tests/features/pipeline_execution.feature
Update scenario(s) to three-stage pipelines; add scenarios for middle-stage fail-fast and final-stage failure asserting per-stage metadata.
Docs
docs/cuprum-design.md, docs/users-guide.md
Document PipelineResult.failure_index and PipelineResult.failure. State fail-fast termination semantics and how failing stage is surfaced.
Roadmap
docs/roadmap.md
Mark Phase 2 pipeline failure policy (fail-fast, terminate downstream, surface failing stage) as completed.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Pipeline
    participant Spawner as Spawner/Launcher
    participant Stage1 as Stage 1
    participant Stage2 as Stage 2
    participant Stage3 as Stage 3
    participant Monitor as Wait/Monitor
    participant Cleanup as Cleaner

    User->>Pipeline: run(parts)
    Pipeline->>Spawner: _spawn_pipeline_processes(parts, config)
    Spawner->>Stage1: start (stdin/stdout/stderr)
    Spawner->>Stage2: start (pipe from Stage1)
    Spawner->>Stage3: start (pipe from Stage2)
    Spawner-->>Monitor: return processes & stream tasks

    par execution
        Stage1->>Monitor: exit 0
        Stage2->>Monitor: exit non-zero
        Stage3->>Monitor: running / blocked on pipe
    end

    Monitor->>Monitor: detect first non-zero (failure_index)
    rect rgb(255,230,230)
        Monitor->>Stage3: _terminate_pipeline_remaining_stages(failure_index)
        Monitor->>Cleanup: _cleanup_spawned_processes()
    end

    Monitor->>Pipeline: assemble PipelineResult(failure_index, per-stage results)
    Pipeline-->>User: return PipelineResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on cuprum/_pipeline_internals.py for concurrency, wait-state transitions, escalation semantics and correct handling of BrokenPipe/ConnectionResetError.
  • Verify integration points in cuprum/sh.py: ensure delegation preserves previous API contracts and failure_index/failure semantics.
  • Review stream correctness in cuprum/_streams.py for encoding, backpressure and writer error handling.
  • Inspect new and updated tests (cuprum/unittests/test_pipeline.py, tests/behaviour/test_pipeline_execution.py, feature file) for coverage of fail-fast and final-stage failure cases.

Poem

🔧 Pipes align and processes hum,

Stages race until the bad one’s done,
Fail-fast bells ring, downstreams sleep,
Results reveal the index to keep,
Internal gears now tidy and spry.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 86.44% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and concisely summarises the main change: implementing fail-fast pipeline execution with failure metadata exposure.
Description check ✅ Passed The description comprehensively covers the changeset, detailing fail-fast semantics, new internal modules, API extensions, and test/documentation updates.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch terragon/implement-pipeline-failure-policy-vr9prp

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

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 19, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

cuprum/sh.py

Comment on file

        Command results for each pipeline stage, in execution order. For stages
        whose stdout is streamed into the next stage, ``stdout`` is ``None``.
        The final stage carries captured stdout when enabled.
    failure_index:

❌ New issue: Lines of Code in a Single File
This module has 674 lines of code, improve code health by reducing it to 400

@leynos

leynos commented Dec 19, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

cuprum/sh.py

Comment on file

    stream_tasks: list[asyncio.Task[str | None]],
    cancel_grace: float,
) -> list[int]:
) -> _PipelineWaitResult:

❌ New issue: Complex Method
_wait_for_pipeline has a cyclomatic complexity of 10, threshold = 9

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

…dules

- Extracted internal pipeline execution coordination and fail-fast semantics
  into `cuprum/_pipeline_internals.py`.
- Added internal stream handling utilities for subprocess I/O in
  `cuprum/_streams.py`.
- Cleaned up `cuprum/sh.py` by importing relevant internals and removing
  duplicated code.
- Improved code modularity and separation of concerns for pipeline and
  streaming logic.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Add fail-fast policy for pipeline execution and surface failing stage Refactor _wait_for_pipeline; add fail-fast surface Dec 19, 2025
@leynos
leynos marked this pull request as ready for review December 19, 2025 02:55
sourcery-ai[bot]

This comment was marked as resolved.

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

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3a2fb53 and 7dd89d0.

📒 Files selected for processing (9)
  • cuprum/_pipeline_internals.py (1 hunks)
  • cuprum/_streams.py (1 hunks)
  • cuprum/sh.py (2 hunks)
  • cuprum/unittests/test_pipeline.py (3 hunks)
  • docs/cuprum-design.md (4 hunks)
  • docs/roadmap.md (1 hunks)
  • docs/users-guide.md (1 hunks)
  • tests/behaviour/test_pipeline_execution.py (3 hunks)
  • tests/features/pipeline_execution.feature (1 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/**/*.md: Use markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
When new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve, proactively update the relevant file(s) in the docs/ directory to reflect the latest state.
All documentation must adhere to the documentation style guide at docs/documentation-style-guide.md.
Record any design decisions made in the relevant design document.

Files:

  • docs/cuprum-design.md
  • docs/users-guide.md
  • docs/roadmap.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: For Markdown files (.md only), ensure linting passes by running make markdownlint.
For Markdown files, validate Mermaid diagrams by running make nixie.
Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Markdown tables and headings must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes.
Validate Markdown files using make markdownlint.
Run make fmt after any documentation changes to format all Markdown files and fix table markup.
Validate Mermaid diagrams in Markdown files by running make nixie.

Files:

  • docs/cuprum-design.md
  • docs/users-guide.md
  • docs/roadmap.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/cuprum-design.md
  • docs/users-guide.md
  • docs/roadmap.md
docs/**/*.{md,mdx,rst,txt}

📄 CodeRabbit inference engine (docs/documentation-style-guide.md)

docs/**/*.{md,mdx,rst,txt}: Use British English based on Oxford English Dictionary (en-GB-oxendict) conventions: use -ize suffixes (realize, organization), -lyse suffixes (analyse, paralyse, catalyse), -our suffixes (colour, behaviour, neighbour), -re suffixes (calibre, centre, fibre), double 'l' (cancelled, counsellor, cruellest), maintain 'e' (likeable, liveable, rateable), -ogue suffixes (analogue, catalogue)
The word 'outwith' is acceptable in documentation
Use the Oxford comma in documentation: 'ships, planes, and hovercraft' where it aids comprehension
Treat company names as collective nouns in documentation, for example 'Lille Industries are expanding'
Write headings in sentence case in documentation
Use Markdown headings (#, ##, ###, and so on) in order without skipping levels
Always provide a language identifier for fenced code blocks in documentation; use 'plaintext' for non-code text
Use - as the first level bullet and renumber lists when items change in documentation
Prefer inline links using [text](url) or angle brackets around the URL in documentation
Ensure blank lines before and after bulleted lists and fenced blocks in documentation
Ensure tables have a delimiter line below the header row in documentation
Expand any uncommon acronym on first use in documentation, for example 'Continuous Integration (CI)'
Wrap paragraphs at 80 columns in documentation
Wrap code at 120 columns in documentation
Do not wrap tables in documentation
Use footnotes referenced with [^label] in documentation
Include Mermaid diagrams in documentation where they add clarity
When embedding figures in documentation, use ![alt text](path/to/image) and provide brief alt text describing the content
Add a short description before each Mermaid diagram in documentation so screen readers can understand it

Files:

  • docs/cuprum-design.md
  • docs/users-guide.md
  • docs/roadmap.md
docs/**/*.{md,mdx,rst,txt,rs}

📄 CodeRabbit inference engine (docs/documentation-style-guide.md)

Keep US spelling when used in API contexts, for example 'color'

Files:

  • docs/cuprum-design.md
  • docs/users-guide.md
  • docs/roadmap.md
docs/**/*.{md,mdx}

📄 CodeRabbit inference engine (docs/documentation-style-guide.md)

Follow markdownlint recommendations for Markdown formatting

Files:

  • docs/cuprum-design.md
  • docs/users-guide.md
  • docs/roadmap.md
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: For Python files, ensure testing passes all relevant unit and behavioral tests by running make test.
For Python files, ensure linting passes by running make lint.
For Python files, ensure formatting adheres to standards by running make check-fmt and applying make fmt if needed.
For Python files, ensure type checking passes by running make typecheck.
For Python development, refer to Python-specific guidelines in the .rules/ directory, including Python Code Style Guidelines, Context Managers, Exceptions and Logging, Generators, Project Configuration, Return Patterns, and Typing.

**/*.py: Use snake_case for Python filenames (e.g., http_client.py, task_queue.py)
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix private/internal functions and variables with a single underscore (_)
Enable full static type coverage using Pyright and maintain typing throughout the codebase
Use TypedDict or Dataclass for structured data, preferring @DataClass(slots=True) for internal-only usage
Avoid using Any type; use Unknown, generics, or cast() with documentation instead
Provide explicit return type annotations (e.g., -> None, -> str) for all public functions and class methods
Enforce strict mode in Pyright and treat all Pyright warnings as CI errors; use # pyright: ignore sparingly with explanation
Avoid side effects at import time; modules should not modify global state or perform actions on import
Use .env or settings modules for environment-specific configuration; never hardcode secrets
Use Ruff for formatting; let Ruff handle whitespace and formatting entirely
Use NumPy-format docstrings for public functions, classes, and modules
Use inline comments to explain tricky or non-obvious code logic and decisions

**/*.py: Use context managers (with contextlib.contextmanager or class-based __enter__/__exit__) to encapsulate setup and teardown logic for resource management (f...

Files:

  • cuprum/unittests/test_pipeline.py
  • cuprum/_streams.py
  • cuprum/_pipeline_internals.py
  • cuprum/sh.py
  • tests/behaviour/test_pipeline_execution.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep C90 / mccabe complexity ≤ 9

  • Follow single responsibility and CQRS (command/query segregation)
  • Prefer structural pattern matching to
  • Prefer structural pattern matching over isinstance() or imperative decomposition.
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.
  • All path manipulation must be performed using pathlib for cross platform safety. Do not use string manipulation for extraction of path components or filename ele...

Files:

  • cuprum/unittests/test_pipeline.py
  • cuprum/_streams.py
  • cuprum/_pipeline_internals.py
  • cuprum/sh.py
  • tests/behaviour/test_pipeline_execution.py
**/unittests/test_*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

Colocate unit tests with code using an unittests subdirectory with test_ prefix (e.g., user_auth/unittests/test_models.py)

Files:

  • cuprum/unittests/test_pipeline.py
**/test_*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

**/test_*.py: Use pytest idioms: prefer fixtures over setup/teardown methods, parametrize broadly, avoid unnecessary mocks
Group related tests using class with method names prefixed by test_
Write tests from a user's perspective; test public behavior rather than internals
Avoid mocking too much; prefer test doubles only for external services or non-deterministic behaviors

Files:

  • cuprum/unittests/test_pipeline.py
  • tests/behaviour/test_pipeline_execution.py
**/*test*.py

📄 CodeRabbit inference engine (.rules/python-exception-design-raising-handling-and-logging.md)

Use specific exception types and message constraints with pytest.raises(SpecificError, match=r"pattern") in tests; avoid overly broad exception assertions (B017)

Files:

  • cuprum/unittests/test_pipeline.py
  • tests/behaviour/test_pipeline_execution.py
docs/users-guide.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/users-guide.md: Ensure new functionality is clearly documented in the docs/users-guide.md file.
Ensure revised functionality is clearly documented in the docs/users-guide.md file.

docs/users-guide.md: Document builder pattern in docs/users-guide.md for constructing SafeCmd instances with typed argv handling
Document hook usage patterns, including basic logging hook that emits start/exit events compatible with logging
Provide a scaffold and guidance for project-specific builders, including a template module and checklist in docs/users-guide.md

Files:

  • docs/users-guide.md
🧬 Code graph analysis (3)
cuprum/_streams.py (1)
cuprum/unittests/test_pipeline.py (6)
  • read (29-34)
  • write (47-49)
  • drain (51-57)
  • write_eof (59-60)
  • close (62-63)
  • wait_closed (65-66)
cuprum/sh.py (2)
cuprum/_pipeline_internals.py (5)
  • _merge_env (44-52)
  • _PipelineWaitResult (461-463)
  • _prepare_pipeline_config (100-117)
  • _terminate_process (55-74)
  • _wait_for_pipeline (411-457)
cuprum/_streams.py (5)
  • _close_stream_writer (94-118)
  • _consume_stream (26-51)
  • _pump_stream (54-75)
  • _StreamConfig (16-23)
  • _write_chunk (121-140)
tests/behaviour/test_pipeline_execution.py (2)
tests/helpers/catalogue.py (1)
  • python_catalogue (17-26)
cuprum/sh.py (4)
  • PipelineResult (123-161)
  • ok (117-119)
  • ok (154-156)
  • failure (147-151)
🪛 LanguageTool
docs/cuprum-design.md

[style] ~411-~411: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ..., PipelineResult.stdoutisNone`. - When a stage exits non-zero, Cuprum terminat...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🔍 Remote MCP

Let me try a web search for relevant best practices and patterns:Based on the search results, I have gathered relevant context for reviewing this PR. Let me compile a focused summary of the key architectural and technical considerations.

Summary of Relevant Context for PR Review

Asyncio Subprocess Termination Patterns & Graceful Shutdown

Calling terminate() starts the termination process but doesn't wait for it to complete; you should usually await process.wait() to actually wait for the subprocess to finish. The PR implements this pattern via the _terminate_process and _terminate_pipeline_remaining_stages helpers in _pipeline_internals.py.

The recommended pattern pairs terminate() with an awaited wait(), often within a try-except block or with a timeout; if termination times out, escalate to kill() to forcefully shutdown. This aligns with the PR's fail-fast termination design that uses both graceful termination (SIGTERM) with timeout escalation to forceful termination (SIGKILL).

Pipe and Stream I/O Deadlock Prevention

Use the communicate() method when using pipes to avoid deadlock conditions where child processes generate output that fills the OS pipe buffer. The new _streams.py module implements dedicated stream consumption (_consume_stream, _pump_stream) to prevent this via async StreamReader/StreamWriter handling, which is important for multi-stage pipelines with I/O capture.

Cyclomatic Complexity Reduction via Refactoring

The EXTRACT METHOD refactoring is the primary weapon to address "bumpy road" code complexity that contains multiple chunks of nested logic. The PR directly addresses this by:

  • Extracting task-completion handling into helper methods
  • Splitting _wait_for_pipeline responsibilities across _process_completed_task and _finalize_pipeline_wait helpers
  • Separating pipeline internals into dedicated modules

Nested control structures increase cyclomatic complexity and make code harder to understand; refactor nested structures into simpler sequential logic or extract them into separate functions. The PR's modularization strategy aligns with this guidance.

File Size and Maintainability Considerations

Refactoring involves splitting large functions into smaller, more manageable functions, removing redundant code, and simplifying complex conditional statements. The target reduction from ~674-1059 lines to ~400-450 lines in sh.py follows industry guidance on function and module sizing for maintainability.

Key Review Considerations

  1. Graceful Termination with Escalation: Verify that _terminate_process correctly implements SIGTERM→timeout→SIGKILL escalation for downstream stages.
  2. Stream Safety: Confirm _streams.py helpers correctly prevent deadlocks in multi-stage pipelines with I/O capture via proper async backpressure handling (drain()).
  3. Complexity Reduction: Validate that extracted helpers (_process_completed_task, _finalize_pipeline_wait) preserve all exit-code collection and failure-index logic originally in _wait_for_pipeline.
  4. API Stability: Ensure PipelineResult.failure_index and .failure additions are backward-compatible and properly integrate with existing pipeline semantics.
  5. Test Coverage: Verify fail-fast behavior across early, middle, and final-stage failures; confirm per-stage exit codes and unexpected pipe failures are correctly surfaced.
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: Sourcery review
🔇 Additional comments (36)
docs/users-guide.md (1)

146-148: LGTM!

The fail-fast documentation is clear and aligns with the implementation. The result.failure / result.failure_index surface is correctly referenced.

cuprum/_streams.py (6)

1-13: LGTM!

Module structure follows best practices: deferred annotations, TYPE_CHECKING guard for asyncio, and sensible chunk size constant.


15-24: LGTM!

Clean configuration dataclass with appropriate frozen/slots settings.


26-51: LGTM!

Stream consumption logic correctly handles capture/echo modes with proper chunked reading and final decoding.


54-75: LGTM!

The deadlock prevention pattern (continuing to drain stdout when downstream closes) is correctly implemented. This aligns with best practices for subprocess pipe handling.


94-118: LGTM with a note.

The defensive error suppression is appropriate for pipe cleanup where writer state may be indeterminate. The getattr fallback for wait_closed handles compatibility gracefully.


121-140: LGTM!

Efficient sink writing that bypasses redundant encoding when the underlying buffer is accessible. The blocking write trade-off is well-documented.

docs/roadmap.md (1)

49-51: LGTM!

The roadmap update correctly reflects the completed implementation. The PR delivers fail-fast termination, downstream stage cleanup, and failure surface via failure_index/failure, with comprehensive test coverage for early, middle, and late stage failures.

cuprum/unittests/test_pipeline.py (4)

11-20: LGTM!

Importing internal symbols for unit testing internal behaviour is appropriate.


184-185: LGTM!

The new assertions correctly validate the failure property returns the expected stage and failure_index is set appropriately.


288-324: LGTM!

Well-designed test double with explicit control over termination sequencing via the ready event. The signal codes (-15 for SIGTERM, -9 for SIGKILL) are accurate.


327-429: LGTM!

Comprehensive test coverage for fail-fast behaviour across all failure positions:

  • Early stage (0): downstream stages 1 and 2 terminated
  • Middle stage (1): both upstream (0) and downstream (2) terminated
  • Final stage (2): no termination needed as all stages already complete

The ready-event pattern provides deterministic control over completion order.

tests/features/pipeline_execution.feature (1)

15-28: LGTM!

BDD scenarios comprehensively cover fail-fast behaviour for first, middle, and final stage failures. The step wording is clear and testable.

docs/cuprum-design.md (3)

392-413: LGTM!

The PipelineResult documentation correctly describes the new failure surface. The parallel "When..." structure in the contract notes is appropriate for clarity, despite the style lint.


571-573: LGTM!

Design decision documentation aligns with implementation: fail-fast terminates remaining stages and surfaces the triggering stage.


974-978: LGTM!

Error propagation policy is clearly documented, consistent with the implementation and other documentation sections.

cuprum/sh.py (2)

122-161: LGTM!

The failure_index field and failure property are correctly implemented:

  • Proper typing with int | None
  • Sensible default of None for success cases
  • Clean property implementation that handles the None case

391-414: LGTM!

Pipeline execution cleanly delegates to _run_pipeline from the internal module, maintaining a clear separation between public API and implementation details.

cuprum/_pipeline_internals.py (14)

1-18: LGTM!

Clean module structure with proper use of TYPE_CHECKING guard for type-only imports and deferred annotation evaluation.


22-27: LGTM!

Sound approach to avoid circular imports by deferring module access to runtime.


30-41: LGTM!

Clean separation of hook invocation with clear docstring.


44-52: LGTM!

Deferred os import avoids import-time side effects. Dict merge with |= is idiomatic.


55-74: LGTM!

Correct implementation of graceful termination with SIGTERM→timeout→SIGKILL escalation. Exception handling appropriately guards against race conditions where the process exits between checks.


77-117: LGTM!

Well-structured configuration dataclass with computed properties. Follows @dataclass(frozen=True, slots=True) guideline.


120-162: Acceptable orchestration complexity.

The PLR0914 suppression is justified here. This is a coordination function that necessarily tracks multiple concerns (processes, tasks, results). The logic flow remains readable.


165-222: LGTM!

Clear separation between stream configuration and task creation. Docstrings effectively explain the FD selection logic.


225-302: LGTM!

Robust cleanup implementation. Catching BaseException at line 293 is correct—it ensures cleanup on KeyboardInterrupt and SystemExit, not just standard exceptions.


305-354: LGTM!

Good decomposition of pipe orchestration concerns. Correctly treats BrokenPipeError and ConnectionResetError as expected conditions when downstream stages terminate early.


411-457: LGTM!

Clean decomposition of the wait loop into _process_completed_task and _finalize_pipeline_wait helpers. The fail-fast logic is clear: first non-zero exit triggers termination of remaining stages.


486-541: LGTM!

Correct use of asyncio.shield to prevent wait task cancellation during graceful termination. The strict=True in zip at line 525 ensures process and task list lengths match.


544-551: LGTM!

Clean after-hook invocation with strict=True enforcement on zip.


19-19: _MIN_PIPELINE_STAGES is imported and used in cuprum/sh.py (line 376). This is appropriate modularisation—defining the constant in the internals module and exporting it for external use is correct.

Likely an incorrect or invalid review comment.

tests/behaviour/test_pipeline_execution.py (4)

44-57: LGTM!

Clear scenario definitions with descriptive docstrings.


148-187: LGTM!

Clear fixture separation for each failure scenario. The 2-second sleeps are necessary to validate fail-fast termination of long-running stages.


222-237: LGTM!

Assertions correctly verify fail-fast semantics: the failure index must precede the final stage, and subsequent stages must have non-zero exit codes (from termination signals).


240-251: LGTM!

Correct assertions for final-stage failure: preceding stages complete successfully, and failure references the last stage.

Comment thread cuprum/_pipeline_internals.py Outdated
Comment thread cuprum/sh.py Outdated
- Extracted general _terminate_process_with_wait() to reuse termination logic with customizable completion check and waiter.
- Simplified _terminate_process() and _terminate_process_via_wait_task() using the new helper.
- Enhanced _wait_for_pipeline() and associated pipeline wait state management by consolidating wait task tracking and error cleanup.
- Removed unnecessary stream_tasks parameter from internal wait and cleanup functions for clearer ownership and responsibilities.
- Improved exception handling in _run_pipeline to properly cancel and await stream tasks on failure.
- Added _PipelineWaitState data class to encapsulate state for pipeline process wait tasks.
- Updated imports and test code to align with refactoring.

This refactor improves code clarity, error handling, and reuse in pipeline subprocess termination and waiting mechanisms.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Refactor _wait_for_pipeline; add fail-fast surface Implement fail-fast pipeline; surface failing stage in results Dec 19, 2025
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 19, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

cuprum/unittests/test_pipeline.py

Comment on lines +183 to +184

    assert result.failure is result.stages[-1]
    assert result.failure_index == 1

❌ New issue: Code Duplication
The module contains 5 functions with similar structure: test_pipeline_run_sync_failure_sets_ok_false_and_final_to_failed_stage,test_pipeline_run_sync_success_has_no_failure,test_wait_for_pipeline_fail_fast_early_stage_failure_terminates_downstream,test_wait_for_pipeline_fail_fast_last_stage_failure_records_failure_index and 1 more functions

@coderabbitai

This comment was marked as resolved.

@leynos

leynos commented Dec 19, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Overall Comments
- In the new `_wait_for_pipeline` implementation, `stream_tasks` are only awaited in the error path (`_cleanup_pipeline_on_error`); on the successful/completed path they are never awaited, which may leak tasks and underlying streams—consider explicitly awaiting `stream_tasks` (or delegating to a shared helper) in the non-error path as was done previously.
- There is now overlap between `_terminate_process` and `_terminate_process_via_wait_task` (similar terminate/kill/grace logic); consider consolidating the common termination logic into a single helper to reduce duplication and keep failure-handling semantics in one place.

## Individual Comments

### Comment 1
<location> `cuprum/_pipeline_internals.py:411-420` </location>
<code_context>
+async def _wait_for_pipeline(
</code_context>

<issue_to_address>
**suggestion:** Responsibility for `stream_tasks` and waiting on them is split between `_wait_for_pipeline` and `_run_pipeline`, which makes the lifecycle harder to follow.

Right now `_wait_for_pipeline` only uses `stream_tasks` on the error path, while the caller must await them on the success path. This makes its contract unclear: it accepts `stream_tasks` but doesn’t own them in all cases. Please either (a) have `_wait_for_pipeline` fully own awaiting/cancelling `stream_tasks` and only return exit codes + failure index, or (b) remove `stream_tasks` from its parameters and keep it focused solely on exit-code/fail-fast behavior, so ownership of the stream tasks is explicit and less error-prone.

Suggested implementation:

```python
async def _wait_for_pipeline(
    processes: list[asyncio.subprocess.Process],
    *,
    pipe_tasks: list[asyncio.Task[None]],
    cancel_grace: float,
) -> _PipelineWaitResult:

```

To fully implement the suggested ownership change (option (b) in your comment), you should also:

1. **Update all call sites of `_wait_for_pipeline` (likely in `_run_pipeline`)**:
   - Stop passing `stream_tasks` to `_wait_for_pipeline`.
   - Ensure the caller is solely responsible for awaiting and/or cancelling `stream_tasks` on both success and error paths. For example:
     - After `_wait_for_pipeline` returns, the caller should `await` or `asyncio.gather` all `stream_tasks`, handling cancellation and exceptions as appropriate.

2. **Remove all remaining references to `stream_tasks` inside `_wait_for_pipeline`**:
   - Any logic in `_wait_for_pipeline` that cancels or awaits `stream_tasks` on error must be deleted or relocated to the caller.
   - `_wait_for_pipeline` should now only:
     - Wait on the pipeline processes / `pipe_tasks`.
     - Implement fail-fast / exit-code aggregation.
     - Handle subprocess cleanup on cancellation.
   - This keeps `_wait_for_pipeline` focused on process/pipe lifecycle and makes `stream_tasks` lifecycle management explicit at the call site.

3. **Adjust type hints and docs (if present elsewhere)**:
   - If `_PipelineWaitResult` or any docstrings mention stream handling responsibility, update the wording to clarify that `_wait_for_pipeline` no longer owns `stream_tasks`.

These changes will make ownership of `stream_tasks` explicit: `_run_pipeline` (or other callers) fully own their lifecycle, and `_wait_for_pipeline` is limited to exit-code and cancellation behavior for the subprocess pipeline itself.
</issue_to_address>

### Comment 2
<location> `cuprum/unittests/test_pipeline.py:184-185` </location>
<code_context>

     assert isinstance(result, PipelineResult)
     assert result.ok is False
+    assert result.failure is result.stages[-1]
+    assert result.failure_index == 1
     assert result.final is result.stages[-1]
     assert result.final.exit_code == 1
</code_context>

<issue_to_address>
**suggestion (testing):** Add a unit test that validates `PipelineResult.failure`/`failure_index` for a fully successful pipeline

To fully cover the new API, please also add a separate test for the all-success case (every stage exits with 0) that asserts `result.ok is True`, `result.failure is None`, and `result.failure_index is None`, so we verify the "no failure" semantics don’t regress.

Suggested implementation:

```python
    assert isinstance(result, PipelineResult)
    assert result.ok is False
    assert result.failure is result.stages[-1]
    assert result.failure_index == 1
    assert result.final is result.stages[-1]
    assert result.final.exit_code == 1
    assert len(result.stages) == 2

    with pytest.raises(ValueError, match="at least two stages"):
        Pipeline((only,))


def test_pipeline_result_all_success_has_no_failure(tmp_path):
    """
    Validate that PipelineResult.failure / failure_index are None
    when all pipeline stages succeed.
    """
    # Arrange: build a pipeline where every stage exits with code 0.
    # The exact configuration may need to match the helpers used elsewhere
    # in this test module (see <additional_changes> below).
    pipeline_config = _prepare_pipeline_config(
        tmp_path,
        python_catalogue,
        stages=[
            ("python", "-c", "import sys; sys.exit(0)"),
            ("python", "-c", "import sys; sys.exit(0)"),
        ],
    )

    pipeline = Pipeline(pipeline_config)

    # Act
    result = _wait_for_pipeline(pipeline)

    # Assert
    assert isinstance(result, PipelineResult)
    assert result.ok is True
    assert result.failure is None
    assert result.failure_index is None
    assert result.final is result.stages[-1]
    assert all(stage.exit_code == 0 for stage in result.stages)


class _StubPipelineWaitProcess:

```

below).
    pipeline_config = _prepare_pipeline_config(
        tmp_path,
        python_catalogue,
        stages=[
            ("python", "-c", "import sys; sys.exit(0)"),
            ("python", "-c", "import sys; sys.exit(0)"),
        ],
    )

    pipeline = Pipeline(pipeline_config)

    # Act
    result = _wait_for_pipeline(pipeline)

    # Assert
    assert isinstance(result, PipelineResult)
    assert result.ok is True
    assert result.failure is None
    assert result.failure_index is None
    assert result.final is result.stages[-1]
    assert all(stage.exit_code == 0 for stage in result.stages)


class _StubPipelineWaitProcess:
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
I only see the assertion section of the failure-case test and the helper imports, so you may need to align the new test with the existing conventions in `test_pipeline.py`:

1. **Align pipeline construction**  
   - If other tests construct a pipeline via a helper (e.g. `_prepare_pipeline_config` + `Pipeline(...)`, or some `run_pipeline(...)` helper), mirror that exact pattern instead of the placeholder `Pipeline(pipeline_config)` shown here.
   - If `_prepare_pipeline_config` has a different signature (e.g. `stages` named differently, or positional args), adjust the call accordingly.

2. **Command for successful stages**  
   - If the rest of the file uses a different way to define stages (e.g. objects instead of tuples, catalogue lookups instead of raw commands, or wrappers like `_pump_stream`), update the `stages=[ ... ]` list to follow that pattern while ensuring both configured stages exit with code `0`.

3. **Imports / fixtures**  
   - If `Pipeline` is not yet imported in this file (it might be, but it’s not visible in the snippet), add the appropriate import at the top of the file.
   - If `python_catalogue` is not used as a plain reference but is instead provided as a pytest fixture, change the test signature to accept it as a parameter, e.g.:
     ```python
     def test_pipeline_result_all_success_has_no_failure(tmp_path, python_catalogue):
         ...
     ```
     or drop it from the call if your helpers don’t require it.

4. **Exit-code assertions**  
   - If `stage.exit_code` is not the right attribute name (for example, if the stage object exposes `returncode` or similar), update the `assert all(...)` line accordingly to match the existing stage API.
</issue_to_address>

@coderabbitai

coderabbitai Bot commented Dec 19, 2025

Copy link
Copy Markdown

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the pipeline internals to check _wait_for_pipeline implementation
cat cuprum/_pipeline_internals.py

Length of output: 17876


🏁 Script executed:

#!/bin/bash
# Read the test file to check if success case test was added
cat cuprum/unittests/test_pipeline.py

Length of output: 13841


🏁 Script executed:

#!/bin/bash
# Search for _terminate_process functions to check for duplication
rg -n "^async def _terminate_process" cuprum/

Length of output: 213


@leynos: Yes, all the review comments have been successfully resolved! Here's the status of each issue:

✅ Comment 1: stream_tasks Lifecycle & Ownership

Resolved. The stream_tasks parameter has been removed entirely from _wait_for_pipeline (line 411 in _pipeline_internals.py). The function now only accepts pipe_tasks and cancel_grace:

async def _wait_for_pipeline(
    processes: list[asyncio.subprocess.Process],
    *,
    pipe_tasks: list[asyncio.Task[None]],
    cancel_grace: float,
) -> _PipelineWaitResult:

The caller (_run_pipeline) now fully owns the stream_tasks lifecycle and awaits them on the success path (lines 156-159):

stderr_by_stage = [
    None if task is None else await task for task in stderr_tasks
]
final_stdout = None if stdout_task is None else await stdout_task

This makes ownership explicit and prevents task leaks.

✅ Comment 2: Termination Logic Consolidation

Resolved. The common termination logic has been extracted into _terminate_process_with_wait (lines 68-95), which both _terminate_process and _terminate_process_via_wait_task now delegate to. This eliminates duplication and centralises the terminate→timeout→kill escalation logic:

  • _terminate_process: Uses process.returncode and process.wait()
  • _terminate_process_via_wait_task: Uses wait_task.done() and asyncio.shield(wait_task)
  • Both call the shared _terminate_process_with_wait helper

✅ Comment 3: stream_tasks Ownership Split

Resolved. As noted above, stream_tasks is no longer passed to _wait_for_pipeline, eliminating the split ownership issue. The lifecycle is now entirely managed by _run_pipeline.

✅ Comment 4: Missing Success-Case Test

Resolved. The test test_pipeline_run_sync_success_has_no_failure (lines 190-210 in test_pipeline.py) validates the all-success case:

assert result.ok is True
assert result.failure is None
assert result.failure_index is None

This ensures the "no failure" semantics are properly tested and won't regress.


All structural issues have been addressed. The refactoring maintains backward compatibility whilst improving code clarity and lifecycle management. 🎉

…and termination

- Introduced helper _run_test_pipeline to reduce duplication in pipeline stage exit code tests.
- Refactored existing synchronous tests to use the helper for clarity.
- Added async _exercise_wait_for_pipeline helper to test _wait_for_pipeline with stub processes.
- Added assertions to verify pipeline failure indices and proper termination of downstream processes on failures.
- Improved coverage for fail-fast behavior of pipeline stages in async scenarios.
- Removed duplicated test code by consolidating common test setup logic.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 19, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

cuprum/unittests/test_pipeline.py

Comment on file

    with scoped(allowlist=frozenset([python_program])):
        result = pipeline.run_sync()
    result = _run_test_pipeline([0, 1])

❌ New issue: Code Duplication
The module contains 5 functions with similar structure: test_pipeline_run_sync_failure_sets_ok_false_and_final_to_failed_stage,test_pipeline_run_sync_success_has_no_failure,test_wait_for_pipeline_fail_fast_early_stage_failure_terminates_downstream,test_wait_for_pipeline_fail_fast_last_stage_failure_records_failure_index and 1 more functions

@coderabbitai

This comment was marked as resolved.

Refactor pipeline run_sync and fail-fast asyncio tests to use pytest
parametrize for multiple scenarios covering success and failure cases.

- Combines success and failure cases into single parameterized test for
  run_sync failure semantics.
- Parameterizes fail-fast pipeline tests for early, middle, and final stage
  failures, verifying termination and failure indexes.
- Enhances test clarity and coverage by consolidating similar tests.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos

leynos commented Dec 19, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix:

cuprum/unittests/test_pipeline.py

Comment on lines +487 to +517

def test_wait_for_pipeline_fail_fast_scenarios(
    scenario: str,
    exit_codes: tuple[int, int, int],
    ready_stages: frozenset[int],
    *,
    expected_failure_index: int,
    expected_exit_codes: list[int],
    terminated_stages: frozenset[int],
) -> None:
    """Validate fail-fast termination behaviour across different failure scenarios.

    Tests that:
    - Early stage failures terminate all downstream stages
    - Middle stage failures terminate all downstream stages
    - Final stage failures record failure index without terminating others
    """
    p0, p1, p2, result = asyncio.run(
        _exercise_wait_for_pipeline(
            exit_codes=exit_codes,
            ready_stages=ready_stages,
        ),
    )

    _assert_pipeline_failure(
        result,
        failure_index=expected_failure_index,
        exit_codes=expected_exit_codes,
    )

    for idx, process in enumerate([p0, p1, p2]):
        _assert_stage_terminated(process, should_terminate=(idx in terminated_stages))

❌ New issue: Excess Number of Function Arguments
test_wait_for_pipeline_fail_fast_scenarios has 6 arguments, max arguments = 4

@coderabbitai

This comment was marked as resolved.

Refactored the fail-fast pipeline test scenarios by introducing a frozen dataclass `_FailFastScenario` to encapsulate test parameters. Updated parametrized test cases to use instances of this dataclass instead of multiple separate parameters, enhancing readability and maintainability of the test code.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos

leynos commented Dec 19, 2025

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Dec 19, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

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: 2

📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7dd89d0 and 65547de.

📒 Files selected for processing (4)
  • cuprum/_pipeline_internals.py (1 hunks)
  • cuprum/_testing.py (1 hunks)
  • cuprum/sh.py (2 hunks)
  • cuprum/unittests/test_pipeline.py (4 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: For Python files, ensure testing passes all relevant unit and behavioral tests by running make test.
For Python files, ensure linting passes by running make lint.
For Python files, ensure formatting adheres to standards by running make check-fmt and applying make fmt if needed.
For Python files, ensure type checking passes by running make typecheck.
For Python development, refer to Python-specific guidelines in the .rules/ directory, including Python Code Style Guidelines, Context Managers, Exceptions and Logging, Generators, Project Configuration, Return Patterns, and Typing.

**/*.py: Use snake_case for Python filenames (e.g., http_client.py, task_queue.py)
Use PascalCase for class names
Use snake_case for variable and function names
Use UPPER_SNAKE_CASE for module-level constants
Prefix private/internal functions and variables with a single underscore (_)
Enable full static type coverage using Pyright and maintain typing throughout the codebase
Use TypedDict or Dataclass for structured data, preferring @DataClass(slots=True) for internal-only usage
Avoid using Any type; use Unknown, generics, or cast() with documentation instead
Provide explicit return type annotations (e.g., -> None, -> str) for all public functions and class methods
Enforce strict mode in Pyright and treat all Pyright warnings as CI errors; use # pyright: ignore sparingly with explanation
Avoid side effects at import time; modules should not modify global state or perform actions on import
Use .env or settings modules for environment-specific configuration; never hardcode secrets
Use Ruff for formatting; let Ruff handle whitespace and formatting entirely
Use NumPy-format docstrings for public functions, classes, and modules
Use inline comments to explain tricky or non-obvious code logic and decisions

**/*.py: Use context managers (with contextlib.contextmanager or class-based __enter__/__exit__) to encapsulate setup and teardown logic for resource management (f...

Files:

  • cuprum/unittests/test_pipeline.py
  • cuprum/_testing.py
  • cuprum/sh.py
  • cuprum/_pipeline_internals.py

⚙️ CodeRabbit configuration file

**/*.py: - Keep C90 / mccabe complexity ≤ 9

  • Follow single responsibility and CQRS (command/query segregation)
  • Prefer structural pattern matching to
  • Prefer structural pattern matching over isinstance() or imperative decomposition.
  • Docstrings must follow the numpy style guide. Use a single-line summary for private functions and methods, and full structured docs for all public interfaces.
  • Move conditionals with >2 branches to predicate/helper functions
  • Avoid eval, exec, pickle, monkey-patching, ctypes, unsafe shell
  • Every module must begin with a triple-quoted docstring explaining its purpose, utility, and usage, including example calls if appropriate.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lint suppressions:
    • Blanket # noqa, file-level skips, and categories are forbidden
    • Only narrow in-line disables (# noqa: XYZ) are permitted, and must be accompanied by FIXME: or a ticket link, and used only as a last resort.
  • Use pytest fixtures for shared setup (conftest.py or fixtures/)
  • Replace duplicate tests with @pytest.mark.parametrize
  • Prefer pytest-mock or unittest.mock for stubs/mocks
  • Use assert …, "message" over bare asserts
  • Reflect all API/behaviour changes in docs/ and update roadmap on completion
  • Files must not exceed 400 logical lines:
    • Decompose large modules into subpackages
    • Split large match/case or dispatch tables by domain and collocate with targets if appropriate
    • Move bulky data (fixtures, templates) to external files for parsing at runtime
  • Mutable defaults and shadowed built-ins are forbidden
  • All code must have clear type hints using modern style (A | B, list[str], class Foo[A]:, type Bar = int, etc.), with ABC imports drawn from the correct stdlib module.
  • All path manipulation must be performed using pathlib for cross platform safety. Do not use string manipulation for extraction of path components or filename ele...

Files:

  • cuprum/unittests/test_pipeline.py
  • cuprum/_testing.py
  • cuprum/sh.py
  • cuprum/_pipeline_internals.py
**/unittests/test_*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

Colocate unit tests with code using an unittests subdirectory with test_ prefix (e.g., user_auth/unittests/test_models.py)

Files:

  • cuprum/unittests/test_pipeline.py
**/test_*.py

📄 CodeRabbit inference engine (.rules/python-00.md)

**/test_*.py: Use pytest idioms: prefer fixtures over setup/teardown methods, parametrize broadly, avoid unnecessary mocks
Group related tests using class with method names prefixed by test_
Write tests from a user's perspective; test public behavior rather than internals
Avoid mocking too much; prefer test doubles only for external services or non-deterministic behaviors

Files:

  • cuprum/unittests/test_pipeline.py
**/*test*.py

📄 CodeRabbit inference engine (.rules/python-exception-design-raising-handling-and-logging.md)

Use specific exception types and message constraints with pytest.raises(SpecificError, match=r"pattern") in tests; avoid overly broad exception assertions (B017)

Files:

  • cuprum/unittests/test_pipeline.py
  • cuprum/_testing.py
🧬 Code graph analysis (3)
cuprum/unittests/test_pipeline.py (5)
cuprum/context.py (1)
  • scoped (230-262)
cuprum/_pipeline_internals.py (2)
  • _PipelineWaitResult (382-384)
  • _wait_for_pipeline (460-504)
cuprum/_streams.py (1)
  • _pump_stream (54-75)
cuprum/sh.py (11)
  • Pipeline (361-406)
  • PipelineResult (115-153)
  • run_sync (330-357)
  • run_sync (398-406)
  • ok (109-111)
  • ok (146-148)
  • final (134-136)
  • failure (139-143)
  • stdout (151-153)
  • run (211-328)
  • run (383-396)
cuprum/catalogue.py (1)
  • allowlist (67-69)
cuprum/sh.py (2)
cuprum/_pipeline_internals.py (4)
  • _merge_env (44-52)
  • _run_before_hooks (30-41)
  • _run_pipeline (136-186)
  • _terminate_process (55-65)
cuprum/_streams.py (2)
  • _consume_stream (26-51)
  • _StreamConfig (16-23)
cuprum/_pipeline_internals.py (3)
cuprum/_streams.py (3)
  • _consume_stream (26-51)
  • _pump_stream (54-75)
  • _StreamConfig (16-23)
cuprum/context.py (2)
  • current_context (176-178)
  • check_allowed (68-79)
cuprum/sh.py (4)
  • CommandResult (81-111)
  • ExecutionContext (157-185)
  • SafeCmd (189-357)
  • stdout (151-153)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (36)
cuprum/unittests/test_pipeline.py (12)

1-21: LGTM on imports and module structure.

The import reorganization to use cuprum._testing addresses the prior review comment about brittle re-exports. The dedicated testing surface cleanly separates test-facing internals from the public API.


24-92: Well-designed test doubles.

The stub classes properly simulate async subprocess behaviour with configurable failure scenarios and call tracking. The _StubSpawnProcess correctly mimics SIGTERM exit code (-15) when returncode is unset.


94-143: Composition tests correctly verify pipeline construction semantics.


176-198: Streaming test correctly validates inter-stage communication.


251-296: Pump stream tests thoroughly verify backpressure and error handling.


298-335: Spawn failure cleanup test correctly validates resource cleanup.


337-344: Validation test correctly enforces minimum stage constraint.


346-383: Stub correctly simulates signal-based process termination for fail-fast tests.


422-447: Assertion helpers improve test readability and reduce duplication.


449-458: Scenario dataclass cleanly encapsulates parametrized test configuration.


473-482: Clarify termination behaviour in scenario ID and docstring.

The scenario ID says "middle-stage-failure-terminates-downstream" and the docstring states "Middle stage failures terminate all downstream stages", but the expected behaviour terminates both stage 0 (upstream) and stage 2 (downstream). Either update the naming/documentation to reflect that all other stages are terminated, or verify this is the intended fail-fast behaviour.


385-420: Use NumPy-format docstrings.

Same as _run_test_pipeline, use NumPy format for docstrings.

Proposed fix
 async def _exercise_wait_for_pipeline(
     exit_codes: tuple[int, int, int],
     ready_stages: frozenset[int],
 ) -> tuple[...]:
     """Execute _wait_for_pipeline with stub processes and custom exit scenarios.
 
-    Args:
-        exit_codes: Exit code for each of the three stages.
-        ready_stages: Set of stage indices that should be immediately ready.
+    Parameters
+    ----------
+    exit_codes:
+        Exit code for each of the three stages.
+    ready_stages:
+        Set of stage indices that should be immediately ready.
 
-    Returns:
-        Tuple of (process0, process1, process2, wait_result).
+    Returns
+    -------
+    tuple
+        Tuple of (process0, process1, process2, wait_result).
 
     """

Likely an incorrect or invalid review comment.

cuprum/_testing.py (1)

1-52: Clean test-facing surface for internal helpers.

This module properly addresses the prior review comment about brittle re-exports via # noqa: F401. The _EXPORTS dictionary pattern for building __all__ and subsequent deletion keeps the module namespace clean.

cuprum/sh.py (2)

17-27: Clean import delegation to internal modules.

The imports from _pipeline_internals and _streams properly centralise runtime logic while keeping the public API surface in sh.py.


124-144: Fail-fast metadata correctly exposed via failure_index and failure property.

The failure property safely handles the None case and correctly indexes into stages. The docstring clearly documents the semantics.

cuprum/_pipeline_internals.py (21)

1-28: Module docstring and imports are appropriate.

The TYPE_CHECKING guard correctly avoids runtime circular imports while enabling static analysis.


30-42: Hook coordination correctly validates allowlist and returns after hooks.


44-53: Environment merge correctly handles None case with lazy os import.


55-91: Termination logic correctly handles race conditions and escalates to SIGKILL.

The exception handling for ProcessLookupError and OSError properly handles cases where the process exits between check and signal. The noqa: UP041 annotation is appropriate for maintaining explicit asyncio semantics.


93-134: Configuration dataclass cleanly encapsulates pipeline runtime options.


136-187: Pipeline orchestration correctly owns stream task lifecycle.

The _run_pipeline function properly awaits stream tasks on success (lines 159-162) and cancels/gathers them on error (lines 164-166), addressing the prior review comment about ownership. The noqa: PLR0914 is justified for this coordination function.


189-218: Stage stream FD configuration correctly implements pipeline wiring semantics.


220-247: Capture task creation correctly handles per-stage stream requirements.


249-274: Spawn cleanup correctly terminates processes and cancels capture tasks.


276-327: Process spawning correctly implements cleanup on failure.


329-342: Pipe task creation correctly wires adjacent stage streams.


344-353: Stream task flattening correctly filters None tasks.


355-379: Pipe result handling correctly distinguishes expected from unexpected failures.

BrokenPipeError and ConnectionResetError are correctly treated as expected when downstream processes terminate early (e.g., head). Other exceptions are properly surfaced.


381-405: Wait state classes correctly encapsulate pipeline completion tracking.


407-424: Error cleanup correctly terminates processes and documents stream task ownership.


426-445: Completed task processing correctly triggers fail-fast termination on non-zero exit.


447-458: Pipeline wait finalization correctly avoids surfacing failures during exception handling.


460-505: Pipeline wait correctly coordinates task completion and fail-fast semantics.

The asyncio.wait with FIRST_COMPLETED enables reactive fail-fast behaviour. The fallback to -1 for None exit codes (line 488) is a defensive measure that shouldn't trigger in normal execution paths.


507-519: Wait task-based termination correctly uses asyncio.shield to preserve shared state.


521-554: Remaining stage termination correctly terminates all non-failed, still-running stages.

The implementation terminates both upstream and downstream stages (skipping only the failed stage and already-completed stages). This explains the test scenario where a middle-stage failure terminates stages 0 and 2. Update the test scenario documentation to reflect this "terminate all remaining" behaviour rather than "terminate downstream".


556-564: After-hook invocation correctly iterates with strict zip validation.

Comment thread cuprum/unittests/test_pipeline.py
Comment thread cuprum/unittests/test_pipeline.py
Refactored docstrings in test_pipeline.py to use NumPy style sections for parameters and returns, improving readability and consistency.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title Implement fail-fast pipeline; surface failing stage in results Implement fail-fast pipeline with failure metadata and internals Dec 20, 2025
@leynos
leynos merged commit b76557d into main Dec 20, 2025
4 checks passed
@leynos
leynos deleted the terragon/implement-pipeline-failure-policy-vr9prp branch December 20, 2025 01:10
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.

1 participant