Skip to content

Commit ea08120

Browse files
henryiiijoerick
andauthored
feat: support multiple commands on iOS (#2432)
* feat: support multiple commands on iOS Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * Move split_command into a util module * (unrelated) fix test docstring * Implement short-circuit behaviour on test-command --------- Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: Joe Rickerby <joerick@mac.com>
1 parent f8168e2 commit ea08120

File tree

3 files changed

+102
-55
lines changed

3 files changed

+102
-55
lines changed

cibuildwheel/platforms/ios.py

Lines changed: 48 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from ..options import Options
2626
from ..selector import BuildSelector
2727
from ..util import resources
28-
from ..util.cmd import call, shell
28+
from ..util.cmd import call, shell, split_command
2929
from ..util.file import (
3030
CIBW_CACHE_PATH,
3131
copy_test_sources,
@@ -618,62 +618,58 @@ def build(options: Options, tmp_path: Path) -> None:
618618
)
619619
raise errors.FatalError(msg)
620620

621-
test_command_parts = shlex.split(build_options.test_command)
622-
if test_command_parts[0:2] != ["python", "-m"]:
623-
first_part = test_command_parts[0]
624-
if first_part == "pytest":
625-
# pytest works exactly the same as a module, so we
626-
# can just run it as a module.
627-
log.warning(
628-
unwrap_preserving_paragraphs(f"""
629-
iOS tests configured with a test command which doesn't start
630-
with 'python -m'. iOS tests must execute python modules - other
631-
entrypoints are not supported.
632-
633-
cibuildwheel will try to execute it as if it started with
634-
'python -m'. If this works, all you need to do is add that to
635-
your test command.
636-
637-
Test command: {build_options.test_command!r}
638-
""")
639-
)
640-
else:
641-
msg = unwrap_preserving_paragraphs(
642-
f"""
643-
iOS tests configured with a test command which doesn't start
644-
with 'python -m'. iOS tests must execute python modules - other
645-
entrypoints are not supported.
646-
647-
Test command: {build_options.test_command!r}
648-
"""
649-
)
650-
raise errors.FatalError(msg)
651-
else:
652-
# the testbed run command actually doesn't want the
653-
# python -m prefix - it's implicit, so we remove it
654-
# here.
655-
test_command_parts = test_command_parts[2:]
656-
621+
test_command_list = shlex.split(build_options.test_command)
657622
try:
658-
call(
659-
"python",
660-
testbed_path,
661-
"run",
662-
*(["--verbose"] if build_options.build_verbosity > 0 else []),
663-
"--",
664-
*test_command_parts,
665-
env=test_env,
666-
)
667-
failed = False
623+
for test_command_parts in split_command(test_command_list):
624+
match test_command_parts:
625+
case ["python", "-m", *rest]:
626+
final_command = rest
627+
case ["pytest", *rest]:
628+
# pytest works exactly the same as a module, so we
629+
# can just run it as a module.
630+
msg = unwrap_preserving_paragraphs(f"""
631+
iOS tests configured with a test command which doesn't start
632+
with 'python -m'. iOS tests must execute python modules - other
633+
entrypoints are not supported.
634+
635+
cibuildwheel will try to execute it as if it started with
636+
'python -m'. If this works, all you need to do is add that to
637+
your test command.
638+
639+
Test command: {build_options.test_command!r}
640+
""")
641+
log.warning(msg)
642+
final_command = ["pytest", *rest]
643+
case _:
644+
msg = unwrap_preserving_paragraphs(
645+
f"""
646+
iOS tests configured with a test command which doesn't start
647+
with 'python -m'. iOS tests must execute python modules - other
648+
entrypoints are not supported.
649+
650+
Test command: {build_options.test_command!r}
651+
"""
652+
)
653+
raise errors.FatalError(msg)
654+
655+
call(
656+
"python",
657+
testbed_path,
658+
"run",
659+
*(["--verbose"] if build_options.build_verbosity > 0 else []),
660+
"--",
661+
*final_command,
662+
env=test_env,
663+
)
668664
except subprocess.CalledProcessError:
669-
failed = True
670-
671-
log.step_end(success=not failed)
672-
673-
if failed:
665+
# catches the first test command failure in the loop,
666+
# implementing short-circuiting
667+
log.step_end(success=False)
674668
log.error(f"Test suite failed on {config.identifier}")
675669
sys.exit(1)
676670

671+
log.step_end()
672+
677673
# We're all done here; move it to output (overwrite existing)
678674
if compatible_wheel is None:
679675
output_wheel = build_options.output_dir.joinpath(built_wheel.name)

cibuildwheel/util/cmd.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import subprocess
55
import sys
66
import typing
7-
from collections.abc import Mapping
7+
from collections.abc import Iterator, Mapping
88
from typing import Final, Literal
99

1010
from ..errors import FatalError
@@ -81,3 +81,18 @@ def shell(
8181
command = " ".join(commands)
8282
print(f"+ {command}")
8383
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
84+
85+
86+
def split_command(lst: list[str]) -> Iterator[list[str]]:
87+
"""
88+
Split a shell-style command, as returned by shlex.split, into a sequence
89+
of commands, separated by '&&'.
90+
"""
91+
items = list[str]()
92+
for item in lst:
93+
if item == "&&":
94+
yield items
95+
items = []
96+
else:
97+
items.append(item)
98+
yield items

test/test_ios.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
8686
"CIBW_BUILD": "cp313-*",
8787
"CIBW_XBUILD_TOOLS": "does-exist",
8888
"CIBW_TEST_SOURCES": "tests",
89-
"CIBW_TEST_COMMAND": "python -m unittest discover tests test_platform.py",
89+
"CIBW_TEST_COMMAND": "python -m this && python -m unittest discover tests test_platform.py",
9090
"CIBW_BUILD_VERBOSITY": "1",
9191
**build_config,
9292
},
@@ -102,6 +102,9 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
102102
captured = capfd.readouterr()
103103
assert "'does-exist' will be included in the cross-build environment" in captured.out
104104

105+
# Make sure the first command ran
106+
assert "Zen of Python" in captured.out
107+
105108

106109
@pytest.mark.serial
107110
def test_no_test_sources(tmp_path, capfd):
@@ -134,7 +137,10 @@ def test_no_test_sources(tmp_path, capfd):
134137

135138

136139
def test_ios_testing_with_placeholder(tmp_path, capfd):
137-
"""Build will run tests with the {project} placeholder."""
140+
"""
141+
Tests with the {project} placeholder are not supported on iOS, because the test command
142+
is run in the simulator.
143+
"""
138144
skip_if_ios_testing_not_supported()
139145

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

161167

168+
@pytest.mark.serial
169+
def test_ios_test_command_short_circuit(tmp_path, capfd):
170+
skip_if_ios_testing_not_supported()
171+
172+
project_dir = tmp_path / "project"
173+
basic_project = test_projects.new_c_project()
174+
basic_project.files.update(basic_project_files)
175+
basic_project.generate(project_dir)
176+
177+
with pytest.raises(subprocess.CalledProcessError):
178+
# `python -m not_a_module` will fail, so `python -m this` should not be run.
179+
utils.cibuildwheel_run(
180+
project_dir,
181+
add_env={
182+
"CIBW_PLATFORM": "ios",
183+
"CIBW_BUILD": "cp313-*",
184+
"CIBW_XBUILD_TOOLS": "",
185+
"CIBW_TEST_SOURCES": "tests",
186+
"CIBW_TEST_COMMAND": "python -m not_a_module && python -m this",
187+
"CIBW_BUILD_VERBOSITY": "1",
188+
},
189+
)
190+
191+
captured = capfd.readouterr()
192+
193+
assert "No module named not_a_module" in captured.out + captured.err
194+
# assert that `python -m this` was not run
195+
assert "Zen of Python" not in captured.out + captured.err
196+
197+
162198
def test_missing_xbuild_tool(tmp_path, capfd):
163199
"""Build will fail if xbuild-tools references a non-existent tool."""
164200
skip_if_ios_testing_not_supported()

0 commit comments

Comments
 (0)