From c136aefe20305485117c0adc3ba17c516bc05331 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Mon, 10 Mar 2025 20:29:07 +0000 Subject: [PATCH] chore: optimise unit tests (#2293) * optimise unit tests - Cache loading the config TOML Unit test time: 13.1s -> 10.5s * optimise unit tests - use the same pyproject validator across the tests Unit test time: 10.5s -> 5.7s * optimise unit tests - selector optimisation Unit test time: 5.7s -> 5.1s * optimise unit tests - build options computation cache Unit test time: 5.1s -> 3.2s * Use functools.cache rather than functools.lru_cache * Don't overwrite build_options when applying functools.cache --- cibuildwheel/options.py | 10 ++++++-- cibuildwheel/selector.py | 15 +++++------ cibuildwheel/util/resources.py | 3 +++ unit_test/validate_schema_test.py | 41 ++++++++++++++++--------------- 4 files changed, 38 insertions(+), 31 deletions(-) diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index 232e5c96..3febe36e 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -591,6 +591,10 @@ class Options: except FileNotFoundError: self.pyproject_toml = None + # cache the build options method so repeated calls don't need to + # resolve the options again + self.build_options = functools.cache(self._compute_build_options) + @functools.cached_property def config_file_path(self) -> Path | None: args = self.command_line_arguments @@ -667,9 +671,11 @@ class Options: allow_empty=allow_empty, ) - def build_options(self, identifier: str | None) -> BuildOptions: + def _compute_build_options(self, identifier: str | None) -> BuildOptions: """ - Compute BuildOptions for a single run configuration. + Compute BuildOptions for a single run configuration. Normally accessed + through the `build_options` method, which is the same but the result + is cached. """ with self.reader.identifier(identifier): diff --git a/cibuildwheel/selector.py b/cibuildwheel/selector.py index 1f417b7e..bd0ce597 100644 --- a/cibuildwheel/selector.py +++ b/cibuildwheel/selector.py @@ -1,7 +1,7 @@ -import fnmatch import itertools from dataclasses import dataclass from enum import Enum +from fnmatch import fnmatch from typing import Any import bracex @@ -18,9 +18,10 @@ def selector_matches(patterns: str, string: str) -> bool: expansion. For example, 'cp{36,37}-*' would match either of 'cp36-*' or 'cp37-*'. """ + patterns_list = patterns.split() expanded_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in patterns_list) - return any(fnmatch.fnmatch(string, pat) for pat in expanded_patterns) + return any(fnmatch(string, pat) for pat in expanded_patterns) class EnableGroup(Enum): @@ -59,15 +60,11 @@ class BuildSelector: return False # filter out groups that are not enabled - if EnableGroup.CPythonFreeThreading not in self.enable and selector_matches( - "cp3??t-*", build_id - ): + if EnableGroup.CPythonFreeThreading not in self.enable and fnmatch(build_id, "cp3??t-*"): return False - if EnableGroup.CPythonPrerelease not in self.enable and selector_matches( - "cp314*", build_id - ): + if EnableGroup.CPythonPrerelease not in self.enable and fnmatch(build_id, "cp314*"): return False - if EnableGroup.PyPy not in self.enable and selector_matches("pp*", build_id): + if EnableGroup.PyPy not in self.enable and fnmatch(build_id, "pp*"): return False should_build = selector_matches(self.build_config, build_id) diff --git a/cibuildwheel/util/resources.py b/cibuildwheel/util/resources.py index 7e78bc88..e27f1f89 100644 --- a/cibuildwheel/util/resources.py +++ b/cibuildwheel/util/resources.py @@ -1,3 +1,4 @@ +import functools import tomllib from pathlib import Path from typing import Final @@ -16,6 +17,8 @@ VIRTUALENV: Final[Path] = PATH / "virtualenv.toml" CIBUILDWHEEL_SCHEMA: Final[Path] = PATH / "cibuildwheel.schema.json" +# this value is cached because it's used a lot in unit tests +@functools.cache def read_python_configs(config: PlatformName) -> list[dict[str, str]]: with BUILD_PLATFORMS.open("rb") as f: loaded_file = tomllib.load(f) diff --git a/unit_test/validate_schema_test.py b/unit_test/validate_schema_test.py index 1670d18c..85f5eae6 100644 --- a/unit_test/validate_schema_test.py +++ b/unit_test/validate_schema_test.py @@ -10,15 +10,22 @@ from cibuildwheel.util import resources DIR = Path(__file__).parent.resolve() -def test_validate_default_schema(): +@pytest.fixture(scope="session") +def validator() -> validate_pyproject.api.Validator: + """ + Reuse the validator for all tests, to keep unit tests fast. + """ + return validate_pyproject.api.Validator() + + +def test_validate_default_schema(validator: validate_pyproject.api.Validator) -> None: with resources.DEFAULTS.open("rb") as f: example = tomllib.load(f) - validator = validate_pyproject.api.Validator() assert validator(example) is not None -def test_validate_container_engine(): +def test_validate_container_engine(validator: validate_pyproject.api.Validator) -> None: """ This test checks container engine can be overridden - it used to be a global option but is now a build option. @@ -38,12 +45,13 @@ def test_validate_container_engine(): """ ) - validator = validate_pyproject.api.Validator() assert validator(example) is not None @pytest.mark.parametrize("platform", ["macos", "windows"]) -def test_validate_bad_container_engine(platform: str) -> None: +def test_validate_bad_container_engine( + validator: validate_pyproject.api.Validator, platform: str +) -> None: """ container-engine is not a valid option for macos or windows """ @@ -54,12 +62,11 @@ def test_validate_bad_container_engine(platform: str) -> None: """ ) - validator = validate_pyproject.api.Validator() with pytest.raises(validate_pyproject.error_reporting.ValidationError): validator(example) -def test_overrides_select(): +def test_overrides_select(validator: validate_pyproject.api.Validator) -> None: example = tomllib.loads( """ [[tool.cibuildwheel.overrides]] @@ -68,11 +75,10 @@ def test_overrides_select(): """ ) - validator = validate_pyproject.api.Validator() assert validator(example) is not None -def test_overrides_no_select(): +def test_overrides_no_select(validator: validate_pyproject.api.Validator) -> None: example = tomllib.loads( """ [[tool.cibuildwheel.overrides]] @@ -80,12 +86,11 @@ def test_overrides_no_select(): """ ) - validator = validate_pyproject.api.Validator() with pytest.raises(validate_pyproject.error_reporting.ValidationError): validator(example) -def test_overrides_only_select(): +def test_overrides_only_select(validator: validate_pyproject.api.Validator) -> None: example = tomllib.loads( """ [[tool.cibuildwheel.overrides]] @@ -93,12 +98,11 @@ def test_overrides_only_select(): """ ) - validator = validate_pyproject.api.Validator() with pytest.raises(validate_pyproject.error_reporting.ValidationError): validator(example) -def test_overrides_valid_inherit(): +def test_overrides_valid_inherit(validator: validate_pyproject.api.Validator) -> None: example = tomllib.loads( """ [[tool.cibuildwheel.overrides]] @@ -108,11 +112,10 @@ def test_overrides_valid_inherit(): """ ) - validator = validate_pyproject.api.Validator() assert validator(example) is not None -def test_overrides_invalid_inherit(): +def test_overrides_invalid_inherit(validator: validate_pyproject.api.Validator) -> None: example = tomllib.loads( """ [[tool.cibuildwheel.overrides]] @@ -122,12 +125,11 @@ def test_overrides_invalid_inherit(): """ ) - validator = validate_pyproject.api.Validator() with pytest.raises(validate_pyproject.error_reporting.ValidationError): validator(example) -def test_overrides_invalid_inherit_value(): +def test_overrides_invalid_inherit_value(validator: validate_pyproject.api.Validator) -> None: example = tomllib.loads( """ [[tool.cibuildwheel.overrides]] @@ -137,12 +139,11 @@ def test_overrides_invalid_inherit_value(): """ ) - validator = validate_pyproject.api.Validator() with pytest.raises(validate_pyproject.error_reporting.ValidationError): validator(example) -def test_docs_examples(): +def test_docs_examples(validator: validate_pyproject.api.Validator) -> None: """ Parse out all the configuration examples, build valid TOML out of them, and make sure they pass. @@ -182,5 +183,5 @@ def test_docs_examples(): print(example_txt) print() example = tomllib.loads(example_txt) - validator = validate_pyproject.api.Validator() + assert validator(example) is not None