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 <joerick@mac.com> * Add shlex quoting to test-sources. * Restructure ctypes example to avoid issues running from the project folder. --------- Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
co-authored by
Joe Rickerby
parent
4d0d9f6fd0
commit
0b82613d6f
@@ -143,6 +143,7 @@ Options
|
|||||||
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
|
| | [`CIBW_DEPENDENCY_VERSIONS`](https://cibuildwheel.pypa.io/en/stable/options/#dependency-versions) | Specify how cibuildwheel controls the versions of the tools it uses |
|
||||||
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
|
| **Testing** | [`CIBW_TEST_COMMAND`](https://cibuildwheel.pypa.io/en/stable/options/#test-command) | Execute a shell command to test each built wheel |
|
||||||
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
|
| | [`CIBW_BEFORE_TEST`](https://cibuildwheel.pypa.io/en/stable/options/#before-test) | Execute a shell command before testing each wheel |
|
||||||
|
| | [`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 |
|
||||||
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.pypa.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
|
| | [`CIBW_TEST_REQUIRES`](https://cibuildwheel.pypa.io/en/stable/options/#test-requires) | Install Python dependencies before running the tests |
|
||||||
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.pypa.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
|
| | [`CIBW_TEST_EXTRAS`](https://cibuildwheel.pypa.io/en/stable/options/#test-extras) | Install your wheel for testing using extras_require |
|
||||||
| | [`CIBW_TEST_SKIP`](https://cibuildwheel.pypa.io/en/stable/options/#test-skip) | Skip running tests on some builds |
|
| | [`CIBW_TEST_SKIP`](https://cibuildwheel.pypa.io/en/stable/options/#test-skip) | Skip running tests on some builds |
|
||||||
|
|||||||
+14
-4
@@ -21,12 +21,12 @@ from .typing import PathOrStr
|
|||||||
from .util import (
|
from .util import (
|
||||||
BuildFrontendConfig,
|
BuildFrontendConfig,
|
||||||
BuildSelector,
|
BuildSelector,
|
||||||
|
copy_test_sources,
|
||||||
find_compatible_wheel,
|
find_compatible_wheel,
|
||||||
get_build_verbosity_extra_flags,
|
get_build_verbosity_extra_flags,
|
||||||
prepare_command,
|
prepare_command,
|
||||||
read_python_configs,
|
read_python_configs,
|
||||||
split_config_settings,
|
split_config_settings,
|
||||||
test_fail_cwd_file,
|
|
||||||
unwrap,
|
unwrap,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -401,9 +401,19 @@ def build_in_container(
|
|||||||
package=container_package_dir,
|
package=container_package_dir,
|
||||||
wheel=wheel_to_test,
|
wheel=wheel_to_test,
|
||||||
)
|
)
|
||||||
test_cwd = testing_temp_dir / "test_cwd"
|
|
||||||
container.call(["mkdir", "-p", test_cwd])
|
if build_options.test_sources:
|
||||||
container.copy_into(test_fail_cwd_file, test_cwd / "test_fail.py")
|
test_cwd = testing_temp_dir / "test_cwd"
|
||||||
|
container.call(["mkdir", "-p", test_cwd])
|
||||||
|
copy_test_sources(
|
||||||
|
build_options.test_sources,
|
||||||
|
build_options.package_dir,
|
||||||
|
test_cwd,
|
||||||
|
copy_into=container.copy_into,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# There are no test sources. Run the tests in the project directory.
|
||||||
|
test_cwd = PurePosixPath(container_project_path)
|
||||||
|
|
||||||
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
|
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -31,6 +31,7 @@ from .util import (
|
|||||||
BuildSelector,
|
BuildSelector,
|
||||||
call,
|
call,
|
||||||
combine_constraints,
|
combine_constraints,
|
||||||
|
copy_test_sources,
|
||||||
detect_ci_provider,
|
detect_ci_provider,
|
||||||
download,
|
download,
|
||||||
find_compatible_wheel,
|
find_compatible_wheel,
|
||||||
@@ -44,7 +45,6 @@ from .util import (
|
|||||||
read_python_configs,
|
read_python_configs,
|
||||||
shell,
|
shell,
|
||||||
split_config_settings,
|
split_config_settings,
|
||||||
test_fail_cwd_file,
|
|
||||||
unwrap,
|
unwrap,
|
||||||
virtualenv,
|
virtualenv,
|
||||||
)
|
)
|
||||||
@@ -736,9 +736,17 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
wheel=repaired_wheel,
|
wheel=repaired_wheel,
|
||||||
)
|
)
|
||||||
|
|
||||||
test_cwd = identifier_tmp_dir / "test_cwd"
|
if build_options.test_sources:
|
||||||
test_cwd.mkdir(exist_ok=True)
|
test_cwd = identifier_tmp_dir / "test_cwd"
|
||||||
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
|
test_cwd.mkdir(exist_ok=True)
|
||||||
|
copy_test_sources(
|
||||||
|
build_options.test_sources,
|
||||||
|
build_options.package_dir,
|
||||||
|
test_cwd,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# There are no test sources. Run the tests in the project directory.
|
||||||
|
test_cwd = Path(".").resolve()
|
||||||
|
|
||||||
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -9,7 +9,7 @@ import enum
|
|||||||
import functools
|
import functools
|
||||||
import shlex
|
import shlex
|
||||||
import textwrap
|
import textwrap
|
||||||
from collections.abc import Generator, Iterable, Set
|
from collections.abc import Callable, Generator, Iterable, Set
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal, Mapping, Sequence, Union # noqa: TID251
|
from typing import Any, Literal, Mapping, Sequence, Union # noqa: TID251
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ class BuildOptions:
|
|||||||
dependency_constraints: DependencyConstraints | None
|
dependency_constraints: DependencyConstraints | None
|
||||||
test_command: str | None
|
test_command: str | None
|
||||||
before_test: str | None
|
before_test: str | None
|
||||||
|
test_sources: list[str]
|
||||||
test_requires: list[str]
|
test_requires: list[str]
|
||||||
test_extras: str
|
test_extras: str
|
||||||
test_groups: list[str]
|
test_groups: list[str]
|
||||||
@@ -171,11 +172,12 @@ class ListFormat(OptionFormat):
|
|||||||
A format that joins lists with a separator.
|
A format that joins lists with a separator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, sep: str) -> None:
|
def __init__(self, sep: str, quote: Callable[[str], str] | None = None) -> None:
|
||||||
self.sep = sep
|
self.sep = sep
|
||||||
|
self.quote = quote if quote else lambda s: s
|
||||||
|
|
||||||
def format_list(self, value: SettingList) -> str:
|
def format_list(self, value: SettingList) -> str:
|
||||||
return self.sep.join(str(v) for v in value)
|
return self.sep.join(self.quote(str(v)) for v in value)
|
||||||
|
|
||||||
def merge_values(self, before: str, after: str) -> str:
|
def merge_values(self, before: str, after: str) -> str:
|
||||||
return f"{before}{self.sep}{after}"
|
return f"{before}{self.sep}{after}"
|
||||||
@@ -711,6 +713,11 @@ class Options:
|
|||||||
dependency_versions = self.reader.get("dependency-versions")
|
dependency_versions = self.reader.get("dependency-versions")
|
||||||
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
|
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
|
||||||
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
|
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
|
||||||
|
test_sources = shlex.split(
|
||||||
|
self.reader.get(
|
||||||
|
"test-sources", option_format=ListFormat(sep=" ", quote=shlex.quote)
|
||||||
|
)
|
||||||
|
)
|
||||||
test_requires = self.reader.get(
|
test_requires = self.reader.get(
|
||||||
"test-requires", option_format=ListFormat(sep=" ")
|
"test-requires", option_format=ListFormat(sep=" ")
|
||||||
).split()
|
).split()
|
||||||
@@ -819,6 +826,7 @@ class Options:
|
|||||||
return BuildOptions(
|
return BuildOptions(
|
||||||
globals=self.globals,
|
globals=self.globals,
|
||||||
test_command=test_command,
|
test_command=test_command,
|
||||||
|
test_sources=test_sources,
|
||||||
test_requires=[*test_requires, *test_requirements_from_groups],
|
test_requires=[*test_requires, *test_requirements_from_groups],
|
||||||
test_extras=test_extras,
|
test_extras=test_extras,
|
||||||
test_groups=test_groups,
|
test_groups=test_groups,
|
||||||
|
|||||||
+12
-4
@@ -21,6 +21,7 @@ from .util import (
|
|||||||
BuildSelector,
|
BuildSelector,
|
||||||
call,
|
call,
|
||||||
combine_constraints,
|
combine_constraints,
|
||||||
|
copy_test_sources,
|
||||||
download,
|
download,
|
||||||
ensure_node,
|
ensure_node,
|
||||||
extract_zip,
|
extract_zip,
|
||||||
@@ -31,7 +32,6 @@ from .util import (
|
|||||||
read_python_configs,
|
read_python_configs,
|
||||||
shell,
|
shell,
|
||||||
split_config_settings,
|
split_config_settings,
|
||||||
test_fail_cwd_file,
|
|
||||||
virtualenv,
|
virtualenv,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -387,9 +387,17 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
package=build_options.package_dir.resolve(),
|
package=build_options.package_dir.resolve(),
|
||||||
)
|
)
|
||||||
|
|
||||||
test_cwd = identifier_tmp_dir / "test_cwd"
|
if build_options.test_sources:
|
||||||
test_cwd.mkdir(exist_ok=True)
|
test_cwd = identifier_tmp_dir / "test_cwd"
|
||||||
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
|
test_cwd.mkdir(exist_ok=True)
|
||||||
|
copy_test_sources(
|
||||||
|
build_options.test_sources,
|
||||||
|
build_options.package_dir,
|
||||||
|
test_cwd,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# There are no test sources. Run the tests in the project directory.
|
||||||
|
test_cwd = Path(".").resolve()
|
||||||
|
|
||||||
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||||
|
|
||||||
|
|||||||
@@ -426,6 +426,21 @@
|
|||||||
],
|
],
|
||||||
"title": "CIBW_TEST_EXTRAS"
|
"title": "CIBW_TEST_EXTRAS"
|
||||||
},
|
},
|
||||||
|
"test-sources": {
|
||||||
|
"description": "Test files that are required by the test environment",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "CIBW_TEST_SOURCES"
|
||||||
|
},
|
||||||
"test-groups": {
|
"test-groups": {
|
||||||
"description": "Install extra groups when testing",
|
"description": "Install extra groups when testing",
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
@@ -529,6 +544,9 @@
|
|||||||
"test-extras": {
|
"test-extras": {
|
||||||
"$ref": "#/$defs/inherit"
|
"$ref": "#/$defs/inherit"
|
||||||
},
|
},
|
||||||
|
"test-sources": {
|
||||||
|
"$ref": "#/$defs/inherit"
|
||||||
|
},
|
||||||
"test-requires": {
|
"test-requires": {
|
||||||
"$ref": "#/$defs/inherit"
|
"$ref": "#/$defs/inherit"
|
||||||
}
|
}
|
||||||
@@ -618,6 +636,9 @@
|
|||||||
"test-extras": {
|
"test-extras": {
|
||||||
"$ref": "#/properties/test-extras"
|
"$ref": "#/properties/test-extras"
|
||||||
},
|
},
|
||||||
|
"test-sources": {
|
||||||
|
"$ref": "#/properties/test-sources"
|
||||||
|
},
|
||||||
"test-groups": {
|
"test-groups": {
|
||||||
"$ref": "#/properties/test-groups"
|
"$ref": "#/properties/test-groups"
|
||||||
},
|
},
|
||||||
@@ -728,6 +749,9 @@
|
|||||||
"test-extras": {
|
"test-extras": {
|
||||||
"$ref": "#/properties/test-extras"
|
"$ref": "#/properties/test-extras"
|
||||||
},
|
},
|
||||||
|
"test-sources": {
|
||||||
|
"$ref": "#/properties/test-sources"
|
||||||
|
},
|
||||||
"test-groups": {
|
"test-groups": {
|
||||||
"$ref": "#/properties/test-groups"
|
"$ref": "#/properties/test-groups"
|
||||||
},
|
},
|
||||||
@@ -776,6 +800,9 @@
|
|||||||
"test-extras": {
|
"test-extras": {
|
||||||
"$ref": "#/properties/test-extras"
|
"$ref": "#/properties/test-extras"
|
||||||
},
|
},
|
||||||
|
"test-sources": {
|
||||||
|
"$ref": "#/properties/test-sources"
|
||||||
|
},
|
||||||
"test-groups": {
|
"test-groups": {
|
||||||
"$ref": "#/properties/test-groups"
|
"$ref": "#/properties/test-groups"
|
||||||
},
|
},
|
||||||
@@ -837,6 +864,9 @@
|
|||||||
"test-extras": {
|
"test-extras": {
|
||||||
"$ref": "#/properties/test-extras"
|
"$ref": "#/properties/test-extras"
|
||||||
},
|
},
|
||||||
|
"test-sources": {
|
||||||
|
"$ref": "#/properties/test-sources"
|
||||||
|
},
|
||||||
"test-groups": {
|
"test-groups": {
|
||||||
"$ref": "#/properties/test-groups"
|
"$ref": "#/properties/test-groups"
|
||||||
},
|
},
|
||||||
@@ -885,6 +915,9 @@
|
|||||||
"test-extras": {
|
"test-extras": {
|
||||||
"$ref": "#/properties/test-extras"
|
"$ref": "#/properties/test-extras"
|
||||||
},
|
},
|
||||||
|
"test-sources": {
|
||||||
|
"$ref": "#/properties/test-sources"
|
||||||
|
},
|
||||||
"test-groups": {
|
"test-groups": {
|
||||||
"$ref": "#/properties/test-groups"
|
"$ref": "#/properties/test-groups"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ repair-wheel-command = ""
|
|||||||
|
|
||||||
test-command = ""
|
test-command = ""
|
||||||
before-test = ""
|
before-test = ""
|
||||||
|
test-sources = []
|
||||||
test-requires = []
|
test-requires = []
|
||||||
test-extras = []
|
test-extras = []
|
||||||
test-groups = []
|
test-groups = []
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# this file is copied to the testing cwd, to raise the below error message if
|
|
||||||
# pytest/unittest is run from there.
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestStringMethods(unittest.TestCase):
|
|
||||||
def test_fail(self):
|
|
||||||
self.fail(
|
|
||||||
"cibuildwheel executes tests from a different working directory to "
|
|
||||||
"your project. This ensures only your wheel is imported, preventing "
|
|
||||||
"Python from accessing files that haven't been packaged into the "
|
|
||||||
"wheel. Please specify a path to your tests when invoking pytest "
|
|
||||||
"using the {project} placeholder, e.g. `pytest {project}` or "
|
|
||||||
"`pytest {project}/tests`. cibuildwheel will replace {project} with "
|
|
||||||
"the path to your project."
|
|
||||||
)
|
|
||||||
+38
-3
@@ -17,7 +17,7 @@ import time
|
|||||||
import typing
|
import typing
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Generator, Iterable, Mapping, MutableMapping, Sequence
|
from collections.abc import Callable, Generator, Iterable, Mapping, MutableMapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from functools import lru_cache, total_ordering
|
from functools import lru_cache, total_ordering
|
||||||
@@ -36,6 +36,7 @@ from packaging.utils import parse_wheel_filename
|
|||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
from platformdirs import user_cache_path
|
from platformdirs import user_cache_path
|
||||||
|
|
||||||
|
from . import errors
|
||||||
from ._compat import tomllib
|
from ._compat import tomllib
|
||||||
from .architecture import Architecture
|
from .architecture import Architecture
|
||||||
from .errors import FatalError
|
from .errors import FatalError
|
||||||
@@ -66,8 +67,6 @@ install_certifi_script: Final[Path] = resources_dir / "install_certifi.py"
|
|||||||
|
|
||||||
free_thread_enable_313: Final[Path] = resources_dir / "free-threaded-enable-313.xml"
|
free_thread_enable_313: Final[Path] = resources_dir / "free-threaded-enable-313.xml"
|
||||||
|
|
||||||
test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py"
|
|
||||||
|
|
||||||
|
|
||||||
class EnableGroups(enum.Enum):
|
class EnableGroups(enum.Enum):
|
||||||
"""
|
"""
|
||||||
@@ -425,6 +424,42 @@ def move_file(src_file: Path, dst_file: Path) -> Path:
|
|||||||
return Path(resulting_file).resolve(strict=True)
|
return Path(resulting_file).resolve(strict=True)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_into_local(src: Path, dst: PurePath) -> None:
|
||||||
|
"""Copy a path from src to dst, regardless of whether it's a file or a directory."""
|
||||||
|
# Ensure the target folder location exists
|
||||||
|
Path(dst.parent).mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
if src.is_dir():
|
||||||
|
shutil.copytree(src, dst)
|
||||||
|
else:
|
||||||
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_test_sources(
|
||||||
|
test_sources: list[str],
|
||||||
|
package_dir: Path,
|
||||||
|
test_dir: PurePath,
|
||||||
|
copy_into: Callable[[Path, PurePath], None] = copy_into_local,
|
||||||
|
) -> None:
|
||||||
|
"""Copy the list of test sources from the package to the test directory.
|
||||||
|
|
||||||
|
:param test_sources: A list of test paths, relative to the package_dir.
|
||||||
|
:param package_dir: The root of the package directory.
|
||||||
|
:param test_dir: The folder where test sources should be placed.
|
||||||
|
:param copy_info: The copy function to use. By default, does a local
|
||||||
|
filesystem copy; but an OCIContainer.copy_info method (or equivalent)
|
||||||
|
can be provided.
|
||||||
|
"""
|
||||||
|
for test_path in test_sources:
|
||||||
|
source = package_dir.resolve() / test_path
|
||||||
|
|
||||||
|
if not source.exists():
|
||||||
|
msg = f"Test source {test_path} does not exist."
|
||||||
|
raise errors.FatalError(msg)
|
||||||
|
|
||||||
|
copy_into(source, test_dir / test_path)
|
||||||
|
|
||||||
|
|
||||||
class DependencyConstraints:
|
class DependencyConstraints:
|
||||||
def __init__(self, base_file_path: Path):
|
def __init__(self, base_file_path: Path):
|
||||||
assert base_file_path.exists()
|
assert base_file_path.exists()
|
||||||
|
|||||||
+12
-4
@@ -27,6 +27,7 @@ from .util import (
|
|||||||
BuildSelector,
|
BuildSelector,
|
||||||
call,
|
call,
|
||||||
combine_constraints,
|
combine_constraints,
|
||||||
|
copy_test_sources,
|
||||||
download,
|
download,
|
||||||
extract_zip,
|
extract_zip,
|
||||||
find_compatible_wheel,
|
find_compatible_wheel,
|
||||||
@@ -38,7 +39,6 @@ from .util import (
|
|||||||
read_python_configs,
|
read_python_configs,
|
||||||
shell,
|
shell,
|
||||||
split_config_settings,
|
split_config_settings,
|
||||||
test_fail_cwd_file,
|
|
||||||
unwrap,
|
unwrap,
|
||||||
virtualenv,
|
virtualenv,
|
||||||
)
|
)
|
||||||
@@ -572,9 +572,17 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
package=options.globals.package_dir.resolve(),
|
package=options.globals.package_dir.resolve(),
|
||||||
wheel=repaired_wheel,
|
wheel=repaired_wheel,
|
||||||
)
|
)
|
||||||
test_cwd = identifier_tmp_dir / "test_cwd"
|
if build_options.test_sources:
|
||||||
test_cwd.mkdir()
|
test_cwd = identifier_tmp_dir / "test_cwd"
|
||||||
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
|
test_cwd.mkdir()
|
||||||
|
copy_test_sources(
|
||||||
|
build_options.test_sources,
|
||||||
|
build_options.package_dir,
|
||||||
|
test_cwd,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# There are no test sources. Run the tests in the project directory.
|
||||||
|
test_cwd = Path(".").resolve()
|
||||||
|
|
||||||
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
|
||||||
|
|
||||||
|
|||||||
+41
-5
@@ -1424,17 +1424,22 @@ Platform-specific environment variables are also available:<br/>
|
|||||||
> Execute a shell command to test each built wheel
|
> Execute a shell command to test each built wheel
|
||||||
|
|
||||||
Shell command to run tests after the build. The wheel will be installed
|
Shell command to run tests after the build. The wheel will be installed
|
||||||
automatically and available for import from the tests. To ensure the wheel is
|
automatically and available for import from the tests. If this variable is not
|
||||||
imported by your tests (instead of your source copy), **tests are not run from
|
set, your wheel will not be installed after building.
|
||||||
your project directory**. Use the placeholders `{project}` and `{package}` when
|
|
||||||
specifying paths in your project. If this variable is not set, your wheel will
|
By default, tests are executed from from your project directory. When specifying
|
||||||
not be installed after building.
|
`CIBW_TEST_COMMAND`, you can use the placeholders `{project}` and `{package}` to
|
||||||
|
pass in the location of your test code:
|
||||||
|
|
||||||
- `{project}` is an absolute path to the project root - the working directory
|
- `{project}` is an absolute path to the project root - the working directory
|
||||||
where cibuildwheel was called.
|
where cibuildwheel was called.
|
||||||
- `{package}` is the path to the package being built - the `package_dir`
|
- `{package}` is the path to the package being built - the `package_dir`
|
||||||
argument supplied to cibuildwheel on the command line.
|
argument supplied to cibuildwheel on the command line.
|
||||||
|
|
||||||
|
Alternatively, you can use the [`CIBW_TEST_SOURCES`](#test-sources) setting to
|
||||||
|
create a temporary folder populated with a specific subset of project files to
|
||||||
|
run your test suite.
|
||||||
|
|
||||||
The command is run in a shell, so you can write things like `cmd1 && cmd2`.
|
The command is run in a shell, so you can write things like `cmd1 && cmd2`.
|
||||||
|
|
||||||
Platform-specific environment variables are also available:<br/>
|
Platform-specific environment variables are also available:<br/>
|
||||||
@@ -1549,6 +1554,37 @@ Platform-specific environment variables are also available:<br/>
|
|||||||
In configuration files, you can use an array, and the items will be joined with `&&`.
|
In configuration files, you can use an array, and the items will be joined with `&&`.
|
||||||
|
|
||||||
|
|
||||||
|
### `CIBW_TEST_SOURCES` {: #test-sources}
|
||||||
|
> Files and folders from the source tree that are copied into an isolated tree before running the tests
|
||||||
|
|
||||||
|
A space-separated list of files and folders, relative to the root of the
|
||||||
|
project, required for running the tests. If specified, these files and folders
|
||||||
|
will be copied into a temporary folder, and that temporary folder will be used
|
||||||
|
as the working directory for running the test suite.
|
||||||
|
|
||||||
|
Platform-specific environment variables are also available:<br/>
|
||||||
|
`CIBW_TEST_SOURCES_MACOS` | `CIBW_TEST_SOURCES_WINDOWS` | `CIBW_TEST_SOURCES_LINUX` | `CIBW_TEST_SOURCES_PYODIDE`
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
!!! tab examples "Environment variables"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Copy the "tests" folder, plus "data/test-image.png" from the source folder to the test folder.
|
||||||
|
CIBW_TEST_SOURCES: tests data/test-image.png
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! tab examples "pyproject.toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Copy the "tests" folder, plus "data/test-image.png" from the source folder to the test folder.
|
||||||
|
[tool.cibuildwheel]
|
||||||
|
test-sources = ["tests", "data/test-image.png"]
|
||||||
|
```
|
||||||
|
|
||||||
|
In configuration files, you can use an array, and the items will be joined with a space.
|
||||||
|
|
||||||
|
|
||||||
### `CIBW_TEST_REQUIRES` {: #test-requires}
|
### `CIBW_TEST_REQUIRES` {: #test-requires}
|
||||||
> Install Python dependencies before running the tests
|
> Install Python dependencies before running the tests
|
||||||
|
|
||||||
|
|||||||
@@ -101,18 +101,19 @@ ctypes_project.files["setup.py"] = textwrap.dedent(
|
|||||||
setup(
|
setup(
|
||||||
name="ctypesexample",
|
name="ctypesexample",
|
||||||
version="1.0.0",
|
version="1.0.0",
|
||||||
|
package_dir = {"": "src"},
|
||||||
py_modules = ["ctypesexample.summing"],
|
py_modules = ["ctypesexample.summing"],
|
||||||
ext_modules=[
|
ext_modules=[
|
||||||
CTypesExtension(
|
CTypesExtension(
|
||||||
"ctypesexample.csumlib",
|
"ctypesexample.csumlib",
|
||||||
["ctypesexample/csumlib.c"],
|
["src/ctypesexample/csumlib.c"],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
cmdclass={'build_ext': build_ext, 'bdist_wheel': bdist_wheel_abi_none},
|
cmdclass={'build_ext': build_ext, 'bdist_wheel': bdist_wheel_abi_none},
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
ctypes_project.files["ctypesexample/csumlib.c"] = textwrap.dedent(
|
ctypes_project.files["src/ctypesexample/csumlib.c"] = textwrap.dedent(
|
||||||
"""
|
"""
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#define LIBRARY_API __declspec(dllexport)
|
#define LIBRARY_API __declspec(dllexport)
|
||||||
@@ -136,7 +137,7 @@ ctypes_project.files["ctypesexample/csumlib.c"] = textwrap.dedent(
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
ctypes_project.files["ctypesexample/summing.py"] = textwrap.dedent(
|
ctypes_project.files["src/ctypesexample/summing.py"] = textwrap.dedent(
|
||||||
"""
|
"""
|
||||||
import ctypes
|
import ctypes
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|||||||
+36
-27
@@ -186,34 +186,43 @@ def test_failing_test(tmp_path):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_runner", ["pytest", "unittest"])
|
@pytest.mark.parametrize("test_runner", ["pytest", "unittest"])
|
||||||
def test_bare_pytest_invocation(
|
def test_bare_pytest_invocation(tmp_path: Path, test_runner: str) -> None:
|
||||||
tmp_path: Path, capfd: pytest.CaptureFixture[str], test_runner: str
|
"""Check that if a user runs a bare test suite, it runs in the project folder"""
|
||||||
) -> None:
|
|
||||||
"""Check that if a user runs pytest in the the test cwd, it raises a helpful error"""
|
|
||||||
project_dir = tmp_path / "project"
|
project_dir = tmp_path / "project"
|
||||||
output_dir = tmp_path / "output"
|
|
||||||
project_with_a_test.generate(project_dir)
|
project_with_a_test.generate(project_dir)
|
||||||
|
|
||||||
with pytest.raises(subprocess.CalledProcessError):
|
actual_wheels = utils.cibuildwheel_run(
|
||||||
utils.cibuildwheel_run(
|
project_dir,
|
||||||
project_dir,
|
add_env={
|
||||||
output_dir=output_dir,
|
"CIBW_TEST_REQUIRES": "pytest" if test_runner == "pytest" else "",
|
||||||
add_env={
|
"CIBW_TEST_COMMAND": (
|
||||||
"CIBW_TEST_REQUIRES": "pytest" if test_runner == "pytest" else "",
|
"python -m pytest"
|
||||||
"CIBW_TEST_COMMAND": (
|
if test_runner == "pytest"
|
||||||
"python -m pytest" if test_runner == "pytest" else "python -m unittest"
|
else "python -m unittest discover test spam_test.py"
|
||||||
),
|
),
|
||||||
# Skip CPython 3.8 on macOS arm64, see comment above in
|
},
|
||||||
# 'test_failing_test'
|
|
||||||
"CIBW_SKIP": "cp38-macosx_arm64",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert len(os.listdir(output_dir)) == 0
|
|
||||||
|
|
||||||
captured = capfd.readouterr()
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"Please specify a path to your tests when invoking pytest using the {project} placeholder"
|
|
||||||
in captured.out + captured.err
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# check that we got the right wheels
|
||||||
|
expected_wheels = utils.expected_wheels("spam", "0.1.0")
|
||||||
|
assert set(actual_wheels) == set(expected_wheels)
|
||||||
|
|
||||||
|
|
||||||
|
def test_test_sources(tmp_path):
|
||||||
|
project_dir = tmp_path / "project"
|
||||||
|
project_with_a_test.generate(project_dir)
|
||||||
|
|
||||||
|
# build and test the wheels in the test cwd, after copying in the test sources.
|
||||||
|
actual_wheels = utils.cibuildwheel_run(
|
||||||
|
project_dir,
|
||||||
|
add_env={
|
||||||
|
"CIBW_TEST_REQUIRES": "pytest",
|
||||||
|
"CIBW_TEST_COMMAND": "pytest",
|
||||||
|
"CIBW_TEST_COMMAND_WINDOWS": "pytest",
|
||||||
|
"CIBW_TEST_SOURCES": "test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# also check that we got the right wheels
|
||||||
|
expected_wheels = utils.expected_wheels("spam", "0.1.0")
|
||||||
|
assert set(actual_wheels) == set(expected_wheels)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ skip = ["*musllinux*"]
|
|||||||
environment = {FOO="BAR"}
|
environment = {FOO="BAR"}
|
||||||
|
|
||||||
test-command = "pyproject"
|
test-command = "pyproject"
|
||||||
|
test-sources = ["test", "other dir"]
|
||||||
|
|
||||||
manylinux-x86_64-image = "manylinux1"
|
manylinux-x86_64-image = "manylinux1"
|
||||||
|
|
||||||
@@ -74,11 +75,13 @@ def test_options_1(tmp_path, monkeypatch):
|
|||||||
local = options.build_options("cp38-manylinux_x86_64")
|
local = options.build_options("cp38-manylinux_x86_64")
|
||||||
assert local.manylinux_images is not None
|
assert local.manylinux_images is not None
|
||||||
assert local.test_command == "pyproject"
|
assert local.test_command == "pyproject"
|
||||||
|
assert local.test_sources == ["test", "other dir"]
|
||||||
assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux1"]
|
assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux1"]
|
||||||
|
|
||||||
local = options.build_options("cp37-manylinux_x86_64")
|
local = options.build_options("cp37-manylinux_x86_64")
|
||||||
assert local.manylinux_images is not None
|
assert local.manylinux_images is not None
|
||||||
assert local.test_command == "pyproject-override"
|
assert local.test_command == "pyproject-override"
|
||||||
|
assert local.test_sources == ["test", "other dir"]
|
||||||
assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux2014"]
|
assert local.manylinux_images["x86_64"] == pinned_x86_64_container_image["manylinux2014"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shlex
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -23,6 +24,7 @@ test-command = "pyproject"
|
|||||||
test-requires = "something"
|
test-requires = "something"
|
||||||
test-extras = ["one", "two"]
|
test-extras = ["one", "two"]
|
||||||
test-groups = ["three", "four"]
|
test-groups = ["three", "four"]
|
||||||
|
test-sources = ["five", "six and seven"]
|
||||||
|
|
||||||
manylinux-x86_64-image = "manylinux1"
|
manylinux-x86_64-image = "manylinux1"
|
||||||
|
|
||||||
@@ -50,6 +52,10 @@ def test_simple_settings(tmp_path, platform, fname):
|
|||||||
|
|
||||||
assert options_reader.get("test-command") == "pyproject"
|
assert options_reader.get("test-command") == "pyproject"
|
||||||
assert options_reader.get("archs", option_format=ListFormat(" ")) == "auto"
|
assert options_reader.get("archs", option_format=ListFormat(" ")) == "auto"
|
||||||
|
assert (
|
||||||
|
options_reader.get("test-sources", option_format=ListFormat(" ", quote=shlex.quote))
|
||||||
|
== "five 'six and seven'"
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("test-requires", option_format=ListFormat(" "))
|
options_reader.get("test-requires", option_format=ListFormat(" "))
|
||||||
== {"windows": "something", "macos": "else", "linux": "other many"}[platform]
|
== {"windows": "something", "macos": "else", "linux": "other many"}[platform]
|
||||||
@@ -90,6 +96,7 @@ def test_envvar_override(tmp_path, platform):
|
|||||||
"CIBW_TEST_GROUPS": "mgroup two",
|
"CIBW_TEST_GROUPS": "mgroup two",
|
||||||
"CIBW_TEST_REQUIRES_LINUX": "scod",
|
"CIBW_TEST_REQUIRES_LINUX": "scod",
|
||||||
"CIBW_TEST_GROUPS_LINUX": "lgroup",
|
"CIBW_TEST_GROUPS_LINUX": "lgroup",
|
||||||
|
"CIBW_TEST_SOURCES": 'first "second third"',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,6 +106,10 @@ def test_envvar_override(tmp_path, platform):
|
|||||||
assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_24"
|
assert options_reader.get("manylinux-x86_64-image") == "manylinux_2_24"
|
||||||
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
assert options_reader.get("manylinux-i686-image") == "manylinux2014"
|
||||||
|
|
||||||
|
assert (
|
||||||
|
options_reader.get("test-sources", option_format=ListFormat(" ", quote=shlex.quote))
|
||||||
|
== 'first "second third"'
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
options_reader.get("test-requires", option_format=ListFormat(" "))
|
options_reader.get("test-requires", option_format=ListFormat(" "))
|
||||||
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
|
== {"windows": "docs", "macos": "docs", "linux": "scod"}[platform]
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import textwrap
|
import textwrap
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
|
from unittest.mock import Mock, call
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from cibuildwheel import errors
|
||||||
from cibuildwheel.util import (
|
from cibuildwheel.util import (
|
||||||
FlexibleVersion,
|
FlexibleVersion,
|
||||||
|
copy_test_sources,
|
||||||
find_compatible_wheel,
|
find_compatible_wheel,
|
||||||
fix_ansi_codes_for_github_actions,
|
fix_ansi_codes_for_github_actions,
|
||||||
format_safe,
|
format_safe,
|
||||||
@@ -221,3 +224,144 @@ def test_flexible_version_comparisons():
|
|||||||
assert FlexibleVersion("1.0.1-rhel") > FlexibleVersion("1.0")
|
assert FlexibleVersion("1.0.1-rhel") > FlexibleVersion("1.0")
|
||||||
assert FlexibleVersion("1.0.1-rhel") < FlexibleVersion("1.1")
|
assert FlexibleVersion("1.0.1-rhel") < FlexibleVersion("1.1")
|
||||||
assert FlexibleVersion("1.0.1") == FlexibleVersion("v1.0.1")
|
assert FlexibleVersion("1.0.1") == FlexibleVersion("v1.0.1")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_project(tmp_path):
|
||||||
|
"""Create a directory structure that contains a range of files."""
|
||||||
|
project_path = tmp_path / "project"
|
||||||
|
|
||||||
|
(project_path / "src/deep").mkdir(parents=True)
|
||||||
|
(project_path / "tests/deep").mkdir(parents=True)
|
||||||
|
(project_path / "other").mkdir(parents=True)
|
||||||
|
|
||||||
|
(project_path / "pyproject.toml").write_text("A pyproject.toml file")
|
||||||
|
(project_path / "test.cfg").write_text("A test config file")
|
||||||
|
|
||||||
|
(project_path / "src/__init__.py").write_text("source init")
|
||||||
|
(project_path / "src/module.py").write_text("source module")
|
||||||
|
(project_path / "src/deep/__init__.py").write_text("deep source init")
|
||||||
|
|
||||||
|
(project_path / "tests/test_module.py").write_text("test module")
|
||||||
|
(project_path / "tests/deep/test_module.py").write_text("deep test module")
|
||||||
|
(project_path / "tests/deep/__init__.py").write_text("deep test init")
|
||||||
|
|
||||||
|
(project_path / "other/module.py").write_text("other module")
|
||||||
|
|
||||||
|
return project_path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_sources", "expected", "not_expected"),
|
||||||
|
[
|
||||||
|
# Empty test_sources copies nothing.
|
||||||
|
pytest.param(
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
"pyproject.toml",
|
||||||
|
"test.cfg",
|
||||||
|
"other/module.py",
|
||||||
|
"src/__init__.py",
|
||||||
|
"src/module.py",
|
||||||
|
"src/deep/__init__.py",
|
||||||
|
"tests/test_module.py",
|
||||||
|
"tests/deep/__init__.py",
|
||||||
|
"tests/deep/test_module.py",
|
||||||
|
],
|
||||||
|
id="empty",
|
||||||
|
),
|
||||||
|
# Single standalone files
|
||||||
|
pytest.param(
|
||||||
|
["pyproject.toml", "tests/deep/test_module.py"],
|
||||||
|
["pyproject.toml", "tests/deep/test_module.py"],
|
||||||
|
[
|
||||||
|
"test.cfg",
|
||||||
|
"other/module.py",
|
||||||
|
"src/__init__.py",
|
||||||
|
"src/module.py",
|
||||||
|
"src/deep/__init__.py",
|
||||||
|
"tests/test_module.py",
|
||||||
|
"tests/deep/__init__.py",
|
||||||
|
],
|
||||||
|
id="single-file",
|
||||||
|
),
|
||||||
|
# A full Directory
|
||||||
|
pytest.param(
|
||||||
|
["tests"],
|
||||||
|
[
|
||||||
|
"tests/test_module.py",
|
||||||
|
"tests/deep/__init__.py",
|
||||||
|
"tests/deep/test_module.py",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"pyproject.toml",
|
||||||
|
"test.cfg",
|
||||||
|
"other/module.py",
|
||||||
|
"src/__init__.py",
|
||||||
|
"src/module.py",
|
||||||
|
"src/deep/__init__.py",
|
||||||
|
],
|
||||||
|
id="top-level-directory",
|
||||||
|
),
|
||||||
|
# A partial deep directory
|
||||||
|
pytest.param(
|
||||||
|
["tests/deep"],
|
||||||
|
[
|
||||||
|
"tests/deep/__init__.py",
|
||||||
|
"tests/deep/test_module.py",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"pyproject.toml",
|
||||||
|
"test.cfg",
|
||||||
|
"other/module.py",
|
||||||
|
"src/__init__.py",
|
||||||
|
"src/module.py",
|
||||||
|
"src/deep/__init__.py",
|
||||||
|
"tests/test_module.py",
|
||||||
|
],
|
||||||
|
id="partial-directory",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_copy_test_sources(tmp_path, sample_project, test_sources, expected, not_expected):
|
||||||
|
"""Test sources can be copied into the test directory."""
|
||||||
|
target = tmp_path / "somewhere/test_cwd"
|
||||||
|
copy_test_sources(test_sources, sample_project, target)
|
||||||
|
|
||||||
|
for path in expected:
|
||||||
|
assert (tmp_path / "somewhere/test_cwd" / path).is_file()
|
||||||
|
|
||||||
|
for path in not_expected:
|
||||||
|
assert not (tmp_path / "somewhere/test_cwd" / path).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_copy_test_sources_missing_file(tmp_path, sample_project):
|
||||||
|
"""If test_sources references a folder that doesn't exist, an error is raised."""
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
errors.FatalError,
|
||||||
|
match=r"Test source tests/does_not_exist.py does not exist.",
|
||||||
|
):
|
||||||
|
copy_test_sources(
|
||||||
|
["pyproject.toml", "tests/does_not_exist.py"],
|
||||||
|
sample_project,
|
||||||
|
tmp_path / "somewhere/test_cwd",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_copy_test_sources_alternate_copy_into(sample_project):
|
||||||
|
"""If an alternate copy_into method is provided, it is used."""
|
||||||
|
|
||||||
|
target = PurePath("/container/test_cwd")
|
||||||
|
copy_into = Mock()
|
||||||
|
|
||||||
|
copy_test_sources(["pyproject.toml", "tests"], sample_project, target, copy_into=copy_into)
|
||||||
|
|
||||||
|
copy_into.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(sample_project / "pyproject.toml", target / "pyproject.toml"),
|
||||||
|
call(sample_project / "tests", target / "tests"),
|
||||||
|
],
|
||||||
|
any_order=True,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user