[3.13] gh-137973: Add a non-parallel test plan to the iOS testbed project (GH-138018) (#138039)

Modifies the iOS testbed project to add a test plan. This simplifies the iOS
test runner, as we can now use the built-in log streaming to see test results.
It also allows for some other affordances, like providing a default LLDB config,
and using a standardized mechanism for specifying test arguments.
(cherry picked from commit 2ba2287b85)

Co-authored-by: Russell Keith-Magee <russell@keith-magee.com>
This commit is contained in:
Miss Islington (bot) 2025-08-22 08:30:58 +02:00 committed by GitHub
parent 41c2c8f01c
commit a73515e747
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 271 additions and 346 deletions

1
.gitignore vendored
View file

@ -80,7 +80,6 @@ iOS/testbed/Python.xcframework/ios-*/lib
iOS/testbed/Python.xcframework/ios-*/Python.framework
iOS/testbed/iOSTestbed.xcodeproj/project.xcworkspace
iOS/testbed/iOSTestbed.xcodeproj/xcuserdata
iOS/testbed/iOSTestbed.xcodeproj/xcshareddata
Mac/Makefile
Mac/PythonLauncher/Info.plist
Mac/PythonLauncher/Makefile

View file

@ -372,6 +372,17 @@ You can also open the testbed project in Xcode by running:
This will allow you to use the full Xcode suite of tools for debugging.
The arguments used to run the test suite are defined as part of the test plan.
To modify the test plan, select the test plan node of the project tree (it
should be the first child of the root node), and select the "Configurations"
tab. Modify the "Arguments Passed On Launch" value to change the testing
arguments.
The test plan also disables parallel testing, and specifies the use of the
``iOSTestbed.lldbinit`` file for providing configuration of the debugger. The
default debugger configuration disables automatic breakpoints on the
``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals.
App Store Compliance
====================

View file

@ -0,0 +1,3 @@
The iOS test runner has been simplified, resolving some issues that have
been observed using the runner in GitHub Actions and Azure Pipelines test
environments.

View file

@ -293,7 +293,7 @@ project, and then boot and prepare the iOS simulator.
Debugging test failures
-----------------------
Running ``make test`` generates a standalone version of the ``iOS/testbed``
Running ``make testios`` generates a standalone version of the ``iOS/testbed``
project, and runs the full test suite. It does this using ``iOS/testbed``
itself - the folder is an executable module that can be used to create and run
a clone of the testbed project.
@ -316,12 +316,26 @@ This is the equivalent of running ``python -m test -W test_os`` on a desktop
Python build. Any arguments after the ``--`` will be passed to testbed as if
they were arguments to ``python -m`` on a desktop machine.
Testing in Xcode
^^^^^^^^^^^^^^^^
You can also open the testbed project in Xcode by running::
$ open my-testbed/iOSTestbed.xcodeproj
This will allow you to use the full Xcode suite of tools for debugging.
The arguments used to run the test suite are defined as part of the test plan.
To modify the test plan, select the test plan node of the project tree (it
should be the first child of the root node), and select the "Configurations"
tab. Modify the "Arguments Passed On Launch" value to change the testing
arguments.
The test plan also disables parallel testing, and specifies the use of the
``iOSTestbed.lldbinit`` file for providing configuration of the debugger. The
default debugger configuration disables automatic breakpoints on the
``SIGINT``, ``SIGUSR1``, ``SIGUSR2``, and ``SIGXFSZ`` signals.
Testing on an iOS device
^^^^^^^^^^^^^^^^^^^^^^^^
@ -336,40 +350,3 @@ select the root node of the project tree (labeled "iOSTestbed"), then the
(this will likely be your own name), and plug in a physical device to your
macOS machine with a USB cable. You should then be able to select your physical
device from the list of targets in the pulldown in the Xcode titlebar.
Running specific tests
^^^^^^^^^^^^^^^^^^^^^^
As the test suite is being executed on an iOS simulator, it is not possible to
pass in command line arguments to configure test suite operation. To work
around this limitation, the arguments that would normally be passed as command
line arguments are configured as part of the ``iOSTestbed-Info.plist`` file
that is used to configure the iOS testbed app. In this file, the ``TestArgs``
key is an array containing the arguments that would be passed to ``python -m``
on the command line (including ``test`` in position 0, the name of the test
module to be executed).
Disabling automated breakpoints
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
By default, Xcode will inserts an automatic breakpoint whenever a signal is
raised. The Python test suite raises many of these signals as part of normal
operation; unless you are trying to diagnose an issue with signals, the
automatic breakpoints can be inconvenient. However, they can be disabled by
creating a symbolic breakpoint that is triggered at the start of the test run.
Select "Debug > Breakpoints > Create Symbolic Breakpoint" from the Xcode menu, and
populate the new brewpoint with the following details:
* **Name**: IgnoreSignals
* **Symbol**: UIApplicationMain
* **Action**: Add debugger commands for:
- ``process handle SIGINT -n true -p true -s false``
- ``process handle SIGUSR1 -n true -p true -s false``
- ``process handle SIGUSR2 -n true -p true -s false``
- ``process handle SIGXFSZ -n true -p true -s false``
* Check the "Automatically continue after evaluating" box.
All other details can be left blank. When the process executes the
``UIApplicationMain`` entry point, the breakpoint will trigger, run the debugger
commands to disable the automatic breakpoints, and automatically resume.

View file

@ -1,134 +1,29 @@
import argparse
import asyncio
import fcntl
import json
import os
import plistlib
import re
import shutil
import subprocess
import sys
import tempfile
from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path
DECODE_ARGS = ("UTF-8", "backslashreplace")
# The system log prefixes each line:
# 2025-01-17 16:14:29.090 Df iOSTestbed[23987:1fd393b4] (Python) ...
# 2025-01-17 16:14:29.090 E iOSTestbed[23987:1fd393b4] (Python) ...
# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ...
# 2025-01-17 16:14:29.093742+0800 iOSTestbed[23987:1fd393b4] ...
LOG_PREFIX_REGEX = re.compile(
r"^\d{4}-\d{2}-\d{2}" # YYYY-MM-DD
r"\s+\d+:\d{2}:\d{2}\.\d+" # HH:MM:SS.sss
r"\s+\w+" # Df/E
r"\s+\d+:\d{2}:\d{2}\.\d+\+\d{4}" # HH:MM:SS.ssssss+ZZZZ
r"\s+iOSTestbed\[\d+:\w+\]" # Process/thread ID
r"\s+\(Python\)\s" # Logger name
)
# Work around a bug involving sys.exit and TaskGroups
# (https://github.com/python/cpython/issues/101515).
def exit(*args):
raise MySystemExit(*args)
class MySystemExit(Exception):
pass
class SimulatorLock:
# An fcntl-based filesystem lock that can be used to ensure that
def __init__(self, timeout):
self.filename = Path(tempfile.gettempdir()) / "python-ios-testbed"
self.timeout = timeout
self.fd = None
async def acquire(self):
# Ensure the lockfile exists
self.filename.touch(exist_ok=True)
# Try `timeout` times to acquire the lock file, with a 1 second pause
# between each attempt. Report status every 10 seconds.
for i in range(0, self.timeout):
try:
fd = os.open(self.filename, os.O_RDWR | os.O_TRUNC, 0o644)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
os.close(fd)
if i % 10 == 0:
print("... waiting", flush=True)
await asyncio.sleep(1)
else:
self.fd = fd
return
# If we reach the end of the loop, we've exceeded the allowed number of
# attempts.
raise ValueError("Unable to obtain lock on iOS simulator creation")
def release(self):
# If a lock is held, release it.
if self.fd is not None:
# Release the lock.
fcntl.flock(self.fd, fcntl.LOCK_UN)
os.close(self.fd)
self.fd = None
# All subprocesses are executed through this context manager so that no matter
# what happens, they can always be cancelled from another task, and they will
# always be cleaned up on exit.
@asynccontextmanager
async def async_process(*args, **kwargs):
process = await asyncio.create_subprocess_exec(*args, **kwargs)
try:
yield process
finally:
if process.returncode is None:
# Allow a reasonably long time for Xcode to clean itself up,
# because we don't want stale emulators left behind.
timeout = 10
process.terminate()
try:
await asyncio.wait_for(process.wait(), timeout)
except TimeoutError:
print(
f"Command {args} did not terminate after {timeout} seconds "
f" - sending SIGKILL"
)
process.kill()
# Even after killing the process we must still wait for it,
# otherwise we'll get the warning "Exception ignored in __del__".
await asyncio.wait_for(process.wait(), timeout=1)
async def async_check_output(*args, **kwargs):
async with async_process(
*args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs
) as process:
stdout, stderr = await process.communicate()
if process.returncode == 0:
return stdout.decode(*DECODE_ARGS)
else:
raise subprocess.CalledProcessError(
process.returncode,
args,
stdout.decode(*DECODE_ARGS),
stderr.decode(*DECODE_ARGS),
)
# Select a simulator device to use.
async def select_simulator_device():
def select_simulator_device():
# List the testing simulators, in JSON format
raw_json = await async_check_output(
"xcrun", "simctl", "list", "-j"
)
raw_json = subprocess.check_output(["xcrun", "simctl", "list", "-j"])
json_data = json.loads(raw_json)
# Any device will do; we'll look for "SE" devices - but the name isn't
@ -145,7 +40,10 @@ async def select_simulator_device():
for devicetype in json_data["devicetypes"]
if devicetype["productFamily"] == "iPhone"
and (
("iPhone " in devicetype["name"] and devicetype["name"].endswith("e"))
(
"iPhone " in devicetype["name"]
and devicetype["name"].endswith("e")
)
or "iPhone SE " in devicetype["name"]
)
)
@ -153,127 +51,42 @@ async def select_simulator_device():
return se_simulators[-1][1]
# Return a list of UDIDs associated with booted simulators
async def list_devices():
try:
# List the testing simulators, in JSON format
raw_json = await async_check_output(
"xcrun", "simctl", "--set", "testing", "list", "-j"
)
json_data = json.loads(raw_json)
# Filter out the booted iOS simulators
return [
simulator["udid"]
for runtime, simulators in json_data["devices"].items()
for simulator in simulators
if runtime.split(".")[-1].startswith("iOS") and simulator["state"] == "Booted"
]
except subprocess.CalledProcessError as e:
# If there's no ~/Library/Developer/XCTestDevices folder (which is the
# case on fresh installs, and in some CI environments), `simctl list`
# returns error code 1, rather than an empty list. Handle that case,
# but raise all other errors.
if e.returncode == 1:
return []
else:
raise
async def find_device(initial_devices, lock):
while True:
new_devices = set(await list_devices()).difference(initial_devices)
if len(new_devices) == 0:
await asyncio.sleep(1)
elif len(new_devices) == 1:
udid = new_devices.pop()
print(f"{datetime.now():%Y-%m-%d %H:%M:%S}: New test simulator detected")
print(f"UDID: {udid}", flush=True)
lock.release()
return udid
else:
exit(f"Found more than one new device: {new_devices}")
async def log_stream_task(initial_devices, lock):
# Wait up to 5 minutes for the build to complete and the simulator to boot.
udid = await asyncio.wait_for(find_device(initial_devices, lock), 5 * 60)
# Stream the iOS device's logs, filtering out messages that come from the
# XCTest test suite (catching NSLog messages from the test method), or
# Python itself (catching stdout/stderr content routed to the system log
# with config->use_system_logger).
def xcode_test(location, simulator, verbose):
# Build and run the test suite on the named simulator.
args = [
"xcrun",
"simctl",
"--set",
"testing",
"spawn",
udid,
"log",
"stream",
"--style",
"compact",
"--predicate",
(
'senderImagePath ENDSWITH "/iOSTestbedTests.xctest/iOSTestbedTests"'
' OR senderImagePath ENDSWITH "/Python.framework/Python"'
),
]
async with async_process(
*args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as process:
suppress_dupes = False
while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
# Strip the prefix from each log line
line = LOG_PREFIX_REGEX.sub("", line)
# The iOS log streamer can sometimes lag; when it does, it outputs
# a warning about messages being dropped... often multiple times.
# Only print the first of these duplicated warnings.
if line.startswith("=== Messages dropped "):
if not suppress_dupes:
suppress_dupes = True
sys.stdout.write(line)
else:
suppress_dupes = False
sys.stdout.write(line)
sys.stdout.flush()
async def xcode_test(location, simulator, verbose):
# Run the test suite on the named simulator
print("Starting xcodebuild...", flush=True)
args = [
"xcodebuild",
"test",
"-project",
str(location / "iOSTestbed.xcodeproj"),
"-scheme",
"iOSTestbed",
"-destination",
f"platform=iOS Simulator,name={simulator}",
"-resultBundlePath",
str(location / f"{datetime.now():%Y%m%d-%H%M%S}.xcresult"),
"-derivedDataPath",
str(location / "DerivedData"),
]
if not verbose:
args += ["-quiet"]
verbosity_args = [] if verbose else ["-quiet"]
async with async_process(
*args,
print("Building test project...")
subprocess.run(
["xcodebuild", "build-for-testing"] + args + verbosity_args,
check=True,
)
print("Running test project...")
# Test execution *can't* be run -quiet; verbose mode
# is how we see the output of the test output.
process = subprocess.Popen(
["xcodebuild", "test-without-building"] + args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as process:
while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
sys.stdout.write(line)
sys.stdout.flush()
)
while line := (process.stdout.readline()).decode(*DECODE_ARGS):
# Strip the timestamp/process prefix from each log line
line = LOG_PREFIX_REGEX.sub("", line)
sys.stdout.write(line)
sys.stdout.flush()
status = await asyncio.wait_for(process.wait(), timeout=1)
exit(status)
status = process.wait(timeout=5)
exit(status)
def clone_testbed(
@ -310,7 +123,7 @@ def clone_testbed(
sys.exit(13)
print("Cloning testbed project:")
print(f" Cloning {source}...", end="", flush=True)
print(f" Cloning {source}...", end="")
shutil.copytree(source, target, symlinks=True)
print(" done")
@ -318,7 +131,7 @@ def clone_testbed(
sim_framework_path = xc_framework_path / "ios-arm64_x86_64-simulator"
if framework is not None:
if framework.suffix == ".xcframework":
print(" Installing XCFramework...", end="", flush=True)
print(" Installing XCFramework...", end="")
if xc_framework_path.is_dir():
shutil.rmtree(xc_framework_path)
else:
@ -328,7 +141,7 @@ def clone_testbed(
)
print(" done")
else:
print(" Installing simulator framework...", end="", flush=True)
print(" Installing simulator framework...", end="")
if sim_framework_path.is_dir():
shutil.rmtree(sim_framework_path)
else:
@ -344,10 +157,9 @@ def clone_testbed(
):
# XCFramework is a relative symlink. Rewrite the symlink relative
# to the new location.
print(" Rewriting symlink to XCframework...", end="", flush=True)
print(" Rewriting symlink to XCframework...", end="")
orig_xc_framework_path = (
source
/ xc_framework_path.readlink()
source / xc_framework_path.readlink()
).resolve()
xc_framework_path.unlink()
xc_framework_path.symlink_to(
@ -360,13 +172,11 @@ def clone_testbed(
sim_framework_path.is_symlink()
and not sim_framework_path.readlink().is_absolute()
):
print(" Rewriting symlink to simulator framework...", end="", flush=True)
print(" Rewriting symlink to simulator framework...", end="")
# Simulator framework is a relative symlink. Rewrite the symlink
# relative to the new location.
orig_sim_framework_path = (
source
/ "Python.XCframework"
/ sim_framework_path.readlink()
source / "Python.XCframework" / sim_framework_path.readlink()
).resolve()
sim_framework_path.unlink()
sim_framework_path.symlink_to(
@ -379,7 +189,7 @@ def clone_testbed(
print(" Using pre-existing iOS framework.")
for app_src in apps:
print(f" Installing app {app_src.name!r}...", end="", flush=True)
print(f" Installing app {app_src.name!r}...", end="")
app_target = target / f"iOSTestbed/app/{app_src.name}"
if app_target.is_dir():
shutil.rmtree(app_target)
@ -389,54 +199,31 @@ def clone_testbed(
print(f"Successfully cloned testbed: {target.resolve()}")
def update_plist(testbed_path, args):
# Add the test runner arguments to the testbed's Info.plist file.
info_plist = testbed_path / "iOSTestbed" / "iOSTestbed-Info.plist"
with info_plist.open("rb") as f:
info = plistlib.load(f)
def update_test_plan(testbed_path, args):
# Modify the test plan to use the requested test arguments.
test_plan_path = testbed_path / "iOSTestbed.xctestplan"
with test_plan_path.open("r", encoding="utf-8") as f:
test_plan = json.load(f)
info["TestArgs"] = args
test_plan["defaultOptions"]["commandLineArgumentEntries"] = [
{"argument": arg} for arg in args
]
with info_plist.open("wb") as f:
plistlib.dump(info, f)
with test_plan_path.open("w", encoding="utf-8") as f:
json.dump(test_plan, f, indent=2)
async def run_testbed(simulator: str | None, args: list[str], verbose: bool=False):
def run_testbed(simulator: str | None, args: list[str], verbose: bool = False):
location = Path(__file__).parent
print("Updating plist...", end="", flush=True)
update_plist(location, args)
print(" done.", flush=True)
print("Updating test plan...", end="")
update_test_plan(location, args)
print(" done.")
if simulator is None:
simulator = await select_simulator_device()
print(f"Running test on {simulator}", flush=True)
simulator = select_simulator_device()
print(f"Running test on {simulator}")
# We need to get an exclusive lock on simulator creation, to avoid issues
# with multiple simulators starting and being unable to tell which
# simulator is due to which testbed instance. See
# https://github.com/python/cpython/issues/130294 for details. Wait up to
# 10 minutes for a simulator to boot.
print("Obtaining lock on simulator creation...", flush=True)
simulator_lock = SimulatorLock(timeout=10*60)
await simulator_lock.acquire()
print("Simulator lock acquired.", flush=True)
# Get the list of devices that are booted at the start of the test run.
# The simulator started by the test suite will be detected as the new
# entry that appears on the device list.
initial_devices = await list_devices()
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(log_stream_task(initial_devices, simulator_lock))
tg.create_task(xcode_test(location, simulator=simulator, verbose=verbose))
except* MySystemExit as e:
raise SystemExit(*e.exceptions[0].args) from None
except* subprocess.CalledProcessError as e:
# Extract it from the ExceptionGroup so it can be handled by `main`.
raise e.exceptions[0]
finally:
simulator_lock.release()
xcode_test(location, simulator=simulator, verbose=verbose)
def main():
@ -488,12 +275,16 @@ def main():
run.add_argument(
"--simulator",
help=(
"The name of the simulator to use (eg: 'iPhone 16e'). Defaults to ",
"the most recently released 'entry level' iPhone device."
)
"The name of the simulator to use (eg: 'iPhone 16e'). Defaults to "
"the most recently released 'entry level' iPhone device. Device "
"architecture and OS version can also be specified; e.g., "
"`--simulator 'iPhone 16 Pro,arch=arm64,OS=26.0'` would run on "
"an ARM64 iPhone 16 Pro simulator running iOS 26.0."
),
)
run.add_argument(
"-v", "--verbose",
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
)
@ -512,13 +303,16 @@ def main():
clone_testbed(
source=Path(__file__).parent.resolve(),
target=Path(context.location).resolve(),
framework=Path(context.framework).resolve() if context.framework else None,
framework=Path(context.framework).resolve()
if context.framework
else None,
apps=[Path(app) for app in context.apps],
)
elif context.subcommand == "run":
if test_args:
if not (
Path(__file__).parent / "Python.xcframework/ios-arm64_x86_64-simulator/bin"
Path(__file__).parent
/ "Python.xcframework/ios-arm64_x86_64-simulator/bin"
).is_dir():
print(
f"Testbed does not contain a compiled iOS framework. Use "
@ -527,15 +321,15 @@ def main():
)
sys.exit(20)
asyncio.run(
run_testbed(
simulator=context.simulator,
verbose=context.verbose,
args=test_args,
)
run_testbed(
simulator=context.simulator,
verbose=context.verbose,
args=test_args,
)
else:
print(f"Must specify test arguments (e.g., {sys.argv[0]} run -- test)")
print(
f"Must specify test arguments (e.g., {sys.argv[0]} run -- test)"
)
print()
parser.print_help(sys.stderr)
sys.exit(21)

View file

@ -0,0 +1,4 @@
process handle SIGINT -n true -p true -s false
process handle SIGUSR1 -n true -p true -s false
process handle SIGUSR2 -n true -p true -s false
process handle SIGXFSZ -n true -p true -s false

View file

@ -70,6 +70,7 @@
607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = "<group>"; };
608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = "<group>"; };
608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = "<group>"; };
60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -95,6 +96,7 @@
607A66092B0EFA380010BFC8 = {
isa = PBXGroup;
children = (
60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */,
607A664A2B0EFB310010BFC8 /* Python.xcframework */,
607A66142B0EFA380010BFC8 /* iOSTestbed */,
607A66302B0EFA3A0010BFC8 /* iOSTestbedTests */,
@ -379,7 +381,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
@ -434,7 +436,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
@ -460,7 +462,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -491,7 +493,7 @@
INFOPLIST_KEY_UIMainStoryboardFile = Main;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -514,7 +516,7 @@
DEVELOPMENT_TEAM = 3HEZE76D99;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@ -534,7 +536,7 @@
DEVELOPMENT_TEAM = 3HEZE76D99;
GENERATE_INFOPLIST_FILE = YES;
HEADER_SEARCH_PATHS = "\"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers\"";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = org.python.iOSTestbedTests;
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607A66112B0EFA380010BFC8"
BuildableName = "iOSTestbed.app"
BlueprintName = "iOSTestbed"
ReferencedContainer = "container:iOSTestbed.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "/Users/rkm/projects/pyspamsum/localtest/iOSTestbed.lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:iOSTestbed.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607A662C2B0EFA3A0010BFC8"
BuildableName = "iOSTestbedTests.xctest"
BlueprintName = "iOSTestbedTests"
ReferencedContainer = "container:iOSTestbed.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607A66112B0EFA380010BFC8"
BuildableName = "iOSTestbed.app"
BlueprintName = "iOSTestbed"
ReferencedContainer = "container:iOSTestbed.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "607A66112B0EFA380010BFC8"
BuildableName = "iOSTestbed.app"
BlueprintName = "iOSTestbed"
ReferencedContainer = "container:iOSTestbed.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,46 @@
{
"configurations" : [
{
"id" : "F5A95CE4-1ADE-4A6E-A0E1-CDBAE26DF0C5",
"name" : "Test Scheme Action",
"options" : {
}
}
],
"defaultOptions" : {
"commandLineArgumentEntries" : [
{
"argument" : "test"
},
{
"argument" : "-uall"
},
{
"argument" : "--single-process"
},
{
"argument" : "--rerun"
},
{
"argument" : "-W"
}
],
"targetForVariableExpansion" : {
"containerPath" : "container:iOSTestbed.xcodeproj",
"identifier" : "607A66112B0EFA380010BFC8",
"name" : "iOSTestbed"
}
},
"testTargets" : [
{
"parallelizable" : false,
"target" : {
"containerPath" : "container:iOSTestbed.xcodeproj",
"identifier" : "607A662C2B0EFA3A0010BFC8",
"name" : "iOSTestbedTests"
}
}
],
"version" : 1
}

View file

@ -41,18 +41,6 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>TestArgs</key>
<array>
<string>test</string> <!-- Invoke "python -m test" -->
<string>-uall</string> <!-- Enable all resources -->
<string>--single-process</string> <!-- always run all tests sequentially in a single process -->
<string>--rerun</string> <!-- Re-run failed tests in verbose mode -->
<string>-W</string> <!-- Display test output on failure -->
<!-- To run a subset of tests, add the test names below; e.g.,
<string>test_os</string>
<string>test_sys</string>
-->
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>

View file

@ -38,16 +38,20 @@
// Arguments to pass into the test suite runner.
// argv[0] must identify the process; any subsequent arg
// will be handled as if it were an argument to `python -m test`
test_args = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"TestArgs"];
// The processInfo arguments contain the binary that is running,
// followed by the arguments defined in the test plan. This means:
// run_module = test_args[1]
// argv = ["iOSTestbed"] + test_args[2:]
test_args = [[NSProcessInfo processInfo] arguments];
if (test_args == NULL) {
NSLog(@"Unable to identify test arguments.");
}
argv = malloc(sizeof(char *) * ([test_args count] + 1));
NSLog(@"Test arguments: %@", test_args);
argv = malloc(sizeof(char *) * ([test_args count] - 1));
argv[0] = "iOSTestbed";
for (int i = 1; i < [test_args count]; i++) {
argv[i] = [[test_args objectAtIndex:i] UTF8String];
for (int i = 1; i < [test_args count] - 1; i++) {
argv[i] = [[test_args objectAtIndex:i+1] UTF8String];
}
NSLog(@"Test command: %@", test_args);
// Generate an isolated Python configuration.
NSLog(@"Configuring isolated Python...");
@ -66,7 +70,7 @@
// Ensure that signal handlers are installed
config.install_signal_handlers = 1;
// Run the test module.
config.run_module = Py_DecodeLocale([[test_args objectAtIndex:0] UTF8String], NULL);
config.run_module = Py_DecodeLocale([[test_args objectAtIndex:1] UTF8String], NULL);
// For debugging - enable verbose mode.
// config.verbose = 1;
@ -99,7 +103,7 @@
}
NSLog(@"Configure argc/argv...");
status = PyConfig_SetBytesArgv(&config, [test_args count], (char**) argv);
status = PyConfig_SetBytesArgv(&config, [test_args count] - 1, (char**) argv);
if (PyStatus_Exception(status)) {
XCTFail(@"Unable to configure argc/argv: %s", status.err_msg);
PyConfig_Clear(&config);