Skip to content

Commit 898f0a4

Browse files
joerickmhsmith
andauthored
fix: change to the iOS testing option semantics (#2363)
* Change to the iOS testing option semantics Don't assume the presence of `python -m` in the test command. Less magic and allows more option reuse between platforms. * Update schema * Add a test for this warning * Don't try to execute a test-command when it doesn't look like a module * Update docs/options.md Co-authored-by: Malcolm Smith <[email protected]> * Only allow invalid test command if the first part is 'pytest' * Responses to code review from @freakboy3742 * Fixes post-merge --------- Co-authored-by: Malcolm Smith <[email protected]>
1 parent 1bc7e90 commit 898f0a4

File tree

5 files changed

+195
-68
lines changed

5 files changed

+195
-68
lines changed

cibuildwheel/platforms/ios.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
download,
3333
move_file,
3434
)
35-
from ..util.helpers import prepare_command
35+
from ..util.helpers import prepare_command, unwrap_preserving_paragraphs
3636
from ..util.packaging import (
3737
combine_constraints,
3838
find_compatible_wheel,
@@ -593,14 +593,51 @@ def build(options: Options, tmp_path: Path) -> None:
593593
)
594594

595595
log.step("Running test suite...")
596+
597+
test_command_parts = shlex.split(build_options.test_command)
598+
if test_command_parts[0:2] != ["python", "-m"]:
599+
first_part = test_command_parts[0]
600+
if first_part == "pytest":
601+
# pytest works exactly the same as a module, so we
602+
# can just run it as a module.
603+
log.warning(
604+
unwrap_preserving_paragraphs(f"""
605+
iOS tests configured with a test command which doesn't start
606+
with 'python -m'. iOS tests must execute python modules - other
607+
entrypoints are not supported.
608+
609+
cibuildwheel will try to execute it as if it started with
610+
'python -m'. If this works, all you need to do is add that to
611+
your test command.
612+
613+
Test command: {build_options.test_command!r}
614+
""")
615+
)
616+
else:
617+
msg = unwrap_preserving_paragraphs(
618+
f"""
619+
iOS tests configured with a test command which doesn't start
620+
with 'python -m'. iOS tests must execute python modules - other
621+
entrypoints are not supported.
622+
623+
Test command: {build_options.test_command!r}
624+
"""
625+
)
626+
raise errors.FatalError(msg)
627+
else:
628+
# the testbed run command actually doesn't want the
629+
# python -m prefix - it's implicit, so we remove it
630+
# here.
631+
test_command_parts = test_command_parts[2:]
632+
596633
try:
597634
call(
598635
"python",
599636
testbed_path,
600637
"run",
601638
*(["--verbose"] if build_options.build_verbosity > 0 else []),
602639
"--",
603-
*(shlex.split(build_options.test_command)),
640+
*test_command_parts,
604641
env=build_env,
605642
)
606643
failed = False

cibuildwheel/util/helpers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,21 @@ def unwrap(text: str) -> str:
7676
return re.sub(r"\s+", " ", text)
7777

7878

79+
def unwrap_preserving_paragraphs(text: str) -> str:
80+
"""
81+
Unwraps multi-line text to a single line, but preserves paragraphs
82+
"""
83+
# remove initial line indent
84+
text = textwrap.dedent(text)
85+
# remove leading/trailing whitespace
86+
text = text.strip()
87+
88+
paragraphs = text.split("\n\n")
89+
# remove consecutive whitespace
90+
paragraphs = [re.sub(r"\s+", " ", paragraph) for paragraph in paragraphs]
91+
return "\n\n".join(paragraphs)
92+
93+
7994
def parse_key_value_string(
8095
key_value_string: str,
8196
positional_arg_names: Sequence[str] | None = None,

docs/options.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1253,7 +1253,7 @@ run your test suite.
12531253

12541254
On all platforms other than iOS, the command is run in a shell, so you can write things like `cmd1 && cmd2`.
12551255

1256-
On iOS, the value of the `CIBW_TEST_COMMAND` setting is interpreted as the arguments to pass to `python -m` - that is, a Python module name, followed by arguments that will be assigned to `sys.argv`. Shell commands cannot be used.
1256+
On iOS, the value of the `CIBW_TEST_COMMAND` setting must follow the format `python -m MODULE [ARGS...]` - where MODULE is a Python module name, followed by arguments that will be assigned to `sys.argv`. Other commands cannot be used.
12571257

12581258
Platform-specific environment variables are also available:<br/>
12591259
`CIBW_TEST_COMMAND_MACOS` | `CIBW_TEST_COMMAND_WINDOWS` | `CIBW_TEST_COMMAND_LINUX` | `CIBW_TEST_COMMAND_IOS` | `CIBW_TEST_COMMAND_PYODIDE`
@@ -1273,6 +1273,10 @@ Platform-specific environment variables are also available:<br/>
12731273
CIBW_TEST_COMMAND: >
12741274
pytest ./tests &&
12751275
python ./test.py
1276+
1277+
# run tests on ios
1278+
CIBW_TEST_SOURCES_IOS: tests
1279+
CIBW_TEST_COMMAND_IOS: python -m pytest ./tests
12761280
```
12771281

12781282
!!! tab examples "pyproject.toml"
@@ -1290,6 +1294,11 @@ Platform-specific environment variables are also available:<br/>
12901294
"pytest ./tests",
12911295
"python ./test.py",
12921296
]
1297+
1298+
# run tests on ios
1299+
[tool.cibuildwheel.ios]
1300+
test-sources = ["tests"]
1301+
test-command = "python -m pytest ./tests"
12931302
```
12941303

12951304
In configuration files, you can use an array, and the items will be joined with `&&`.

test/test_ios.py

Lines changed: 87 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import platform
55
import shutil
66
import subprocess
7+
import textwrap
78

89
import pytest
910

@@ -73,28 +74,17 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
7374
"CIBW_BUILD": "cp313-*",
7475
"CIBW_XBUILD_TOOLS": "does-exist",
7576
"CIBW_TEST_SOURCES": "tests",
76-
"CIBW_TEST_COMMAND": "unittest discover tests test_platform.py",
77+
"CIBW_TEST_COMMAND": "python -m unittest discover tests test_platform.py",
7778
"CIBW_BUILD_VERBOSITY": "1",
7879
**build_config,
7980
},
8081
)
8182

8283
# The expected wheels were produced.
83-
ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_")
84-
platform_machine = platform.machine()
85-
86-
if platform_machine == "x86_64":
87-
expected_wheels = {
88-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl",
89-
}
90-
91-
elif platform_machine == "arm64":
92-
expected_wheels = {
93-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl",
94-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl",
95-
}
96-
97-
assert set(actual_wheels) == expected_wheels
84+
expected_wheels = utils.expected_wheels(
85+
"spam", "0.1.0", platform="ios", python_abi_tags=["cp313-cp313"]
86+
)
87+
assert set(actual_wheels) == set(expected_wheels)
9888

9989
# The user was notified that the cross-build tool was found.
10090
captured = capfd.readouterr()
@@ -119,7 +109,7 @@ def test_no_test_sources(tmp_path, capfd):
119109
add_env={
120110
"CIBW_PLATFORM": "ios",
121111
"CIBW_BUILD": "cp313-*",
122-
"CIBW_TEST_COMMAND": "tests",
112+
"CIBW_TEST_COMMAND": "python -m tests",
123113
},
124114
)
125115

@@ -146,7 +136,7 @@ def test_missing_xbuild_tool(tmp_path, capfd):
146136
add_env={
147137
"CIBW_PLATFORM": "ios",
148138
"CIBW_BUILD": "cp313-*",
149-
"CIBW_TEST_COMMAND": "tests",
139+
"CIBW_TEST_COMMAND": "python -m tests",
150140
"CIBW_XBUILD_TOOLS": "does-not-exist",
151141
},
152142
)
@@ -180,21 +170,13 @@ def test_no_xbuild_tool_definition(tmp_path, capfd):
180170
)
181171

182172
# The expected wheels were produced.
183-
ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_")
184-
platform_machine = platform.machine()
185-
186-
if platform_machine == "x86_64":
187-
expected_wheels = {
188-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl",
189-
}
190-
191-
elif platform_machine == "arm64":
192-
expected_wheels = {
193-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl",
194-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl",
195-
}
196-
197-
assert set(actual_wheels) == expected_wheels
173+
expected_wheels = utils.expected_wheels(
174+
"spam",
175+
"0.1.0",
176+
platform="ios",
177+
python_abi_tags=["cp313-cp313"],
178+
)
179+
assert set(actual_wheels) == set(expected_wheels)
198180

199181
# The user was notified that there was no cross-build tool definition.
200182
captured = capfd.readouterr()
@@ -225,23 +207,79 @@ def test_empty_xbuild_tool_definition(tmp_path, capfd):
225207
},
226208
)
227209

228-
# The expected wheels were produced.
229-
ios_version = os.getenv("IPHONEOS_DEPLOYMENT_TARGET", "13.0").replace(".", "_")
230-
platform_machine = platform.machine()
231-
232-
if platform_machine == "x86_64":
233-
expected_wheels = {
234-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_x86_64_iphonesimulator.whl",
235-
}
236-
237-
elif platform_machine == "arm64":
238-
expected_wheels = {
239-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphoneos.whl",
240-
f"spam-0.1.0-cp313-cp313-ios_{ios_version}_arm64_iphonesimulator.whl",
241-
}
242-
243-
assert set(actual_wheels) == expected_wheels
210+
expected_wheels = utils.expected_wheels(
211+
"spam", "0.1.0", platform="ios", python_abi_tags=["cp313-cp313"]
212+
)
213+
assert set(actual_wheels) == set(expected_wheels)
244214

245215
# The warnings about cross-build notifications were silenced.
246216
captured = capfd.readouterr()
247217
assert "Your project configuration does not define any cross-build tools." not in captured.err
218+
219+
220+
@pytest.mark.serial
221+
def test_ios_test_command_without_python_dash_m(tmp_path, capfd):
222+
"""pytest should be able to run without python -m, but it should warn."""
223+
if utils.get_platform() != "macos":
224+
pytest.skip("this test can only run on macOS")
225+
if utils.get_xcode_version() < (13, 0):
226+
pytest.skip("this test only works with Xcode 13.0 or greater")
227+
228+
project_dir = tmp_path / "project"
229+
230+
project = test_projects.new_c_project()
231+
project.files["tests/__init__.py"] = ""
232+
project.files["tests/test_spam.py"] = textwrap.dedent("""
233+
import spam
234+
def test_spam():
235+
assert spam.filter("spam") == 0
236+
assert spam.filter("ham") != 0
237+
""")
238+
project.generate(project_dir)
239+
240+
actual_wheels = utils.cibuildwheel_run(
241+
project_dir,
242+
add_env={
243+
"CIBW_PLATFORM": "ios",
244+
"CIBW_BUILD": "cp313-*",
245+
"CIBW_TEST_COMMAND": "pytest ./tests",
246+
"CIBW_TEST_SOURCES": "tests",
247+
"CIBW_TEST_REQUIRES": "pytest",
248+
"CIBW_XBUILD_TOOLS": "",
249+
},
250+
)
251+
252+
expected_wheels = utils.expected_wheels(
253+
"spam", "0.1.0", platform="ios", python_abi_tags=["cp313-cp313"]
254+
)
255+
assert set(actual_wheels) == set(expected_wheels)
256+
257+
out, err = capfd.readouterr()
258+
259+
assert "iOS tests configured with a test command which doesn't start with 'python -m'" in err
260+
261+
262+
def test_ios_test_command_invalid(tmp_path, capfd):
263+
"""Test command should raise an error if it's clearly invalid."""
264+
if utils.get_platform() != "macos":
265+
pytest.skip("this test can only run on macOS")
266+
if utils.get_xcode_version() < (13, 0):
267+
pytest.skip("this test only works with Xcode 13.0 or greater")
268+
269+
project_dir = tmp_path / "project"
270+
basic_project = test_projects.new_c_project()
271+
basic_project.files["./my_test_script.sh"] = "echo hello"
272+
basic_project.generate(project_dir)
273+
274+
with pytest.raises(subprocess.CalledProcessError):
275+
utils.cibuildwheel_run(
276+
project_dir,
277+
add_env={
278+
"CIBW_PLATFORM": "ios",
279+
"CIBW_TEST_COMMAND": "./my_test_script.sh",
280+
"CIBW_TEST_SOURCES": "./my_test_script.sh",
281+
"CIBW_XBUILD_TOOLS": "",
282+
},
283+
)
284+
out, err = capfd.readouterr()
285+
assert "iOS tests configured with a test command which doesn't start with 'python -m'" in err

0 commit comments

Comments
 (0)