Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions cibuildwheel/platforms/ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
10 changes: 6 additions & 4 deletions cibuildwheel/platforms/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,18 +398,20 @@ 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,
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)
# 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)

Expand Down
11 changes: 8 additions & 3 deletions cibuildwheel/platforms/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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)

Expand Down
10 changes: 6 additions & 4 deletions cibuildwheel/platforms/pyodide.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,17 +522,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)

Expand Down
22 changes: 12 additions & 10 deletions cibuildwheel/platforms/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions cibuildwheel/resources/testing_temp_dir_file.py
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions cibuildwheel/util/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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
Expand Down
34 changes: 17 additions & 17 deletions docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

- `{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.

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.

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.
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:

- 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.

- You can use the `{package}` or `{project}` placeholders in your
`test-command` to refer to the package being built or the project root,
respectively.

- `{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`.

Expand Down
11 changes: 8 additions & 3 deletions test/test_ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):


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):
Expand All @@ -109,13 +109,18 @@ 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_missing_xbuild_tool(tmp_path, capfd):
Expand Down
48 changes: 30 additions & 18 deletions test/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,28 +191,40 @@ 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",
},
)

# check that we got the right wheels
expected_wheels = utils.expected_wheels("spam", "0.1.0")
assert set(actual_wheels) == set(expected_wheels)
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
)


def test_test_sources(tmp_path):
Expand Down
Loading