From 56ce99048cf1a94df585ae691199fd5d71d45b2a Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Thu, 12 Nov 2026 10:07:57 -0500 Subject: [PATCH 1/2] Re-tune .test_durations from a CI run of the whole coverage check The durations come from the "Store test durations" workflow, run on main, and balance the coverage shards now that the coverage check runs qLDPC's compiled functions as plain Python. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .test_durations | 246 ++++++++++++++++++++++++------------------------ 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/.test_durations b/.test_durations index 14f794e70..955efda3b 100644 --- a/.test_durations +++ b/.test_durations @@ -1,125 +1,125 @@ { - "src/qldpc/_binary_test.py": 0.3840937230000492, - "src/qldpc/_compilation_test.py": 18.410674168999982, - "src/qldpc/_deprecations/core_test.py": 2.693219572000018, - "src/qldpc/_deprecations/decoders_test.py": 2.020075918999936, - "src/qldpc/_deprecations/lookup_test.py": 0.05361337400002242, - "src/qldpc/_deprecations/noise_model_test.py": 0.026130230000021015, - "src/qldpc/_utils_test.py": 0.030265607999965027, - "src/qldpc/abstract/_coefficients_test.py": 1.2923382780000168, - "src/qldpc/abstract/_howell_test.py": 13.091316581000015, - "src/qldpc/abstract/groups_test.py": 22.886419415000013, - "src/qldpc/abstract/linalg_test.py": 25.89832450999998, - "src/qldpc/abstract/matrix_rings_test.py": 0.27769390799998916, - "src/qldpc/abstract/monomials_test.py": 0.04283704399999522, - "src/qldpc/abstract/ring_array_test.py": 17.81751324000001, - "src/qldpc/abstract/rings_test.py": 47.02450449299998, - "src/qldpc/abstract/wedderburn_artin_test.py": 41.85797762399993, - "src/qldpc/cache_test.py": 0.02743295099996601, - "src/qldpc/circuits/_post_selection_test.py": 0.011836176000002752, - "src/qldpc/circuits/benchmarking/logical_errors_test.py": 0.04567747399997302, - "src/qldpc/circuits/benchmarking/state_prep_diagnostics_test.py": 0.5136233000000061, - "src/qldpc/circuits/bookkeeping_test.py": 0.026130589999979748, - "src/qldpc/circuits/certification/_fault_search_test.py": 2.3318079309999646, - "src/qldpc/circuits/certification/fault_distance_test.py": 0.08515223099999503, - "src/qldpc/circuits/certification/fault_propagation_test.py": 23.90419024800005, - "src/qldpc/circuits/common_test.py": 0.03691633499998659, - "src/qldpc/circuits/encoding_test.py": 0.4398325739999791, - "src/qldpc/circuits/layering_test.py": 0.10981092699999806, - "src/qldpc/circuits/memory/alpha_syndrome_test.py": 5.026541029000029, - "src/qldpc/circuits/memory/memory_test.py": 0.3172731000000084, - "src/qldpc/circuits/memory/syndrome_measurement_test.py": 0.36430565700001694, - "src/qldpc/circuits/noise_model/_operations_test.py": 0.03668986500004223, - "src/qldpc/circuits/noise_model/channels_test.py": 0.27920965399997044, - "src/qldpc/circuits/noise_model/noise_model_test.py": 0.4462039290000632, - "src/qldpc/circuits/noise_model/rules_test.py": 0.05794653299999197, - "src/qldpc/circuits/noise_model/scheduling_test.py": 0.11403342999999211, - "src/qldpc/circuits/transforms/qubit_remapping_test.py": 0.022409612999993556, - "src/qldpc/circuits/transforms/transpilation_cz_test.py": 0.8660386040000105, - "src/qldpc/circuits/transversal_test.py": 1.7736309930000118, - "src/qldpc/code_style_test.py": 1.4603208789999087, - "src/qldpc/codes/_code_capacity_test.py": 0.35854958799998826, - "src/qldpc/codes/classical_test.py": 12.01613595400002, - "src/qldpc/codes/common_test.py": 38.93772880200011, - "src/qldpc/codes/distance/_binary_test.py": 0.09660154199994508, - "src/qldpc/codes/distance/_counting/local_cliffords_test.py": 1.762009051000021, - "src/qldpc/codes/distance/_counting/macwilliams_test.py": 0.04758126199999424, - "src/qldpc/codes/distance/_counting/scan_test.py": 14.401316051000073, - "src/qldpc/codes/distance/_search/affine_instance_test.py": 0.5022259669999869, - "src/qldpc/codes/distance/_search/affine_search_test.py": 1.9361046999999445, - "src/qldpc/codes/distance/_search/automorphism_families_test.py": 10.195362701000008, - "src/qldpc/codes/distance/_search/brouwer_zimmermann_test.py": 28.39258421600009, - "src/qldpc/codes/distance/_search/brute_force_test.py": 0.5622239110000038, - "src/qldpc/codes/distance/_search/completion_bounds_test.py": 0.9394132229999883, - "src/qldpc/codes/distance/_search/connected_support_test.py": 29.579610028000012, - "src/qldpc/codes/distance/_search/coset_leader_search_test.py": 77.95609898599989, - "src/qldpc/codes/distance/_search/coset_routes_test.py": 0.8368327989999784, - "src/qldpc/codes/distance/_search/information_sets_test.py": 38.329937457, - "src/qldpc/codes/distance/_search/matroid_partition_test.py": 0.10772970700000428, - "src/qldpc/codes/distance/_tuning_test.py": 0.7197705740000231, - "src/qldpc/codes/distance/core_test.py": 0.38586432900000034, - "src/qldpc/codes/distance/layering_test.py": 0.07043381299999396, - "src/qldpc/codes/distance/logical_counts_test.py": 71.41953304300003, - "src/qldpc/codes/distance/readme_test.py": 0.033556835999988266, - "src/qldpc/codes/monte_carlo_test.py": 0.1355664230000002, - "src/qldpc/codes/quantum/balanced_product_test.py": 0.11340090600002384, - "src/qldpc/codes/quantum/hypergraph_product_test.py": 8.009336041999958, - "src/qldpc/codes/quantum/lifted_product_test.py": 34.9678463690001, - "src/qldpc/codes/quantum/named_test.py": 0.3531235929999781, - "src/qldpc/codes/quantum/pair_partition_test.py": 2.576791806999978, - "src/qldpc/codes/quantum/quantum_tanner_test.py": 6.586561005999982, - "src/qldpc/codes/quantum/self_orthogonal_test.py": 0.24026798099998814, - "src/qldpc/codes/quantum/surface_test.py": 4.463069235999981, - "src/qldpc/codes/quantum/trivial_test.py": 0.07923908199998664, - "src/qldpc/codes/quantum/two_block_test.py": 4.571120583999978, - "src/qldpc/contributor_docs_test.py": 0.03398326700002485, - "src/qldpc/decoders/_capabilities_test.py": 0.02100946700001316, - "src/qldpc/decoders/_resolution_test.py": 0.6021752380000152, - "src/qldpc/decoders/adapters/_observable_decoders_test.py": 0.023889726000035694, - "src/qldpc/decoders/adapters/error_decoders_test.py": 0.03142626199998233, - "src/qldpc/decoders/common_test.py": 0.05947234600002105, - "src/qldpc/decoders/construction/factories_test.py": 0.036607213000010574, - "src/qldpc/decoders/construction/specs_test.py": 0.509510149000036, - "src/qldpc/decoders/custom/_lookup_enumeration_test.py": 0.22315531399996758, - "src/qldpc/decoders/custom/composition_test.py": 0.06568813300000897, - "src/qldpc/decoders/custom/guf_test.py": 0.117813190999982, - "src/qldpc/decoders/custom/ilp_test.py": 1.0856736909999682, - "src/qldpc/decoders/custom/lookup_test.py": 10.462976353999977, - "src/qldpc/decoders/dems_test.py": 0.17943274200000303, - "src/qldpc/decoders/external/frontier_test.py": 0.06548490200000856, - "src/qldpc/decoders/external/ldpc_test.py": 0.15771036799998228, - "src/qldpc/decoders/external/pymatching_test.py": 0.14491510199998459, - "src/qldpc/decoders/external/relay_bp_test.py": 0.33674270900002057, - "src/qldpc/decoders/external/tesseract_test.py": 2.759481020999985, - "src/qldpc/decoders/layering_test.py": 0.1417268670000027, - "src/qldpc/decoders/legacy_api_test.py": 0.05432614499999033, - "src/qldpc/decoders/protocols_test.py": 0.04389182600000652, - "src/qldpc/decoders/sinter/core_test.py": 0.26316626699998835, - "src/qldpc/decoders/sinter/subgraph_test.py": 0.4781192530000027, - "src/qldpc/decoders/sinter/window_test.py": 0.34938739900002247, - "src/qldpc/experimental/circuits/benchmarking/consumer_profiles_test.py": 0.03273971999996661, - "src/qldpc/experimental/circuits/benchmarking/consumers_test.py": 4.811712548000017, - "src/qldpc/experimental/circuits/benchmarking/residual_weights_test.py": 13.621699300000053, - "src/qldpc/experimental/circuits/transforms/deferred_measurement_test.py": 1.5713328720000135, - "src/qldpc/experimental/objects/_packed_circuits_test.py": 0.1790262060000316, - "src/qldpc/experimental/objects/_packed_paulis_test.py": 0.1299114530000196, - "src/qldpc/experimental/objects/_symplectic_test.py": 9.095334775000012, - "src/qldpc/experimental/objects/rotated_pauli_string_test.py": 29.386113379000008, - "src/qldpc/experimental/surgery/_compiler_test.py": 1.5799820289999786, - "src/qldpc/experimental/surgery/bridge_test.py": 5.407987394999964, - "src/qldpc/experimental/surgery/cheeger_test.py": 2.6428626300000033, - "src/qldpc/experimental/surgery/circuit_test.py": 20.7198455999999, - "src/qldpc/experimental/surgery/construction_test.py": 0.15132828199996595, - "src/qldpc/experimental/surgery/gadget_test.py": 5.202850378000008, - "src/qldpc/external/_bliss_test.py": 1.800296731000003, - "src/qldpc/external/_codes_test.py": 0.215958827999998, - "src/qldpc/external/_groups_test.py": 0.08347137799997029, - "src/qldpc/external/gap_test.py": 0.08948482499999955, - "src/qldpc/external/sqetch_test.py": 0.1596427279999233, - "src/qldpc/import_test.py": 2.523732501999987, - "src/qldpc/math_test.py": 4.619417182999882, - "src/qldpc/objects/_symmetries_test.py": 0.013803770000009763, - "src/qldpc/objects/complexes_test.py": 3.908042248000015, - "src/qldpc/objects/paulis_test.py": 0.014633334999999192, - "src/qldpc/objects/tanner_graphs_test.py": 0.026749089000006165 + "src/qldpc/_binary_test.py": 0.5847472119999964, + "src/qldpc/_code_style_test.py": 1.2970039760000276, + "src/qldpc/_compilation_test.py": 32.62183307100004, + "src/qldpc/_deprecations/core_test.py": 1.6302075679999177, + "src/qldpc/_deprecations/decoders_test.py": 2.4254841480000096, + "src/qldpc/_deprecations/lookup_test.py": 0.05008923599996251, + "src/qldpc/_deprecations/noise_model_test.py": 0.021064259000013408, + "src/qldpc/_utils_test.py": 0.041464308000030314, + "src/qldpc/abstract/_coefficients_test.py": 12.289768151000004, + "src/qldpc/abstract/_howell_test.py": 15.381788098999976, + "src/qldpc/abstract/groups_test.py": 14.827298233000036, + "src/qldpc/abstract/linalg_test.py": 2.8614883119999774, + "src/qldpc/abstract/matrix_rings_test.py": 0.27819685400001504, + "src/qldpc/abstract/monomials_test.py": 0.03522722699995029, + "src/qldpc/abstract/ring_array_test.py": 0.6873900900001786, + "src/qldpc/abstract/rings_test.py": 7.774672811000016, + "src/qldpc/abstract/wedderburn_artin_test.py": 53.31887763699997, + "src/qldpc/cache_test.py": 0.0329175039999825, + "src/qldpc/circuits/_post_selection_test.py": 0.02029945200001748, + "src/qldpc/circuits/benchmarking/logical_errors_test.py": 0.046082980000051066, + "src/qldpc/circuits/benchmarking/state_prep_diagnostics_test.py": 0.43635157999997887, + "src/qldpc/circuits/bookkeeping_test.py": 0.031090200000022605, + "src/qldpc/circuits/certification/_fault_search_test.py": 4.1709775200000365, + "src/qldpc/circuits/certification/fault_distance_test.py": 0.08055184099995927, + "src/qldpc/circuits/certification/fault_propagation_test.py": 0.7093239749999753, + "src/qldpc/circuits/common_test.py": 0.05251393899993673, + "src/qldpc/circuits/encoding_test.py": 0.32327242199997386, + "src/qldpc/circuits/layering_test.py": 0.1951993630000004, + "src/qldpc/circuits/memory/alpha_syndrome_test.py": 3.5666344239999717, + "src/qldpc/circuits/memory/memory_test.py": 0.23330260700004146, + "src/qldpc/circuits/memory/syndrome_measurement_test.py": 0.4938332309999396, + "src/qldpc/circuits/noise_model/_operations_test.py": 0.03370235200003435, + "src/qldpc/circuits/noise_model/channels_test.py": 0.1807790380000256, + "src/qldpc/circuits/noise_model/noise_model_test.py": 0.4076538489999564, + "src/qldpc/circuits/noise_model/rules_test.py": 0.06359271399983868, + "src/qldpc/circuits/noise_model/scheduling_test.py": 0.18126575699983505, + "src/qldpc/circuits/transforms/qubit_remapping_test.py": 0.034213070000049584, + "src/qldpc/circuits/transforms/transpilation_cz_test.py": 0.7634595939998974, + "src/qldpc/circuits/transversal_test.py": 1.310085418, + "src/qldpc/codes/_code_capacity_test.py": 0.22198324599986563, + "src/qldpc/codes/classical_test.py": 15.391275937000017, + "src/qldpc/codes/common_test.py": 26.784854826999933, + "src/qldpc/codes/distance/_binary_test.py": 0.08285654599998793, + "src/qldpc/codes/distance/_counting/local_cliffords_test.py": 0.08459483900003306, + "src/qldpc/codes/distance/_counting/macwilliams_test.py": 0.03776247800001897, + "src/qldpc/codes/distance/_counting/scan_test.py": 43.60956116600019, + "src/qldpc/codes/distance/_search/affine_instance_test.py": 0.3076261809999892, + "src/qldpc/codes/distance/_search/affine_search_test.py": 1.9724498190000475, + "src/qldpc/codes/distance/_search/automorphism_families_test.py": 7.35761096899995, + "src/qldpc/codes/distance/_search/brouwer_zimmermann_test.py": 39.782575755999986, + "src/qldpc/codes/distance/_search/brute_force_test.py": 0.4518073669998728, + "src/qldpc/codes/distance/_search/completion_bounds_test.py": 13.372170476999997, + "src/qldpc/codes/distance/_search/connected_support_test.py": 11.082601886000006, + "src/qldpc/codes/distance/_search/coset_leader_search_test.py": 65.32570762699997, + "src/qldpc/codes/distance/_search/coset_routes_test.py": 1.1687280330001215, + "src/qldpc/codes/distance/_search/information_sets_test.py": 5.882419345000017, + "src/qldpc/codes/distance/_search/matroid_partition_test.py": 0.2451248269999553, + "src/qldpc/codes/distance/_tuning_test.py": 3.0101151249999702, + "src/qldpc/codes/distance/core_test.py": 0.44284194900006923, + "src/qldpc/codes/distance/layering_test.py": 0.14614034799998876, + "src/qldpc/codes/distance/logical_counts_test.py": 16.569209497999935, + "src/qldpc/codes/distance/readme_test.py": 0.04052554900002292, + "src/qldpc/codes/monte_carlo_test.py": 0.45003609299999425, + "src/qldpc/codes/quantum/balanced_product_test.py": 0.12326273499999729, + "src/qldpc/codes/quantum/hypergraph_product_test.py": 7.302948340000285, + "src/qldpc/codes/quantum/lifted_product_test.py": 24.13757928699991, + "src/qldpc/codes/quantum/named_test.py": 0.2790796120000323, + "src/qldpc/codes/quantum/pair_partition_test.py": 1.9909207680000236, + "src/qldpc/codes/quantum/quantum_tanner_test.py": 5.555577377999981, + "src/qldpc/codes/quantum/self_orthogonal_test.py": 0.39037559099989494, + "src/qldpc/codes/quantum/surface_test.py": 2.8186013440000295, + "src/qldpc/codes/quantum/trivial_test.py": 0.03456062500001167, + "src/qldpc/codes/quantum/two_block_test.py": 3.2037718950000453, + "src/qldpc/contributor_docs_test.py": 0.04961218799996914, + "src/qldpc/decoders/_capabilities_test.py": 0.03417486600002917, + "src/qldpc/decoders/_resolution_test.py": 1.023169547000009, + "src/qldpc/decoders/adapters/_observable_decoders_test.py": 0.025017315000013696, + "src/qldpc/decoders/adapters/error_decoders_test.py": 0.04913262999997414, + "src/qldpc/decoders/common_test.py": 0.04873809800005802, + "src/qldpc/decoders/construction/factories_test.py": 0.032067473999944696, + "src/qldpc/decoders/construction/specs_test.py": 0.48636401600003865, + "src/qldpc/decoders/custom/_lookup_enumeration_test.py": 0.14681728100012492, + "src/qldpc/decoders/custom/composition_test.py": 0.11315363199997819, + "src/qldpc/decoders/custom/guf_test.py": 0.16528298500003302, + "src/qldpc/decoders/custom/ilp_test.py": 0.47505384000001527, + "src/qldpc/decoders/custom/lookup_test.py": 7.653618583999929, + "src/qldpc/decoders/dems_test.py": 0.1393242919999409, + "src/qldpc/decoders/external/frontier_test.py": 0.057543985999899405, + "src/qldpc/decoders/external/ldpc_test.py": 0.16305410200001802, + "src/qldpc/decoders/external/pymatching_test.py": 0.13436409400003413, + "src/qldpc/decoders/external/relay_bp_test.py": 0.34757976100013366, + "src/qldpc/decoders/external/tesseract_test.py": 2.553449109999974, + "src/qldpc/decoders/layering_test.py": 0.1297469109999838, + "src/qldpc/decoders/legacy_api_test.py": 0.049830948999925795, + "src/qldpc/decoders/protocols_test.py": 0.02739932599996564, + "src/qldpc/decoders/sinter/core_test.py": 0.23161123599987832, + "src/qldpc/decoders/sinter/subgraph_test.py": 0.44540675299992927, + "src/qldpc/decoders/sinter/window_test.py": 0.2736423159999788, + "src/qldpc/experimental/circuits/benchmarking/consumer_profiles_test.py": 0.0515187830000059, + "src/qldpc/experimental/circuits/benchmarking/consumers_test.py": 3.9540059130001453, + "src/qldpc/experimental/circuits/benchmarking/residual_weights_test.py": 2.2839714510000135, + "src/qldpc/experimental/circuits/transforms/deferred_measurement_test.py": 1.9534641950000093, + "src/qldpc/experimental/objects/_packed_circuits_test.py": 0.134179583999952, + "src/qldpc/experimental/objects/_packed_paulis_test.py": 0.11523488500000667, + "src/qldpc/experimental/objects/_symplectic_test.py": 1.1807687600000634, + "src/qldpc/experimental/objects/rotated_pauli_string_test.py": 1.1040226639999275, + "src/qldpc/experimental/surgery/_compiler_test.py": 1.0646277209999369, + "src/qldpc/experimental/surgery/bridge_test.py": 3.7442207660001827, + "src/qldpc/experimental/surgery/cheeger_test.py": 1.575921746000006, + "src/qldpc/experimental/surgery/circuit_test.py": 15.021637316999986, + "src/qldpc/experimental/surgery/construction_test.py": 0.20431640499998593, + "src/qldpc/experimental/surgery/gadget_test.py": 3.9635906030000854, + "src/qldpc/external/_bliss_test.py": 2.373623261000006, + "src/qldpc/external/_codes_test.py": 0.15963218399991774, + "src/qldpc/external/_groups_test.py": 0.0658751089999896, + "src/qldpc/external/gap_test.py": 0.06996867700001985, + "src/qldpc/external/sqetch_test.py": 0.11746730300001218, + "src/qldpc/import_test.py": 2.2368929680000065, + "src/qldpc/math_test.py": 1.8971713850000071, + "src/qldpc/objects/_symmetries_test.py": 0.016617980000006582, + "src/qldpc/objects/complexes_test.py": 5.6525995959999875, + "src/qldpc/objects/paulis_test.py": 0.023654445999966356, + "src/qldpc/objects/tanner_graphs_test.py": 0.022136988000028168 } From af3f7d1175d30e5815f893fc5f27d59f3bb98ea6 Mon Sep 17 00:00:00 2001 From: "Michael A. Perlin" Date: Thu, 12 Nov 2026 10:15:39 -0500 Subject: [PATCH 2/2] Add a script that downloads .test_durations from the latest "Store test durations" run It writes the file into the checkout that it runs from, and reports test files that the new file lacks or names that are not test files. It uses GITHUB_TOKEN or GH_TOKEN, or else git's stored credential for github.com, and sends the token only to GitHub's API. The contributing guide and the workflow point to it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/test-durations.yaml | 5 +- checks/_fetch_test_durations.py | 121 ++++++++++++++++++++++++++ docs/source/contributing/index.rst | 1 + 3 files changed, 125 insertions(+), 2 deletions(-) create mode 100755 checks/_fetch_test_durations.py diff --git a/.github/workflows/test-durations.yaml b/.github/workflows/test-durations.yaml index ea550678b..a7b077f2f 100644 --- a/.github/workflows/test-durations.yaml +++ b/.github/workflows/test-durations.yaml @@ -1,7 +1,8 @@ # This workflow measures how long each test file takes in the per-module coverage check on a CI # runner, and uploads the result as .test_durations, which balances the coverage shards (see -# checks/coverage_.py --shard). Run it from the Actions tab, and commit the file that it uploads. -# Keep its set-up in step with the coverage job of continuous-integration.yaml. +# checks/coverage_.py --shard). Run it from the Actions tab, then download the file that it uploads +# with checks/_fetch_test_durations.py, and commit it. Keep its set-up in step with the coverage +# job of continuous-integration.yaml. name: Store test durations diff --git a/checks/_fetch_test_durations.py b/checks/_fetch_test_durations.py new file mode 100755 index 000000000..0fc537b3f --- /dev/null +++ b/checks/_fetch_test_durations.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 + +"""Download .test_durations from the latest successful run of the "Store test durations" workflow. + +CI balances its coverage shards by the durations in .test_durations (see ``coverage_.py +--shard``). To re-tune them, start the workflow (``.github/workflows/test-durations.yaml``) from +the Actions tab, wait for it to finish, run this script, and commit the updated file. The script +reports the test files that the new file lacks, and the names in it that are not test files. + +Downloading an artifact needs a GitHub token, even from a public repository. The script uses +GITHUB_TOKEN or GH_TOKEN if one is set, or else the token that git uses for github.com. +""" + +import io +import json +import os +import pathlib +import subprocess +import sys +import urllib.error +import urllib.request +import zipfile +from typing import Any + +REPOSITORY = "qLDPCOrg/qLDPC" +WORKFLOW = "test-durations.yaml" +ARTIFACT = "test-durations" +DURATIONS_FILE = ".test_durations" +ROOT = pathlib.Path(__file__).resolve().parent.parent + + +def main() -> int: + """Write the durations of the latest successful run, and report mismatches with the tests.""" + token = _get_token() + runs = _get_json(f"actions/workflows/{WORKFLOW}/runs?branch=main&status=success", token) + if not runs["workflow_runs"]: + sys.exit(f"No successful run of {WORKFLOW} on main.") + run = runs["workflow_runs"][0] + artifacts = _get_json(f"actions/runs/{run['id']}/artifacts", token)["artifacts"] + artifact = next((item for item in artifacts if item["name"] == ARTIFACT), None) + if artifact is None or artifact["expired"]: + sys.exit(f"The run {run['html_url']} has no {ARTIFACT} artifact to download.") + with zipfile.ZipFile(io.BytesIO(_download(artifact["archive_download_url"], token))) as archive: + contents = archive.read(DURATIONS_FILE) + durations = json.loads(contents) + (ROOT / DURATIONS_FILE).write_bytes(contents) + print(f"Wrote {DURATIONS_FILE} from {run['html_url']}, started {run['run_started_at']}.") + + listed = subprocess.run( + ["git", "ls-files", "--", "src/*_test.py"], + cwd=ROOT, + capture_output=True, + text=True, + check=True, + ) + test_files = set(listed.stdout.split()) + missing = sorted(test_files - durations.keys()) + unknown = sorted(durations.keys() - test_files) + for name in missing: + print(f"Missing test file: {name}") + for name in unknown: + print(f"Not a test file: {name}") + return 1 if missing or unknown else 0 + + +def _get_token() -> str: + """A GitHub token from GITHUB_TOKEN or GH_TOKEN, or else the token that git uses for GitHub.""" + for variable in ("GITHUB_TOKEN", "GH_TOKEN"): + if token := os.environ.get(variable): + return token + credential = subprocess.run( + ["git", "credential", "fill"], + input="protocol=https\nhost=github.com\n\n", + capture_output=True, + text=True, + check=False, + env={**os.environ, "GIT_TERMINAL_PROMPT": "0"}, + ) + for line in credential.stdout.splitlines(): + if line.startswith("password="): + return line.removeprefix("password=") + sys.exit("Set GITHUB_TOKEN, or let git store a credential for github.com.") + + +def _get_json(path: str, token: str) -> Any: + """The JSON response of a GitHub API request for this repository.""" + request = urllib.request.Request( + f"https://api.github.com/repos/{REPOSITORY}/{path}", headers=_get_headers(token) + ) + with urllib.request.urlopen(request) as response: + return json.load(response) + + +def _download(url: str, token: str) -> bytes: + """Download an artifact, whose URL redirects to storage that must not receive the token.""" + opener = urllib.request.build_opener(_NoRedirect) + try: + opener.open(urllib.request.Request(url, headers=_get_headers(token))) + except urllib.error.HTTPError as error: + if error.code != 302: + raise + with urllib.request.urlopen(error.headers["Location"]) as response: + return response.read() + raise RuntimeError(f"{url} did not redirect to the artifact.") + + +def _get_headers(token: str) -> dict[str, str]: + """The headers of an authenticated GitHub API request.""" + return {"Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}"} + + +class _NoRedirect(urllib.request.HTTPRedirectHandler): + """A handler that reports a redirect as an error rather than following it.""" + + def redirect_request(self, *args: object, **kwargs: object) -> None: + """Follow no redirect.""" + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/source/contributing/index.rst b/docs/source/contributing/index.rst index 4b33bdc8b..1e4230e54 100644 --- a/docs/source/contributing/index.rst +++ b/docs/source/contributing/index.rst @@ -123,6 +123,7 @@ 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 makes the compiled runs. Run these locally only in the extended checks, or as targeted tests of a change to compiled code. +To re-balance the coverage shards, run the "Store test durations" workflow from the Actions tab, then ``python checks/_fetch_test_durations.py``, and commit the updated ``.test_durations``. ``all_.py`` runs most checks in one command. It skips the per-module coverage gate and ``pyproject.toml`` formatting.