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/.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 } 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.