Skip to content

Commit 6d0c1ee

Browse files
ci: include coverage from running build scripts (#6007)
* ci: include coverage from running build scripts * fix passing codecov token to report-coverage action * completely skip coverage on windows-11-arm for now * correct condition * fixups, add coverage to cross-compilation jobs * install uv in cross compilation job * cleanups, try cargo-llvm-cov branch * fix typo * specify package to install for testing * typo * fixup cross compile step * switch cross-compilation jobs to debug * skip containers for cross compile tests * fixups * set linker for cross-compilation job * correct secret * fixup report names * use released cargo-llvm-cov * Apply suggestions from code review Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com> Co-authored-by: David Hewitt <mail@davidhewitt.dev> * try running in container * fixup cross-compilation * cross compilation host coverage only * final tidy ups --------- Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com>
1 parent 072d544 commit 6d0c1ee

5 files changed

Lines changed: 148 additions & 69 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: 'Prepare coverage'
2+
description: 'Installs cargo-llvm-cov and prepares the environment for coverage collection'
3+
inputs:
4+
host_only:
5+
description: 'Whether to prepare the environment for host-only coverage collection'
6+
required: false
7+
default: "false"
8+
runs:
9+
using: "composite"
10+
steps:
11+
- name: Install cargo-llvm-cov
12+
uses: taiki-e/install-action@cargo-llvm-cov
13+
14+
- name: Prepare coverage environment
15+
shell: bash
16+
run: |
17+
cargo llvm-cov clean --workspace
18+
uvx nox -s set-coverage-env -- ${{ inputs.host_only == 'true' && '--coverage-host-only' || '' }}
19+
env:
20+
CARGO_LLVM_COV_SETUP: "yes"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: 'Report coverage'
2+
description: 'Generate coverage report using cargo-llvm-cov and upload it to Codecov'
3+
inputs:
4+
name:
5+
description: 'The name of the coverage report'
6+
required: true
7+
token:
8+
description: 'Codecov upload token'
9+
required: true
10+
runs:
11+
using: "composite"
12+
steps:
13+
- name: Generate coverage report
14+
shell: bash
15+
run: uvx nox -s generate-coverage-report -- --codecov --output-path=coverage.json
16+
17+
- name: Upload coverage report
18+
uses: codecov/codecov-action@v6
19+
with:
20+
files: coverage.json
21+
name: ${{ inputs.name }}
22+
token: ${{ inputs.token }}

.github/workflows/build.yml

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
on:
22
workflow_call:
33
inputs:
4+
sha:
5+
required: true
6+
type: string
47
os:
58
required: true
69
type: string
@@ -34,14 +37,7 @@ jobs:
3437
steps:
3538
- uses: actions/checkout@v6.0.2
3639
with:
37-
# For PRs, we need to run on the real PR head, not the resultant merge of the PR into the target branch.
38-
#
39-
# This is necessary for coverage reporting to make sense; we then get exactly the coverage change
40-
# between the base branch and the real PR head.
41-
#
42-
# If it were run on the merge commit the problem is that the coverage potentially does not align
43-
# with the commit diff, because the merge may affect line numbers.
44-
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
40+
ref: ${{ inputs.sha }}
4541

4642
# installs using setup-python do not work for arm macOS 3.9 and below
4743
- if: ${{ !(inputs.os == 'macos-latest' && contains(fromJSON('["3.8", "3.9"]'), inputs.python-version) && inputs.python-architecture == 'x64') }}
@@ -70,7 +66,7 @@ jobs:
7066
toolchain: ${{ inputs.rust }}
7167
targets: ${{ inputs.rust-target }}
7268
# rust-src needed to correctly format errors, see #1865
73-
components: rust-src,llvm-tools-preview
69+
components: rust-src
7470

7571
# On windows 32 bit, we are running on an x64 host, so we need to specifically set the target
7672
# NB we don't do this for *all* jobs because it breaks coverage of proc macros to have an
@@ -119,13 +115,8 @@ jobs:
119115
if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && !startsWith(inputs.python-version, 'graalpy')) }}
120116
run: nox -s ffi-check
121117

122-
- name: Install cargo-llvm-cov
123-
uses: taiki-e/install-action@cargo-llvm-cov
124-
125-
- name: Prepare coverage environment
126-
run: |
127-
cargo llvm-cov clean --workspace --profraw-only
128-
nox -s set-coverage-env
118+
- uses: ./.github/actions/prepare-coverage
119+
if: ${{ inputs.os != 'windows-11-arm' }} # https://github.com/rust-lang/rust/issues/150123
129120

130121
- name: Build docs
131122
run: nox -s docs
@@ -139,23 +130,9 @@ jobs:
139130
env:
140131
CARGO_TARGET_DIR: ${{ github.workspace }}/target
141132

142-
- name: Generate coverage report
143-
# needs investigation why llvm-cov fails on windows-11-arm
144-
continue-on-error: ${{ inputs.os == 'windows-11-arm' }}
145-
run: cargo llvm-cov
146-
--package=pyo3
147-
--package=pyo3-build-config
148-
--package=pyo3-macros-backend
149-
--package=pyo3-macros
150-
--package=pyo3-ffi
151-
report --codecov --output-path coverage.json
152-
153-
- name: Upload coverage report
154-
uses: codecov/codecov-action@v6
155-
# needs investigation why llvm-cov fails on windows-11-arm
156-
continue-on-error: ${{ inputs.os == 'windows-11-arm' }}
133+
- uses: ./.github/actions/report-coverage
134+
if: ${{ inputs.os != 'windows-11-arm' }} # https://github.com/rust-lang/rust/issues/150123
157135
with:
158-
files: coverage.json
159136
name: ${{ inputs.os }}/${{ inputs.python-version }}/${{ inputs.rust }}
160137
token: ${{ secrets.CODECOV_TOKEN }}
161138

.github/workflows/ci.yml

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ concurrency:
1212

1313
env:
1414
CARGO_TERM_COLOR: always
15+
NOX_DEFAULT_VENV_BACKEND: uv
16+
UV_PYTHON: 3.14
1517

1618
jobs:
1719
fmt:
@@ -39,6 +41,15 @@ jobs:
3941
outputs:
4042
MSRV: ${{ steps.resolve-msrv.outputs.MSRV }}
4143
verbose: ${{ runner.debug == '1' }}
44+
save-cache: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }}
45+
# For PRs, we need to run on the real PR head, not the resultant merge of the PR into the target branch.
46+
#
47+
# This is necessary for coverage reporting to make sense; we then get exactly the coverage change
48+
# between the base branch and the real PR head.
49+
#
50+
# If it were run on the merge commit the problem is that the coverage potentially does not align
51+
# with the commit diff, because the merge may affect line numbers.
52+
coverage-sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
4253
steps:
4354
- uses: actions/checkout@v6.0.2
4455
- uses: actions/setup-python@v6
@@ -138,6 +149,7 @@ jobs:
138149
needs: [fmt, resolve]
139150
uses: ./.github/workflows/build.yml
140151
with:
152+
sha: ${{ needs.resolve.outputs.coverage-sha }}
141153
os: ${{ matrix.platform.os }}
142154
python-version: ${{ matrix.python-version }}
143155
python-architecture: ${{ matrix.platform.python-architecture }}
@@ -231,6 +243,7 @@ jobs:
231243
needs: [fmt, resolve]
232244
uses: ./.github/workflows/build.yml
233245
with:
246+
sha: ${{ needs.resolve.outputs.coverage-sha }}
234247
os: ${{ matrix.platform.os }}
235248
python-version: ${{ matrix.python-version }}
236249
python-architecture: ${{ matrix.platform.python-architecture }}
@@ -617,20 +630,26 @@ jobs:
617630
- run: python3 -m nox -s test
618631

619632
test-version-limits:
620-
needs: [fmt]
633+
needs: [fmt, resolve]
621634
if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }}
622635
runs-on: ubuntu-latest
623636
steps:
624637
- uses: actions/checkout@v6.0.2
625-
- uses: actions/setup-python@v6
626638
with:
627-
python-version: "3.14"
628-
- uses: Swatinem/rust-cache@v2
639+
ref: ${{ needs.resolve.outputs.coverage-sha }}
640+
- uses: astral-sh/setup-uv@v7
629641
with:
630-
save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }}
642+
save-cache: ${{ needs.resolve.outputs.save-cache }}
631643
- uses: dtolnay/rust-toolchain@stable
632-
- run: python3 -m pip install --upgrade pip && pip install nox[uv]
633-
- run: python3 -m nox -s test-version-limits
644+
- uses: Swatinem/rust-cache@v2
645+
with:
646+
save-if: ${{ needs.resolve.outputs.save-cache }}
647+
- uses: ./.github/actions/prepare-coverage
648+
- run: uvx nox -s test-version-limits
649+
- uses: ./.github/actions/report-coverage
650+
with:
651+
name: ${{ github.job }}
652+
token: ${{ secrets.CODECOV_TOKEN }}
634653

635654
check-feature-powerset:
636655
needs: [fmt, resolve]
@@ -662,7 +681,7 @@ jobs:
662681
- run: python3 -m nox -s check-feature-powerset -- ${{ matrix.rust != 'stable' && 'minimal-versions' || '' }}
663682

664683
test-cross-compilation:
665-
needs: [fmt]
684+
needs: [fmt, resolve]
666685
if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }}
667686
runs-on: ${{ matrix.os }}
668687
name: test-cross-compilation ${{ matrix.os }} -> ${{ matrix.target }}
@@ -686,45 +705,72 @@ jobs:
686705
target: "x86_64-pc-windows-gnu"
687706
# TODO: remove pyo3/generate-import-lib feature when maturin supports cross compiling to Windows without it
688707
flags: "-i python3.13 --features pyo3/generate-import-lib"
708+
apt-packages: mingw-w64 llvm
689709
# windows x86_64 -> aarch64
690710
- os: "windows-latest"
691711
target: "aarch64-pc-windows-msvc"
692712
flags: "-i python3.13 --features pyo3/generate-import-lib"
693713
steps:
694714
- uses: actions/checkout@v6.0.2
715+
with:
716+
ref: ${{ needs.resolve.outputs.coverage-sha }}
717+
- uses: astral-sh/setup-uv@v7
718+
with:
719+
save-cache: ${{ needs.resolve.outputs.save-cache }}
695720
- uses: actions/setup-python@v6
696721
with:
697-
python-version: "3.14"
722+
python-version: ${{ env.UV_PYTHON }}
698723
- uses: Swatinem/rust-cache@v2
699724
with:
700725
workspaces: examples/maturin-starter
701726
save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }}
702727
key: ${{ matrix.target }}
703-
- name: Setup cross-compiler
704-
if: ${{ matrix.target == 'x86_64-pc-windows-gnu' }}
705-
run: sudo apt-get install -y mingw-w64 llvm
728+
- name: Setup cross-compiler packages
729+
if: ${{ matrix.apt-packages }}
730+
run: sudo apt-get install -y ${{ matrix.apt-packages }}
731+
- uses: ./.github/actions/prepare-coverage
732+
with:
733+
host_only: true
706734
- name: Compile version-specific library
707735
uses: PyO3/maturin-action@v1
708736
with:
709737
target: ${{ matrix.target }}
738+
args: --profile=dev -m examples/maturin-starter/Cargo.toml ${{ matrix.flags }}
710739
manylinux: ${{ matrix.manylinux }}
711-
args: --release -m examples/maturin-starter/Cargo.toml ${{ matrix.flags }}
740+
docker-options: >-
741+
-e LLVM_PROFILE_FILE
742+
-e __CARGO_LLVM_COV_RUSTC_WRAPPER
743+
-e __CARGO_LLVM_COV_RUSTC_WRAPPER_RUSTFLAGS
744+
-e __CARGO_LLVM_COV_RUSTC_WRAPPER_CRATE_NAMES
745+
-v /home/runner/.cargo/bin/cargo-llvm-cov:/home/runner/.cargo/bin/cargo-llvm-cov
712746
- name: Compile abi3 library
713747
uses: PyO3/maturin-action@v1
714748
with:
715749
target: ${{ matrix.target }}
750+
args: --profile=dev -m examples/maturin-starter/Cargo.toml --features abi3 ${{ matrix.flags }}
716751
manylinux: ${{ matrix.manylinux }}
717-
args: --release -m examples/maturin-starter/Cargo.toml --features abi3 ${{ matrix.flags }}
752+
docker-options: >-
753+
-e LLVM_PROFILE_FILE
754+
-e __CARGO_LLVM_COV_RUSTC_WRAPPER
755+
-e __CARGO_LLVM_COV_RUSTC_WRAPPER_RUSTFLAGS
756+
-e __CARGO_LLVM_COV_RUSTC_WRAPPER_CRATE_NAMES
757+
-v /home/runner/.cargo/bin/cargo-llvm-cov:/home/runner/.cargo/bin/cargo-llvm-cov
758+
- uses: ./.github/actions/report-coverage
759+
with:
760+
name: ${{ github.job }}/${{ matrix.os }}/${{ matrix.target }}
761+
token: ${{ secrets.CODECOV_TOKEN }}
718762

719763
test-cross-compilation-windows:
720-
needs: [fmt]
764+
needs: [fmt, resolve]
721765
if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }}
722766
runs-on: ubuntu-latest
723767
steps:
724768
- uses: actions/checkout@v6.0.2
725-
- uses: actions/setup-python@v6
726769
with:
727-
python-version: "3.14"
770+
ref: ${{ needs.resolve.outputs.coverage-sha }}
771+
- uses: astral-sh/setup-uv@v7
772+
with:
773+
save-cache: ${{ needs.resolve.outputs.save-cache }}
728774
- uses: dtolnay/rust-toolchain@stable
729775
with:
730776
targets: x86_64-pc-windows-gnu,x86_64-pc-windows-msvc
@@ -734,12 +780,17 @@ jobs:
734780
with:
735781
path: ~/.cache/cargo-xwin
736782
key: cargo-xwin-cache
783+
- name: Setup cross-compiler
784+
run: sudo apt-get install -y mingw-w64 llvm
785+
- uses: ./.github/actions/prepare-coverage
786+
with:
787+
host_only: true
737788
- name: Test cross compile to Windows
738-
run: |
739-
set -ex
740-
sudo apt-get install -y mingw-w64 llvm
741-
pip install nox
742-
nox -s test-cross-compilation-windows
789+
run: uvx nox -s test-cross-compilation-windows
790+
- uses: ./.github/actions/report-coverage
791+
with:
792+
name: ${{ github.job }}
793+
token: ${{ secrets.CODECOV_TOKEN }}
743794

744795
test-introspection:
745796
needs: [fmt]

noxfile.py

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,16 @@
5151
FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
5252

5353

54-
def _get_output(*args: str) -> str:
55-
return subprocess.run(args, capture_output=True, text=True, check=True).stdout
54+
def _get_output(*args: str, env: Optional[Dict[str, str]] = None) -> str:
55+
try:
56+
return subprocess.run(
57+
args, capture_output=True, text=True, check=True, stdin=None, env=env
58+
).stdout
59+
except subprocess.CalledProcessError as e:
60+
print(f"Command {args} failed with exit code {e.returncode}")
61+
print(f"stdout:\n{e.stdout}")
62+
print(f"stderr:\n{e.stderr}")
63+
raise nox.command.CommandFailed() from e
5664

5765

5866
def _parse_supported_interpreter_version(
@@ -165,18 +173,14 @@ def coverage(session: nox.Session) -> None:
165173
def set_coverage_env(session: nox.Session) -> None:
166174
"""For use in GitHub Actions to set coverage environment variables."""
167175
with open(os.environ["GITHUB_ENV"], "a") as env_file:
168-
for k, v in _get_coverage_env().items():
176+
for k, v in _get_coverage_env(*session.posargs).items():
169177
print(f"{k}={v}", file=env_file)
170178

171179

172180
@nox.session(name="generate-coverage-report", venv_backend="none")
173181
def generate_coverage_report(session: nox.Session) -> None:
174-
cov_format = "codecov"
175-
output_file = "coverage.json"
176-
177-
if "lcov" in session.posargs:
178-
cov_format = "lcov"
179-
output_file = "lcov.info"
182+
# default to `--html` report if no additional arguments provided (convenient for local use)
183+
posargs = ("--html",) if not session.posargs else tuple(session.posargs)
180184

181185
_run_cargo(
182186
session,
@@ -186,10 +190,9 @@ def generate_coverage_report(session: nox.Session) -> None:
186190
"--package=pyo3-macros-backend",
187191
"--package=pyo3-macros",
188192
"--package=pyo3-ffi",
193+
"--include-build-script",
189194
"report",
190-
f"--{cov_format}",
191-
"--output-path",
192-
output_file,
195+
*posargs,
193196
)
194197

195198

@@ -1615,10 +1618,16 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]:
16151618
_HOST_LINE_START = "host: "
16161619

16171620

1618-
def _get_coverage_env() -> Dict[str, str]:
1619-
env = {}
1620-
output = _get_output("cargo", "llvm-cov", "show-env")
1621+
def _get_coverage_env(*flags: str) -> Dict[str, str]:
1622+
llvm_cov_execution_env = os.environ.copy()
1623+
# prevent llvm-cov from hanging asking to install llvm-tools-preview
1624+
# (allow user to override this, if they wish, e.g. in CI)
1625+
llvm_cov_execution_env.setdefault("CARGO_LLVM_COV_SETUP", "no")
1626+
output = _get_output(
1627+
"cargo", "llvm-cov", "show-env", *flags, env=llvm_cov_execution_env
1628+
)
16211629

1630+
env = {}
16221631
for line in output.strip().splitlines():
16231632
(key, value) = line.split("=", maxsplit=1)
16241633
# Strip single or double quotes from the variable value

0 commit comments

Comments
 (0)