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>
This commit is contained in:
co-authored by
Joe Rickerby
parent
f8168e2a89
commit
ea08120e25
@@ -25,7 +25,7 @@ from ..logger import log
|
|||||||
from ..options import Options
|
from ..options import Options
|
||||||
from ..selector import BuildSelector
|
from ..selector import BuildSelector
|
||||||
from ..util import resources
|
from ..util import resources
|
||||||
from ..util.cmd import call, shell
|
from ..util.cmd import call, shell, split_command
|
||||||
from ..util.file import (
|
from ..util.file import (
|
||||||
CIBW_CACHE_PATH,
|
CIBW_CACHE_PATH,
|
||||||
copy_test_sources,
|
copy_test_sources,
|
||||||
@@ -618,14 +618,16 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
)
|
)
|
||||||
raise errors.FatalError(msg)
|
raise errors.FatalError(msg)
|
||||||
|
|
||||||
test_command_parts = shlex.split(build_options.test_command)
|
test_command_list = shlex.split(build_options.test_command)
|
||||||
if test_command_parts[0:2] != ["python", "-m"]:
|
try:
|
||||||
first_part = test_command_parts[0]
|
for test_command_parts in split_command(test_command_list):
|
||||||
if first_part == "pytest":
|
match test_command_parts:
|
||||||
|
case ["python", "-m", *rest]:
|
||||||
|
final_command = rest
|
||||||
|
case ["pytest", *rest]:
|
||||||
# pytest works exactly the same as a module, so we
|
# pytest works exactly the same as a module, so we
|
||||||
# can just run it as a module.
|
# can just run it as a module.
|
||||||
log.warning(
|
msg = unwrap_preserving_paragraphs(f"""
|
||||||
unwrap_preserving_paragraphs(f"""
|
|
||||||
iOS tests configured with a test command which doesn't start
|
iOS tests configured with a test command which doesn't start
|
||||||
with 'python -m'. iOS tests must execute python modules - other
|
with 'python -m'. iOS tests must execute python modules - other
|
||||||
entrypoints are not supported.
|
entrypoints are not supported.
|
||||||
@@ -636,8 +638,9 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
|
|
||||||
Test command: {build_options.test_command!r}
|
Test command: {build_options.test_command!r}
|
||||||
""")
|
""")
|
||||||
)
|
log.warning(msg)
|
||||||
else:
|
final_command = ["pytest", *rest]
|
||||||
|
case _:
|
||||||
msg = unwrap_preserving_paragraphs(
|
msg = unwrap_preserving_paragraphs(
|
||||||
f"""
|
f"""
|
||||||
iOS tests configured with a test command which doesn't start
|
iOS tests configured with a test command which doesn't start
|
||||||
@@ -648,32 +651,25 @@ def build(options: Options, tmp_path: Path) -> None:
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
raise errors.FatalError(msg)
|
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:]
|
|
||||||
|
|
||||||
try:
|
|
||||||
call(
|
call(
|
||||||
"python",
|
"python",
|
||||||
testbed_path,
|
testbed_path,
|
||||||
"run",
|
"run",
|
||||||
*(["--verbose"] if build_options.build_verbosity > 0 else []),
|
*(["--verbose"] if build_options.build_verbosity > 0 else []),
|
||||||
"--",
|
"--",
|
||||||
*test_command_parts,
|
*final_command,
|
||||||
env=test_env,
|
env=test_env,
|
||||||
)
|
)
|
||||||
failed = False
|
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
failed = True
|
# catches the first test command failure in the loop,
|
||||||
|
# implementing short-circuiting
|
||||||
log.step_end(success=not failed)
|
log.step_end(success=False)
|
||||||
|
|
||||||
if failed:
|
|
||||||
log.error(f"Test suite failed on {config.identifier}")
|
log.error(f"Test suite failed on {config.identifier}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
log.step_end()
|
||||||
|
|
||||||
# We're all done here; move it to output (overwrite existing)
|
# We're all done here; move it to output (overwrite existing)
|
||||||
if compatible_wheel is None:
|
if compatible_wheel is None:
|
||||||
output_wheel = build_options.output_dir.joinpath(built_wheel.name)
|
output_wheel = build_options.output_dir.joinpath(built_wheel.name)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import typing
|
import typing
|
||||||
from collections.abc import Mapping
|
from collections.abc import Iterator, Mapping
|
||||||
from typing import Final, Literal
|
from typing import Final, Literal
|
||||||
|
|
||||||
from ..errors import FatalError
|
from ..errors import FatalError
|
||||||
@@ -81,3 +81,18 @@ def shell(
|
|||||||
command = " ".join(commands)
|
command = " ".join(commands)
|
||||||
print(f"+ {command}")
|
print(f"+ {command}")
|
||||||
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
|
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
|
||||||
|
|||||||
+38
-2
@@ -86,7 +86,7 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
|
|||||||
"CIBW_BUILD": "cp313-*",
|
"CIBW_BUILD": "cp313-*",
|
||||||
"CIBW_XBUILD_TOOLS": "does-exist",
|
"CIBW_XBUILD_TOOLS": "does-exist",
|
||||||
"CIBW_TEST_SOURCES": "tests",
|
"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",
|
"CIBW_BUILD_VERBOSITY": "1",
|
||||||
**build_config,
|
**build_config,
|
||||||
},
|
},
|
||||||
@@ -102,6 +102,9 @@ def test_ios_platforms(tmp_path, build_config, monkeypatch, capfd):
|
|||||||
captured = capfd.readouterr()
|
captured = capfd.readouterr()
|
||||||
assert "'does-exist' will be included in the cross-build environment" in captured.out
|
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
|
@pytest.mark.serial
|
||||||
def test_no_test_sources(tmp_path, capfd):
|
def test_no_test_sources(tmp_path, capfd):
|
||||||
@@ -134,7 +137,10 @@ def test_no_test_sources(tmp_path, capfd):
|
|||||||
|
|
||||||
|
|
||||||
def test_ios_testing_with_placeholder(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()
|
skip_if_ios_testing_not_supported()
|
||||||
|
|
||||||
project_dir = tmp_path / "project"
|
project_dir = tmp_path / "project"
|
||||||
@@ -159,6 +165,36 @@ def test_ios_testing_with_placeholder(tmp_path, capfd):
|
|||||||
assert "iOS tests cannot use placeholders" in captured.out + captured.err
|
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):
|
def test_missing_xbuild_tool(tmp_path, capfd):
|
||||||
"""Build will fail if xbuild-tools references a non-existent tool."""
|
"""Build will fail if xbuild-tools references a non-existent tool."""
|
||||||
skip_if_ios_testing_not_supported()
|
skip_if_ios_testing_not_supported()
|
||||||
|
|||||||
Reference in New Issue
Block a user