Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
100 changes: 48 additions & 52 deletions cibuildwheel/platforms/ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from ..options import Options
from ..selector import BuildSelector
from ..util import resources
from ..util.cmd import call, shell
from ..util.cmd import call, shell, split_command
from ..util.file import (
CIBW_CACHE_PATH,
copy_test_sources,
Expand Down Expand Up @@ -618,62 +618,58 @@ def build(options: Options, tmp_path: Path) -> None:
)
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]
if first_part == "pytest":
# pytest works exactly the same as a module, so we
# can just run it as a module.
log.warning(
unwrap_preserving_paragraphs(f"""
iOS tests configured with a test command which doesn't start
with 'python -m'. iOS tests must execute python modules - other
entrypoints are not supported.

cibuildwheel will try to execute it as if it started with
'python -m'. If this works, all you need to do is add that to
your test command.

Test command: {build_options.test_command!r}
""")
)
else:
msg = unwrap_preserving_paragraphs(
f"""
iOS tests configured with a test command which doesn't start
with 'python -m'. iOS tests must execute python modules - other
entrypoints are not supported.

Test command: {build_options.test_command!r}
"""
)
raise errors.FatalError(msg)
else:
# the testbed run command actually doesn't want the
# python -m prefix - it's implicit, so we remove it
# here.
test_command_parts = test_command_parts[2:]

test_command_list = shlex.split(build_options.test_command)
try:
call(
"python",
testbed_path,
"run",
*(["--verbose"] if build_options.build_verbosity > 0 else []),
"--",
*test_command_parts,
env=test_env,
)
failed = False
for test_command_parts in split_command(test_command_list):
match test_command_parts:
case ["python", "-m", *rest]:
final_command = rest
case ["pytest", *rest]:
# pytest works exactly the same as a module, so we
# can just run it as a module.
msg = unwrap_preserving_paragraphs(f"""
iOS tests configured with a test command which doesn't start
with 'python -m'. iOS tests must execute python modules - other
entrypoints are not supported.

cibuildwheel will try to execute it as if it started with
'python -m'. If this works, all you need to do is add that to
your test command.

Test command: {build_options.test_command!r}
""")
log.warning(msg)
final_command = ["pytest", *rest]
case _:
msg = unwrap_preserving_paragraphs(
f"""
iOS tests configured with a test command which doesn't start
with 'python -m'. iOS tests must execute python modules - other
entrypoints are not supported.

Test command: {build_options.test_command!r}
"""
)
raise errors.FatalError(msg)

call(
"python",
testbed_path,
"run",
*(["--verbose"] if build_options.build_verbosity > 0 else []),
"--",
*final_command,
env=test_env,
)
except subprocess.CalledProcessError:
failed = True

log.step_end(success=not failed)

if failed:
# catches the first test command failure in the loop,
# implementing short-circuiting
log.step_end(success=False)
log.error(f"Test suite failed on {config.identifier}")
sys.exit(1)

log.step_end()

# We're all done here; move it to output (overwrite existing)
if compatible_wheel is None:
output_wheel = build_options.output_dir.joinpath(built_wheel.name)
Expand Down
17 changes: 16 additions & 1 deletion cibuildwheel/util/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import subprocess
import sys
import typing
from collections.abc import Mapping
from collections.abc import Iterator, Mapping
from typing import Final, Literal

from ..errors import FatalError
Expand Down Expand Up @@ -81,3 +81,18 @@ def shell(
command = " ".join(commands)
print(f"+ {command}")
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)


def split_command(lst: list[str]) -> Iterator[list[str]]:
"""
Split a shell-style command, as returned by shlex.split, into a sequence
of commands, separated by '&&'.
"""
items = list[str]()
for item in lst:
if item == "&&":
yield items
items = []
else:
items.append(item)
yield items
40 changes: 38 additions & 2 deletions test/test_ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
"CIBW_BUILD": "cp313-*",
"CIBW_XBUILD_TOOLS": "does-exist",
"CIBW_TEST_SOURCES": "tests",
"CIBW_TEST_COMMAND": "python -m unittest discover tests test_platform.py",
"CIBW_TEST_COMMAND": "python -m this && python -m unittest discover tests test_platform.py",
"CIBW_BUILD_VERBOSITY": "1",
**build_config,
},
Expand All @@ -102,6 +102,9 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
captured = capfd.readouterr()
assert "'does-exist' will be included in the cross-build environment" in captured.out

# Make sure the first command ran
assert "Zen of Python" in captured.out


@pytest.mark.serial
def test_no_test_sources(tmp_path, capfd):
Expand Down Expand Up @@ -134,7 +137,10 @@ def test_no_test_sources(tmp_path, capfd):


def test_ios_testing_with_placeholder(tmp_path, capfd):
"""Build will run tests with the {project} placeholder."""
"""
Tests with the {project} placeholder are not supported on iOS, because the test command
is run in the simulator.
"""
skip_if_ios_testing_not_supported()

project_dir = tmp_path / "project"
Expand All @@ -159,6 +165,36 @@ def test_ios_testing_with_placeholder(tmp_path, capfd):
assert "iOS tests cannot use placeholders" in captured.out + captured.err


@pytest.mark.serial
def test_ios_test_command_short_circuit(tmp_path, capfd):
skip_if_ios_testing_not_supported()

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):
# `python -m not_a_module` will fail, so `python -m this` should not be run.
utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_PLATFORM": "ios",
"CIBW_BUILD": "cp313-*",
"CIBW_XBUILD_TOOLS": "",
"CIBW_TEST_SOURCES": "tests",
"CIBW_TEST_COMMAND": "python -m not_a_module && python -m this",
"CIBW_BUILD_VERBOSITY": "1",
},
)

captured = capfd.readouterr()

assert "No module named not_a_module" in captured.out + captured.err
# assert that `python -m this` was not run
assert "Zen of Python" not in captured.out + captured.err


def test_missing_xbuild_tool(tmp_path, capfd):
"""Build will fail if xbuild-tools references a non-existent tool."""
skip_if_ios_testing_not_supported()
Expand Down