Skip to content

Run the pytest suite in CI - #16

Merged
abrainwood merged 7 commits into
mainfrom
ci/pytest-workflow
Sep 16, 2026
Merged

abrainwood merged 7 commits into
mainfrom
ci/pytest-workflow

Conversation

@abrainwood

@abrainwood abrainwood commented Sep 15, 2026 •

Copy link
Copy Markdown
Owner

Closes #15

Adds .github/workflows/tests.yml running pytest -n 4 on push to main and on PRs (ubuntu-24.04, timeout-minutes: 15 on the job, timeout-minutes: 5 on the pytest step), using the pip cache and a single fully hash-pinned lock, requirements-test.lock.

Design

  • requirements-test.lock is generated with uv pip compile --universal --python-version 3.12 --generate-hashes requirements-test.txt (regenerate via make test-lock-regen). --universal resolves and preserves environment markers instead of evaluating them against the machine doing the compile, so the lock is correct for Linux CI even though it's regenerated on a macOS dev machine (darwin-only extras like pyobjc keep their sys_platform marker instead of leaking into the Linux install unconditionally).
  • make test-install installs setuptools first - piped straight from the extracted, hash-pinned block of requirements-test.lock into pip install --require-hashes -r /dev/stdin (the extraction fails loudly if the lock has no setuptools entry) - then everything else with --require-hashes --no-build-isolation --only-binary :all: --no-binary $(SDIST_ONLY). setuptools is also a direct entry in requirements-test.txt so it's always locked, not just present because acme happens to depend on it.
  • pytest.ini sets timeout_method = thread. The default signal method fires its alarm inside whatever's running when the timer expires - if that's an asyncio event-loop callback, the resulting exception gets swallowed by the loop's callback dispatcher instead of failing the test, so a hung test hangs the run until the job timeout, with no failure output. thread fails it in about a second.
  • README and Makefile install from the same lock CI uses.

Proof

🤖 Generated with Claude Code

https://claude.ai/code/session_019FJHinb3pMJEwTKrnMXMcG

abrainwood and others added 7 commits September 15, 2026 22:47
Fork CI only ran HACS/hassfest/SonarCloud, so a green PR said nothing
about the test suite. Add a tests.yml workflow that installs pinned
deps from requirements-test.txt and runs pytest -n 4, keeping
pytest.ini's timeout. README and Makefile now install from the same
pinned file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FJHinb3pMJEwTKrnMXMcG
Signed-off-by: Andrew Brainwood <andrew.brainwood@gmail.com>
Temporary commit to confirm the new pytest workflow fails the PR on a
real assertion failure. Reverted in the following commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FJHinb3pMJEwTKrnMXMcG
Signed-off-by: Andrew Brainwood <andrew.brainwood@gmail.com>
This reverts commit 1d35c60.

Signed-off-by: Andrew Brainwood <andrew.brainwood@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FJHinb3pMJEwTKrnMXMcG
SonarCloud flagged the pytest workflow: pip install without
--only-binary :all: (S8541) and dependencies without locked resolved
versions (S8544).

Resolve requirements-test.txt into requirements-test.lock (full hashes,
installed with --only-binary :all:) via `uv pip compile
--python-platform x86_64-manylinux_2_28`, targeting the Linux CI
runner directly - pip-compile on this dev machine (macOS) evaluates
environment markers against the host, which pulled darwin-only extras
(pyobjc) into a lock meant for Linux.

Four transitive test deps (fnvhash, mock-open, paho-mqtt, pyric)
publish no wheel at all on PyPI, so --only-binary can't place them.
They're split into requirements-test-sdist.lock and installed
separately with --no-deps (none of the four have their own
dependencies). scripts/regen_test_lock.py discovers that split by
re-resolving with --only-binary :all: and reading off what uv rejects,
so a new sdist-only dependency fails the regen loudly instead of
silently breaking CI.

Runner pinned to ubuntu-24.04 (glibc 2.39) to match the manylinux_2_28
lock target. Workflow gets explicit `permissions: contents: read`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FJHinb3pMJEwTKrnMXMcG
Signed-off-by: Andrew Brainwood <andrew.brainwood@gmail.com>
The unquoted second colon in ":all:" was parsed as a new mapping key,
so GitHub Actions rejected the whole workflow file. Confirmed with a
local YAML parse; caught this only after the first push failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FJHinb3pMJEwTKrnMXMcG
Signed-off-by: Andrew Brainwood <andrew.brainwood@gmail.com>
Code review of 7620d8a asked for a simpler design.

Replace requirements-test.lock + requirements-test-sdist.lock +
scripts/regen_test_lock.py with one requirements-test.lock, generated
with `uv pip compile --universal`. --universal preserves environment
markers instead of evaluating them against the compiling machine, so
it resolves correctly for Linux CI even when regenerated on macOS
(darwin-only extras like pyobjc keep their sys_platform marker instead
of leaking in unconditionally).

The four packages with no wheel at all (fnvhash, mock-open, paho-mqtt,
pyric) stay in the one lock, hashed, and get a --no-binary carve-out
at install time (Makefile SDIST_ONLY) instead of a separate lock file
and a script to split it.

setuptools is installed first, hash-pinned from the lock, so the
--no-build-isolation build of those four sdists doesn't reach for an
unpinned setuptools. Verified with a --no-cache-dir install: every
package comes from the lock, no unpinned download.

CI now just runs `make test-install`; the workflow carries no
rationale comments, that lives in the PR body.

actions/checkout and actions/setup-python bumped to v7 (current
majors). Job gets timeout-minutes: 15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FJHinb3pMJEwTKrnMXMcG
Signed-off-by: Andrew Brainwood <andrew.brainwood@gmail.com>
Re-review of 8edfebe surfaced three issues.

pytest.ini: set timeout_method = thread. Proved with a throwaway test
(a synchronous sleep scheduled via loop.call_soon, marked
@pytest.mark.timeout(1), not committed): under the default signal
method the SIGALRM fires inside the asyncio callback but the
exception it raises gets swallowed by the event loop's callback
dispatcher, so the test falsely passes instead of failing - a hung
test in CI would look like nothing happened rather than time out.
Under thread the same test correctly fails in ~2s with the real
traceback pointing at the stuck callback. Added timeout-minutes: 5 on
the "Run pytest" step as a backstop.

Makefile test-install: piped the extracted setuptools block straight
into `pip install -r /dev/stdin` instead of writing it to a fixed
/tmp path, and made the extraction fail loudly (via awk's END block)
if requirements-test.lock has no setuptools entry - tested against a
lock with that entry stripped out. SHELL/.SHELLFLAGS pin bash with
pipefail so a failure in the awk half of the pipe actually fails the
target instead of being masked by pip succeeding on empty stdin.

Added setuptools==84.0.0 to requirements-test.txt so it's a direct,
locked requirement rather than only appearing because acme happens to
depend on it; regenerated requirements-test.lock with
`make test-lock-regen` and confirmed the autogenerated header stayed
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FJHinb3pMJEwTKrnMXMcG
Signed-off-by: Andrew Brainwood <andrew.brainwood@gmail.com>
@sonarqubecloud

Copy link
Copy Markdown

@abrainwood
abrainwood merged commit d712bc1 into main Sep 16, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Run the pytest suite in CI

1 participant