From 560b9110227fa46cd0f36ed2ad6e351ca0f3a636 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Wed, 11 Nov 2026 21:41:53 -0500 Subject: [PATCH 1/6] Run compiled functions as plain Python in the coverage check, and drop the per-test variants The coverage check sets QLDPC_DISABLE_COMPILATION, so that qLDPC's compiled functions run as plain Python, which coverage traces, and no longer skips tests marked compiled. The fixtures, name lists, compiled and interpreted parameters, .py_func swaps and the compiled marker, which ran each test of compiled code twice, are gone. CI runs the test files of the modules with compiled functions, and the distance tests with the set-up code compiled too, with compilation on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pyproject.toml | 5 -- src/qldpc/_binary_test.py | 18 ------- src/qldpc/_compilation.py | 6 ++- .../certification/_fault_search_test.py | 43 ++-------------- src/qldpc/codes/distance/README.md | 5 +- .../_counting/local_cliffords_test.py | 8 +-- .../distance/_counting/macwilliams_test.py | 8 +-- .../codes/distance/_counting/scan_test.py | 30 +++-------- .../distance/_search/affine_search_test.py | 9 ---- .../_search/automorphism_families_test.py | 2 - .../_search/completion_bounds_test.py | 28 +++-------- .../_search/connected_support_test.py | 20 -------- .../_search/coset_leader_search_test.py | 40 ++------------- .../distance/_search/information_sets_test.py | 11 +--- src/qldpc/codes/distance/conftest.py | 50 +------------------ src/qldpc/codes/distance/core_test.py | 4 +- .../codes/distance/logical_counts_test.py | 12 ++--- src/qldpc/conftest.py | 2 +- .../objects/_packed_paulis_test.py | 19 ------- .../experimental/objects/_symplectic_test.py | 25 ---------- .../objects/rotated_pauli_string_test.py | 27 +--------- 21 files changed, 44 insertions(+), 328 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 245242639..6c54830d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -154,11 +154,6 @@ show_error_codes = true [tool.pytest] ini_options.addopts = "--disable-socket --strict-markers" # forbid network calls and unknown markers -# checks/coverage_.py skips the tests marked compiled, and a CI job of their own runs them (see the -# testing standard). -ini_options.markers = [ - "compiled: runs Numba-compiled code, which coverage cannot trace, rather than its Python version", -] ini_options.filterwarnings = [ "error::DeprecationWarning", # tests use the modern API "error::FutureWarning", # tests opt in to future changes diff --git a/src/qldpc/_binary_test.py b/src/qldpc/_binary_test.py index 0afb2c063..8e5802862 100644 --- a/src/qldpc/_binary_test.py +++ b/src/qldpc/_binary_test.py @@ -18,21 +18,6 @@ # linear algebra of unpacked binary matrices -@pytest.fixture( - params=[ - pytest.param(False, marks=pytest.mark.compiled, id="compiled"), - pytest.param(True, id="python"), - ] -) -def binary_kernels(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> None: - """Run the compiled functions, or their Python versions, which coverage can trace.""" - if request.param: - for name in ("pack_bits", "unpack_bits", "row_reduce_packed", "get_rank_packed"): - monkeypatch.setattr(binary, name, getattr(binary, name).py_func) - monkeypatch.setattr(binary, "get_inverse", binary.get_inverse.py_func) - - -@pytest.mark.usefixtures("binary_kernels") @pytest.mark.parametrize("height, width", [(5, 130), (0, 7), (3, 0)]) def test_row_reduction_and_rank_match_galois(height: int, width: int) -> None: """In the default column order, row reduction gives the reduced row echelon form.""" @@ -47,7 +32,6 @@ def test_row_reduction_and_rank_match_galois(height: int, width: int) -> None: assert rank == np.count_nonzero(expected.any(axis=1)) -@pytest.mark.usefixtures("binary_kernels") @pytest.mark.parametrize("columns", ["subset", "repeated"]) def test_row_reduction_over_selected_columns(columns: str) -> None: """Pivots come from the selected columns, and labels undergo the same row operations.""" @@ -96,7 +80,6 @@ def test_get_kernel(shape: tuple[int, int]) -> None: # compiled linear algebra -@pytest.mark.usefixtures("binary_kernels") @pytest.mark.parametrize("width", [0, 64, 65, 130]) def test_pack_bits(width: int) -> None: """Column j goes to bit j % 64 of word j // 64, and unpacking inverts packing.""" @@ -109,7 +92,6 @@ def test_pack_bits(width: int) -> None: np.testing.assert_array_equal(binary.unpack_bits(packed, width), matrix) -@pytest.mark.usefixtures("binary_kernels") def test_get_inverse() -> None: """The inverse of a product of unitriangular matrices and a permutation inverts it.""" rng = np.random.default_rng(70) diff --git a/src/qldpc/_compilation.py b/src/qldpc/_compilation.py index 7b71c707d..5fa62abd3 100644 --- a/src/qldpc/_compilation.py +++ b/src/qldpc/_compilation.py @@ -48,7 +48,8 @@ functions that other compiled code calls: every search needs them, and the disk cache makes compiling them a one-time cost. Use :func:`compiled_on_request` for the larger functions that orchestrate a search, which select and bound its information sets, build its basis families, and -drive its scan. Compiling them pays off only in a program that makes very many searches. A +drive its scan. Compiling them pays off only in a program that makes very many searches. Define +them only in :mod:`qldpc.codes.distance`, since CI compiles them only in a run of its tests. A function that is neither runs as ordinary Python. A small always-compiled helper that hot loops call can be marked ``inline=True`` (see :func:`always_compiled`). A compiled-on-request function may call always-compiled and compiled-on-request functions. An always-compiled function may call @@ -60,7 +61,8 @@ compiled-on-request functions as they are, and qLDPC neither uses nor cleans up its directories of compiled code. Coverage tools can then trace every line of these functions, and errors in them show Python tracebacks. Numba's own ``NUMBA_DISABLE_JIT`` does the same for every package that uses -numba, such as galois, which then runs slowly too. +numba, such as galois, which then runs slowly too. qLDPC's coverage check sets +``QLDPC_DISABLE_COMPILATION``. """ from __future__ import annotations diff --git a/src/qldpc/circuits/certification/_fault_search_test.py b/src/qldpc/circuits/certification/_fault_search_test.py index 4465dcc68..f308dd5a2 100644 --- a/src/qldpc/circuits/certification/_fault_search_test.py +++ b/src/qldpc/circuits/certification/_fault_search_test.py @@ -19,33 +19,6 @@ from . import _fault_search -@pytest.fixture( - params=[ - pytest.param(False, marks=pytest.mark.compiled, id="compiled"), - pytest.param(True, id="python"), - ] -) -def kernels(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> None: - """Run the compiled listing, or its Python version for coverage.""" - if request.param: - names = ( - "_list_class_codewords", - "_expand_class_tuples", - "_table_should_grow", - "_reduce_flags", - "_pauli_weight", - "_find_syndrome", - "_find_syndrome_slot", - "_get_key_slots", - "_get_light_pairs", - "_grow_syndrome_table", - "_get_closing_table", - "_sort_bucket", - ) - for name in names: - monkeypatch.setattr(_fault_search, name, getattr(_fault_search, name).py_func) - - def _irreducible_faults( flags: Sequence[int], fault_weight: int, num_starts: int ) -> list[tuple[int, ...]]: @@ -91,7 +64,6 @@ def find_violation(syndromes: npt.NDArray[np.uint64], weight: int) -> int: return checked, lister.find_first(fault_weight, find_violation) -@pytest.mark.usefixtures("kernels") @pytest.mark.parametrize("collide", [False, True]) def test_listing_matches_brute_force( collide: bool, pytestconfig: pytest.Config, monkeypatch: pytest.MonkeyPatch @@ -149,7 +121,6 @@ def test_listing_matches_brute_force( assert result == violation -@pytest.mark.usefixtures("kernels") @pytest.mark.parametrize("size", [1, 2]) def test_closing_tables_sort_sets_stably_by_hash(size: int, pytestconfig: pytest.Config) -> None: """A table lists the sets in lexicographic order, sorted stably by hash, as Python sorts them. @@ -173,7 +144,6 @@ def test_closing_tables_sort_sets_stably_by_hash(size: int, pytestconfig: pytest assert sets.tolist() == [list(classes) for classes in expected_sets] -@pytest.mark.usefixtures("kernels") def test_repeated_syndromes_are_checked_once() -> None: """The faults {0, 1} and {0, 2} on |000> leave X on all qubits, so only the first is checked. @@ -183,7 +153,6 @@ def test_repeated_syndromes_are_checked_once() -> None: assert _check_faults(lister, 2, ()) == ([7], None) -@pytest.mark.usefixtures("kernels") @pytest.mark.parametrize("collide", [False, True]) def test_light_pairs_match_brute_force( collide: bool, pytestconfig: pytest.Config, monkeypatch: pytest.MonkeyPatch @@ -212,7 +181,6 @@ def test_light_pairs_match_brute_force( assert pair == expected -@pytest.mark.usefixtures("kernels") @pytest.mark.parametrize("css_weight", [False, True]) def test_skipped_faults_satisfy_the_bound(css_weight: bool, pytestconfig: pytest.Config) -> None: """Calls with increasing fault weights check each syndrome once, and find a violation. @@ -270,7 +238,6 @@ def test_skipped_faults_satisfy_the_bound(css_weight: bool, pytestconfig: pytest assert result is None or result in expected -@pytest.mark.usefixtures("kernels") def test_syndromes_of_listed_faults_are_not_checked_again(monkeypatch: pytest.MonkeyPatch) -> None: """A fault is not checked if an earlier listed fault of smaller fault weight has its syndrome. @@ -315,12 +282,13 @@ def test_pair_table_peak_memory_is_within_its_estimate(monkeypatch: pytest.Monke The classes have distinct random hashes, so that no bucket of the table is merge sorted, and so that the pairs have distinct keys, which need the most slots. - The table is built by the Python versions of its compiled functions, since only allocations by - Python are traced. A small table is built first, since loading the compiled code that the - Python versions call would add allocations of its own. + The table is built by the Python versions of its compiled functions, even when they are + compiled, since tracemalloc traces only allocations by Python. A small table is built first, + since loading the compiled code that the Python versions call would add allocations of its own. """ for name in ("_get_closing_table", "_sort_bucket", "_get_key_slots", "_find_syndrome_slot"): - monkeypatch.setattr(_fault_search, name, getattr(_fault_search, name).py_func) + function = getattr(_fault_search, name) + monkeypatch.setattr(_fault_search, name, getattr(function, "py_func", function)) num_classes = 200 hashes = np.random.default_rng(0).integers(2**64, size=num_classes, dtype=np.uint64) _fault_search._get_closing_map(hashes[:3], 2) @@ -335,7 +303,6 @@ def test_pair_table_peak_memory_is_within_its_estimate(monkeypatch: pytest.Monke assert peak <= num_pairs * _fault_search._BYTES_PER_PAIR + 2**14 -@pytest.mark.usefixtures("kernels") def test_key_slots_find_the_first_entry_of_each_key(pytestconfig: pytest.Config) -> None: """A lookup in sorted keys finds the first entry of a key, or none for a key that is absent. diff --git a/src/qldpc/codes/distance/README.md b/src/qldpc/codes/distance/README.md index 533cb9da6..408556ba7 100644 --- a/src/qldpc/codes/distance/README.md +++ b/src/qldpc/codes/distance/README.md @@ -132,9 +132,8 @@ It affects only qLDPC, whereas `NUMBA_DISABLE_JIT=1` also runs other packages th `python checks/pytest_.py src/qldpc/codes/distance` runs the tests. `python checks/coverage_.py src/qldpc/codes/distance/.py` checks one module's coverage. The validation commands in `docs/source/contributing/index.rst` list the other checks. -- Coverage cannot see compiled code, so tests also run compiled functions as plain Python. - `conftest.py` holds the fixtures for this, and the helpers that several test files share, such as a check of counts against brute-force enumeration. - The variants of tests that run compiled code carry the `compiled` marker (see `docs/source/contributing/standards/testing.rst`). +- `conftest.py` holds the fixtures and helpers that several test files share, such as a check of counts against brute-force enumeration. +- Coverage cannot see compiled code, so the coverage check runs compiled functions as plain Python, and CI runs these tests again with them compiled (see `docs/source/contributing/standards/testing.rst`). - `python -m pytest src/qldpc/codes/distance --compile-search-setup` runs the tests with the set-up code of searches compiled. Tests that replace a compiled-on-request function or a function that one calls, or that compare plain with compiled set-up code, request the `plain_search_setup` fixture of `conftest.py`, whose docstring says why. - `readme_test.py` fails if this page omits a module, or names a file that does not exist. diff --git a/src/qldpc/codes/distance/_counting/local_cliffords_test.py b/src/qldpc/codes/distance/_counting/local_cliffords_test.py index b45cc4b5e..7a47ca5be 100644 --- a/src/qldpc/codes/distance/_counting/local_cliffords_test.py +++ b/src/qldpc/codes/distance/_counting/local_cliffords_test.py @@ -71,12 +71,8 @@ def test_invalid_local_matrices_are_rejected() -> None: local_cliffords.get_local_matrices(invalid, 2) -@pytest.mark.parametrize("compiled", [pytest.param(True, marks=pytest.mark.compiled), False]) -def test_apply_local_maps_the_target_columns_by_the_matrix( - pytestconfig: pytest.Config, compiled: bool -) -> None: +def test_apply_local_maps_the_target_columns_by_the_matrix(pytestconfig: pytest.Config) -> None: """The target entries of every row are multiplied by the matrix, and other columns are kept.""" - apply = local_cliffords.apply_local if compiled else local_cliffords.apply_local.py_func rng = np.random.default_rng(pytestconfig.getoption("randomly_seed")) positions = np.array([1, 4, 7], dtype=np.int64) matrix = np.array([[1, 1, 0], [0, 1, 1], [1, 1, 1]]) @@ -87,7 +83,7 @@ def test_apply_local_maps_the_target_columns_by_the_matrix( expected = rows.copy() expected[:, positions] = rows[:, positions] @ matrix.T % 2 local_x, _ = local_cliffords.get_local_matrices(matrix, 3) - np.testing.assert_array_equal(apply(rows, positions, local_x), expected) + np.testing.assert_array_equal(local_cliffords.apply_local(rows, positions, local_x), expected) np.testing.assert_array_equal(rows, original) diff --git a/src/qldpc/codes/distance/_counting/macwilliams_test.py b/src/qldpc/codes/distance/_counting/macwilliams_test.py index e3481bda1..cb3904c52 100644 --- a/src/qldpc/codes/distance/_counting/macwilliams_test.py +++ b/src/qldpc/codes/distance/_counting/macwilliams_test.py @@ -14,17 +14,13 @@ from . import macwilliams as mw -@pytest.mark.parametrize("compiled", [pytest.param(True, marks=pytest.mark.compiled), False]) @pytest.mark.parametrize("num_bits", [8, 70]) -def test_logical_counts(monkeypatch: pytest.MonkeyPatch, num_bits: int, compiled: bool) -> None: +def test_logical_counts(monkeypatch: pytest.MonkeyPatch, num_bits: int) -> None: """Count two disjoint four-qubit blocks, with any remaining qubits free, at every weight. A logical has even weight on each block, and the checks are the words of weights 0, 4 and 8 that are constant on each block. At length 70, middle weights exceed 64-bit integers. """ - if not compiled: - monkeypatch.setattr(mw, "_add_span_weights", mw._add_span_weights.py_func) - monkeypatch.setattr(mw, "_transform_weights", mw._transform_weights.py_func) add_span_weights = mw._add_span_weights chunk_sizes = [] @@ -45,7 +41,7 @@ def record_chunk(*args: Any) -> None: ) expected = kernel - {0: 1, 4: 2, 8: 1}.get(weight, 0) assert enumerators.count_logical_ops(weight) == (expected, expected) - # Each native call of the walk visits one Gray-code step, the given chunk size. + # Each call of the walk visits one Gray-code step, the given chunk size. assert chunk_sizes and set(chunk_sizes) == {1} diff --git a/src/qldpc/codes/distance/_counting/scan_test.py b/src/qldpc/codes/distance/_counting/scan_test.py index 8d8aab967..243324369 100644 --- a/src/qldpc/codes/distance/_counting/scan_test.py +++ b/src/qldpc/codes/distance/_counting/scan_test.py @@ -35,7 +35,6 @@ from . import scan -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("max_sets", [1, 3, 16]) def test_counts_against_enumeration(tune: Tune, pytestconfig: pytest.Config, max_sets: int) -> None: """Counts match an independent enumeration, as do those of codes deformed after a count.""" @@ -50,7 +49,6 @@ def test_counts_against_enumeration(tune: Tune, pytestconfig: pytest.Config, max assert_counts_match_enumeration(deformed) -@pytest.mark.usefixtures("compilation_mode") def test_a_deformed_self_dual_counter_shares_its_basis_family() -> None: """A self-dual code stays self-dual under a SWEL-preserving gate, and keeps one basis family.""" counter = lc.CSSLogicalCounter(codes.SteaneCode()) @@ -60,7 +58,6 @@ def test_a_deformed_self_dual_counter_shares_its_basis_family() -> None: assert_counts_match_enumeration(deformed) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("method", METHODS) def test_scans_resume_after_every_chunk( tune: Tune, pytestconfig: pytest.Config, method: DistanceMethod @@ -72,7 +69,6 @@ def test_scans_resume_after_every_chunk( assert_counts_match_enumeration(counter, method=method) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("max_sets", [4, 16]) def test_overlapping_basis_families_count_every_word_once(tune: Tune, max_sets: int) -> None: """Large basis families of overlapping sets give the counts of a brute-force scan.""" @@ -132,24 +128,13 @@ def record(*args: Any) -> information_sets.BasisFamily: assert sizes and max(sizes) <= 4 -@pytest.mark.usefixtures("plain_search_setup") -def test_python_kernels(monkeypatch: pytest.MonkeyPatch, tune: Tune) -> None: - """Run the Python versions of the compiled kernels, which coverage can trace. +def test_counts_with_tiny_chunks_and_forced_tables(tune: Tune) -> None: + """Counts by every method are exact with tiny chunks and forced completion tables. - The compiled helpers that the scan imports by name run as Python too. + Tiny chunks make every scan stop and resume repeatedly, and forced completion tables make the + steps of two or more rows use completion tables. The counters include deformed ones, and a + count above the distance raises an error. """ - for name in ( - "_count_chunk", - "_count_chunk_skipping", - "_visit_last_rows", - "_is_owned", - "_get_masked_rank", - "advance_support", - "get_completion_bound", - ): - monkeypatch.setattr(scan, name, getattr(scan, name).py_func) - # Tiny chunks make every scan stop and resume repeatedly, and forced completion tables make the - # steps of two or more rows skip completions. tune(scan_chunk_size=1, **FORCED_TABLES) code = get_random_css_code(np.random.default_rng(3), 14, 4, 4) assert_counts_match_enumeration(lc.CSSLogicalCounter(code)) @@ -331,8 +316,7 @@ def test_step_tables_choose_and_keep_tables(tune: Tune) -> None: assert step_tables.select(2, 0)[0] is completion_bounds.NO_TABLES -@pytest.mark.parametrize("compiled", [False, pytest.param(True, marks=pytest.mark.compiled)]) -def test_skipping_chunks_stop_at_a_lighter_nontrivial_word(compiled: bool) -> None: +def test_skipping_chunks_stop_at_a_lighter_nontrivial_word() -> None: """A step with tables stops at a nontrivial word lighter than the target, if asked to.""" # Three systematic rows [I | P], each followed by a distinct label and by its tail P. tails = np.array([[1, 0, 1, 0], [0, 1, 1, 0], [1, 1, 0, 1]], dtype=np.uint8) @@ -340,7 +324,7 @@ def test_skipping_chunks_stop_at_a_lighter_nontrivial_word(compiled: bool) -> No labels = np.eye(3, dtype=np.uint8) step_rows = np.concatenate([pack_bits(words), pack_bits(labels), pack_bits(tails)], axis=1) tables = completion_bounds.CompletionTables.build(pack_bits(tails), 8) - count_chunk = scan._count_chunk_skipping if compiled else scan._count_chunk_skipping.py_func + count_chunk = scan._count_chunk_skipping support = np.zeros(2, dtype=np.int64) prefixes = np.zeros((3, 1, step_rows.shape[1]), dtype=np.uint64) histogram = np.zeros(1, dtype=np.int64) diff --git a/src/qldpc/codes/distance/_search/affine_search_test.py b/src/qldpc/codes/distance/_search/affine_search_test.py index 28243f278..df636a791 100644 --- a/src/qldpc/codes/distance/_search/affine_search_test.py +++ b/src/qldpc/codes/distance/_search/affine_search_test.py @@ -17,7 +17,6 @@ from . import affine_search, completion_bounds -@pytest.mark.usefixtures("compilation_mode") def test_family_scan() -> None: """Chunked scans of a family enumerate each combination of basis rows once. @@ -86,7 +85,6 @@ def test_family_scan() -> None: assert (best, depth, visited) == (expected, -1, 0) -@pytest.mark.usefixtures("compilation_mode") def test_family_scans_weigh_every_set_of_the_support_that_reaches_the_cutoff() -> None: """A scan returns at the cutoff only after it weighs the support in every set. @@ -111,7 +109,6 @@ def test_family_scans_weigh_every_set_of_the_support_that_reaches_the_cutoff() - assert (best, visited) == (1, 1) -@pytest.mark.usefixtures("compilation_mode") def test_scan_of_one_information_set(pytestconfig: pytest.Config) -> None: """A scan of one information set finds the lightest eligible word of a layer, in chunks. @@ -197,7 +194,6 @@ def test_scan_of_one_information_set(pytestconfig: pytest.Config) -> None: ) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("css_weight", [False, True]) def test_coset_layer_scans_keep_the_first_lightest_word( css_weight: bool, pytestconfig: pytest.Config @@ -268,7 +264,6 @@ def get_weight(bits: npt.NDArray[np.uint8]) -> int: np.testing.assert_array_equal(representative, expected_word) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("css_weight", [False, True]) def test_coset_layer_scans_keep_the_first_of_equally_light_words(css_weight: bool) -> None: """A scan keeps the first of the equally light words ``001``, ``010``, and ``100`` of layer one. @@ -296,7 +291,6 @@ def test_coset_layer_scans_keep_the_first_of_equally_light_words(css_weight: boo assert representative[0] == 0b001 -@pytest.mark.usefixtures("compilation_mode") def test_coset_layer_scans_reach_their_last_support() -> None: """A layer visits every support, including the last one. @@ -341,7 +335,6 @@ def _get_tail_weights( ] -@pytest.mark.usefixtures("compilation_mode") def test_coset_tail_scans_weigh_words_by_their_pivot_rows_and_tails( pytestconfig: pytest.Config, ) -> None: @@ -407,7 +400,6 @@ def test_coset_tail_scans_weigh_words_by_their_pivot_rows_and_tails( assert tuple(best_support) == expected_support -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("deficiency", [0, 1, 3]) def test_coset_tail_scans_with_tables_skip_only_completions_that_reach_the_skip_bound( deficiency: int, pytestconfig: pytest.Config @@ -476,7 +468,6 @@ def test_coset_tail_scans_with_tables_skip_only_completions_that_reach_the_skip_ assert total_skipped -@pytest.mark.usefixtures("compilation_mode") def test_coset_tail_scans_below_the_best_skip_only_completions_that_cannot_lower_it( pytestconfig: pytest.Config, ) -> None: diff --git a/src/qldpc/codes/distance/_search/automorphism_families_test.py b/src/qldpc/codes/distance/_search/automorphism_families_test.py index 465ab3aed..ab9db9aa8 100644 --- a/src/qldpc/codes/distance/_search/automorphism_families_test.py +++ b/src/qldpc/codes/distance/_search/automorphism_families_test.py @@ -44,7 +44,6 @@ def test_automorphisms_must_map_both_spaces_onto_themselves() -> None: automorphism_families.verify_automorphisms(basis, labels, np.array([swap])) -@pytest.mark.usefixtures("compilation_mode") def test_automorphism_families_certify_like_the_family_of_all_images() -> None: """The certificate is that of the images of the scanned set under every group element. @@ -90,7 +89,6 @@ def test_automorphism_families_certify_like_the_family_of_all_images() -> None: assert certificate.lower_bound(weight) == expected -@pytest.mark.usefixtures("compilation_mode") def test_scanned_sets_balance_two_orbits() -> None: """For two orbits, the scanned set has the fewest pivots in its fuller orbit that any can have. diff --git a/src/qldpc/codes/distance/_search/completion_bounds_test.py b/src/qldpc/codes/distance/_search/completion_bounds_test.py index a159bd23a..8865bfbfe 100644 --- a/src/qldpc/codes/distance/_search/completion_bounds_test.py +++ b/src/qldpc/codes/distance/_search/completion_bounds_test.py @@ -71,10 +71,7 @@ def get_bound_oracle( ) -@pytest.mark.parametrize("interpreted", [pytest.param(False, marks=pytest.mark.compiled), True]) -def test_bounds_are_exact_relaxations( - interpreted: bool, pytestconfig: pytest.Config, monkeypatch: pytest.MonkeyPatch -) -> None: +def test_bounds_are_exact_relaxations(pytestconfig: pytest.Config) -> None: """Bounds equal their definition on blocks, and never exceed the lightest completion. The prefixes are sums of rows, the only prefixes that a scan bounds. The cases include a @@ -83,17 +80,12 @@ def test_bounds_are_exact_relaxations( """ rng = np.random.default_rng(pytestconfig.getoption("randomly_seed")) get_bound = cb.get_completion_bound - if interpreted: - monkeypatch.setattr( - cb, "_fill_tables", getattr(cb._fill_tables, "py_func", cb._fill_tables) - ) - get_bound = getattr(get_bound, "py_func", get_bound) for num_rows, num_columns in [(2, 1), (3, 5), (6, 21), (7, 70)]: tails = get_random_tails(rng, num_rows, num_columns) rows = pack_bits(tails) - # the compiled variant also checks the default width of 16 columns, which is slow in Python, - # and both variants keep two widths, so that they check that merging never weakens a bound - widths = (4, 8) if interpreted else (4, 8, 16) + # the default width of 16 columns, and narrower ones, to check that merging never weakens a + # bound; 16-column tables of the longest tails are slow to build in plain Python + widths = (4, 8, 16) if num_columns < 64 else (4, 8) all_tables = {bits: cb.CompletionTables.build(rows, bits) for bits in widths} for first, _ in itertools.product(range(num_rows - 1), range(6)): selection = rng.integers(0, 2, num_rows).astype(bool) @@ -124,7 +116,7 @@ def test_bounds_ignore_ones_past_the_tails() -> None: tails = np.eye(3, dtype=np.uint8) tables = cb.CompletionTables.build(pack_bits(tails), 8) prefix = pack_bits(np.array([[1, 1, 1, 0, 0, 1, 1, 1]], dtype=np.uint8))[0] - bound = cb.get_completion_bound.py_func(tables.tables, tables.offsets, 8, 0, prefix, 10**9) + bound = cb.get_completion_bound(tables.tables, tables.offsets, 8, 0, prefix, 10**9) assert bound == get_bound_oracle(tails, np.ones(3, dtype=np.uint8), 0, 8) == 1 @@ -161,15 +153,9 @@ def test_tables_within_a_limit_are_built_only_if_they_fit_and_skip_something() - assert cb.build_tables_within(rows[:1], 8, MAX_VALUE) is None -@pytest.mark.parametrize("interpreted", [pytest.param(False, marks=pytest.mark.compiled), True]) -def test_sampled_supports_are_counted_exactly( - interpreted: bool, pytestconfig: pytest.Config -) -> None: +def test_sampled_supports_are_counted_exactly(pytestconfig: pytest.Config) -> None: """The count of skipped sampled supports matches bounds computed independently.""" rng = np.random.default_rng(pytestconfig.getoption("randomly_seed")) - count = cb._count_skipped_samples - if interpreted: - count = getattr(count, "py_func", count) tails = get_random_tails(rng, 9, 37) rows = pack_bits(tails) for weight in (2, 3, 5): @@ -185,7 +171,7 @@ def test_sampled_supports_are_counted_exactly( ) for limit in range(4): expected = sum(bound >= limit for bound in bounds) - assert count(rows, supports, limit, block_bits, 37) == expected + assert cb._count_skipped_samples(rows, supports, limit, block_bits, 37) == expected def test_selection_follows_its_policy() -> None: diff --git a/src/qldpc/codes/distance/_search/connected_support_test.py b/src/qldpc/codes/distance/_search/connected_support_test.py index 77dab7b58..30fd0cd82 100644 --- a/src/qldpc/codes/distance/_search/connected_support_test.py +++ b/src/qldpc/codes/distance/_search/connected_support_test.py @@ -248,7 +248,6 @@ def get_qubit_problem(code: codes.QuditCode) -> Problem: } -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("name", SEARCHED_PROBLEMS) def test_the_distance_matches_enumeration(name: str) -> None: """The distance search, and a search through each weight limit, agree with an enumeration.""" @@ -267,7 +266,6 @@ def test_the_distance_matches_enumeration(name: str) -> None: ) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("name", SEARCHED_PROBLEMS) def test_each_root_finds_the_lightest_witnesses_that_it_roots(name: str) -> None: """The search from a variable alone, through the distance, finds the witnesses that it roots. @@ -291,7 +289,6 @@ def test_each_root_finds_the_lightest_witnesses_that_it_roots(name: str) -> None assert witness is None or (min(witness) == root and is_witness(problem, witness)) -@pytest.mark.usefixtures("compilation_mode") def test_each_support_is_visited_once_above_its_least_variable() -> None: """With every word excluded, a search visits the supports that grow from each root once. @@ -316,7 +313,6 @@ def test_each_support_is_visited_once_above_its_least_variable() -> None: assert num_states == {2: 5, 4: 6 + 2 + 1 + 1} -@pytest.mark.usefixtures("compilation_mode") def test_automorphisms_reduce_the_roots_and_keep_the_distance() -> None: """A cyclic code has a symmetry that leaves one root, and so fewer states. @@ -338,7 +334,6 @@ def test_automorphisms_reduce_the_roots_and_keep_the_distance() -> None: assert symmetric.num_states < plain.num_states -@pytest.mark.usefixtures("compilation_mode") def test_each_root_excludes_the_orbits_before_its_own() -> None: """The search from the root of an orbit uses no variable of an earlier orbit. @@ -366,7 +361,6 @@ def test_each_root_excludes_the_orbits_before_its_own() -> None: assert np.all(orbits[witness] >= orbit) -@pytest.mark.usefixtures("compilation_mode") def test_each_root_ranks_lowest_in_its_orbit() -> None: """The ranks keep the order of the variables within each orbit, so each root ranks lowest. @@ -390,7 +384,6 @@ def test_each_root_ranks_lowest_in_its_orbit() -> None: assert witness is not None and list(witness) == list(cycles[1]) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("minimum_states", [0, 30, _tuning.MAX_VALUE]) def test_automorphisms_are_found_once_by_the_threshold(minimum_states: int) -> None: """A search asks for automorphisms at most once, and by the time it reaches the threshold. @@ -416,7 +409,6 @@ def test_automorphisms_are_found_once_by_the_threshold(minimum_states: int) -> N assert bool(found) == (minimum_states < _tuning.MAX_VALUE) -@pytest.mark.usefixtures("compilation_mode") def test_a_limit_without_states_does_not_ask_for_automorphisms() -> None: """A search without an estimate of its states asks for automorphisms only before a raised limit. @@ -436,7 +428,6 @@ def test_a_limit_without_states_does_not_ask_for_automorphisms() -> None: assert not found -@pytest.mark.usefixtures("compilation_mode") def test_a_cutoff_raises_the_first_limit_to_a_multiple_of_the_divisor( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -493,7 +484,6 @@ def test_wrong_automorphisms_are_rejected() -> None: search.find_witness(3) -@pytest.mark.usefixtures("compilation_mode") def test_sectors_are_searched_together(monkeypatch: pytest.MonkeyPatch) -> None: """The tetrahedral code has X-distance 7 and Z-distance 3: the Z sector bounds the X search. @@ -515,7 +505,6 @@ def record_limit(search: cs.SupportSearch, max_weight: int) -> npt.NDArray[np.in assert max(limits) == 3 -@pytest.mark.usefixtures("compilation_mode") def test_the_logical_test_reads_every_block() -> None: """A witness may fail only tests past the first 64, in a later block of each packed column. @@ -550,7 +539,6 @@ def get_random_qubit_problems(rng: np.random.Generator, num_problems: int) -> It yield get_qubit_problem(codes.QuditCode(matrix, is_subsystem_code=code.is_subsystem_code)) -@pytest.mark.usefixtures("compilation_mode") def test_random_qubit_problems_match_enumeration(pytestconfig: pytest.Config) -> None: """Qubit problems agree with an enumeration at every weight limit, and from every root. @@ -612,7 +600,6 @@ def test_random_qubit_problems_match_enumeration(pytestconfig: pytest.Config) -> assert witness is None or is_symplectic_witness(problem, witness) -@pytest.mark.usefixtures("compilation_mode") def test_qubit_automorphisms_reduce_the_roots_and_keep_the_distance() -> None: """The cyclic shift of the five-qubit code leaves one root, and so fewer states.""" problem = QUBIT_PROBLEMS["five-qubit"] @@ -626,7 +613,6 @@ def test_qubit_automorphisms_reduce_the_roots_and_keep_the_distance() -> None: assert symmetric.num_states < plain.num_states -@pytest.mark.usefixtures("compilation_mode") def test_the_scale_of_weights_leaves_room_for_every_weight_limit() -> None: """Weights in units of the capped scale, times a weight limit as large as the width, fit. @@ -658,7 +644,6 @@ def find_witness(search: cs.SupportSearch, max_weight: int) -> npt.NDArray[np.in assert len(limits) == 2 * 16 // 2 -@pytest.mark.usefixtures("compilation_mode") def test_cutoffs_and_upper_bounds(monkeypatch: pytest.MonkeyPatch) -> None: """A search can stop early at a witness within its cutoff, or at a known upper bound.""" problem = PROBLEMS["toric-X"] @@ -689,7 +674,6 @@ def record_limit(search: cs.SupportSearch, max_weight: int) -> npt.NDArray[np.in assert cs.get_distance_connected_support([components], cutoff=3) == 3 -@pytest.mark.usefixtures("compilation_mode") def test_a_problem_without_logical_operators_has_no_distance() -> None: """Without a vector outside the excluded rows, the distance is one more than the width.""" trivial = (np.array([[1, 1]], dtype=np.uint8), np.array([[1, 1]], dtype=np.uint8)) @@ -704,7 +688,6 @@ def test_a_problem_without_logical_operators_has_no_distance() -> None: assert cs.get_distance_connected_support([one_qubit], cutoff=0, homogeneous=False) == 2 -@pytest.mark.usefixtures("compilation_mode") def test_degenerate_checks() -> None: """Checks may be missing, zero, or repeated, and may be arrays over GF(2).""" no_checks = np.zeros((0, 3), dtype=np.uint8) @@ -751,7 +734,6 @@ def get_vector( } -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("name", SYNDROME_PROBLEMS) def test_syndrome_queries_match_enumeration(name: str) -> None: """A query finds a vector with a syndrome exactly if the limit reaches the least weight. @@ -787,7 +769,6 @@ def test_syndrome_queries_match_enumeration(name: str) -> None: assert search.num_states == 0 -@pytest.mark.usefixtures("compilation_mode") def test_a_syndrome_query_leaves_no_state_for_later_roots() -> None: """A search from a root of -1 that finds nothing leaves the searches from variables unchanged. @@ -834,7 +815,6 @@ def search_from(roots: list[int], syndrome: tuple[int, ...]) -> list[int]: assert search_from([-1, root], syndrome) == search_from([root], no_syndrome) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("name", ["nested-restrictions", "five-qubit-state"]) def test_memoizing_changes_no_result_or_count_of_states(name: str) -> None: """A syndrome query that memoizes overlaps and repairs finds what one without it finds. diff --git a/src/qldpc/codes/distance/_search/coset_leader_search_test.py b/src/qldpc/codes/distance/_search/coset_leader_search_test.py index 1f182ccbd..23c4d524d 100644 --- a/src/qldpc/codes/distance/_search/coset_leader_search_test.py +++ b/src/qldpc/codes/distance/_search/coset_leader_search_test.py @@ -262,7 +262,6 @@ def _block_minimum( return min(max(a, b) for a, b in totals) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize( ("css_weight", "css_singles_table"), [(False, True), (True, True), (True, False)] ) @@ -298,7 +297,6 @@ def test_bounds_match_exhaustive_minima( assert bound.lower_bound > max_weight -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("css_weight", [False, True]) def test_batches_find_each_class_above_max_weight( css_weight: bool, pytestconfig: pytest.Config @@ -322,7 +320,6 @@ def test_batches_find_each_class_above_max_weight( assert found == np.flatnonzero(minima > max_weight).tolist() -@pytest.mark.usefixtures("compilation_mode") def test_batches_return_bounds_above_max_weight_plus_one() -> None: """On |0000> with CSS weight, the syndrome 1111 needs X on every qubit, so weight four. @@ -337,7 +334,6 @@ def test_batches_return_bounds_above_max_weight_plus_one() -> None: assert oracle.get_min_weight_bound(0b1111, 2).lower_bound == 4 -@pytest.mark.usefixtures("compilation_mode") @_BLOCK_CASES def test_batches_read_syndromes_of_several_words( block: tuple[str, ...], css_weight: bool, pytestconfig: pytest.Config @@ -367,7 +363,6 @@ def test_batches_read_syndromes_of_several_words( assert found == np.flatnonzero(np.array(minima) > max_weight).tolist() -@pytest.mark.usefixtures("compilation_mode") @_BLOCK_CASES def test_batches_read_syndromes_of_fewer_words( block: tuple[str, ...], css_weight: bool, pytestconfig: pytest.Config @@ -398,7 +393,6 @@ def test_batches_read_syndromes_of_fewer_words( assert found == np.flatnonzero(np.array(minima) > max_weight).tolist() -@pytest.mark.usefixtures("compilation_mode") def test_x_and_z_syndromes_span_several_words(pytestconfig: pytest.Config) -> None: """On |0>^70 |+>^70, the syndromes of the X and Z parts have 70 bits, more than one word. @@ -443,7 +437,6 @@ def test_css_weight_counts_the_larger_sector() -> None: assert not css.get_min_weight_bound(3, 0).within_max_weight -@pytest.mark.usefixtures("compilation_mode") def test_css_weight_minimizes_both_sectors_jointly() -> None: """On a four-qubit path graph state, Z^4 is equivalent to pure X and to pure Z, but not to both. @@ -458,7 +451,6 @@ def test_css_weight_minimizes_both_sectors_jointly() -> None: assert oracle.get_min_weight_bound(15, 2).within_max_weight -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize(("syndrome", "minimum"), [(44, 2), (108, 3)]) def test_rank_deficient_information_sets_give_valid_bounds(syndrome: int, minimum: int) -> None: """Rank-deficient sets give valid bounds whether a weight-two error exists or not. @@ -481,7 +473,6 @@ def test_rank_deficient_information_sets_give_valid_bounds(syndrome: int, minimu assert bound.lower_bound > 2 -@pytest.mark.usefixtures("compilation_mode") def test_exhausted_search_gives_the_exact_minimum() -> None: """On a state with stabilizers Y, Z, Z, Z, Z, the syndrome 11111 has CSS weight four. @@ -499,7 +490,6 @@ def test_exhausted_search_gives_the_exact_minimum() -> None: assert oracle.get_min_weight_bound(0b11111, 4).within_max_weight -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("css_weight", [False, True]) def test_wide_states_span_several_packed_words(css_weight: bool) -> None: """Eleven GHZ blocks use 99 bits for the physical weight, more than one 64-bit word. @@ -574,7 +564,6 @@ def test_bounds_match_per_block_minima( ] -@pytest.mark.usefixtures("compilation_mode") def test_queries_return_the_word_and_bound_of_the_search_order() -> None: """Single and batch queries return the representatives and bounds of the search order. @@ -612,7 +601,6 @@ def test_queries_return_the_word_and_bound_of_the_search_order() -> None: assert (index, batch_bound) == (2, expected) -@pytest.mark.usefixtures("compilation_mode") def test_css_bounds_match_exhaustive_minima(pytestconfig: pytest.Config) -> None: """On random CSS states, both queries for the CSS weight agree with exhaustive minima. @@ -644,7 +632,6 @@ def test_css_bounds_match_exhaustive_minima(pytestconfig: pytest.Config) -> None assert found == np.flatnonzero(minima[syndromes] > max_weight).tolist() -@pytest.mark.usefixtures("compilation_mode") def test_css_weight_searches_of_even_stabilizers_stop_at_bounds_of_each_class_parity() -> None: """Searches of Pauli errors by CSS weight stop at the bounds of each class, rounded to parity. @@ -702,7 +689,6 @@ def test_css_weight_searches_of_even_stabilizers_stop_at_bounds_of_each_class_pa assert num_sooner -@pytest.mark.usefixtures("compilation_mode") def test_cached_searches_answer_only_for_their_stabilizer_group( pytestconfig: pytest.Config, ) -> None: @@ -727,7 +713,6 @@ def test_cached_searches_answer_only_for_their_stabilizer_group( assert _get_syndrome(matrix, bound.representative) == syndrome -@pytest.mark.usefixtures("compilation_mode") def test_oracles_use_the_tuning_with_which_they_are_built(monkeypatch: pytest.MonkeyPatch) -> None: """An oracle builds its searches with the tuning in force, keyed by the fields that they read. @@ -781,7 +766,7 @@ def record(*args: Any, random_column_orders: int) -> information_sets.BasisFamil assert bool(orders) == reads -@pytest.mark.usefixtures("compilation_mode") +@pytest.mark.usefixtures("plain_search_setup") def test_oracles_build_other_families_with_the_tuning_they_are_built_with( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -852,7 +837,6 @@ def record(*args: Any, **kwargs: Any) -> tuple[npt.NDArray[np.uint8], npt.NDArra assert calls == [(5, 2)] -@pytest.mark.usefixtures("compilation_mode") def test_oracles_under_other_gates_do_not_share_searches(tune: Tune) -> None: """An oracle answers as a fresh oracle under the threshold of its gate, whatever is cached. @@ -888,7 +872,6 @@ def test_oracles_under_other_gates_do_not_share_searches(tune: Tune) -> None: assert answers_now == answers[minimum_work] -@pytest.mark.usefixtures("compilation_mode") def test_cached_pauli_searches_answer_for_their_own_stabilizers_and_weight() -> None: """Oracles built in turn answer exactly for their own stabilizers and weight. @@ -920,7 +903,6 @@ def test_cached_pauli_searches_answer_for_their_own_stabilizers_and_weight() -> _assert_exact_answers(oracle, matrix, css_weight) -@pytest.mark.usefixtures("compilation_mode") def test_cached_pauli_searches_ignore_later_changes_to_the_stabilizer_array(tune: Tune) -> None: """Oracles answer for the stabilizers that they were built from, when an array changes later. @@ -1006,7 +988,10 @@ def record(*args: Any) -> tuple[int, int, int]: return plan_steps(*args) monkeypatch.setattr(information_sets, "plan_steps", record) - get_stopping_step = coset_leader_search._get_stopping_step.py_func + # Take the Python version even when the function is compiled, since only it calls the recording + # function. + function = coset_leader_search._get_stopping_step + get_stopping_step = getattr(function, "py_func", function) certificates = [ information_sets.Certificate(0, (0,)), information_sets.Certificate(1, (1,) * 64), @@ -1141,7 +1126,6 @@ def get_least_bound(syndrome: int, max_weight: int) -> int: assert found == [syndrome for syndrome in syndromes if minima[syndrome] > max_weight] -@pytest.mark.usefixtures("compilation_mode") def test_cosets_of_a_rank_deficient_code_match_brute_force() -> None: """The coset searches of a rank-deficient code match the minima of all words. @@ -1157,7 +1141,6 @@ def test_cosets_of_a_rank_deficient_code_match_brute_force() -> None: _assert_classical_cosets_match_brute_force(_RANK_DEFICIENT_CODE) -@pytest.mark.usefixtures("compilation_mode") def test_cosets_of_an_even_code_have_bounds_of_their_parity() -> None: """Coset searches of an even code give each coset with no light word a bound of its parity. @@ -1175,7 +1158,6 @@ def test_cosets_of_an_even_code_have_bounds_of_their_parity() -> None: _assert_classical_cosets_match_brute_force(np.hstack([_RANK_DEFICIENT_CODE, parities])) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize( "code", [EVEN_ROUTES_CODE, TWO_FAMILIES_CODE], ids=["even", "two-families"] ) @@ -1197,7 +1179,6 @@ def test_cosets_match_brute_force_with_other_families( _assert_classical_cosets_match_brute_force(code) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize( "matrix, css_weight", [ @@ -1231,7 +1212,6 @@ def test_oracles_answer_exactly_with_other_families( assert found == np.flatnonzero(minima > max_weight).tolist() -@pytest.mark.usefixtures("compilation_mode") def test_answers_do_not_depend_on_earlier_queries(pytestconfig: pytest.Config, tune: Tune) -> None: """The answers of a search depend only on its code, the syndrome, and the maximum weight. @@ -1268,7 +1248,6 @@ def get_answers(order: str) -> dict[tuple[int, int], MinWeightBound]: assert get_answers("up") != answers[0] -@pytest.mark.usefixtures("compilation_mode") def test_searches_visit_only_the_sets_that_add_to_their_bounds(pytestconfig: pytest.Config) -> None: """A search skips exactly the sets that add nothing to the bound of its stopping step. @@ -1558,7 +1537,6 @@ def _count_skipped_supports( return skipped -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("block_bits", [None, 4]) def test_searches_by_tails_answer_as_searches_of_whole_words( block_bits: int | None, pytestconfig: pytest.Config @@ -1666,7 +1644,6 @@ def test_searches_by_tails_answer_as_searches_of_whole_words( assert skipped or not (block_bits and must_skip) -@pytest.mark.usefixtures("compilation_mode") def test_queries_of_routes_by_tails_weigh_the_tails_of_their_record( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -1702,7 +1679,6 @@ def get_zero_tails(*args: Any, **kwargs: Any) -> coset_leader_search._CosetTails assert search.find_first_not_within_max_weight(packed_syndromes, 2) == (len(syndromes), None) -@pytest.mark.usefixtures("compilation_mode") def test_queries_of_routes_by_tails_skip_completions_by_the_tables_of_their_record( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -1833,7 +1809,6 @@ def _assert_min_weights_match_brute_force( assert np.all(exact | ((least <= weights) & (weights <= floors))) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize( "tuning", [ @@ -1879,7 +1854,6 @@ def test_min_weights_of_cosets_match_brute_force( _assert_min_weights_match_brute_force(search, early_stop_syndromes, minima, len(checks), rng) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize( "tuning", [ @@ -1947,7 +1921,6 @@ def test_min_weights_match_exhaustive_minima( assert np.array_equal(weights, np.minimum(minima[syndromes], max_weight + 1)) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("tuning", [{}, {"overlapping_minimum_work": 0}], ids=["words", "tails"]) def test_min_weight_searches_above_max_weight_examine_as_queries( tuning: dict[str, int], tune: Tune @@ -2061,7 +2034,6 @@ def _assert_exact_answers( assert weight <= max_weight if bound.within_max_weight else bound.lower_bound > max_weight -@pytest.mark.usefixtures("compilation_mode") def test_connected_support_answers_match_exhaustive_minima(pytestconfig: pytest.Config) -> None: """The connected-support engine answers every query exactly, by either measure of weight. @@ -2109,7 +2081,6 @@ def test_connected_support_answers_match_exhaustive_minima(pytestconfig: pytest. assert bound.lower_bound == 2 and weight <= max_weight -@pytest.mark.usefixtures("compilation_mode") def test_connected_support_batches_read_syndromes_of_several_words( pytestconfig: pytest.Config, tune: Tune ) -> None: @@ -2214,7 +2185,6 @@ def test_connected_support_rejects_what_it_cannot_search() -> None: CosetLeaderSearch([[1, 0]], method=method) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("method", ["brouwer_zimmermann", "connected_support"]) def test_batches_reject_syndromes_out_of_range(method: CosetSearchMethod) -> None: """Batches of either method reject a syndrome at or above ``2**num_qubits``. diff --git a/src/qldpc/codes/distance/_search/information_sets_test.py b/src/qldpc/codes/distance/_search/information_sets_test.py index 147f5a20d..ac0366c03 100644 --- a/src/qldpc/codes/distance/_search/information_sets_test.py +++ b/src/qldpc/codes/distance/_search/information_sets_test.py @@ -81,7 +81,6 @@ def test_a_basis_family_stacks_its_sets() -> None: assert no_labels.labels.shape == (3, 3, 0) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("basis", [PARTIAL_BASIS, EVEN_BASIS]) def test_family_certificates(basis: npt.NDArray[np.uint8]) -> None: """Rank-deficient blocks and inferred parity give certificates valid for every word.""" @@ -102,7 +101,6 @@ def test_weight_divisor_inference() -> None: assert get_family(odd, None, weight_divisor=2).certificate.weight_divisor == 2 -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize( "ranks, upper_bound, retained", [([10, 6, 2], 6, [10, 6]), ([10, 6, 2], 12, [10, 6, 2]), ([6, 2], 6, [6, 2])], @@ -125,7 +123,6 @@ def test_prune_family(ranks: list[int], upper_bound: int, retained: list[int]) - assert pruned.certificate.work(upper_bound) <= basis_family.certificate.work(upper_bound) -@pytest.mark.usefixtures("compilation_mode") def test_overlapping_family_certificates() -> None: """Check pivot coverage and label transformations against exhaustive codeword weights.""" @@ -146,7 +143,6 @@ def test_overlapping_family_certificates() -> None: _assert_certificate_bounds_unvisited_words(basis, basis_family) -@pytest.mark.usefixtures("compilation_mode") def test_a_cheap_search_keeps_its_disjoint_basis_family() -> None: """No overlapping basis family is built if the work of the disjoint one is below the minimum.""" basis = get_independent_rows(np.asarray(qldpc.codes.HammingCode(4).generator)) @@ -158,7 +154,6 @@ def test_a_cheap_search_keeps_its_disjoint_basis_family() -> None: assert not len(_get_overlapping_family(basis, labels, disjoint, 3, 0, minimum_work=0).ranks) -@pytest.mark.usefixtures("compilation_mode") def test_overlapping_family_guards() -> None: """Reject unhelpful coverage and terminate repeated attempts at the same pivot set.""" @@ -184,7 +179,6 @@ def test_overlapping_family_guards() -> None: assert not len(_get_overlapping_family(small, None, disjoint, 3, minimum_work=0).ranks) -@pytest.mark.usefixtures("compilation_mode") def test_certificate_follows_its_definition(pytestconfig: pytest.Config) -> None: """The bounds, work estimate, and number of sets that certify a bound equal their definitions. @@ -275,7 +269,6 @@ def test_weight_bounds_divide_the_step_bounds(pytestconfig: pytest.Config) -> No assert np.array_equal(certificate.get_weight_bounds(factor, parity), expected_bounds) -@pytest.mark.usefixtures("compilation_mode") def test_plans_follow_their_definition(pytestconfig: pytest.Config) -> None: """The stopping step and the work of a plan follow their definitions. @@ -591,7 +584,6 @@ def test_searches_of_overlapping_sets_use_linear_programming_when_costly() -> No assert not disjoint.get_search_certificate(3, 0, DENOMINATOR).pivot_blocks -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("dimension, num_sets", [(300, 16), (60, 8192)]) def test_work_estimates_saturate(dimension: int, num_sets: int) -> None: """Work estimates for huge searches stay large and positive, rather than overflowing.""" @@ -606,7 +598,6 @@ def test_binomials_are_exact_where_they_fit_and_cannot_be_modified() -> None: assert 0 < binomials[35] < math.comb(70, 35) -@pytest.mark.usefixtures("compilation_mode") @pytest.mark.parametrize("max_sets, num_sets", [(1, 0), (3, 0), (4, 4), (16, 4)]) def test_the_size_limit_bounds_the_overlapping_basis_family(max_sets: int, num_sets: int) -> None: """A cheaper basis family of overlapping sets needs four sets, so a lower limit finds none.""" @@ -617,7 +608,6 @@ def test_the_size_limit_bounds_the_overlapping_basis_family(max_sets: int, num_s assert len(overlapping.ranks) == num_sets -@pytest.mark.usefixtures("compilation_mode") def test_layers_visit_each_support_once_with_its_sums(pytestconfig: pytest.Config) -> None: """A layer visits the combinations of rows in order, each with its sum in every set. @@ -650,6 +640,7 @@ def test_layers_visit_each_support_once_with_its_sums(pytestconfig: pytest.Confi assert visited == expected +@pytest.mark.usefixtures("plain_search_setup") def test_functions_agree_in_plain_python_and_compiled(pytestconfig: pytest.Config) -> None: """Pruned and replacement basis families are identical, bit for bit, plain and compiled.""" rng = np.random.default_rng(pytestconfig.getoption("randomly_seed")) diff --git a/src/qldpc/codes/distance/conftest.py b/src/qldpc/codes/distance/conftest.py index e9830cc30..6af71c170 100644 --- a/src/qldpc/codes/distance/conftest.py +++ b/src/qldpc/codes/distance/conftest.py @@ -3,7 +3,7 @@ """Shared fixtures and helpers for exact-distance, binary-algebra, BZ, and count tests.""" import itertools -from collections.abc import Iterable, Iterator +from collections.abc import Iterable from typing import Any, NamedTuple import galois @@ -18,13 +18,7 @@ from ..conftest import Tune from . import logical_counts as lc from ._binary import get_nested_code_basis -from ._search import ( - affine_search, - automorphism_families, - connected_support, - coset_leader_search, - information_sets, -) +from ._search import information_sets @pytest.fixture @@ -82,46 +76,6 @@ def record_certificate( return stages -@pytest.fixture(params=["python", pytest.param("compiled", marks=pytest.mark.compiled)]) -def compilation_mode( - request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch -) -> Iterator[None]: - """Run the compiled functions of the distance searches as plain Python, or compiled. - - In the plain Python mode, the compiled-on-request functions run as Python. So do the - always-compiled functions that the scans of layers, the basis family stages, the automorphism - families, the connected-support search, and the coset-leader search define or import from this - package, such as ``completion_bounds.get_completion_bound`` in the scans of layers. Coverage - can trace them. The compiled mode carries the ``compiled`` marker. It compiles the - compiled-on-request functions once per session, and restores plain Python afterwards. - """ - if request.param == "python": - _compilation.set_on_request_functions_compiled(False) - package = __name__.rpartition(".")[0] - for module in ( - affine_search, - automorphism_families, - connected_support, - information_sets, - coset_leader_search, - ): - for name, function in list(vars(module).items()): - if hasattr(function, "py_func") and function.py_func.__module__.startswith( - f"{package}." - ): - monkeypatch.setattr(module, name, function.py_func) - yield - else: - _compilation.set_on_request_functions_compiled(True) - try: - yield - finally: - # Undo the test's patches first, since a patched name may be restored to compiled code, - # which switching the compilation off then replaces by plain Python. - monkeypatch.undo() - _compilation.set_on_request_functions_compiled(False) - - def get_binary_matrix(rows: Iterable[str]) -> npt.NDArray[np.uint8]: """The binary matrix with the given rows of digits.""" return np.array([[int(bit) for bit in row] for row in rows], dtype=np.uint8) diff --git a/src/qldpc/codes/distance/core_test.py b/src/qldpc/codes/distance/core_test.py index bf5caf3ab..089e81567 100644 --- a/src/qldpc/codes/distance/core_test.py +++ b/src/qldpc/codes/distance/core_test.py @@ -64,7 +64,7 @@ def test_deprecated_block_size_argument() -> None: assert records[0].filename == __file__ -@pytest.mark.usefixtures("force_information_sets", "compilation_mode") +@pytest.mark.usefixtures("force_information_sets") def test_brouwer_zimmermann_information_set_search() -> None: """Information-set search excludes stabilizers, finds logical products, and honors cutoffs.""" # The Hamming-weight-one stabilizer is lighter than every logical operator. With batches of one @@ -300,7 +300,7 @@ def test_quantum_distances_use_qubit_automorphisms(tune: Tune) -> None: search(get_automorphisms=lambda: transposition[None]) -@pytest.mark.usefixtures("force_information_sets", "compilation_mode") +@pytest.mark.usefixtures("force_information_sets") def test_brouwer_zimmermann_random_cross_checks() -> None: """Information-set search agrees with exhaustive enumeration on random nested codes.""" rng = np.random.default_rng(118) diff --git a/src/qldpc/codes/distance/logical_counts_test.py b/src/qldpc/codes/distance/logical_counts_test.py index 363a87e2d..c21f33756 100644 --- a/src/qldpc/codes/distance/logical_counts_test.py +++ b/src/qldpc/codes/distance/logical_counts_test.py @@ -190,22 +190,16 @@ def test_methods_agree_on_random_codes(num_qubits: int, rank_x: int, rank_z: int ) == counter.count_logical_ops(count_pauli=True, method="brute_force") -@pytest.mark.parametrize( - "compiled", [False, pytest.param(True, marks=pytest.mark.compiled)], ids=["python", "compiled"] -) @pytest.mark.usefixtures("plain_search_setup") -def test_compile_search_setup(compiled: bool, monkeypatch: pytest.MonkeyPatch) -> None: +def test_compile_search_setup(monkeypatch: pytest.MonkeyPatch) -> None: """Calling ``compile_search_setup`` up front leaves the counts and distances unchanged. The test starts from functions that no other test has compiled. Its counters are deformed after - a count, so that they move their kept basis families through a named and a general gate. When - the set-up code is compiled, nothing compiles afterwards. The coverage check skips that - variant, so another runs the same steps with ``set_on_request_functions_compiled`` disabled. + a count, so that they move their kept basis families through a named and a general gate. Once + the set-up code is compiled, nothing compiles afterwards. """ fresh = [replace(item, compiled=None) for item in _compilation._ON_REQUEST_FUNCTIONS] monkeypatch.setattr(_compilation, "_ON_REQUEST_FUNCTIONS", fresh) - if not compiled: - monkeypatch.setattr(lc, "set_on_request_functions_compiled", lambda _: None) code = qldpc.codes.SurfaceCode(5) def get_results() -> list[qldpc.codes.CSSLogicalCounts]: diff --git a/src/qldpc/conftest.py b/src/qldpc/conftest.py index 9fb323319..15e33b62c 100644 --- a/src/qldpc/conftest.py +++ b/src/qldpc/conftest.py @@ -247,7 +247,7 @@ def wedderburn_artin_rings_to_test( def pytest_addoption(parser: pytest.Parser) -> None: - """Add the options of test runs with compiled code.""" + """Add the options of CI's compiled runs (see docs/source/contributing/index.rst).""" # Explicit defaults let pytest set these options when it loads this file after reading the # command line, as in a run of ``pytest src``. parser.addoption( diff --git a/src/qldpc/experimental/objects/_packed_paulis_test.py b/src/qldpc/experimental/objects/_packed_paulis_test.py index 3da1dcdf2..228b5c5ad 100644 --- a/src/qldpc/experimental/objects/_packed_paulis_test.py +++ b/src/qldpc/experimental/objects/_packed_paulis_test.py @@ -15,22 +15,6 @@ # Qubit counts with a partial word, a full word, and more than one word. _NUM_QUBITS = (1, 64, 65) -_KERNELS = ( - "count_y", - "is_scalar", - "anticommutes", - "left_multiply", - "conjugate_by_s_rotation", - "conjugate_by_local_gate", -) - - -@pytest.fixture(params=[pytest.param("compiled", marks=pytest.mark.compiled), "python"]) -def kernels(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> None: - """Run the compiled kernels, or their Python versions for coverage.""" - if request.param == "python": - for name in _KERNELS: - monkeypatch.setattr(packed, name, getattr(packed, name).py_func) def _random_string(rng: np.random.Generator, num_qubits: int) -> stim.PauliString: @@ -69,7 +53,6 @@ def test_stim_round_trip(rng: np.random.Generator) -> None: assert packed.pack_axes([], 65)[0].shape == (0, 4) -@pytest.mark.usefixtures("kernels") def test_products_and_commutation(rng: np.random.Generator) -> None: for num_qubits in _NUM_QUBITS: first, second = _random_string(rng, num_qubits), _random_string(rng, num_qubits) @@ -86,7 +69,6 @@ def test_products_and_commutation(rng: np.random.Generator) -> None: assert packed.is_scalar(packed.from_stim(-stim.PauliString(num_qubits))[0]) -@pytest.mark.usefixtures("kernels") def test_conjugation_by_s_rotations(rng: np.random.Generator) -> None: """All one-qubit Pauli strings and axes, and random ones on more qubits.""" pairs = [ @@ -126,7 +108,6 @@ def test_conjugation_tables() -> None: array[0, 0] = 0 -@pytest.mark.usefixtures("kernels") @pytest.mark.parametrize( "name, qubits", [ diff --git a/src/qldpc/experimental/objects/_symplectic_test.py b/src/qldpc/experimental/objects/_symplectic_test.py index 5b196fac8..617a59ecc 100644 --- a/src/qldpc/experimental/objects/_symplectic_test.py +++ b/src/qldpc/experimental/objects/_symplectic_test.py @@ -15,26 +15,6 @@ from qldpc.experimental.objects import _symplectic as symplectic from qldpc.experimental.objects.conftest import get_random_pauli_string -_KERNELS = ( - "canonicalize", - "_row_reduce", - "_get_bit", - "_set_dual", - "_get_image_of_dual", - "_triangularize", - "_restricted_entry", - "_choose_vector", - "_is_continuable", -) - - -@pytest.fixture(params=[pytest.param("compiled", marks=pytest.mark.compiled), "python"]) -def kernels(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> None: - """Run the compiled kernels, or their Python versions for coverage.""" - if request.param == "python": - for name in _KERNELS: - monkeypatch.setattr(symplectic, name, getattr(symplectic, name).py_func) - def _pack_axes( axes: list[stim.PauliString], num_qubits: int @@ -91,7 +71,6 @@ def _get_rank(matrix: npt.NDArray[np.uint8]) -> int: return rank -@pytest.mark.usefixtures("kernels") def test_canonical_forms(rng: np.random.Generator) -> None: """Canonical forms equal their inputs, use at most rank(M - I) + 1 axes, and are canonical.""" for num_qubits in (1, 2, 3, 65): @@ -120,7 +99,6 @@ def test_canonical_forms(rng: np.random.Generator) -> None: np.testing.assert_array_equal(result[2], new_phases) -@pytest.mark.usefixtures("kernels") def test_rotations_about_the_identity() -> None: """Rotations about the identity are global phases, so they do not change canonical forms.""" axes = [stim.PauliString(axis) for axis in ("+XZ", "-YY")] @@ -173,7 +151,6 @@ def test_controlled_z_needs_an_extra_rotation() -> None: assert _get_tableau(new_core, new_words, new_phases, 2) == expected -@pytest.mark.usefixtures("kernels") @pytest.mark.parametrize( "letters", [("YY", "Y_", "XX"), ("XY_", "ZZY", "YZ_", "Y_X")], ids=["pair", "pair-lookahead"] ) @@ -186,7 +163,6 @@ def test_choices_of_sums_of_basis_vectors(letters: tuple[str, ...]) -> None: assert _get_tableau(*symplectic.canonicalize(core, words, phases), num_qubits) == tableau -@pytest.mark.usefixtures("kernels") def test_lookahead_avoids_an_extra_rotation() -> None: """An input found by search, for which choices without lookahead need an extra rotation. @@ -202,7 +178,6 @@ def test_lookahead_avoids_an_extra_rotation() -> None: assert len(new_words) == 3 -@pytest.mark.usefixtures("kernels") def test_steps_without_continuable_choices() -> None: """An input found by search, whose decomposition reaches steps without continuable choices. diff --git a/src/qldpc/experimental/objects/rotated_pauli_string_test.py b/src/qldpc/experimental/objects/rotated_pauli_string_test.py index c6ec37e3e..69ead406c 100644 --- a/src/qldpc/experimental/objects/rotated_pauli_string_test.py +++ b/src/qldpc/experimental/objects/rotated_pauli_string_test.py @@ -14,7 +14,7 @@ import stim from qldpc.experimental.conftest import get_circuit_unitary -from qldpc.experimental.objects import RotatedPauliString, rotated_pauli_string +from qldpc.experimental.objects import RotatedPauliString from qldpc.experimental.objects.conftest import ( equal_up_to_phase, get_pauli_matrix, @@ -23,30 +23,10 @@ get_rotation_matrix, ) -_KERNELS = ( - "_apply_packed_circuit", - "_apply_t_rotation", - "_multiply", - "_simplify", - "_merge_t_axes", - "_equal", - "_remove_row", -) - # A core, π/2 axes, and π/4 axes. _Rotations = tuple[stim.PauliString, list[stim.PauliString], list[stim.PauliString]] -@pytest.fixture(params=[pytest.param("compiled", marks=pytest.mark.compiled), "python"]) -def kernels(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> None: - """Run the compiled kernels, or their Python versions for coverage.""" - if request.param == "python": - for name in _KERNELS: - monkeypatch.setattr( - rotated_pauli_string, name, getattr(rotated_pauli_string, name).py_func - ) - - def _get_rotations_matrix( core: stim.PauliString, s_axes: Sequence[stim.PauliString], @@ -85,7 +65,6 @@ def _get_random_operator(rng: np.random.Generator, num_qubits: int) -> RotatedPa return RotatedPauliString.from_rotations(core, s_axes=s_axes, t_axes=t_axes) -@pytest.mark.usefixtures("kernels") def test_simplification_preserves_operators(rng: np.random.Generator) -> None: for num_qubits in (1, 2, 3): for _ in range(10): @@ -98,7 +77,6 @@ def test_simplification_preserves_operators(rng: np.random.Generator) -> None: ) -@pytest.mark.usefixtures("kernels") def test_simplified_forms() -> None: """Simplification merges and cancels rotations, and drops π/4 rotations about the identity. @@ -122,7 +100,6 @@ def test_simplified_forms() -> None: assert RotatedPauliString("X").is_pauli -@pytest.mark.usefixtures("kernels") def test_propagation_through_circuits(rng: np.random.Generator) -> None: """Random circuits, and a fixed one that reaches every case of propagation. @@ -166,7 +143,6 @@ def test_t_gates_make_cliffords_and_then_non_cliffords() -> None: assert equal_up_to_phase(_get_matrix(twice), expected) -@pytest.mark.usefixtures("kernels") def test_canceling_t_rotations_give_equal_operators() -> None: """A π/4 rotation and its inverse change neither propagated operators nor their hashes.""" circuit = stim.Circuit("H 0\nS[T] 0\nCX 0 1\nSPP_DAG[T] X0*Z1") @@ -212,7 +188,6 @@ def test_clifford_operators_have_canonical_forms(rng: np.random.Generator) -> No assert first != second or hash(first) == hash(second) -@pytest.mark.usefixtures("kernels") def test_products_and_adjoints(rng: np.random.Generator) -> None: """Products and adjoints of a fixed pair of operators, and of random operators.""" pairs = [ From d35a81b45147dd6d63299991ab789170419a14a6 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Thu, 12 Nov 2026 07:33:00 -0500 Subject: [PATCH 2/6] Run the compiled CI runs, and replace the rule for tests of compiled code The coverage check sets QLDPC_DISABLE_COMPILATION and no longer skips tests marked compiled. The compiled CI job runs the test files of the modules with compiled functions, and the distance tests with the set-up code compiled too. The testing standard and the validation commands describe them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/continuous-integration.yaml | 39 ++++++++++++------- checks/coverage_.py | 11 ++++-- docs/source/contributing/index.rst | 15 ++++--- .../source/contributing/standards/testing.rst | 8 ++-- 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index d1f7abfe7..a08d1216a 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -1,7 +1,8 @@ # This workflow checks every push to main and every pull request that is ready for review. Its # jobs lint the code and check its formatting and types, build the documentation, run the fast -# example notebooks, run the tests with the per-module coverage check, and run the tests marked -# compiled. build-changes.yaml runs further checks, such as the installation check, only when +# example notebooks, run the tests with the per-module coverage check, and run tests with compiled +# code. The coverage check runs compiled functions as plain Python, so that coverage can trace +# them. build-changes.yaml runs further checks, such as the installation check, only when # pyproject.toml changes. name: Continuous integration @@ -144,13 +145,23 @@ jobs: --jobs "$(nproc)" --shard "${{ matrix.shard }}/$SHARDS" compiled: - name: Compiled variants of tests + name: ${{ matrix.name }} if: github.event.pull_request.draft == false runs-on: ubuntu-latest timeout-minutes: 5 env: # Keep each test worker to one Numba thread, as checks/pytest_.py does. NUMBA_NUM_THREADS: 1 + strategy: + # Let each run finish when the other fails, so that each reports its own failures. + fail-fast: false + matrix: + # See the options in src/qldpc/conftest.py. + include: + - name: "Compiled run: tests of modules with compiled functions" + args: src/qldpc --compiled-module-tests + - name: "Compiled run: distance tests with compiled set-up code" + args: src/qldpc/codes/distance --compile-search-setup steps: - uses: actions/checkout@v6 - name: Set up Python @@ -159,30 +170,30 @@ jobs: python-version: 3.14 enable-cache: true - name: Install package and dependencies - # No other job runs the tests marked compiled. Install the same extras as the coverage job, - # so that none of these tests is skipped for lack of an optional package. + # Install the same extras as the coverage job, so that no test here is skipped for lack of + # an optional package. run: uv sync --compile-bytecode --extra bliss --extra decoders --extra gap --group ci-test - - name: Run the tests marked compiled + - name: Run the tests # Keep each test file's tests on one worker, so that fewer workers compile the same # functions. run: >- - uv run --extra bliss --extra decoders --extra gap --group ci-test python -m pytest src - -m compiled -n "$(nproc)" --dist=loadfile + uv run --extra bliss --extra decoders --extra gap --group ci-test python -m pytest + ${{ matrix.args }} -n "$(nproc)" --dist=loadfile coverage-result: - # Combine the results of the coverage shards and of the tests marked compiled into one check - # for branch protection to require. Unlike the names of the shards, its name does not depend - # on their number. It runs even when a job fails or the run is cancelled, since GitHub counts a - # skipped required check as passing. + # Combine the results of the coverage shards and of the compiled runs into one check for branch + # protection to require. Unlike the names of the shards, its name does not depend on their + # number. It runs even when a job fails or the run is cancelled, since GitHub counts a skipped + # required check as passing. name: Pytest and coverage check if: always() && github.event.pull_request.draft == false needs: [coverage, compiled] runs-on: ubuntu-latest timeout-minutes: 1 steps: - - name: Check that the coverage shards and the tests marked compiled passed + - name: Check that the coverage shards and the compiled runs passed run: | echo "Result of the coverage shards: ${{ needs.coverage.result }}" - echo "Result of the tests marked compiled: ${{ needs.compiled.result }}" + echo "Result of the compiled runs: ${{ needs.compiled.result }}" test "${{ needs.coverage.result }}" = success test "${{ needs.compiled.result }}" = success diff --git a/checks/coverage_.py b/checks/coverage_.py index 14122f9d0..32fc7c028 100755 --- a/checks/coverage_.py +++ b/checks/coverage_.py @@ -3,9 +3,10 @@ """Run the modular coverage check of checks-superstaq with ``--modular --single-run``. -This wrapper also skips the tests marked ``compiled``, sets ``QLDPC_CLEAR_CACHES`` (see -``src/qldpc/conftest.py``), and fails the check for each selected module under ``src/``, other than -a ``conftest.py`` file, that has no test file. +This wrapper also sets ``QLDPC_DISABLE_COMPILATION``, so that qLDPC's compiled functions run as +plain Python, which coverage can trace (see ``src/qldpc/_compilation.py``). It sets +``QLDPC_CLEAR_CACHES`` (see ``src/qldpc/conftest.py``), and fails the check for each selected module +under ``src/``, other than a ``conftest.py`` file, that has no test file. Until a release of checks-superstaq includes ``--single-run``, this runs copies of its coverage modules in ``checks/_superstaq_coverage``. To switch to such a release: @@ -28,6 +29,7 @@ import pytest_ from _superstaq_coverage import coverage_ +from qldpc._compilation import DISABLE_COMPILATION_VARIABLE from qldpc.conftest import CLEAR_CACHES_VARIABLE check_utils = checks_superstaq.check_utils @@ -40,11 +42,12 @@ # Options of the check, which come before the caller's arguments, so that the caller's own options # replace them. The check runs only *.py tests, not notebooks, so it does not need nbmake. -OPTIONS = ("--modular", "--single-run", "--sysmon", "-pno:nbmake", "-mnot(compiled)") +OPTIONS = ("--modular", "--single-run", "--sysmon", "-pno:nbmake") def run(*args: str) -> int: """Run the check with command-line arguments, and return its exit code.""" + os.environ[DISABLE_COMPILATION_VARIABLE] = "1" os.environ[CLEAR_CACHES_VARIABLE] = "1" # checks-superstaq parses the arguments first, so that it handles -h and usage errors. returncode = coverage_.run(*OPTIONS, *args, exclude=EXCLUDE) diff --git a/docs/source/contributing/index.rst b/docs/source/contributing/index.rst index 6434de35b..ea3b8f991 100644 --- a/docs/source/contributing/index.rst +++ b/docs/source/contributing/index.rst @@ -87,9 +87,11 @@ CI's exact environments and commands are in ``.github/workflows/``. - mypy with the repository's typing settings, over source and tests * - ``python checks/coverage_.py`` - Per-module tests and coverage, as in CI: each module under ``src/`` needs its own test file, which must pass and alone cover 100% of the module's statements. - It skips the tests marked ``compiled``. - * - ``python -m pytest src -m compiled`` - - The tests marked ``compiled`` + It runs compiled functions as plain Python. + * - ``python -m pytest src/qldpc --compiled-module-tests`` + - Compiled run, as in CI: the test files of the modules that define compiled functions, with the always-compiled functions compiled + * - ``python -m pytest src/qldpc/codes/distance --compile-search-setup`` + - Compiled run, as in CI: the distance tests, with the compiled-on-request functions compiled too * - ``python checks/build_docs.py`` - Strict Sphinx and notebook documentation build * - ``python checks/all_.py`` @@ -105,11 +107,11 @@ The third tier is the extended checks. * **As needed:** when writing, reviewing, or verifying a change, run targeted tests that cover it. Select tests by function, or run the test file of a changed module, as in ``python -m pytest src/qldpc/codes/common_test.py::test_name``. Run ``pytest_.py`` only on selected paths, since over the whole repository it largely repeats the test run of ``coverage_.py``. - When targeted tests do not settle a question, such as whether a module's own tests fully cover it, run ``coverage_.py`` on that module and its test file. + When targeted tests do not settle a question, such as whether a module's own tests fully cover it or run fast as plain Python, run ``coverage_.py`` on that module and its test file. For example, ``python checks/coverage_.py src/qldpc/codes/common.py src/qldpc/codes/common_test.py`` checks the coverage of one module and its test file. * **Extended:** the checks that CI runs on every pull request, other than the notebook runs. The extended checks are needed only when the maintainer asks for them by name, or has said that they will be away while a change is completed. - Run the anytime checks, ``coverage_.py`` over the whole repository, the tests marked ``compiled``, and ``build_docs.py``. + Run the anytime checks, ``coverage_.py`` over the whole repository, the two compiled runs, and ``build_docs.py``. After fixing a failure that the change causes, verify the fix with the drafting checks, and rerun the failed checks and every extended check that the fix could affect. Report any other failure, such as one that also occurs on the main branch, as an open decision. Report a failure whose fix needs a decision in the same way. @@ -119,8 +121,9 @@ Refreshing the saved outputs of a changed notebook, as the :doc:`writing standar CI runs on pull requests that are ready for review, not on drafts. It runs the checks that need extra dependencies or platforms, such as the notebook and GAP integration tests, so these need not run locally. -It also runs the tests marked ``compiled``, which ``coverage_.py`` skips. +It also runs the two compiled runs, since ``coverage_.py`` runs compiled functions as plain Python. Run these locally only in the extended checks, or as targeted tests of a change to compiled code. +Other ``pytest`` runs, such as of one focused test, compile the always-compiled functions, and compile the compiled-on-request functions only if given ``--compile-search-setup``. ``all_.py`` runs most checks in one command. It skips the per-module coverage gate and ``pyproject.toml`` formatting. diff --git a/docs/source/contributing/standards/testing.rst b/docs/source/contributing/standards/testing.rst index 4d215b98d..dcceafd7a 100644 --- a/docs/source/contributing/standards/testing.rst +++ b/docs/source/contributing/standards/testing.rst @@ -9,9 +9,11 @@ This standard covers what tests should guard and the conditions under which they The gate runs the selected test files together on parallel pytest workers, and credits a module only with the lines that its own test file runs in the test process, not in subprocesses (see the module ``_coverage_contexts`` of checks-superstaq). Between test files on one worker, it resets the repository's shared fixtures and qLDPC's ``functools`` caches, but not other module-level state. Reach a hard-to-reach branch with a focused test rather than a broad scenario. -* Coverage cannot trace Numba-compiled code, so a test of a module's compiled functions needs a variant that runs their Python versions, as the ``binary_kernels`` fixture in ``src/qldpc/_binary_test.py`` does. - Mark the variant that runs the compiled code, never a whole test, with ``pytest.mark.compiled``. - The coverage gate skips the tests so marked, and a separate CI job runs them. +* The coverage gate runs qLDPC's compiled functions as plain Python, so that coverage can trace them (see ``src/qldpc/_compilation.py``). + So size tests of compiled code to run fast as plain Python. + CI also runs some test files with compiled code, in the compiled runs of the :ref:`validation commands `. + Put a test of behavior that only compiled code has in a test file that a compiled run selects, unless the test compiles its own functions with ``numba.njit``, or switches compilation back on, as ``src/qldpc/_compilation_test.py`` does. + Such a test also runs as plain Python in the coverage gate, where it must pass and run every line. * Tests run with network sockets disabled. Mock only external boundaries such as web, subprocess, clipboard, prompt, and optional-package paths, or a module's own thin wrappers around them, never the logic under test, and do not add a live-service test. * Disk caches are bypassed while pytest is imported, so tests must not depend on cache persistence. From 2fffa7638cbee71b58a63bd629e5848632f65a03 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Thu, 12 Nov 2026 08:05:15 -0500 Subject: [PATCH 3/6] Clarify the compiled-run wording, and require compiled-on-request functions to stay in distance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/qldpc/_compilation.py | 8 ++++---- src/qldpc/codes/distance/README.md | 2 +- src/qldpc/codes/distance/_counting/scan_test.py | 6 +++--- .../codes/distance/_search/completion_bounds_test.py | 4 ++-- src/qldpc/conftest.py | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/qldpc/_compilation.py b/src/qldpc/_compilation.py index 5fa62abd3..ae216ce20 100644 --- a/src/qldpc/_compilation.py +++ b/src/qldpc/_compilation.py @@ -49,8 +49,8 @@ compiling them a one-time cost. Use :func:`compiled_on_request` for the larger functions that orchestrate a search, which select and bound its information sets, build its basis families, and drive its scan. Compiling them pays off only in a program that makes very many searches. Define -them only in :mod:`qldpc.codes.distance`, since CI compiles them only in a run of its tests. A -function that is neither runs as ordinary Python. A small always-compiled helper that hot loops +them only in :mod:`qldpc.codes.distance`, whose tests CI runs with them compiled. A function that +is neither runs as ordinary Python. A small always-compiled helper that hot loops call can be marked ``inline=True`` (see :func:`always_compiled`). A compiled-on-request function may call always-compiled and compiled-on-request functions. An always-compiled function may call only always-compiled functions, since it is compiled and cached by itself. @@ -103,8 +103,8 @@ class _NumbaConfig(Protocol): # Numba creates the attributes of its settings dynamically, which type checkers cannot see. _numba_config = cast(_NumbaConfig, config) -# If this environment variable is "1" when qLDPC is imported, the functions that numba would compile -# run as ordinary Python (see the module docstring). +# If this environment variable is "1" when qLDPC is imported, qLDPC's compiled functions run as +# ordinary Python (see the module docstring). DISABLE_COMPILATION_VARIABLE = "QLDPC_DISABLE_COMPILATION" _DISABLED_BY_VARIABLE = os.environ.get(DISABLE_COMPILATION_VARIABLE) == "1" diff --git a/src/qldpc/codes/distance/README.md b/src/qldpc/codes/distance/README.md index 408556ba7..9aa1fc7c6 100644 --- a/src/qldpc/codes/distance/README.md +++ b/src/qldpc/codes/distance/README.md @@ -133,7 +133,7 @@ It affects only qLDPC, whereas `NUMBA_DISABLE_JIT=1` also runs other packages th `python checks/coverage_.py src/qldpc/codes/distance/.py` checks one module's coverage. The validation commands in `docs/source/contributing/index.rst` list the other checks. - `conftest.py` holds the fixtures and helpers that several test files share, such as a check of counts against brute-force enumeration. -- Coverage cannot see compiled code, so the coverage check runs compiled functions as plain Python, and CI runs these tests again with them compiled (see `docs/source/contributing/standards/testing.rst`). +- Coverage cannot trace compiled code, so the coverage check runs compiled functions as plain Python, and CI runs these tests again with them compiled (see `docs/source/contributing/standards/testing.rst`). - `python -m pytest src/qldpc/codes/distance --compile-search-setup` runs the tests with the set-up code of searches compiled. Tests that replace a compiled-on-request function or a function that one calls, or that compare plain with compiled set-up code, request the `plain_search_setup` fixture of `conftest.py`, whose docstring says why. - `readme_test.py` fails if this page omits a module, or names a file that does not exist. diff --git a/src/qldpc/codes/distance/_counting/scan_test.py b/src/qldpc/codes/distance/_counting/scan_test.py index 243324369..ef76d0efe 100644 --- a/src/qldpc/codes/distance/_counting/scan_test.py +++ b/src/qldpc/codes/distance/_counting/scan_test.py @@ -131,9 +131,9 @@ def record(*args: Any) -> information_sets.BasisFamily: def test_counts_with_tiny_chunks_and_forced_tables(tune: Tune) -> None: """Counts by every method are exact with tiny chunks and forced completion tables. - Tiny chunks make every scan stop and resume repeatedly, and forced completion tables make the - steps of two or more rows use completion tables. The counters include deformed ones, and a - count above the distance raises an error. + Tiny chunks make every scan stop and resume repeatedly, and forcing completion tables makes + every step of two or more rows use them. The counters include deformed ones, and a count of + full Paulis above the distance raises an error. """ tune(scan_chunk_size=1, **FORCED_TABLES) code = get_random_css_code(np.random.default_rng(3), 14, 4, 4) diff --git a/src/qldpc/codes/distance/_search/completion_bounds_test.py b/src/qldpc/codes/distance/_search/completion_bounds_test.py index 8865bfbfe..2f05ad88a 100644 --- a/src/qldpc/codes/distance/_search/completion_bounds_test.py +++ b/src/qldpc/codes/distance/_search/completion_bounds_test.py @@ -83,8 +83,8 @@ def test_bounds_are_exact_relaxations(pytestconfig: pytest.Config) -> None: for num_rows, num_columns in [(2, 1), (3, 5), (6, 21), (7, 70)]: tails = get_random_tails(rng, num_rows, num_columns) rows = pack_bits(tails) - # the default width of 16 columns, and narrower ones, to check that merging never weakens a - # bound; 16-column tables of the longest tails are slow to build in plain Python + # The default width of 16 columns, and narrower ones. 16-column tables are slow to build + # as plain Python for 64 or more columns. widths = (4, 8, 16) if num_columns < 64 else (4, 8) all_tables = {bits: cb.CompletionTables.build(rows, bits) for bits in widths} for first, _ in itertools.product(range(num_rows - 1), range(6)): diff --git a/src/qldpc/conftest.py b/src/qldpc/conftest.py index 15e33b62c..282f4dd2a 100644 --- a/src/qldpc/conftest.py +++ b/src/qldpc/conftest.py @@ -240,14 +240,14 @@ def wedderburn_artin_rings_to_test( #################################################################################################### -# runs with compiled code +# compiled runs # The decorators of qldpc._compilation that make numba compile a function. _COMPILING_DECORATORS = frozenset({"always_compiled", "compiled_on_request"}) def pytest_addoption(parser: pytest.Parser) -> None: - """Add the options of CI's compiled runs (see docs/source/contributing/index.rst).""" + """Add the options of the compiled runs (see docs/source/contributing/index.rst).""" # Explicit defaults let pytest set these options when it loads this file after reading the # command line, as in a run of ``pytest src``. parser.addoption( @@ -267,7 +267,7 @@ def pytest_addoption(parser: pytest.Parser) -> None: def pytest_configure(config: pytest.Config) -> None: - """Refuse the options of runs with compiled code while compilation is disabled.""" + """Refuse the options of the compiled runs while compilation is disabled.""" options = [ option for option in ("--compiled-module-tests", "--compile-search-setup") From d7bc699fd2b7982ab55c9f0475f305fc241c3aae Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Thu, 12 Nov 2026 08:18:05 -0500 Subject: [PATCH 4/6] Point to the compilation tests rather than count their rules, and refill a docstring Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/qldpc/_compilation.py | 8 ++++---- src/qldpc/codes/distance/README.md | 7 +------ src/qldpc/codes/distance/_counting/scan_test.py | 8 ++++---- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/qldpc/_compilation.py b/src/qldpc/_compilation.py index ae216ce20..852ed7811 100644 --- a/src/qldpc/_compilation.py +++ b/src/qldpc/_compilation.py @@ -50,10 +50,10 @@ orchestrate a search, which select and bound its information sets, build its basis families, and drive its scan. Compiling them pays off only in a program that makes very many searches. Define them only in :mod:`qldpc.codes.distance`, whose tests CI runs with them compiled. A function that -is neither runs as ordinary Python. A small always-compiled helper that hot loops -call can be marked ``inline=True`` (see :func:`always_compiled`). A compiled-on-request function -may call always-compiled and compiled-on-request functions. An always-compiled function may call -only always-compiled functions, since it is compiled and cached by itself. +is neither runs as ordinary Python. A small always-compiled helper that hot loops call can be +marked ``inline=True`` (see :func:`always_compiled`). A compiled-on-request function may call +always-compiled and compiled-on-request functions. An always-compiled function may call only +always-compiled functions, since it is compiled and cached by itself. If the environment variable ``QLDPC_DISABLE_COMPILATION`` is ``1`` when qLDPC is imported, every function of both kinds runs as ordinary Python, much more slowly. :func:`always_compiled` then diff --git a/src/qldpc/codes/distance/README.md b/src/qldpc/codes/distance/README.md index 9aa1fc7c6..1cbe89d7a 100644 --- a/src/qldpc/codes/distance/README.md +++ b/src/qldpc/codes/distance/README.md @@ -116,12 +116,7 @@ Two decorators in `src/qldpc/_compilation.py` mark them, and its docstring says - `@compiled_on_request` marks parts of the set-up in `_search/information_sets.py` and `_counting/scan.py`, which run as ordinary Python unless a program calls `compile_search_setup()`. Numba looks up the functions that compiled code calls when it compiles it, so compiled code may call NumPy and compiled functions, but not plain Python functions of qLDPC. -Tests in `src/qldpc/_compilation_test.py` check four further rules, and the name of a failing test says which one was broken: - -- Only `src/qldpc/_compilation.py` asks numba for a disk cache, so use `@always_compiled`, not `@njit(cache=True)`. -- An always-compiled function may not call a compiled-on-request one. -- Always-compiled code may use nothing from outside qLDPC except NumPy and numba, because the hash covers only qLDPC's source. -- Other modules must call a compiled-on-request function through its module, and never import it by name. +Tests in `src/qldpc/_compilation_test.py` check further rules, and the name of a failing test says which one was broken. Set `QLDPC_DISABLE_COMPILATION=1` before importing qLDPC to run all of this code as plain Python while debugging. It affects only qLDPC, whereas `NUMBA_DISABLE_JIT=1` also runs other packages that use numba, such as galois, as plain Python, which makes them slow too. diff --git a/src/qldpc/codes/distance/_counting/scan_test.py b/src/qldpc/codes/distance/_counting/scan_test.py index ef76d0efe..17a691230 100644 --- a/src/qldpc/codes/distance/_counting/scan_test.py +++ b/src/qldpc/codes/distance/_counting/scan_test.py @@ -129,11 +129,11 @@ def record(*args: Any) -> information_sets.BasisFamily: def test_counts_with_tiny_chunks_and_forced_tables(tune: Tune) -> None: - """Counts by every method are exact with tiny chunks and forced completion tables. + """Counts are exact with tiny chunks and forced completion tables. - Tiny chunks make every scan stop and resume repeatedly, and forcing completion tables makes - every step of two or more rows use them. The counters include deformed ones, and a count of - full Paulis above the distance raises an error. + Tiny chunks make every scan stop and resume repeatedly, and scans use completion tables wherever + they can. The counters include deformed ones, and a count of full Paulis above the distance + raises an error. """ tune(scan_chunk_size=1, **FORCED_TABLES) code = get_random_css_code(np.random.default_rng(3), 14, 4, 4) From 45f15c5560ff181f49bdc4f8984fd40b499ce2f2 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Thu, 12 Nov 2026 08:26:24 -0500 Subject: [PATCH 5/6] Claim no more than the tiny-chunk count test checks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/qldpc/codes/distance/_counting/scan_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qldpc/codes/distance/_counting/scan_test.py b/src/qldpc/codes/distance/_counting/scan_test.py index 17a691230..a778c1cac 100644 --- a/src/qldpc/codes/distance/_counting/scan_test.py +++ b/src/qldpc/codes/distance/_counting/scan_test.py @@ -131,9 +131,9 @@ def record(*args: Any) -> information_sets.BasisFamily: def test_counts_with_tiny_chunks_and_forced_tables(tune: Tune) -> None: """Counts are exact with tiny chunks and forced completion tables. - Tiny chunks make every scan stop and resume repeatedly, and scans use completion tables wherever - they can. The counters include deformed ones, and a count of full Paulis above the distance - raises an error. + Tiny chunks make scans stop and resume repeatedly, and scans use completion tables wherever they + can. The counters include deformed ones, and a count of full Paulis above the distance raises + an error. """ tune(scan_chunk_size=1, **FORCED_TABLES) code = get_random_css_code(np.random.default_rng(3), 14, 4, 4) From 8e00df2446e9aaf0add68f00804f273c74a18b9f Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Thu, 12 Nov 2026 08:31:23 -0500 Subject: [PATCH 6/6] Name the compiled runs consistently, and test that compiled-on-request functions stay in distance The testing standard names the compiled runs before using the term, and exempts only tests that compile their own functions even in the coverage gate. A test requires every compiled-on-request function to be defined in the distance package, whose tests CI runs with them compiled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/continuous-integration.yaml | 12 ++++++------ docs/source/contributing/index.rst | 7 +++---- docs/source/contributing/standards/testing.rst | 9 ++++----- src/qldpc/_compilation_test.py | 16 ++++++++++++++-- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/.github/workflows/continuous-integration.yaml b/.github/workflows/continuous-integration.yaml index a08d1216a..029baf910 100644 --- a/.github/workflows/continuous-integration.yaml +++ b/.github/workflows/continuous-integration.yaml @@ -1,9 +1,9 @@ -# This workflow checks every push to main and every pull request that is ready for review. Its -# jobs lint the code and check its formatting and types, build the documentation, run the fast -# example notebooks, run the tests with the per-module coverage check, and run tests with compiled -# code. The coverage check runs compiled functions as plain Python, so that coverage can trace -# them. build-changes.yaml runs further checks, such as the installation check, only when -# pyproject.toml changes. +# This workflow checks every push to main and every pull request that is ready for review. Its jobs +# lint the code and check its formatting and types, build the documentation, run the fast example +# notebooks, run the tests with the per-module coverage check, and make the compiled runs. The +# coverage check runs compiled functions as plain Python, so that coverage can trace them. +# build-changes.yaml runs further checks, such as the installation check, only when pyproject.toml +# changes. name: Continuous integration diff --git a/docs/source/contributing/index.rst b/docs/source/contributing/index.rst index ea3b8f991..4b33bdc8b 100644 --- a/docs/source/contributing/index.rst +++ b/docs/source/contributing/index.rst @@ -107,11 +107,11 @@ The third tier is the extended checks. * **As needed:** when writing, reviewing, or verifying a change, run targeted tests that cover it. Select tests by function, or run the test file of a changed module, as in ``python -m pytest src/qldpc/codes/common_test.py::test_name``. Run ``pytest_.py`` only on selected paths, since over the whole repository it largely repeats the test run of ``coverage_.py``. - When targeted tests do not settle a question, such as whether a module's own tests fully cover it or run fast as plain Python, run ``coverage_.py`` on that module and its test file. + When targeted tests do not settle a question, such as whether a module's own tests fully cover it, or pass and run fast as plain Python, run ``coverage_.py`` on that module and its test file. For example, ``python checks/coverage_.py src/qldpc/codes/common.py src/qldpc/codes/common_test.py`` checks the coverage of one module and its test file. * **Extended:** the checks that CI runs on every pull request, other than the notebook runs. The extended checks are needed only when the maintainer asks for them by name, or has said that they will be away while a change is completed. - Run the anytime checks, ``coverage_.py`` over the whole repository, the two compiled runs, and ``build_docs.py``. + Run the anytime checks, ``coverage_.py`` over the whole repository, the compiled runs, and ``build_docs.py``. After fixing a failure that the change causes, verify the fix with the drafting checks, and rerun the failed checks and every extended check that the fix could affect. Report any other failure, such as one that also occurs on the main branch, as an open decision. Report a failure whose fix needs a decision in the same way. @@ -121,9 +121,8 @@ Refreshing the saved outputs of a changed notebook, as the :doc:`writing standar CI runs on pull requests that are ready for review, not on drafts. It runs the checks that need extra dependencies or platforms, such as the notebook and GAP integration tests, so these need not run locally. -It also runs the two compiled runs, since ``coverage_.py`` runs compiled functions as plain Python. +It also makes the compiled runs. Run these locally only in the extended checks, or as targeted tests of a change to compiled code. -Other ``pytest`` runs, such as of one focused test, compile the always-compiled functions, and compile the compiled-on-request functions only if given ``--compile-search-setup``. ``all_.py`` runs most checks in one command. It skips the per-module coverage gate and ``pyproject.toml`` formatting. diff --git a/docs/source/contributing/standards/testing.rst b/docs/source/contributing/standards/testing.rst index dcceafd7a..7a3af9caa 100644 --- a/docs/source/contributing/standards/testing.rst +++ b/docs/source/contributing/standards/testing.rst @@ -9,11 +9,10 @@ This standard covers what tests should guard and the conditions under which they The gate runs the selected test files together on parallel pytest workers, and credits a module only with the lines that its own test file runs in the test process, not in subprocesses (see the module ``_coverage_contexts`` of checks-superstaq). Between test files on one worker, it resets the repository's shared fixtures and qLDPC's ``functools`` caches, but not other module-level state. Reach a hard-to-reach branch with a focused test rather than a broad scenario. -* The coverage gate runs qLDPC's compiled functions as plain Python, so that coverage can trace them (see ``src/qldpc/_compilation.py``). - So size tests of compiled code to run fast as plain Python. - CI also runs some test files with compiled code, in the compiled runs of the :ref:`validation commands `. - Put a test of behavior that only compiled code has in a test file that a compiled run selects, unless the test compiles its own functions with ``numba.njit``, or switches compilation back on, as ``src/qldpc/_compilation_test.py`` does. - Such a test also runs as plain Python in the coverage gate, where it must pass and run every line. +* The coverage gate runs qLDPC's compiled functions as plain Python, so that coverage can trace them. + So every test must be fast as plain Python. + CI also runs some test files with these functions compiled, in the compiled runs of the :ref:`validation commands `. + A test of behavior that only compiled code has must also pass as plain Python, and must be in a test file that a compiled run selects, unless it compiles its own functions even in the coverage gate, as ``src/qldpc/_compilation_test.py`` does. * Tests run with network sockets disabled. Mock only external boundaries such as web, subprocess, clipboard, prompt, and optional-package paths, or a module's own thin wrappers around them, never the logic under test, and do not add a live-service test. * Disk caches are bypassed while pytest is imported, so tests must not depend on cache persistence. diff --git a/src/qldpc/_compilation_test.py b/src/qldpc/_compilation_test.py index c1df7a875..801d119cc 100644 --- a/src/qldpc/_compilation_test.py +++ b/src/qldpc/_compilation_test.py @@ -3,8 +3,7 @@ """Tests for compiled functions, and for running their tests compiled or as plain Python. They cover the disk cache, the registry of compiled-on-request functions, the switch to plain -Python, the options of the test runs with compiled code, and the rules that make compiled functions -safe. +Python, the options of the compiled runs, and the rules for compiled functions. """ from __future__ import annotations @@ -448,6 +447,19 @@ def test_compiled_on_request_functions_are_not_imported_by_name() -> None: assert not imported, f"{path.name} imports {imported} by name" +def test_compiled_on_request_functions_are_defined_in_the_distance_package() -> None: + """CI runs only the distance tests with compiled-on-request functions compiled.""" + distance_package = Path(qldpc.__file__).parent / "codes" / "distance" + paths = [ + path + for path, tree in _get_source_trees() + if _get_functions_decorated_by(tree, "compiled_on_request") + ] + assert paths + outside = [path for path in paths if not path.is_relative_to(distance_package)] + assert not outside, outside + + def test_always_compiled_functions_use_nothing_from_outside_qldpc() -> None: """Only qLDPC's source is hashed, but numba checks its version and implements NumPy itself.""" for path, tree in _get_source_trees():