Revert to running tests from from a temp dir when test-sources is unset (#2420)

* Revert to running tests from from a temp dir when test-sources is unset

* Fix placeholders error message, add test for it

* Add back {project} placeholders to CIBW_TEST_COMMAND in tests & docs

* Update test/test_before_test.py

---------

Co-authored-by: Henry Schreiner <HenrySchreinerIII@gmail.com>
This commit is contained in:
Joe Rickerby
2025-05-28 16:05:16 +01:00
committed by GitHub
co-authored by Henry Schreiner
parent 7298563b25
commit 6379772957
16 changed files with 209 additions and 107 deletions
+31 -11
View File
@@ -559,19 +559,19 @@ def build(options: Options, tmp_path: Path) -> None:
env=test_env,
)
if not build_options.test_sources:
# iOS requires an explicit test-sources, as the project directory
# isn't visible on the simulator.
msg = "Testing on iOS requires a definition of test-sources."
raise errors.FatalError(msg)
testbed_app_path = testbed_path / "iOSTestbed" / "app"
# Copy the test sources to the testbed app
copy_test_sources(
build_options.test_sources,
build_options.package_dir,
testbed_path / "iOSTestbed" / "app",
)
if build_options.test_sources:
copy_test_sources(
build_options.test_sources,
build_options.package_dir,
testbed_app_path,
)
else:
(testbed_app_path / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text()
)
log.step("Installing test requirements...")
# Install the compiled wheel (with any test extras), plus
@@ -598,6 +598,26 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Running test suite...")
# iOS doesn't support placeholders in the test command,
# because the source dir isn't visible on the simulator.
if (
"{project}" in build_options.test_command
or "{package}" in build_options.test_command
):
msg = unwrap_preserving_paragraphs(
f"""
iOS tests configured with a test command that uses the "{{project}}" or
"{{package}}" placeholder. iOS tests cannot use placeholders, because the
source directory is not visible on the simulator.
In addition, iOS tests must run as a Python module, so the test command
must begin with 'python -m'.
Test command: {build_options.test_command!r}
"""
)
raise errors.FatalError(msg)
test_command_parts = shlex.split(build_options.test_command)
if test_command_parts[0:2] != ["python", "-m"]:
first_part = test_command_parts[0]
+6 -4
View File
@@ -398,9 +398,10 @@ def build_in_container(
wheel=wheel_to_test,
)
test_cwd = testing_temp_dir / "test_cwd"
container.call(["mkdir", "-p", test_cwd])
if build_options.test_sources:
test_cwd = testing_temp_dir / "test_cwd"
container.call(["mkdir", "-p", test_cwd])
copy_test_sources(
build_options.test_sources,
build_options.package_dir,
@@ -408,8 +409,9 @@ def build_in_container(
copy_into=container.copy_into,
)
else:
# There are no test sources. Run the tests in the project directory.
test_cwd = PurePosixPath(container_project_path)
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
container.copy_into(resources.TEST_FAIL_CWD_FILE, test_cwd / "test_fail.py")
container.call(["sh", "-c", test_command_prepared], cwd=test_cwd, env=virtualenv_env)
+8 -3
View File
@@ -706,8 +706,9 @@ def build(options: Options, tmp_path: Path) -> None:
wheel=repaired_wheel,
)
test_cwd = identifier_tmp_dir / "test_cwd"
if build_options.test_sources:
test_cwd = identifier_tmp_dir / "test_cwd"
# only create test_cwd if it doesn't already exist - it
# may have been created during a previous `testing_arch`
if not test_cwd.exists():
@@ -718,8 +719,12 @@ def build(options: Options, tmp_path: Path) -> None:
test_cwd,
)
else:
# There are no test sources. Run the tests in the project directory.
test_cwd = Path.cwd()
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
test_cwd.mkdir(exist_ok=True)
(test_cwd / "test_fail.py").write_text(
resources.TEST_FAIL_CWD_FILE.read_text()
)
shell_with_arch(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
+6 -4
View File
@@ -523,17 +523,19 @@ def build(options: Options, tmp_path: Path) -> None:
package=build_options.package_dir.resolve(),
)
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir(exist_ok=True)
if build_options.test_sources:
test_cwd = identifier_tmp_dir / "test_cwd"
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.cwd()
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
(test_cwd / "test_fail.py").write_text(resources.TEST_FAIL_CWD_FILE.read_text())
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
+12 -10
View File
@@ -588,24 +588,26 @@ def build(options: Options, tmp_path: Path) -> None:
# run the tests from a temp dir, with an absolute path in the command
# (this ensures that Python runs the tests against the installed wheel
# and not the repo code)
test_command_prepared = prepare_command(
build_options.test_command,
project=Path.cwd(),
package=options.globals.package_dir.resolve(),
wheel=repaired_wheel,
)
test_cwd = identifier_tmp_dir / "test_cwd"
test_cwd.mkdir()
if build_options.test_sources:
test_cwd = identifier_tmp_dir / "test_cwd"
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.cwd()
# Use the test_fail.py file to raise a nice error if the user
# tries to run tests in the cwd
(test_cwd / "test_fail.py").write_text(resources.TEST_FAIL_CWD_FILE.read_text())
test_command_prepared = prepare_command(
build_options.test_command,
project=Path.cwd(),
package=options.globals.package_dir.resolve(),
wheel=repaired_wheel,
)
shell(test_command_prepared, cwd=test_cwd, env=virtualenv_env)
# we're all done here; move it to output (remove if already exists)
@@ -0,0 +1,36 @@
# this file is copied to the testing cwd, to raise the below error message if
# pytest/unittest is run from there.
import sys
import unittest
from typing import NoReturn
class TestStringMethods(unittest.TestCase):
def test_fail(self) -> NoReturn:
if sys.platform == "ios":
msg = (
"You tried to run tests from the testbed app's working "
"directory, without specifying `test-sources`. "
"On iOS, you must copy your test files to the testbed app by "
"setting the `test-sources` option in your cibuildwheel "
"configuration."
)
else:
msg = (
"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. "
"\n\n"
"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. "
"\n\n"
"Alternatively, you can specify your test files using the "
"`test-sources` option, and cibuildwheel will copy them to the "
"working directory for testing."
)
self.fail(msg)
+1
View File
@@ -17,6 +17,7 @@ CONSTRAINTS: Final[Path] = PATH / "constraints.txt"
VIRTUALENV: Final[Path] = PATH / "virtualenv.toml"
CIBUILDWHEEL_SCHEMA: Final[Path] = PATH / "cibuildwheel.schema.json"
PYTHON_BUILD_STANDALONE_RELEASES: Final[Path] = PATH / "python-build-standalone-releases.json"
TEST_FAIL_CWD_FILE: Final[Path] = PATH / "testing_temp_dir_file.py"
# this value is cached because it's used a lot in unit tests
+6 -17
View File
@@ -17,7 +17,7 @@ cibuildwheel to run tests, add the following YAML to your CI config file:
```yaml
env:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest ./tests"
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
!!! tab "Azure Pipelines"
@@ -27,7 +27,7 @@ cibuildwheel to run tests, add the following YAML to your CI config file:
```yaml
variables:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest ./tests"
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
!!! tab "Travis CI"
@@ -38,18 +38,7 @@ cibuildwheel to run tests, add the following YAML to your CI config file:
env:
global:
- CIBW_TEST_REQUIRES=pytest
- CIBW_TEST_COMMAND="pytest ./tests"
```
!!! tab "AppVeyor"
> appveyor.yml ([docs](https://www.appveyor.com/docs/build-configuration/#environment-variables))
```yaml
environment:
global:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest {project}\\tests"
- CIBW_TEST_COMMAND="pytest {project}/tests"
```
!!! tab "CircleCI"
@@ -61,7 +50,7 @@ cibuildwheel to run tests, add the following YAML to your CI config file:
job_name:
environment:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest ./tests"
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
!!! tab "Gitlab CI"
@@ -72,7 +61,7 @@ cibuildwheel to run tests, add the following YAML to your CI config file:
linux:
variables:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest ./tests"
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
!!! tab "Cirrus CI"
@@ -82,7 +71,7 @@ cibuildwheel to run tests, add the following YAML to your CI config file:
```yaml
env:
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: "pytest ./tests"
CIBW_TEST_COMMAND: "pytest {project}/tests"
```
## Configuration file {: #configuration-file}
+23 -23
View File
@@ -28,7 +28,7 @@ This option can also be set using the [command-line option](#command-line) `--pl
```bash
export CIBW_BUILD='cp37-*'
export CIBW_TEST_COMMAND='pytest ./tests'
export CIBW_TEST_COMMAND='pytest {project}/tests'
cibuildwheel --platform linux .
```
@@ -1284,23 +1284,23 @@ Shell command to run tests after the build. The wheel will be installed
automatically and available for import from the tests. If this variable is not
set, your wheel will not be installed after building.
By default, tests are executed from your project directory. When specifying
`test-command`, you can optionally use the placeholders `{package}` and
`{project}` to pass in the location of your test code:
To ensure the wheel is imported by your tests (instead of your source copy),
**Tests are executed from a temporary directory**, outside of your source
tree. To access your test code, you have a couple of options:
- `{package}` is the path to the package being built - the `package_dir`
argument supplied to cibuildwheel on the command line.
- `{project}` is an absolute path to the project root - the working directory
where cibuildwheel was called.
- You can use the [`test-sources`](#test-sources) setting to copy specific
files from your source tree into the temporary directory. When using
test-sources, use relative paths in your test command, as if they were
relative to the project root.
Using `{package}` or `{project}` used to be required, but since cibuildwheel
3.0, tests are run from the project root by default. This means that you can
use relative paths in your test command, and they will be relative to the
project root.
- You can use the `{package}` or `{project}` placeholders in your
`test-command` to refer to the package being built or the project root,
respectively.
Alternatively, you can use the [`test-sources`](#test-sources) setting to
create a temporary folder populated with a specific subset of project files to
run your test suite.
- `{package}` is the path to the package being built - the `package_dir`
argument supplied to cibuildwheel on the command line.
- `{project}` is an absolute path to the project root - the working
directory where cibuildwheel was called.
On all platforms other than iOS, the command is run in a shell, so you can write things like `cmd1 && cmd2`.
@@ -1318,18 +1318,18 @@ Platform-specific environment variables are also available:<br/>
```toml
[tool.cibuildwheel]
# Run the package tests using `pytest`
test-command = "pytest ./tests"
test-command = "pytest {project}/tests"
# Trigger an install of the package, but run nothing of note
test-command = "echo Wheel installed"
# Multiline example
test-command = [
"pytest ./tests",
"python ./test.py",
"pytest {project}/tests",
"python {project}/test.py",
]
# run tests on ios
# run tests on ios - when test-sources is set, use relative paths, not {project} or {package}
[tool.cibuildwheel.ios]
test-sources = ["tests"]
test-command = "python -m pytest ./tests"
@@ -1341,17 +1341,17 @@ Platform-specific environment variables are also available:<br/>
```yaml
# Run the package tests using `pytest`
CIBW_TEST_COMMAND: pytest ./tests
CIBW_TEST_COMMAND: pytest {project}/tests
# Trigger an install of the package, but run nothing of note
CIBW_TEST_COMMAND: "echo Wheel installed"
# Multi-line example - join with && on all platforms
CIBW_TEST_COMMAND: >
pytest ./tests &&
python ./test.py
pytest {project}/tests &&
python {project}/test.py
# run tests on ios
# run tests on ios - when test-sources is set, use relative paths, not {project} or {package}
CIBW_TEST_SOURCES_IOS: tests
CIBW_TEST_COMMAND_IOS: python -m pytest ./tests
```
+1 -1
View File
@@ -187,7 +187,7 @@ def test_abi_none(tmp_path, capfd):
project_dir,
add_env={
"CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": f"{utils.invoke_pytest()} ./test",
"CIBW_TEST_COMMAND": f"{utils.invoke_pytest()} {{project}}/test",
# limit the number of builds for test performance reasons
"CIBW_BUILD": "cp38-* cp{}{}-* cp313t-* pp310-*".format(*utils.SINGLE_PYTHON_VERSION),
"CIBW_ENABLE": "all",
+2 -2
View File
@@ -62,10 +62,10 @@ def test(tmp_path, build_frontend_env):
"CIBW_TEST_REQUIRES": "pytest",
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test",
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} {{project}}/test",
# pytest fails on GraalPy 24.2.0 on Windows so we skip it there
# until https://github.com/oracle/graalpython/issues/490 is fixed
"CIBW_TEST_COMMAND_WINDOWS": "where graalpy || pytest ./test",
"CIBW_TEST_COMMAND_WINDOWS": "where graalpy || pytest {project}/test",
**build_frontend_env,
},
)
+1 -1
View File
@@ -32,7 +32,7 @@ def test(tmp_path, request):
project_dir,
add_env={
"CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": "pytest ./test",
"CIBW_TEST_COMMAND": "pytest {project}/test",
"CIBW_ARCHS": archs,
# TODO remove me once proper support is added
"CIBW_MANYLINUX_RISCV64_IMAGE": "ghcr.io/mayeut/manylinux_2_35:2025.05.11-1",
+38 -3
View File
@@ -91,8 +91,9 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
assert "'does-exist' will be included in the cross-build environment" in captured.out
@pytest.mark.serial
def test_no_test_sources(tmp_path, capfd):
"""Build will fail if test-sources isn't defined."""
"""Build will provide a helpful error if pytest is run and test-sources is not defined."""
if utils.get_platform() != "macos":
pytest.skip("this test can only run on macOS")
if utils.get_xcode_version() < (13, 0):
@@ -109,13 +110,47 @@ def test_no_test_sources(tmp_path, capfd):
add_env={
"CIBW_PLATFORM": "ios",
"CIBW_BUILD": "cp313-*",
"CIBW_TEST_COMMAND": "python -m tests",
"CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": "python -m pytest",
"CIBW_XBUILD_TOOLS": "",
},
)
# The error message indicates the configuration issue.
captured = capfd.readouterr()
assert "Testing on iOS requires a definition of test-sources." in captured.err
assert (
"you must copy your test files to the testbed app by setting the `test-sources` option"
in captured.out + captured.err
)
def test_ios_testing_with_placeholder(tmp_path, capfd):
"""Build will run tests with the {project} placeholder."""
if utils.get_platform() != "macos":
pytest.skip("this test can only run on macOS")
if utils.get_xcode_version() < (13, 0):
pytest.skip("this test only works with Xcode 13.0 or greater")
project_dir = tmp_path / "project"
basic_project = test_projects.new_c_project()
basic_project.files.update(basic_project_files)
basic_project.generate(project_dir)
with pytest.raises(subprocess.CalledProcessError):
utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_PLATFORM": "ios",
"CIBW_BUILD": "cp313-*",
"CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": "pytest {project}/tests",
"CIBW_XBUILD_TOOLS": "",
},
)
# The error message indicates the configuration issue.
captured = capfd.readouterr()
assert "iOS tests cannot use placeholders" in captured.out + captured.err
def test_missing_xbuild_tool(tmp_path, capfd):
-2
View File
@@ -42,11 +42,9 @@ def test_cross_compiled_build(tmp_path):
@pytest.mark.parametrize(
"test_config",
[
# Run the test suite in the project folder
{
"CIBW_TEST_COMMAND": '''python -c "import platform; print('running tests on ' + platform.machine())"''',
},
# Nominate the set of test sources to copy
{
"CIBW_TEST_COMMAND": "python tests/test_suite.py",
"CIBW_TEST_SOURCES": "tests",
+1 -1
View File
@@ -129,7 +129,7 @@ def test_pyodide_build_and_test(tmp_path, expect_failure):
add_args=["--platform", "pyodide"],
add_env={
"CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": "python -m pytest",
"CIBW_TEST_COMMAND": "python -m pytest {project}",
},
)
# check that the expected wheels are produced
+37 -25
View File
@@ -86,10 +86,10 @@ def test(tmp_path):
"CIBW_TEST_REQUIRES": "pytest",
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test",
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} {{project}}/test",
# pytest fails on GraalPy 24.2.0 on Windows so we skip it there
# until https://github.com/oracle/graalpython/issues/490 is fixed
"CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest ./test",
"CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest {project}/test",
},
)
@@ -109,10 +109,10 @@ def test_extras_require(tmp_path):
"CIBW_TEST_EXTRAS": "test",
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test",
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} {{project}}/test",
# pytest fails on GraalPy 24.2.0 on Windows so we skip it there
# until https://github.com/oracle/graalpython/issues/490 is fixed
"CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest ./test",
"CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest {project}/test",
},
single_python=True,
)
@@ -143,10 +143,10 @@ def test_dependency_groups(tmp_path):
"CIBW_TEST_GROUPS": "dev",
# the 'false ||' bit is to ensure this command runs in a shell on
# mac/linux.
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} ./test",
"CIBW_TEST_COMMAND": f"false || {utils.invoke_pytest()} {{project}}/test",
# pytest fails on GraalPy 24.2.0 on Windows so we skip it there
# until https://github.com/oracle/graalpython/issues/490 is fixed
"CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest ./test",
"CIBW_TEST_COMMAND_WINDOWS": "COLOR 00 || where graalpy || pytest {project}/test",
},
single_python=True,
)
@@ -178,7 +178,7 @@ def test_failing_test(tmp_path):
output_dir=output_dir,
add_env={
"CIBW_TEST_REQUIRES": "pytest",
"CIBW_TEST_COMMAND": f"{utils.invoke_pytest()} ./test",
"CIBW_TEST_COMMAND": f"{utils.invoke_pytest()} {{project}}/test",
# CPython 3.8 when running on macOS arm64 is unusual. The build
# always runs in x86_64, so the arm64 tests are not run. See
# #1169 for reasons why. That means the build succeeds, which
@@ -191,29 +191,41 @@ def test_failing_test(tmp_path):
@pytest.mark.parametrize("test_runner", ["pytest", "unittest"])
def test_bare_pytest_invocation(tmp_path: Path, test_runner: str) -> None:
"""Check that if a user runs a bare test suite, it runs in the project folder"""
def test_bare_pytest_invocation(
tmp_path: Path, capfd: pytest.CaptureFixture[str], test_runner: str
) -> None:
"""
Check that if a user runs pytest in the the test cwd without setting
test-sources, it raises a helpful error
"""
project_dir = tmp_path / "project"
project_with_a_test.generate(project_dir)
output_dir = tmp_path / "output"
actual_wheels = utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_TEST_REQUIRES": "pytest" if test_runner == "pytest" else "",
"CIBW_TEST_COMMAND": (
# pytest fails on GraalPy 24.2.0 on Windows so we skip it there
# until https://github.com/oracle/graalpython/issues/490 fixed
"graalpy.exe -c 1 || python -m pytest"
if test_runner == "pytest"
else "python -m unittest discover test spam_test.py"
),
},
with pytest.raises(subprocess.CalledProcessError):
utils.cibuildwheel_run(
project_dir,
output_dir=output_dir,
add_env={
"CIBW_TEST_REQUIRES": "pytest" if test_runner == "pytest" else "",
"CIBW_TEST_COMMAND": (
"python -m pytest" if test_runner == "pytest" else "python -m unittest"
),
# Skip CPython 3.8 on macOS arm64, see comment above in
# 'test_failing_test'
"CIBW_SKIP": "cp38-macosx_arm64",
},
)
assert len(list(output_dir.iterdir())) == 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"