Skip to content

Commit 0b82613

Browse files
Add a test-sources configuration option. (#2062)
* Add test_sources configuration option. * Add pyodide implementation. * Modify default test behavior to run in the project directory. * Update the test for running in the project directory. * Apply suggestions from code review Co-authored-by: Joe Rickerby <[email protected]> * Add shlex quoting to test-sources. * Restructure ctypes example to avoid issues running from the project folder. --------- Co-authored-by: Joe Rickerby <[email protected]>
1 parent 4d0d9f6 commit 0b82613

16 files changed

+370
-71
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ Options
143143
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
144144
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
145145
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
146+
| | [`CIBW_TEST_SOURCES`](https://cibuildwheel.pypa.io/en/stable/options/#test-sources) | Files and folders from the source tree that are copied into an isolated tree before running the tests |
146147
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.pypa.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
147148
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.pypa.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
148149
| | [`CIBW_TEST_SKIP`](https://cibuildwheel.pypa.io/en/stable/options/#test-skip) | Skip running tests on some builds |

cibuildwheel/linux.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
from .util import (
2222
BuildFrontendConfig,
2323
BuildSelector,
24+
copy_test_sources,
2425
find_compatible_wheel,
2526
get_build_verbosity_extra_flags,
2627
prepare_command,
2728
read_python_configs,
2829
split_config_settings,
29-
test_fail_cwd_file,
3030
unwrap,
3131
)
3232

@@ -401,9 +401,19 @@ def build_in_container(
401401
package=container_package_dir,
402402
wheel=wheel_to_test,
403403
)
404-
test_cwd = testing_temp_dir / "test_cwd"
405-
container.call(["mkdir", "-p", test_cwd])
406-
container.copy_into(test_fail_cwd_file, test_cwd / "test_fail.py")
404+
405+
if build_options.test_sources:
406+
test_cwd = testing_temp_dir / "test_cwd"
407+
container.call(["mkdir", "-p", test_cwd])
408+
copy_test_sources(
409+
build_options.test_sources,
410+
build_options.package_dir,
411+
test_cwd,
412+
copy_into=container.copy_into,
413+
)
414+
else:
415+
# There are no test sources. Run the tests in the project directory.
416+
test_cwd = PurePosixPath(container_project_path)
407417

408418
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
409419

cibuildwheel/macos.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
BuildSelector,
3232
call,
3333
combine_constraints,
34+
copy_test_sources,
3435
detect_ci_provider,
3536
download,
3637
find_compatible_wheel,
@@ -44,7 +45,6 @@
4445
read_python_configs,
4546
shell,
4647
split_config_settings,
47-
test_fail_cwd_file,
4848
unwrap,
4949
virtualenv,
5050
)
@@ -736,9 +736,17 @@ def build(options: Options, tmp_path: Path) -> None:
736736
wheel=repaired_wheel,
737737
)
738738

739-
test_cwd = identifier_tmp_dir / "test_cwd"
740-
test_cwd.mkdir(exist_ok=True)
741-
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
739+
if build_options.test_sources:
740+
test_cwd = identifier_tmp_dir / "test_cwd"
741+
test_cwd.mkdir(exist_ok=True)
742+
copy_test_sources(
743+
build_options.test_sources,
744+
build_options.package_dir,
745+
test_cwd,
746+
)
747+
else:
748+
# There are no test sources. Run the tests in the project directory.
749+
test_cwd = Path(".").resolve()
742750

743751
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
744752

cibuildwheel/options.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import functools
1010
import shlex
1111
import textwrap
12-
from collections.abc import Generator, Iterable, Set
12+
from collections.abc import Callable, Generator, Iterable, Set
1313
from pathlib import Path
1414
from typing import Any, Literal, Mapping, Sequence, Union # noqa: TID251
1515

@@ -92,6 +92,7 @@ class BuildOptions:
9292
dependency_constraints: DependencyConstraints | None
9393
test_command: str | None
9494
before_test: str | None
95+
test_sources: list[str]
9596
test_requires: list[str]
9697
test_extras: str
9798
test_groups: list[str]
@@ -171,11 +172,12 @@ class ListFormat(OptionFormat):
171172
A format that joins lists with a separator.
172173
"""
173174

174-
def __init__(self, sep: str) -> None:
175+
def __init__(self, sep: str, quote: Callable[[str], str] | None = None) -> None:
175176
self.sep = sep
177+
self.quote = quote if quote else lambda s: s
176178

177179
def format_list(self, value: SettingList) -> str:
178-
return self.sep.join(str(v) for v in value)
180+
return self.sep.join(self.quote(str(v)) for v in value)
179181

180182
def merge_values(self, before: str, after: str) -> str:
181183
return f"{before}{self.sep}{after}"
@@ -711,6 +713,11 @@ def build_options(self, identifier: str | None) -> BuildOptions:
711713
dependency_versions = self.reader.get("dependency-versions")
712714
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
713715
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
716+
test_sources = shlex.split(
717+
self.reader.get(
718+
"test-sources", option_format=ListFormat(sep=" ", quote=shlex.quote)
719+
)
720+
)
714721
test_requires = self.reader.get(
715722
"test-requires", option_format=ListFormat(sep=" ")
716723
).split()
@@ -819,6 +826,7 @@ def build_options(self, identifier: str | None) -> BuildOptions:
819826
return BuildOptions(
820827
globals=self.globals,
821828
test_command=test_command,
829+
test_sources=test_sources,
822830
test_requires=[*test_requires, *test_requirements_from_groups],
823831
test_extras=test_extras,
824832
test_groups=test_groups,

cibuildwheel/pyodide.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
BuildSelector,
2222
call,
2323
combine_constraints,
24+
copy_test_sources,
2425
download,
2526
ensure_node,
2627
extract_zip,
@@ -31,7 +32,6 @@
3132
read_python_configs,
3233
shell,
3334
split_config_settings,
34-
test_fail_cwd_file,
3535
virtualenv,
3636
)
3737

@@ -387,9 +387,17 @@ def build(options: Options, tmp_path: Path) -> None:
387387
package=build_options.package_dir.resolve(),
388388
)
389389

390-
test_cwd = identifier_tmp_dir / "test_cwd"
391-
test_cwd.mkdir(exist_ok=True)
392-
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
390+
if build_options.test_sources:
391+
test_cwd = identifier_tmp_dir / "test_cwd"
392+
test_cwd.mkdir(exist_ok=True)
393+
copy_test_sources(
394+
build_options.test_sources,
395+
build_options.package_dir,
396+
test_cwd,
397+
)
398+
else:
399+
# There are no test sources. Run the tests in the project directory.
400+
test_cwd = Path(".").resolve()
393401

394402
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
395403

cibuildwheel/resources/cibuildwheel.schema.json

+33
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,21 @@
426426
],
427427
"title": "CIBW_TEST_EXTRAS"
428428
},
429+
"test-sources": {
430+
"description": "Test files that are required by the test environment",
431+
"oneOf": [
432+
{
433+
"type": "string"
434+
},
435+
{
436+
"type": "array",
437+
"items": {
438+
"type": "string"
439+
}
440+
}
441+
],
442+
"title": "CIBW_TEST_SOURCES"
443+
},
429444
"test-groups": {
430445
"description": "Install extra groups when testing",
431446
"oneOf": [
@@ -529,6 +544,9 @@
529544
"test-extras": {
530545
"$ref": "#/$defs/inherit"
531546
},
547+
"test-sources": {
548+
"$ref": "#/$defs/inherit"
549+
},
532550
"test-requires": {
533551
"$ref": "#/$defs/inherit"
534552
}
@@ -618,6 +636,9 @@
618636
"test-extras": {
619637
"$ref": "#/properties/test-extras"
620638
},
639+
"test-sources": {
640+
"$ref": "#/properties/test-sources"
641+
},
621642
"test-groups": {
622643
"$ref": "#/properties/test-groups"
623644
},
@@ -728,6 +749,9 @@
728749
"test-extras": {
729750
"$ref": "#/properties/test-extras"
730751
},
752+
"test-sources": {
753+
"$ref": "#/properties/test-sources"
754+
},
731755
"test-groups": {
732756
"$ref": "#/properties/test-groups"
733757
},
@@ -776,6 +800,9 @@
776800
"test-extras": {
777801
"$ref": "#/properties/test-extras"
778802
},
803+
"test-sources": {
804+
"$ref": "#/properties/test-sources"
805+
},
779806
"test-groups": {
780807
"$ref": "#/properties/test-groups"
781808
},
@@ -837,6 +864,9 @@
837864
"test-extras": {
838865
"$ref": "#/properties/test-extras"
839866
},
867+
"test-sources": {
868+
"$ref": "#/properties/test-sources"
869+
},
840870
"test-groups": {
841871
"$ref": "#/properties/test-groups"
842872
},
@@ -885,6 +915,9 @@
885915
"test-extras": {
886916
"$ref": "#/properties/test-extras"
887917
},
918+
"test-sources": {
919+
"$ref": "#/properties/test-sources"
920+
},
888921
"test-groups": {
889922
"$ref": "#/properties/test-groups"
890923
},

cibuildwheel/resources/defaults.toml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ repair-wheel-command = ""
1919

2020
test-command = ""
2121
before-test = ""
22+
test-sources = []
2223
test-requires = []
2324
test-extras = []
2425
test-groups = []

cibuildwheel/resources/testing_temp_dir_file.py

-17
This file was deleted.

cibuildwheel/util.py

+38-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import typing
1818
import urllib.request
1919
from collections import defaultdict
20-
from collections.abc import Generator, Iterable, Mapping, MutableMapping, Sequence
20+
from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping, Sequence
2121
from dataclasses import dataclass
2222
from enum import Enum
2323
from functools import lru_cache, total_ordering
@@ -36,6 +36,7 @@
3636
from packaging.version import Version
3737
from platformdirs import user_cache_path
3838

39+
from . import errors
3940
from ._compat import tomllib
4041
from .architecture import Architecture
4142
from .errors import FatalError
@@ -66,8 +67,6 @@
6667

6768
free_thread_enable_313: Final[Path] = resources_dir / "free-threaded-enable-313.xml"
6869

69-
test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py"
70-
7170

7271
class EnableGroups(enum.Enum):
7372
"""
@@ -425,6 +424,42 @@ def move_file(src_file: Path, dst_file: Path) -> Path:
425424
return Path(resulting_file).resolve(strict=True)
426425

427426

427+
def copy_into_local(src: Path, dst: PurePath) -> None:
428+
"""Copy a path from src to dst, regardless of whether it's a file or a directory."""
429+
# Ensure the target folder location exists
430+
Path(dst.parent).mkdir(exist_ok=True, parents=True)
431+
432+
if src.is_dir():
433+
shutil.copytree(src, dst)
434+
else:
435+
shutil.copy(src, dst)
436+
437+
438+
def copy_test_sources(
439+
test_sources: list[str],
440+
package_dir: Path,
441+
test_dir: PurePath,
442+
copy_into: Callable[[Path, PurePath], None] = copy_into_local,
443+
) -> None:
444+
"""Copy the list of test sources from the package to the test directory.
445+
446+
:param test_sources: A list of test paths, relative to the package_dir.
447+
:param package_dir: The root of the package directory.
448+
:param test_dir: The folder where test sources should be placed.
449+
:param copy_info: The copy function to use. By default, does a local
450+
filesystem copy; but an OCIContainer.copy_info method (or equivalent)
451+
can be provided.
452+
"""
453+
for test_path in test_sources:
454+
source = package_dir.resolve() / test_path
455+
456+
if not source.exists():
457+
msg = f"Test source {test_path} does not exist."
458+
raise errors.FatalError(msg)
459+
460+
copy_into(source, test_dir / test_path)
461+
462+
428463
class DependencyConstraints:
429464
def __init__(self, base_file_path: Path):
430465
assert base_file_path.exists()

cibuildwheel/windows.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
BuildSelector,
2828
call,
2929
combine_constraints,
30+
copy_test_sources,
3031
download,
3132
extract_zip,
3233
find_compatible_wheel,
@@ -38,7 +39,6 @@
3839
read_python_configs,
3940
shell,
4041
split_config_settings,
41-
test_fail_cwd_file,
4242
unwrap,
4343
virtualenv,
4444
)
@@ -572,9 +572,17 @@ def build(options: Options, tmp_path: Path) -> None:
572572
package=options.globals.package_dir.resolve(),
573573
wheel=repaired_wheel,
574574
)
575-
test_cwd = identifier_tmp_dir / "test_cwd"
576-
test_cwd.mkdir()
577-
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
575+
if build_options.test_sources:
576+
test_cwd = identifier_tmp_dir / "test_cwd"
577+
test_cwd.mkdir()
578+
copy_test_sources(
579+
build_options.test_sources,
580+
build_options.package_dir,
581+
test_cwd,
582+
)
583+
else:
584+
# There are no test sources. Run the tests in the project directory.
585+
test_cwd = Path(".").resolve()
578586

579587
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
580588

0 commit comments

Comments
 (0)