Skip to content

Commit

Permalink
Add test_sources configuration option.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Oct 30, 2024
1 parent 66d45de commit 23d3ca2
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 10 deletions.
11 changes: 10 additions & 1 deletion cibuildwheel/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .util import (
BuildFrontendConfig,
BuildSelector,
copy_test_sources,
find_compatible_wheel,
get_build_verbosity_extra_flags,
prepare_command,
Expand Down Expand Up @@ -395,7 +396,15 @@ def build_in_container(
)
test_cwd = testing_temp_dir / "test_cwd"
container.call(["mkdir", "-p", test_cwd])
container.copy_into(test_fail_cwd_file, test_cwd / "test_fail.py")
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
build_options.package_dir,
test_cwd,
copy_into=container.copy_into,
)
else:
container.copy_into(test_fail_cwd_file, test_cwd / "test_fail.py")

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

Expand Down
11 changes: 10 additions & 1 deletion cibuildwheel/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
BuildSelector,
call,
combine_constraints,
copy_test_sources,
detect_ci_provider,
download,
find_compatible_wheel,
Expand Down Expand Up @@ -738,7 +739,15 @@ def build(options: Options, tmp_path: Path) -> None:

test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir(exist_ok=True)
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
build_options.package_dir,
test_cwd,
)
else:
# There are no test sources. Copy the test safety file.
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())

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

Expand Down
5 changes: 5 additions & 0 deletions cibuildwheel/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class BuildOptions:
dependency_constraints: DependencyConstraints | None
test_command: str | None
before_test: str | None
test_sources: list[str]
test_requires: list[str]
test_extras: str
build_verbosity: int
Expand Down Expand Up @@ -668,6 +669,9 @@ def build_options(self, identifier: str | None) -> BuildOptions:
dependency_versions = self.reader.get("dependency-versions")
test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && "))
before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && "))
test_sources = self.reader.get(
"test-sources", option_format=ListFormat(sep=" ")
).split()
test_requires = self.reader.get(
"test-requires", option_format=ListFormat(sep=" ")
).split()
Expand Down Expand Up @@ -771,6 +775,7 @@ def build_options(self, identifier: str | None) -> BuildOptions:
return BuildOptions(
globals=self.globals,
test_command=test_command,
test_sources=test_sources,
test_requires=test_requires,
test_extras=test_extras,
before_test=before_test,
Expand Down
33 changes: 33 additions & 0 deletions cibuildwheel/resources/cibuildwheel.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,21 @@
],
"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-requires": {
"description": "Install Python dependencies before running the tests",
"oneOf": [
Expand Down Expand Up @@ -485,6 +500,9 @@
"test-extras": {
"$ref": "#/$defs/inherit"
},
"test-sources": {
"$ref": "#/$defs/inherit"
},
"test-requires": {
"$ref": "#/$defs/inherit"
}
Expand Down Expand Up @@ -571,6 +589,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down Expand Up @@ -675,6 +696,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down Expand Up @@ -720,6 +744,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down Expand Up @@ -778,6 +805,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down Expand Up @@ -823,6 +853,9 @@
"test-extras": {
"$ref": "#/properties/test-extras"
},
"test-sources": {
"$ref": "#/properties/test-sources"
},
"test-requires": {
"$ref": "#/properties/test-requires"
}
Expand Down
1 change: 1 addition & 0 deletions cibuildwheel/resources/defaults.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ repair-wheel-command = ""

test-command = ""
before-test = ""
test-sources = []
test-requires = []
test-extras = []

Expand Down
4 changes: 3 additions & 1 deletion cibuildwheel/resources/testing_temp_dir_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ def test_fail(self):
"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."
"the path to your project. Alternatively, use `test-sources` to "
"specify the files and folders that should be copied into the test "
"environment."
)
39 changes: 38 additions & 1 deletion cibuildwheel/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import typing
import urllib.request
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 enum import Enum
from functools import lru_cache, total_ordering
Expand All @@ -35,6 +35,7 @@
from packaging.version import Version
from platformdirs import user_cache_path

from . import errors
from ._compat import tomllib
from .architecture import Architecture
from .typing import PathOrStr, PlatformName
Expand Down Expand Up @@ -394,6 +395,42 @@ def move_file(src_file: Path, dst_file: Path) -> Path:
return Path(resulting_file).resolve(strict=True)


def copy_into(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,

Check warning on line 413 in cibuildwheel/util.py

View workflow job for this annotation

GitHub Actions / Linters (mypy, flake8, etc.)

W0621

Redefining name 'copy_into' from outer scope (line 398)
) -> 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:
def __init__(self, base_file_path: Path):
assert base_file_path.exists()
Expand Down
11 changes: 10 additions & 1 deletion cibuildwheel/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
BuildSelector,
call,
combine_constraints,
copy_test_sources,
download,
extract_zip,
find_compatible_wheel,
Expand Down Expand Up @@ -574,7 +575,15 @@ def build(options: Options, tmp_path: Path) -> None:
)
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir()
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
build_options.package_dir,
test_cwd,
)
else:
# There are no test sources. Copy the test safety file.
(test_cwd / "test_fail.py").write_text(test_fail_cwd_file.read_text())

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

Expand Down
46 changes: 41 additions & 5 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -1410,17 +1410,24 @@ Platform-specific environment variables are also available:<br/>
> Execute a shell command to test each built wheel
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
imported by your tests (instead of your source copy), **tests are not run from
your project directory**. Use the placeholders `{project}` and `{package}` when
specifying paths in your project. If this variable is not set, your wheel will
not be installed after building.
automatically and available for import from the tests. If this variable is not
set, your wheel will not be installed after building.

To ensure the wheel is imported by your tests (instead of your source copy),
**tests are not run from your project directory** - they are run from a
temporary folder generated by cibuildwheel. To reference test sources, you can
use the placeholders `{project}` and `{package}` in your test command to pass in
the location of your test code:

- `{project}` is an absolute path to the project root - the working directory
where cibuildwheel was called.
- `{package}` is the path to the package being built - the `package_dir`
argument supplied to cibuildwheel on the command line.

Alternatively, you can use the `CIBW_TEST_SOURCES` setting to pass in the list
of files and folders to copy into the temporary folder. This should *not*
include any code that is included as part of your wheel.

The command is run in a shell, so you can write things like `cmd1 && cmd2`.

Platform-specific environment variables are also available:<br/>
Expand Down Expand Up @@ -1535,6 +1542,35 @@ Platform-specific environment variables are also available:<br/>
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 must be copied into the test environment before running the tests.
A space-separated list of files and folders, relative to the root of the
project, required for running the tests.

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}
> Install Python dependencies before running the tests
Expand Down
20 changes: 20 additions & 0 deletions test/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,23 @@ def test_bare_pytest_invocation(
"Please specify a path to your tests when invoking pytest using the {project} placeholder"
in captured.out + captured.err
)


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)
Loading

0 comments on commit 23d3ca2

Please sign in to comment.