Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/mutation-cargo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,30 @@ jobs:
cache-dependency-glob: |
**/workflow_scripts/*.py

- name: Expose libpython for PyO3 test binaries
if: ${{ env.ACT != 'true' }}
shell: bash
run: |
set -euo pipefail
# PyO3 embedding crates (the auto-initialize pattern) link their
# test binaries against the interpreter PyO3's build script resolves
# from PATH. "Setup uv" places a uv-managed CPython ahead of the
# system python3, and its libpython lives under uv's private install
# directory, which is not on the dynamic loader's default search
# path. The baseline `cargo test` binary then aborts at run time with
# "libpython3.NN.so.1.0: cannot open shared object file" and no mutant
# is ever scored (issue #372). Add the resolved interpreter's LIBDIR
# to LD_LIBRARY_PATH for every later step. This is harmless for
# non-PyO3 callers: no test binary links libpython, so nothing loads
# from the added directory.
libdir="$(python3 -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR") or "")')"
if [[ -z "${libdir}" || ! -d "${libdir}" ]]; then
echo "No Python LIBDIR resolved (${libdir:-empty}); leaving LD_LIBRARY_PATH unchanged" >&2
exit 0
fi
echo "Adding ${libdir} to LD_LIBRARY_PATH for PyO3 test binaries"
echo "LD_LIBRARY_PATH=${libdir}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" >> "${GITHUB_ENV}"

- name: Relocate workflow source
# Move the workflow checkout out of the caller's workspace so
# tree-scanning tests in the caller's suite (manifest sweeps,
Expand Down
5 changes: 5 additions & 0 deletions docs/mutation-cargo-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ jobs:
- GitHub disables cron triggers after 60 days of repository
inactivity; a quiet repository silently stops running, which is an
acceptable failure mode for an informational workflow.
- Callers with PyO3 embedding crates (the `auto-initialize` pattern)
are handled automatically: the workflow adds the uv-provisioned
interpreter's `LIBDIR` to `LD_LIBRARY_PATH` before running
cargo-mutants, so test binaries linked against `libpython` load it at
run time. No `setup-commands` workaround is needed.
Comment on lines +102 to +106

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Qualify the automatic-handling claim.

State that this applies when the workflow resolves a valid LIBDIR on hosted runners. The implementation skips the export under ACT=true and intentionally continues unchanged when LIBDIR is missing, so “No setup-commands workaround is needed” is not universally guaranteed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/mutation-cargo-workflow.md` around lines 102 - 106, Qualify the
automatic PyO3 embedding-crate handling description to apply only when the
workflow resolves a valid interpreter LIBDIR on hosted runners. Note that the
LD_LIBRARY_PATH export is skipped when ACT=true and remains unchanged when
LIBDIR is unavailable, so avoid stating unconditionally that no setup-commands
workaround is needed.


## Local validation

Expand Down
32 changes: 32 additions & 0 deletions workflow_scripts/tests/test_mutation_workflow_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,38 @@ def _step_names(steps: list[dict[str, object]]) -> list[object]:
return [step.get("name") for step in steps]


def test_mutants_job_exposes_libpython_before_running() -> None:
"""The mutants job puts libpython on LD_LIBRARY_PATH before cargo-mutants.

PyO3 embedding crates link their test binaries against the uv-managed
interpreter that "Setup uv" places on PATH, whose libpython is not on the
loader's default search path. Without exporting its LIBDIR onto
LD_LIBRARY_PATH the baseline `cargo test` binary aborts at run time and no
mutant is scored (issue #372). The export must land before the
"Run mutation testing" step that builds and runs those binaries.
"""
jobs = _jobs("mutation-cargo.yml")
assert "mutants" in jobs, "mutation-cargo.yml must declare a mutants job"
steps = _steps(jobs["mutants"])
names = _step_names(steps)
exposers = [
index
for index, step in enumerate(steps)
if isinstance(step.get("run"), str)
and "LD_LIBRARY_PATH=" in typ.cast("str", step.get("run"))
and 'get_config_var("LIBDIR")' in typ.cast("str", step.get("run"))
]
Comment on lines +72 to +78

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert persistence through GITHUB_ENV.

Require the shape test to verify that the export script writes to GITHUB_ENV, not merely that it contains the relevant strings. Otherwise, a command that computes or prints LD_LIBRARY_PATH could pass while Run mutation testing still receives no updated environment.

         and "LD_LIBRARY_PATH=" in typ.cast("str", step.get("run"))
         and 'get_config_var("LIBDIR")' in typ.cast("str", step.get("run"))
+        and "GITHUB_ENV" in typ.cast("str", step.get("run"))
+        and ">>" in typ.cast("str", step.get("run"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow_scripts/tests/test_mutation_workflow_shape.py` around lines 72 - 78,
Update the `exposers` predicate in the shape test to require each matching run
script to reference `GITHUB_ENV` and use `>>`, in addition to the existing
`LD_LIBRARY_PATH` and `get_config_var("LIBDIR")` checks, ensuring the computed
environment value is persisted for later steps.

assert exposers, (
"mutation-cargo.yml mutants job must export the interpreter LIBDIR "
"onto LD_LIBRARY_PATH for PyO3 test binaries (issue #372)"
)
assert "Run mutation testing" in names, "mutants job must run cargo-mutants"
run_index = names.index("Run mutation testing")
assert min(exposers) < run_index, (
"the LD_LIBRARY_PATH export must precede the Run mutation testing step"
)


@pytest.mark.parametrize("workflow_name", WORKFLOW_NAMES)
def test_every_workflow_checkout_is_followed_by_relocation(
workflow_name: str,
Expand Down
Loading