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
This commit is contained in:
Joe Rickerby
2025-03-10 21:29:07 +01:00
committed by GitHub
parent bddd438a88
commit c136aefe20
4 changed files with 38 additions and 31 deletions
+8 -2
View File
@@ -591,6 +591,10 @@ class Options:
except FileNotFoundError: except FileNotFoundError:
self.pyproject_toml = None 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 @functools.cached_property
def config_file_path(self) -> Path | None: def config_file_path(self) -> Path | None:
args = self.command_line_arguments args = self.command_line_arguments
@@ -667,9 +671,11 @@ class Options:
allow_empty=allow_empty, 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): with self.reader.identifier(identifier):
+6 -9
View File
@@ -1,7 +1,7 @@
import fnmatch
import itertools import itertools
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from fnmatch import fnmatch
from typing import Any from typing import Any
import bracex 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 expansion. For example, 'cp{36,37}-*' would match either of 'cp36-*' or
'cp37-*'. 'cp37-*'.
""" """
patterns_list = patterns.split() patterns_list = patterns.split()
expanded_patterns = itertools.chain.from_iterable(bracex.expand(p) for p in patterns_list) 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): class EnableGroup(Enum):
@@ -59,15 +60,11 @@ class BuildSelector:
return False return False
# filter out groups that are not enabled # filter out groups that are not enabled
if EnableGroup.CPythonFreeThreading not in self.enable and selector_matches( if EnableGroup.CPythonFreeThreading not in self.enable and fnmatch(build_id, "cp3??t-*"):
"cp3??t-*", build_id
):
return False return False
if EnableGroup.CPythonPrerelease not in self.enable and selector_matches( if EnableGroup.CPythonPrerelease not in self.enable and fnmatch(build_id, "cp314*"):
"cp314*", build_id
):
return False 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 return False
should_build = selector_matches(self.build_config, build_id) should_build = selector_matches(self.build_config, build_id)
+3
View File
@@ -1,3 +1,4 @@
import functools
import tomllib import tomllib
from pathlib import Path from pathlib import Path
from typing import Final from typing import Final
@@ -16,6 +17,8 @@ VIRTUALENV: Final[Path] = PATH / "virtualenv.toml"
CIBUILDWHEEL_SCHEMA: Final[Path] = PATH / "cibuildwheel.schema.json" 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]]: def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
with BUILD_PLATFORMS.open("rb") as f: with BUILD_PLATFORMS.open("rb") as f:
loaded_file = tomllib.load(f) loaded_file = tomllib.load(f)
+21 -20
View File
@@ -10,15 +10,22 @@ from cibuildwheel.util import resources
DIR = Path(__file__).parent.resolve() 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: with resources.DEFAULTS.open("rb") as f:
example = tomllib.load(f) example = tomllib.load(f)
validator = validate_pyproject.api.Validator()
assert validator(example) is not None 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 This test checks container engine can be overridden - it used to be a
global option but is now a build option. 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 assert validator(example) is not None
@pytest.mark.parametrize("platform", ["macos", "windows"]) @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 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): with pytest.raises(validate_pyproject.error_reporting.ValidationError):
validator(example) validator(example)
def test_overrides_select(): def test_overrides_select(validator: validate_pyproject.api.Validator) -> None:
example = tomllib.loads( example = tomllib.loads(
""" """
[[tool.cibuildwheel.overrides]] [[tool.cibuildwheel.overrides]]
@@ -68,11 +75,10 @@ def test_overrides_select():
""" """
) )
validator = validate_pyproject.api.Validator()
assert validator(example) is not None 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( example = tomllib.loads(
""" """
[[tool.cibuildwheel.overrides]] [[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): with pytest.raises(validate_pyproject.error_reporting.ValidationError):
validator(example) validator(example)
def test_overrides_only_select(): def test_overrides_only_select(validator: validate_pyproject.api.Validator) -> None:
example = tomllib.loads( example = tomllib.loads(
""" """
[[tool.cibuildwheel.overrides]] [[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): with pytest.raises(validate_pyproject.error_reporting.ValidationError):
validator(example) validator(example)
def test_overrides_valid_inherit(): def test_overrides_valid_inherit(validator: validate_pyproject.api.Validator) -> None:
example = tomllib.loads( example = tomllib.loads(
""" """
[[tool.cibuildwheel.overrides]] [[tool.cibuildwheel.overrides]]
@@ -108,11 +112,10 @@ def test_overrides_valid_inherit():
""" """
) )
validator = validate_pyproject.api.Validator()
assert validator(example) is not None 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( example = tomllib.loads(
""" """
[[tool.cibuildwheel.overrides]] [[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): with pytest.raises(validate_pyproject.error_reporting.ValidationError):
validator(example) validator(example)
def test_overrides_invalid_inherit_value(): def test_overrides_invalid_inherit_value(validator: validate_pyproject.api.Validator) -> None:
example = tomllib.loads( example = tomllib.loads(
""" """
[[tool.cibuildwheel.overrides]] [[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): with pytest.raises(validate_pyproject.error_reporting.ValidationError):
validator(example) 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 Parse out all the configuration examples, build valid TOML out of them, and
make sure they pass. make sure they pass.
@@ -182,5 +183,5 @@ def test_docs_examples():
print(example_txt) print(example_txt)
print() print()
example = tomllib.loads(example_txt) example = tomllib.loads(example_txt)
validator = validate_pyproject.api.Validator()
assert validator(example) is not None assert validator(example) is not None