Skip to content

ci: GitHub Actions workflow + base pipeline unit tests - #4

Merged
PlayerPlanet merged 3 commits into
mainfrom
ci/github-actions-and-tests
Jun 6, 2026
Merged

PlayerPlanet merged 3 commits into
mainfrom
ci/github-actions-and-tests

Conversation

@PlayerPlanet

Copy link
Copy Markdown
Collaborator

Sets up continuous integration via GitHub Actions and adds comprehensive unit tests for the base QEC pipeline. This gives us automatic test runs and coverage reporting on every push and pull request, so we can catch breakage before merge.

What

.github/workflows/ci.yml — GitHub Actions CI

  • Triggers on push to main and every pull_request to main
  • Python 3.13 on ubuntu-latest
  • Two jobs:
    1. test: installs runtime + test deps, runs pytest with coverage, fails if under 85%
      • Generates term, XML, and HTML coverage reports
      • Uploads to Codecov (optional, fails open)
    2. smoke: runs python -m diffqec.smoke_test end-to-end
  • 20-minute timeout per job
  • Pip cache keyed on requirements.txt and pyproject.toml

tests/test_pipeline.py — 49 new unit tests

Organized by module:

  • TestMakeStimCircuit (8 tests): default Z memory, X memory, case-insensitive memory arg, no_reset flag, noise model application, partial noise keys, distance 5
  • TestMakeQiskitCircuit (9 tests): return type, metadata keys, distance 2/3 counts, X/Z memory, no_reset, depth scaling, noise param ignored
  • TestStimToQiskit (5 tests): basic conversion, dense contiguous indices, strips noise instructions, measurement order length, raises on unhandled gates
  • TestGetCircuitInfo (3 tests): expected keys, qubit classification disjoint, distance-3 counts
  • TestGetQubitLists (2 tests): data/anc split, no-coords fallback
  • TestGetMeasOrder (1 test): order matches circuit
  • TestCountsToMeasurementArray (3 tests): basic conversion, explicit count, truncation
  • TestExtractSyndromesManual (6 tests): noiseless zero detections, output dict keys, shape, error injection, noise model, print summary
  • TestExtractSyndromesStim (3 tests): detector sampler, noiseless zero, extract_syndromes with raw_meas
  • TestBuildEmeraldQubitMap (2 tests): returns dict, import contract
  • TestDecodeHardwareResults (3 tests): mwpm default, diffqec missing model_path raises, unknown decoder
  • TestMainSimulation (2 tests): run_simulation_test runs, main entrypoint works
  • TestPipelineIntegration (2 tests): circuit info consistent, stim→qiskit simulation

tests/__init__.py

Empty package marker for tests/.

pytest.ini

  • testpaths = tests
  • Coverage source: surface_code, extract_syndromes, internal_helpers, run_on_hardware, diffqec
  • Omits: tests/, iqm_utils/, benchmarks/, lumi/, main.py, build_emerald_qubit_rotated.py
  • Filter warnings for qiskit/stim/pymatching deprecations
  • slow and hardware markers for selective deselection

requirements.txt

  • Added pytest>=8.0
  • Added pytest-cov>=5.0

Test results

  • 64 tests passing (49 new + 15 existing diffqec tests)
  • 86% line coverage (CI threshold: 85%)
  • ~5 seconds total runtime in CI

Coverage report

Module Coverage
extract_syndromes.py 100%
internal_helpers.py 100%
diffqec/denoiser.py 100%
diffqec/diffusion.py 100%
diffqec/syndrome_processor.py 100%
surface_code.py 90%
diffqec/model.py 95%
diffqec/data.py 80%
diffqec/integrate.py 81%
run_on_hardware.py 44% (hardware path, not testable in CI)
main.py 76% (print path)

Next steps

When this is merged, every future PR (including the LUMI benchmark) will automatically run the full test suite and report coverage, catching regressions before they reach main.

- .github/workflows/ci.yml: GitHub Actions CI on push/PR to main
  - Python 3.13 setup with pip cache
  - Installs runtime + test dependencies
  - Runs pytest with coverage, fails under 85%
  - Uploads coverage to Codecov (optional)
  - Separate smoke job for end-to-end DiffQEC run
- tests/test_pipeline.py: 49 new unit tests for the base pipeline
  - make_stim_circuit: Z/X memory, no_reset, noise, distance 3/5
  - make_qiskit_circuit: qubit/classical bit counts, X/Z memory, reset
  - stim_to_qiskit: conversion, dense indexing, noise stripping
  - get_circuit_info: keys, qubit classification disjoint
  - internal_helpers: get_qubit_lists, get_meas_order, counts_to_measurement_array
  - extract_syndromes_manual: noiseless, noise, error injection
  - extract_syndromes (Stim m2d path)
  - build_emerald_qubit_map: import contract
  - decode_hardware_results: mwpm default, diffqec path, unknown decoder
  - main.run_simulation_test
  - Pipeline integration: end-to-end Stim + Qiskit paths
- tests/__init__.py: empty package marker
- pytest.ini: pytest config + coverage settings (source paths, omit rules)
- requirements.txt: add pytest>=8.0 and pytest-cov>=5.0

Total: 64 tests passing, 86% line coverage (threshold 85% in CI).
Pytest on CI does not add the project root to sys.path by default, so
'import internal_helpers' and 'import surface_code' fail with
ModuleNotFoundError. Adding a conftest.py at the project root fixes
this for both the test collection and the runtime tests.

Local verification: 64 tests pass, 86% coverage maintained.
The conftest.py approach failed because pytest loads test modules
(which trigger 'from internal_helpers import *' in surface_code.py)
before conftest.py can modify sys.path. Using 'pythonpath = .' in
pytest.ini runs before any test collection, so the project root is
on sys.path from the start.
@PlayerPlanet
PlayerPlanet merged commit 98352e2 into main Jun 6, 2026
2 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.

1 participant