@@ -81,6 +81,36 @@ async def async_check_output(*args, **kwargs):
8181 )
8282
8383
84+ # Select a simulator device to use.
85+ async def select_simulator_device ():
86+ # List the testing simulators, in JSON format
87+ raw_json = await async_check_output (
88+ "xcrun" , "simctl" , "--set" , "testing" , "list" , "-j"
89+ )
90+ json_data = json .loads (raw_json )
91+
92+ # Any device will do; we'll look for "SE" devices - but the name isn't
93+ # consistent over time. Older Xcode versions will use "iPhone SE (Nth
94+ # generation)"; As of 2025, they've started using "iPhone 16e".
95+ #
96+ # When Xcode is updated after a new release, new devices will be available
97+ # and old ones will be dropped from the set available on the latest iOS
98+ # version. Select the one with the highest minimum runtime version - this
99+ # is an indicator of the "newest" released device, which should always be
100+ # supported on the "most recent" iOS version.
101+ se_simulators = sorted (
102+ (devicetype ["minRuntimeVersion" ], devicetype ["name" ])
103+ for devicetype in json_data ["devicetypes" ]
104+ if devicetype ["productFamily" ] == "iPhone"
105+ and (
106+ ("iPhone " in devicetype ["name" ] and devicetype ["name" ].endswith ("e" ))
107+ or "iPhone SE " in devicetype ["name" ]
108+ )
109+ )
110+
111+ return se_simulators [- 1 ][1 ]
112+
113+
84114# Return a list of UDIDs associated with booted simulators
85115async def list_devices ():
86116 try :
@@ -339,11 +369,25 @@ def update_plist(testbed_path, args):
339369 plistlib .dump (info , f )
340370
341371
342- async def run_testbed (simulator : str , args : list [str ], verbose : bool = False ):
372+ async def run_testbed (simulator : str | None , args : list [str ], verbose : bool = False ):
343373 location = Path (__file__ ).parent
344374 print ("Updating plist..." , end = "" , flush = True )
345375 update_plist (location , args )
346- print (" done." )
376+ print (" done." , flush = True )
377+
378+ if simulator is None :
379+ simulator = await select_simulator_device ()
380+ print (f"Running test on { simulator } " , flush = True )
381+
382+ # We need to get an exclusive lock on simulator creation, to avoid issues
383+ # with multiple simulators starting and being unable to tell which
384+ # simulator is due to which testbed instance. See
385+ # https://github.com/python/cpython/issues/130294 for details. Wait up to
386+ # 10 minutes for a simulator to boot.
387+ print ("Obtaining lock on simulator creation..." , flush = True )
388+ simulator_lock = SimulatorLock (timeout = 10 * 60 )
389+ await simulator_lock .acquire ()
390+ print ("Simulator lock acquired." , flush = True )
347391
348392 # Get the list of devices that are booted at the start of the test run.
349393 # The simulator started by the test suite will be detected as the new
@@ -409,8 +453,10 @@ def main():
409453 )
410454 run .add_argument (
411455 "--simulator" ,
412- default = "iPhone SE (3rd Generation)" ,
413- help = "The name of the simulator to use (default: 'iPhone SE (3rd Generation)')" ,
456+ help = (
457+ "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to " ,
458+ "the most recently released 'entry level' iPhone device."
459+ )
414460 )
415461 run .add_argument (
416462 "-v" , "--verbose" ,
0 commit comments