Skip to content

Improve lcov coverage parsing#42

Merged
leynos merged 9 commits intomainfrom
codex/extract-coverage-from-lcov.info-using-python
Jul 28, 2025
Merged

Improve lcov coverage parsing#42
leynos merged 9 commits intomainfrom
codex/extract-coverage-from-lcov.info-using-python

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Jul 27, 2025

Summary

  • extract line coverage percent from lcov files
  • use the new parsing logic when running Rust coverage
  • update tests for lcov coverage parsing

Testing

  • make lint
  • make test

https://chatgpt.com/codex/tasks/task_e_688588977888832283e0264c1eb19b1e

Summary by Sourcery

Improve coverage parsing by extracting line coverage from lcov and Cobertura outputs via new helper functions, integrating them into the Rust and Python coverage scripts, and bolstering error handling and test coverage.

New Features:

  • Add get_line_coverage_percent_from_lcov to extract line coverage from lcov.info files
  • Introduce coverage_parsers module with get_line_coverage_percent_from_cobertura for Cobertura XML

Enhancements:

  • Update run_rust and run_python scripts to use the new parsers for lcov and Cobertura formats
  • Include lxml dependency and improve error handling with proper exit codes and two-decimal formatting

Tests:

  • Add unit tests for lcov parser covering zero lines, missing tags, malformed data, and file errors
  • Add unit tests for Cobertura parser covering per-line detail, root totals fallback, and zero coverage cases

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jul 27, 2025

Reviewer's Guide

This PR enhances coverage parsing by implementing line coverage extraction from lcov files, centralizing Cobertura XML parsing, integrating both parsers into the Rust and Python scripts, updating dependencies, and expanding tests to verify two-decimal formatting and edge cases.

Class diagram for new and refactored coverage parsing functions

classDiagram
    class coverage_parsers {
        +get_line_coverage_percent_from_cobertura(xml_file: Path) str
    }
    class run_rust {
        +get_line_coverage_percent_from_lcov(lcov_file: Path) str
    }
    coverage_parsers <.. run_rust : uses
Loading

Flow diagram for coverage percent extraction logic in Rust script

flowchart TD
    A[Start: Run Rust coverage] --> B{Format?}
    B -- lcov --> C[get_line_coverage_percent_from_lcov]
    B -- cobertura --> D[get_line_coverage_percent_from_cobertura]
    B -- other --> E[extract_percent]
    C --> F[Write percent to GitHub output]
    D --> F
    E --> F
Loading

Flow diagram for coverage percent extraction logic in Python script

flowchart TD
    A[Start: Run Python coverage] --> B{Format?}
    B -- coveragepy --> C[get_line_coverage_percent_from_cobertura]
    B -- other --> D[get_line_coverage_percent_from_cobertura]
    C --> E[Write percent to GitHub output]
    D --> E
Loading

File-Level Changes

Change Details Files
Implement overall line coverage extraction from lcov files
  • Add get_line_coverage_percent_from_lcov function using regex to sum LF/LH entries
  • Handle read errors with typer.Exit and malformed data by returning "0.00"
  • Format computed percentage to two decimal places
scripts/run_rust.py
Integrate lcov and Cobertura parsing into coverage scripts
  • Switch run_rust main to call lcov parser when format is lcov
  • Switch run_python to use get_line_coverage_percent_from_cobertura instead of old XML percent logic
  • Adjust extract_percent error handling to use positional Exit code
scripts/run_rust.py
scripts/run_python.py
Extract Cobertura XML parsing into a shared helper module
  • Create coverage_parsers.py with get_line_coverage_percent_from_cobertura
  • Use Decimal for precise two-decimal rounding
  • Catch read/parse errors and default to "0.00"
scripts/coverage_parsers.py
scripts/run_rust.py
scripts/run_python.py
Update script dependencies to include lxml
  • Add "lxml" to dependencies in run_rust and run_python headers
  • Remove defusedxml from Python script dependencies
scripts/run_rust.py
scripts/run_python.py
Expand and refine tests for coverage parsing
  • Introduce run_rust_module and run_python_module fixtures with stubbed dependencies
  • Add tests covering lcov edge cases (zero/missing/malformed files, permission errors)
  • Update existing test to assert percent formatted with two decimals
  • Add Cobertura line-detail and root-attribute tests
tests/test_scripts.py

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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jul 27, 2025

Summary by CodeRabbit

  • New Features

    • Improved support for parsing and displaying code coverage percentages from both Cobertura XML and LCOV formats.
    • Enhanced error handling and user feedback for coverage report parsing.
  • Bug Fixes

    • More accurate calculation and formatting of coverage percentages, including correct rounding to two decimal places.
  • Tests

    • Added comprehensive tests for coverage parsing, including edge cases and error scenarios, to ensure robust behaviour.

Walkthrough

Introduce new functions to extract line coverage percentages from LCOV and Cobertura XML files. Update main coverage extraction logic in Rust and Python scripts to use these functions based on input format. Replace XML parsing library and logic in the Python script. Add tests covering edge cases and malformed input scenarios.

Changes

File(s) Change Summary
Coverage Parsing Logic
.github/actions/generate-coverage/scripts/coverage_parsers.py
Add new module with get_line_coverage_percent_from_cobertura function to parse Cobertura XML files using lxml and compute coverage percentage with rounding and fallback logic.
Rust Coverage Script
.github/actions/generate-coverage/scripts/run_rust.py
Add get_line_coverage_percent_from_lcov and import get_line_coverage_percent_from_cobertura; update main function to select parsing method by format; add lxml dependency; improve error handling.
Python Coverage Script
.github/actions/generate-coverage/scripts/run_python.py
Remove inline XML parsing function; import and use get_line_coverage_percent_from_cobertura from new parser module; replace defusedxml with lxml.
Tests and Fixtures
.github/actions/generate-coverage/tests/test_scripts.py
Add pytest fixtures for run_rust and run_python modules with monkeypatching; add tests for LCOV and Cobertura coverage extraction including edge cases and error handling; update coverage formatting assertions.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CoverageScript
    participant CoverageFile

    User->>CoverageScript: Run coverage script with format parameter
    CoverageScript->>CoverageFile: Read coverage report file
    alt Format is LCOV
        CoverageScript->>CoverageScript: Parse LCOV file for LF and LH
    else Format is Cobertura XML
        CoverageScript->>CoverageScript: Parse XML with lxml, count covered and total lines
    else Other format
        CoverageScript->>CoverageScript: Use fallback text parsing method
    end
    CoverageScript->>User: Output coverage percentage with two decimals
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

Coverage files once cryptic and terse,
Now parsed with precision, line by line rehearsed.
LCOV and Cobertura, both tamed with care,
Two decimals shining, no detail spared.
Tests stand guard, robust and bright—
Coverage clarity, a coder’s delight!
📊✨🦀

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/extract-coverage-from-lcov.info-using-python

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey @leynos - I've reviewed your changes and found some issues that need to be addressed.

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `.github/actions/generate-coverage/tests/test_scripts.py:26` </location>
<code_context>
     out = tmp_path / "cov.lcov"
     gh = tmp_path / "gh.txt"

+    out.write_text("LF:200\nLH:163\n")
     shell_stubs.register(
         "cargo",
</code_context>

<issue_to_address>
Consider adding tests for edge cases in lcov parsing, such as zero lines found or missing tags.

Currently, only the standard case is tested. Please add tests for scenarios like LF = 0, missing LH, or malformed lcov files to ensure robust parsing.

Suggested implementation:

```python
    out.write_text("LF:200\nLH:163\n")
    shell_stubs.register(
        "cargo",
        stdout="Coverage: 81.5%\n",

    data = gh.read_text().splitlines()
    assert f"file={out}" in data
    assert "percent=81.50" in data


def test_lcov_zero_lines_found(tmp_path: Path, shell_stubs: StubManager) -> None:
    out = tmp_path / "cov_zero.lcov"
    gh = tmp_path / "gh_zero.txt"

    # LF = 0, LH = 0
    out.write_text("LF:0\nLH:0\n")
    shell_stubs.register(
        "cargo",
        stdout="Coverage: 0.0%\n",
    )
    # Simulate the rest of the test as in the main test, or call the function under test
    # For demonstration, we assume the same output file is written
    data = ["file={}".format(out), "percent=0.00"]
    assert f"file={out}" in data
    assert "percent=0.00" in data


def test_lcov_missing_lh_tag(tmp_path: Path, shell_stubs: StubManager) -> None:
    out = tmp_path / "cov_missing_lh.lcov"
    gh = tmp_path / "gh_missing_lh.txt"

    # Only LF present
    out.write_text("LF:100\n")
    shell_stubs.register(
        "cargo",
        stdout="Coverage: 0.0%\n",
    )
    # Simulate the rest of the test as in the main test, or call the function under test
    # For demonstration, we assume the output is handled gracefully
    data = ["file={}".format(out), "percent=0.00"]
    assert f"file={out}" in data
    assert "percent=0.00" in data


def test_lcov_malformed_file(tmp_path: Path, shell_stubs: StubManager) -> None:
    out = tmp_path / "cov_malformed.lcov"
    gh = tmp_path / "gh_malformed.txt"

    # Malformed: non-integer values and missing LF
    out.write_text("LF:abc\nLH:xyz\n")
    shell_stubs.register(
        "cargo",
        stdout="Coverage: 0.0%\n",
    )
    # Simulate the rest of the test as in the main test, or call the function under test
    # For demonstration, we assume the output is handled gracefully
    data = ["file={}".format(out), "percent=0.00"]
    assert f"file={out}" in data
    assert "percent=0.00" in data

```

- If the actual lcov parsing logic is in a helper function, you may want to call that function directly in these tests and assert on its return value or raised exceptions.
- Adjust the test logic to match the actual output and error handling of your codebase (e.g., if errors are logged or exceptions are raised, assert accordingly).
- If the output file or percent calculation is handled differently, update the assertions to match the real behavior.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread .github/actions/generate-coverage/tests/test_scripts.py
Comment thread .github/actions/generate-coverage/scripts/run_rust.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 68ffa5e and 7bc7991.

📒 Files selected for processing (2)
  • .github/actions/generate-coverage/scripts/run_rust.py (2 hunks)
  • .github/actions/generate-coverage/tests/test_scripts.py (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
.github/actions/*/tests/**

📄 CodeRabbit Inference Engine (AGENTS.md)

.github/actions/*/tests/**: Tests for each action must be present in a tests/ directory within the action's directory.
Contract tests must validate that declared inputs and outputs round-trip correctly.

Files:

  • .github/actions/generate-coverage/tests/test_scripts.py
🧬 Code Graph Analysis (1)
.github/actions/generate-coverage/scripts/run_rust.py (1)
.github/actions/ratchet-coverage/scripts/run_coverage.py (1)
  • extract_percent (19-25)
🔇 Additional comments (3)
.github/actions/generate-coverage/scripts/run_rust.py (1)

90-90: Excellent conditional logic for format-specific parsing.

The modification correctly routes LCOV format processing to the new function while preserving existing behaviour for other formats. This maintains backward compatibility and leverages the structured nature of LCOV files for more reliable parsing.

.github/actions/generate-coverage/tests/test_scripts.py (2)

26-26: Correct LCOV test data format.

The test data correctly simulates LCOV format output with appropriate LF (lines found) and LH (lines hit) entries. The values 200 and 163 produce a clean 81.50% calculation for verification.


64-64: Correct assertion update for new formatting.

The assertion properly reflects the new 2-decimal place formatting introduced by percent_from_lcov. The expected value "81.50" correctly matches the calculation from the test data (163/200 * 100).

Comment thread .github/actions/generate-coverage/scripts/run_rust.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
.github/actions/generate-coverage/tests/test_scripts.py (3)

160-167: Approve edge case testing for zero lines found.

This test correctly addresses the past review feedback by testing the zero-division edge case where no lines are found in the LCOV file.


169-174: Approve testing for missing LH tag scenario.

This test correctly addresses the past review feedback by testing the edge case where the LH tag is missing from the LCOV file, ensuring graceful handling.


176-181: Approve malformed file testing but verify error handling.

This test correctly addresses the past review feedback by testing malformed LCOV files. However, verify that the current implementation handles mixed numeric/non-numeric values gracefully, as map(int, ...) could raise ValueError for partially malformed files.

Run this script to verify error handling behaviour:

#!/bin/bash
# Description: Test how the function handles mixed numeric/non-numeric LCOV values

cd .github/actions/generate-coverage

python3 -c "
from pathlib import Path
import tempfile
import sys
sys.path.insert(0, 'scripts')
from run_rust import get_line_coverage_percent_from_lcov

with tempfile.NamedTemporaryFile(mode='w', suffix='.lcov', delete=False) as f:
    f.write('LF:100\nLH:abc\n')  # Mixed valid/invalid
    f.flush()
    
    try:
        result = get_line_coverage_percent_from_lcov(Path(f.name))
        print(f'Result: {result}')
    except Exception as e:
        print(f'Exception: {type(e).__name__}: {e}')
"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df83e64 and f81fcbc.

📒 Files selected for processing (2)
  • .github/actions/generate-coverage/scripts/run_rust.py (2 hunks)
  • .github/actions/generate-coverage/tests/test_scripts.py (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
.github/actions/*/tests/**

📄 CodeRabbit Inference Engine (AGENTS.md)

.github/actions/*/tests/**: Tests for each action must be present in a tests/ directory within the action's directory.
Contract tests must validate that declared inputs and outputs round-trip correctly.

Files:

  • .github/actions/generate-coverage/tests/test_scripts.py
🔇 Additional comments (3)
.github/actions/generate-coverage/scripts/run_rust.py (1)

101-105: LGTM! Clean conditional logic for format-specific parsing.

The conditional logic correctly branches between LCOV file parsing and stdout parsing based on the format parameter whilst maintaining backward compatibility.

.github/actions/generate-coverage/tests/test_scripts.py (2)

26-57: Excellent test fixture for isolated module testing.

The fixture correctly implements dynamic module loading with stubbed dependencies, enabling isolated testing of internal functions without external dependencies. The approach of using importlib and monkeypatch is well-executed.


64-64: Approve test updates for LCOV format consistency.

The test correctly pre-populates the coverage file with LCOV format data and asserts the percentage with two decimal places, matching the new parsing function's output format.

Also applies to: 102-102

Comment thread .github/actions/generate-coverage/scripts/run_rust.py Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
.github/actions/generate-coverage/scripts/run_rust.py (1)

51-73: Add error handling for file operations and malformed content.

The function lacks error handling for file reading failures and malformed content. Add try-except blocks to handle potential exceptions gracefully.

 def get_line_coverage_percent_from_lcov(lcov_file: Path) -> str:
     """Return the overall line coverage percentage from an ``lcov.info`` file.

     Parameters
     ----------
     lcov_file : Path
         Path to the coverage file to read.

     Returns
     -------
     str
         The coverage percentage with two decimal places.
     """
-    text = lcov_file.read_text(encoding="utf-8")
+    try:
+        text = lcov_file.read_text(encoding="utf-8")
+    except (FileNotFoundError, PermissionError, UnicodeDecodeError) as exc:
+        typer.echo(f"Could not read LCOV file {lcov_file}: {exc}", err=True)
+        raise typer.Exit(code=1) from exc

     def total(tag: str) -> int:
-        return sum(map(int, re.findall(rf"^{tag}:(\d+)$", text, flags=re.MULTILINE)))
+        try:
+            return sum(map(int, re.findall(rf"^{tag}:(\d+)$", text, flags=re.MULTILINE)))
+        except ValueError as exc:
+            typer.echo(f"Malformed LCOV file {lcov_file}: invalid {tag} values", err=True)
+            raise typer.Exit(code=1) from exc

     lines_found = total("LF")
     lines_hit = total("LH")
     if lines_found == 0:
         return "0.00"
     return f"{lines_hit / lines_found * 100:.2f}"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f81fcbc and 531bca2.

📒 Files selected for processing (3)
  • .github/actions/generate-coverage/scripts/run_python.py (3 hunks)
  • .github/actions/generate-coverage/scripts/run_rust.py (3 hunks)
  • .github/actions/generate-coverage/tests/test_scripts.py (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
.github/actions/*/tests/**

📄 CodeRabbit Inference Engine (AGENTS.md)

.github/actions/*/tests/**: Tests for each action must be present in a tests/ directory within the action's directory.
Contract tests must validate that declared inputs and outputs round-trip correctly.

Files:

  • .github/actions/generate-coverage/tests/test_scripts.py
🧬 Code Graph Analysis (3)
.github/actions/generate-coverage/scripts/run_rust.py (2)
.github/actions/generate-coverage/scripts/run_python.py (3)
  • get_line_coverage_percent_from_cobertura (46-85)
  • num_or_zero (65-67)
  • lines_from_detail (69-72)
.github/actions/ratchet-coverage/scripts/run_coverage.py (1)
  • extract_percent (19-25)
.github/actions/generate-coverage/scripts/run_python.py (1)
.github/actions/generate-coverage/scripts/run_rust.py (4)
  • get_line_coverage_percent_from_cobertura (76-115)
  • num_or_zero (95-97)
  • lines_from_detail (99-102)
  • total (66-67)
.github/actions/generate-coverage/tests/test_scripts.py (4)
.github/actions/generate-coverage/tests/conftest.py (1)
  • shell_stubs (29-40)
shellstub.py (1)
  • StubManager (39-153)
.github/actions/generate-coverage/scripts/run_rust.py (2)
  • get_line_coverage_percent_from_lcov (51-73)
  • get_line_coverage_percent_from_cobertura (76-115)
.github/actions/generate-coverage/scripts/run_python.py (1)
  • get_line_coverage_percent_from_cobertura (46-85)
🔇 Additional comments (6)
.github/actions/generate-coverage/scripts/run_rust.py (2)

4-4: LGTM!

The addition of lxml dependency is necessary for the new XML parsing functionality.


143-149: LGTM!

The conditional logic correctly selects the appropriate parser based on the format while maintaining backward compatibility.

.github/actions/generate-coverage/scripts/run_python.py (1)

126-126: LGTM!

The updates correctly use the new Cobertura parser for both coverage formats.

Also applies to: 129-129

.github/actions/generate-coverage/tests/test_scripts.py (3)

26-57: LGTM!

The fixtures provide excellent isolation for testing internal functions without external dependencies. The monkeypatching approach is well-implemented.

Also applies to: 183-216


64-64: LGTM!

The test correctly validates the new LCOV parsing logic with proper file content and two-decimal precision formatting.

Also applies to: 102-102


218-260: LGTM!

The Cobertura tests comprehensively cover the parsing logic including detail extraction, fallback to root totals, and edge cases.

Comment thread .github/actions/generate-coverage/scripts/run_python.py
Comment thread .github/actions/generate-coverage/scripts/run_rust.py Outdated
Comment thread .github/actions/generate-coverage/tests/test_scripts.py
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jul 27, 2025

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey @leynos - I've reviewed your changes and found some issues that need to be addressed.

  • I don’t see explicit imports of the new coverage_parsers functions in run_python.py/run_rust.py—make sure you add from coverage_parsers import get_line_coverage_percent_from_cobertura, get_line_coverage_percent_from_lcov (or similar) so the scripts actually compile.
  • The run_rust_module and run_python_module fixtures share a ton of duplicated setup/stubbing logic—consider extracting the common parts into helper functions or shared fixtures to reduce boilerplate and improve readability.
  • In get_line_coverage_percent_from_lcov, you re-raise ValueErrors inside the total helper then catch them to return “0.00”—it might be cleaner to catch parsing errors directly inside total (and call typer.Exit) so the function has a single, consistent error-handling path.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- I don’t see explicit imports of the new coverage_parsers functions in run_python.py/run_rust.py—make sure you add `from coverage_parsers import get_line_coverage_percent_from_cobertura, get_line_coverage_percent_from_lcov` (or similar) so the scripts actually compile.
- The run_rust_module and run_python_module fixtures share a ton of duplicated setup/stubbing logic—consider extracting the common parts into helper functions or shared fixtures to reduce boilerplate and improve readability.
- In get_line_coverage_percent_from_lcov, you re-raise ValueErrors inside the `total` helper then catch them to return “0.00”—it might be cleaner to catch parsing errors directly inside `total` (and call typer.Exit) so the function has a single, consistent error-handling path.

## Individual Comments

### Comment 1
<location> `.github/actions/generate-coverage/scripts/run_rust.py:90` </location>
<code_context>
+    except ValueError:
+        return "0.00"
+
+    if lines_found == 0:
+        return "0.00"
+    return f"{lines_hit / lines_found * 100:.2f}"
</code_context>

<issue_to_address>
Returning 0.00 for zero lines found may obscure configuration issues.

Consider adding a warning or error log when no lines are found to help identify empty or misconfigured lcov files.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    if lines_found == 0:
        return "0.00"
    return f"{lines_hit / lines_found * 100:.2f}"
=======
    if lines_found == 0:
        import logging
        logging.warning("No lines found in lcov data. This may indicate an empty or misconfigured lcov file.")
        return "0.00"
    return f"{lines_hit / lines_found * 100:.2f}"
>>>>>>> REPLACE

</suggested_fix>

### Comment 2
<location> `.github/actions/generate-coverage/scripts/coverage_parsers.py:38` </location>
<code_context>
+        typer.echo(f"Failed to parse coverage XML {xml_file}: {exc}", err=True)
+        return "0.00"
+
+    def num_or_zero(expr: str) -> int:
+        n = root.xpath(f"number({expr})")
+        return 0 if n != n else int(n)
+
+    def lines_from_detail() -> tuple[int, int]:
</code_context>

<issue_to_address>
NaN check via n != n is non-obvious.

Consider using math.isnan(n) for clarity, or add a comment if you prefer the current approach.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    def num_or_zero(expr: str) -> int:
        n = root.xpath(f"number({expr})")
        return 0 if n != n else int(n)
=======
    import math

    def num_or_zero(expr: str) -> int:
        n = root.xpath(f"number({expr})")
        return 0 if math.isnan(n) else int(n)
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `.github/actions/generate-coverage/scripts/run_rust.py:52` </location>
<code_context>
     return match[1]


+def get_line_coverage_percent_from_lcov(lcov_file: Path) -> str:
+    """Return the overall line coverage percentage from an ``lcov.info`` file.
+
</code_context>

<issue_to_address>
Consider moving the LCOV parser to a shared module and replacing the nested ternary with if/elif/else statements for clarity.

```suggestion
I’d extract the LCOV parser into `coverage_parsers.py` alongside Cobertura, then replace the nested ternary in `main()` with a simple if/elif/else. This keeps all parsing logic in one module and makes the flow more readable.

1) Move the LCOV helper into `coverage_parsers.py`:

```python
# coverage_parsers.py
from pathlib import Path
import re
import typer

def get_line_coverage_percent_from_lcov(lcov_file: Path) -> str:
    """Return the overall line coverage percentage from an lcov.info file."""
    try:
        text = lcov_file.read_text(encoding="utf-8")
    except OSError as exc:
        typer.echo(f"Could not read {lcov_file}: {exc}", err=True)
        raise typer.Exit(1) from exc

    def total(tag: str) -> int:
        values = re.findall(rf"^{tag}:(\d+)$", text, flags=re.MULTILINE)
        return sum(int(v) for v in values)

    try:
        lines_found = total("LF")
        lines_hit   = total("LH")
    except ValueError:
        return "0.00"
    if lines_found == 0:
        return "0.00"
    return f"{lines_hit / lines_found * 100:.2f}"
```

2) Remove the inline LCOV function from your runner and update `main()` to:

```python
from coverage_parsers import (
    get_line_coverage_percent_from_lcov,
    get_line_coverage_percent_from_cobertura,
)

    # ...
    try:
        retcode, stdout, stderr = cargo[args].run(retcode=None)
    except ProcessExecutionError as exc:
        retcode, stdout, stderr = exc.retcode, exc.stdout, exc.stderr

    if retcode != 0:
        typer.echo(f"cargo llvm-cov failed with code {retcode}: {stderr}", err=True)
        raise typer.Exit(retcode or 1)

    typer.echo(stdout)
    if fmt == "lcov":
        percent = get_line_coverage_percent_from_lcov(out)
    elif fmt == "cobertura":
        percent = get_line_coverage_percent_from_cobertura(out)
    else:
        percent = extract_percent(stdout)
```

This keeps all parsing functions co-located, and the selection logic becomes linear and easy to follow.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread .github/actions/generate-coverage/scripts/run_rust.py Outdated
Comment thread .github/actions/generate-coverage/scripts/coverage_parsers.py Outdated
Comment thread .github/actions/generate-coverage/scripts/run_rust.py Outdated
Comment thread .github/actions/generate-coverage/scripts/run_rust.py Outdated
Repository owner deleted a comment from coderabbitai Bot Jul 28, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 61ddd09 and d4451ba.

📒 Files selected for processing (1)
  • .github/actions/generate-coverage/scripts/coverage_parsers.py (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
.github/actions/generate-coverage/scripts/coverage_parsers.py (1)
.github/actions/generate-coverage/tests/test_scripts.py (2)
  • getroot (81-82)
  • xpath (56-75)
🔇 Additional comments (2)
.github/actions/generate-coverage/scripts/coverage_parsers.py (2)

1-26: Excellent import structure and dependency management.

The conditional lxml import with immediate failure feedback and the TYPE_CHECKING pattern for Path are well-implemented best practices.


28-87: Well-structured function with comprehensive error handling.

The two-tier parsing approach (detailed lines first, then root attributes) with Decimal-based precision calculations is robust and appropriate for Cobertura XML parsing.

Comment thread .github/actions/generate-coverage/scripts/coverage_parsers.py
@leynos leynos merged commit 22dfa00 into main Jul 28, 2025
1 check passed
@leynos leynos deleted the codex/extract-coverage-from-lcov.info-using-python branch July 28, 2025 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant