Skip to content
Merged
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
48 changes: 48 additions & 0 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,54 @@ and the tracking issue. The `mutants` dev-dependency exists solely so these
test-only attributes resolve. Do not skip mutants that can be exercised by the
workflow's feature set; strengthen the relevant tests instead.

### Workflow contract tests

`.github/workflows/mutation-testing.yml` is a thin caller of the shared
reusable workflow `leynos/shared-actions/.github/workflows/mutation-cargo.yml`.
The heavy lifting — running `cargo-mutants` and summarizing survivors — lives in
`shared-actions`; this repository carries only declarative configuration. The
run is informational only: it never gates a pull request, and survivors are
reported through the job summary and downloadable artefacts for triage into
tests rather than enforced as a blocking check.

The caller currently sets three inputs:

- `exclude-globs: "src/test_support.rs"` — keeps survivors from the unit-test
scaffolding module out of the report, since they are noise rather than
genuine test gaps.
- `extra-args: "--all-features"` — mirrors the repository's canonical test
baseline (`make test` runs with all features enabled), so feature-gated code
is compiled and exercised against mutants.
- `setup-commands` — pins `PG_PASSWORD` for the embedded PostgreSQL cluster.
The `pg-embed-setup-unpriv` library otherwise generates a fresh random
superuser password per run while cluster state persists between runs, so a
second plain `cargo test` in the same job would fail to authenticate; a fixed
password keeps every per-mutant run consistent with the baseline run.

Every other input keeps the shared workflow's default, including `paths`, which
this repository does not set.

`tests/workflow_contracts/mutation_testing_test.py` parses the caller with
PyYAML and pins the shape it must uphold, failing the pull request when the
caller drifts rather than letting the breakage surface only in a scheduled run.
Run it locally with `make test-workflow-contracts` (this wraps
`uv run --with 'pytest>=8' --with 'pyyaml>=6' pytest tests/workflow_contracts -q`).
The test validates:

- job permissions are exactly least-privilege (`contents: read`,
`id-token: write`), and the workflow-level default token scope is empty;
- `concurrency` serializes runs per ref (`cancel-in-progress: false`);
- the daily 09:35 UTC schedule remains, and `workflow_dispatch` is present
without the legacy `branch` input; and
- `uses:` names the shared `mutation-cargo.yml` workflow and ends with a
40-character hexadecimal commit SHA; and
- the `with:` block carries exactly the three inputs above and no others.

The test validates only the SHA's shape, not its value. Dependabot continues to
manage the exact pin, so its updates do not require a matching test change; the
contract merely prevents the reusable workflow from being repointed at a
branch, tag, or abbreviated revision.

## Compile-fail UI tests

Compile-time contracts for macros and type-level behaviour are covered by the
Expand Down
16 changes: 16 additions & 0 deletions tests/workflow_contracts/mutation_testing_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from __future__ import annotations

import re
from pathlib import Path

import yaml
Expand All @@ -38,6 +39,11 @@
}


USES_RE = re.compile(
r"^leynos/shared-actions/.github/workflows/mutation-cargo\.yml@[0-9A-Fa-f]{40}$"
)


def _load() -> dict[str, object]:
"""Parse the workflow file."""
return yaml.safe_load(WORKFLOW_PATH.read_text(encoding="utf-8"))
Expand Down Expand Up @@ -109,6 +115,16 @@ def test_triggers_keep_schedule_and_plain_dispatch() -> None:
)


def test_mutation_job_uses_a_pinned_shared_workflow() -> None:
"""The reusable workflow reference keeps a full commit-SHA pin."""
uses = _mutation_job(_load()).get("uses")
assert isinstance(uses, str), "jobs.mutation.uses must be a string"
assert USES_RE.fullmatch(uses), (
"jobs.mutation.uses must reference mutation-cargo.yml at a "
f"40-character commit SHA, got {uses!r}"
)


def test_with_block_carries_the_caller_configuration() -> None:
"""The caller passes exactly the scaffolding exclusion and feature args."""
with_block = _mutation_job(_load()).get("with")
Expand Down
2 changes: 1 addition & 1 deletion typos.local.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ accepted = ["Center"]
[words.corrections]

[patterns]
ignore = []
ignore = ["`[^`\\n]+`"]

[files]
exclude = []
18 changes: 18 additions & 0 deletions typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1700,6 +1700,24 @@ extend-ignore-re = [
"pluralizers" = "pluralizers"
"pluralizes" = "pluralizes"
"pluralizing" = "pluralizing"
"polymerisable" = "polymerizable"
"polymerisation" = "polymerization"
"polymerisations" = "polymerizations"
"polymerise" = "polymerize"
"polymerised" = "polymerized"
"polymeriser" = "polymerizer"
"polymerisers" = "polymerizers"
"polymerises" = "polymerizes"
"polymerising" = "polymerizing"
"polymerizable" = "polymerizable"
"polymerization" = "polymerization"
"polymerizations" = "polymerizations"
"polymerize" = "polymerize"
"polymerized" = "polymerized"
"polymerizer" = "polymerizer"
"polymerizers" = "polymerizers"
"polymerizes" = "polymerizes"
"polymerizing" = "polymerizing"
"popularisable" = "popularizable"
"popularisation" = "popularization"
"popularisations" = "popularizations"
Expand Down
Loading