mrrc 0.9.0

A Rust library for reading, writing, and manipulating MARC bibliographic records in ISO 2709 binary format
Documentation
name: Code Coverage

# Measures coverage for the Rust core (cargo llvm-cov) and the Python
# wrapper (pytest-cov over mrrc/), enforces per-lane minimums natively,
# and renders reports into the job summary. No external coverage
# service: thresholds fail the job directly and HTML reports upload as
# artifacts. Scheduled weekly; not a PR gate.
#
# Ratchet policy: when a scheduled run reports a figure comfortably
# above a lane's minimum, raise that minimum to just below the figure.

on:
  schedule:
    # Weekly on Mondays at 04:00 UTC, offset from the daily scheduled
    # workflows (ASAN 02:00, fuzz 03:00, audit 05:00, Miri 06:00).
    - cron: '0 4 * * 1'
  workflow_dispatch:

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  RUST_BACKTRACE: 1
  # Minimum line-coverage percentages enforced per lane. The Rust floor
  # is an initial conservative value — measure on the first scheduled
  # run and ratchet. The Python floor sits just below the measured 83%.
  RUST_MIN_LINES: 60
  PYTHON_MIN_LINES: 80

jobs:
  coverage-rust:
    name: Rust coverage (llvm-cov)
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v6.0.3

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@v2
        with:
          tool: cargo-llvm-cov

      - name: Run tests under coverage instrumentation
        # mrrc-python (PyO3 bindings) is excluded: it needs Python linking
        # and its behavior is exercised by the Python lane below.
        # --no-report collects profiles only; the report steps below reuse
        # them without rerunning the tests.
        run: cargo llvm-cov --package mrrc --no-report

      - name: Report to job summary
        run: |
          {
            echo '## Rust line coverage'
            echo '```text'
            cargo llvm-cov report --summary-only
            echo '```'
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Enforce minimum line coverage
        run: cargo llvm-cov report --fail-under-lines "$RUST_MIN_LINES"

      - name: Render HTML report
        if: always()
        run: cargo llvm-cov report --html

      - name: Upload coverage artifacts
        uses: actions/upload-artifact@v7
        if: always()
        with:
          name: coverage-report-rust
          path: target/llvm-cov/html/

  coverage-python:
    name: Python coverage (pytest-cov)
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v6.0.3

      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: '3.12'

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo
        uses: Swatinem/rust-cache@v2

      - name: Install uv
        uses: astral-sh/setup-uv@v8.2.0

      - name: Sync dependencies
        run: uv sync --all-extras

      - name: Build extension
        run: uv run maturin develop --release

      - name: Run pytest with coverage
        # Coverage is measured over the pure-Python wrapper (mrrc/); the
        # compiled _mrrc extension is opaque to coverage.py and its lines
        # are covered by the Rust lane instead.
        run: |
          uv run --with pytest-cov python -m pytest tests/python/ -m "not benchmark" -q \
            --cov=mrrc --cov-report=term

      - name: Report to job summary
        run: |
          {
            echo '## Python line coverage (mrrc/ wrapper)'
            uv run --with pytest-cov python -m coverage report --format=markdown
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Enforce minimum line coverage
        run: uv run --with pytest-cov python -m coverage report --fail-under "$PYTHON_MIN_LINES"

      - name: Render HTML report
        if: failure()
        run: uv run --with pytest-cov python -m coverage html

      - name: Upload coverage artifacts
        uses: actions/upload-artifact@v7
        if: failure()
        with:
          name: coverage-report-python
          path: htmlcov/

  notify-failure:
    name: File failure issue
    needs: [coverage-rust, coverage-python]
    if: (failure() || cancelled()) && github.event_name == 'schedule'
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      issues: write
    steps:
      - name: File or update the failure-tracking issue
        env:
          GH_TOKEN: ${{ github.token }}
          TITLE: "Scheduled coverage run failed"
          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
        run: |
          existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
            --json number,title \
            --jq '[.[] | select(.title == env.TITLE)][0].number // empty')
          if [ -n "$existing" ]; then
            gh issue comment "$existing" --repo "$GITHUB_REPOSITORY" \
              --body "Failed again: $RUN_URL"
          else
            gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" \
              --body "Scheduled run failed: $RUN_URL — a lane either had test failures or dropped below its coverage minimum (RUST_MIN_LINES / PYTHON_MIN_LINES in coverage.yml). The HTML report is attached to the run as an artifact. Later failures will be added here as comments."
          fi