From 958186c10089554c826fafabbe1a41f73075939d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 22 Aug 2025 13:11:50 +0800 Subject: [PATCH] gh-137973: Add a non-parallel test plan to the iOS testbed project (GH-138018) 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 2ba2287b85eea3cc3a71d77c6bcf9eb5670ca05d) Co-authored-by: Russell Keith-Magee --- .gitignore | 1 - Doc/using/ios.rst | 11 + ...-08-21-14-04-50.gh-issue-137873.qxffLt.rst | 3 + iOS/README.rst | 53 +-- iOS/testbed/__main__.py | 358 ++++-------------- iOS/testbed/iOSTestbed.lldbinit | 4 + .../iOSTestbed.xcodeproj/project.pbxproj | 14 +- .../xcschemes/iOSTestbed.xcscheme | 97 +++++ iOS/testbed/iOSTestbed.xctestplan | 46 +++ iOS/testbed/iOSTestbed/iOSTestbed-Info.plist | 12 - iOS/testbed/iOSTestbedTests/iOSTestbedTests.m | 18 +- 11 files changed, 271 insertions(+), 346 deletions(-) create mode 100644 Misc/NEWS.d/next/Tools-Demos/2025-08-21-14-04-50.gh-issue-137873.qxffLt.rst create mode 100644 iOS/testbed/iOSTestbed.lldbinit create mode 100644 iOS/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme create mode 100644 iOS/testbed/iOSTestbed.xctestplan diff --git a/.gitignore b/.gitignore index c945904f6b405b..275532f881df55 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst index 685d8e81add26c..9921fd6114bdc7 100644 --- a/Doc/using/ios.rst +++ b/Doc/using/ios.rst @@ -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 ==================== diff --git a/Misc/NEWS.d/next/Tools-Demos/2025-08-21-14-04-50.gh-issue-137873.qxffLt.rst b/Misc/NEWS.d/next/Tools-Demos/2025-08-21-14-04-50.gh-issue-137873.qxffLt.rst new file mode 100644 index 00000000000000..5b75858560ca1a --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2025-08-21-14-04-50.gh-issue-137873.qxffLt.rst @@ -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. diff --git a/iOS/README.rst b/iOS/README.rst index f0979ba152eb20..4d38e5d7c307d1 100644 --- a/iOS/README.rst +++ b/iOS/README.rst @@ -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. diff --git a/iOS/testbed/__main__.py b/iOS/testbed/__main__.py index 1146bf3b988cda..6a4d9c76d162b4 100644 --- a/iOS/testbed/__main__.py +++ b/iOS/testbed/__main__.py @@ -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"] + + print("Building test project...") + subprocess.run( + ["xcodebuild", "build-for-testing"] + args + verbosity_args, + check=True, + ) - async with async_process( - *args, + 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) - - # 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() + simulator = select_simulator_device() + print(f"Running test on {simulator}") - 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) diff --git a/iOS/testbed/iOSTestbed.lldbinit b/iOS/testbed/iOSTestbed.lldbinit new file mode 100644 index 00000000000000..4cf00dd0f9de1d --- /dev/null +++ b/iOS/testbed/iOSTestbed.lldbinit @@ -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 diff --git a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj b/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj index c7d63909ee2453..18cdafd8127520 100644 --- a/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj +++ b/iOS/testbed/iOSTestbed.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ 607A66592B0F08600010BFC8 /* iOSTestbed-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "iOSTestbed-Info.plist"; sourceTree = ""; }; 608619532CB77BA900F46182 /* app_packages */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app_packages; sourceTree = ""; }; 608619552CB7819B00F46182 /* app */ = {isa = PBXFileReference; lastKnownFileType = folder; path = app; sourceTree = ""; }; + 60FE0EFB2E56BB6D00524F87 /* iOSTestbed.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = iOSTestbed.xctestplan; sourceTree = ""; }; /* 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)"; diff --git a/iOS/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme b/iOS/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme new file mode 100644 index 00000000000000..d093a46f02e95d --- /dev/null +++ b/iOS/testbed/iOSTestbed.xcodeproj/xcshareddata/xcschemes/iOSTestbed.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/testbed/iOSTestbed.xctestplan b/iOS/testbed/iOSTestbed.xctestplan new file mode 100644 index 00000000000000..0c4ab9eb2bad30 --- /dev/null +++ b/iOS/testbed/iOSTestbed.xctestplan @@ -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 +} diff --git a/iOS/testbed/iOSTestbed/iOSTestbed-Info.plist b/iOS/testbed/iOSTestbed/iOSTestbed-Info.plist index a582f42a212783..fea45e1fad6f6f 100644 --- a/iOS/testbed/iOSTestbed/iOSTestbed-Info.plist +++ b/iOS/testbed/iOSTestbed/iOSTestbed-Info.plist @@ -41,18 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - TestArgs - - test - -uall - --single-process - --rerun - -W - - UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m b/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m index 294a06f530501c..cc0a6e4639762a 100644 --- a/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m +++ b/iOS/testbed/iOSTestbedTests/iOSTestbedTests.m @@ -38,16 +38,20 @@ - (void)testPython { // 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 @@ - (void)testPython { // 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 @@ - (void)testPython { } 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);