@@ -80,6 +80,36 @@ async def async_check_output(*args, **kwargs):
8080 )
8181
8282
83+ # Select a simulator device to use.
84+ async def select_simulator_device ():
85+ # List the testing simulators, in JSON format
86+ raw_json = await async_check_output (
87+ "xcrun" , "simctl" , "--set" , "testing" , "list" , "-j"
88+ )
89+ json_data = json .loads (raw_json )
90+
91+ # Any device will do; we'll look for "SE" devices - but the name isn't
92+ # consistent over time. Older Xcode versions will use "iPhone SE (Nth
93+ # generation)"; As of 2025, they've started using "iPhone 16e".
94+ #
95+ # When Xcode is updated after a new release, new devices will be available
96+ # and old ones will be dropped from the set available on the latest iOS
97+ # version. Select the one with the highest minimum runtime version - this
98+ # is an indicator of the "newest" released device, which should always be
99+ # supported on the "most recent" iOS version.
100+ se_simulators = sorted (
101+ (devicetype ["minRuntimeVersion" ], devicetype ["name" ])
102+ for devicetype in json_data ["devicetypes" ]
103+ if devicetype ["productFamily" ] == "iPhone"
104+ and (
105+ ("iPhone " in devicetype ["name" ] and devicetype ["name" ].endswith ("e" ))
106+ or "iPhone SE " in devicetype ["name" ]
107+ )
108+ )
109+
110+ return se_simulators [- 1 ][1 ]
111+
112+
83113# Return a list of UDIDs associated with booted simulators
84114async def list_devices ():
85115 try :
@@ -327,11 +357,25 @@ def update_plist(testbed_path, args):
327357 plistlib .dump (info , f )
328358
329359
330- async def run_testbed (simulator : str , args : list [str ], verbose : bool = False ):
360+ async def run_testbed (simulator : str | None , args : list [str ], verbose : bool = False ):
331361 location = Path (__file__ ).parent
332362 print ("Updating plist..." , end = "" , flush = True )
333363 update_plist (location , args )
334- print (" done." )
364+ print (" done." , flush = True )
365+
366+ if simulator is None :
367+ simulator = await select_simulator_device ()
368+ print (f"Running test on { simulator } " , flush = True )
369+
370+ # We need to get an exclusive lock on simulator creation, to avoid issues
371+ # with multiple simulators starting and being unable to tell which
372+ # simulator is due to which testbed instance. See
373+ # https://github.com/python/cpython/issues/130294 for details. Wait up to
374+ # 10 minutes for a simulator to boot.
375+ print ("Obtaining lock on simulator creation..." , flush = True )
376+ simulator_lock = SimulatorLock (timeout = 10 * 60 )
377+ await simulator_lock .acquire ()
378+ print ("Simulator lock acquired." , flush = True )
335379
336380 # Get the list of devices that are booted at the start of the test run.
337381 # The simulator started by the test suite will be detected as the new
@@ -397,8 +441,10 @@ def main():
397441 )
398442 run .add_argument (
399443 "--simulator" ,
400- default = "iPhone SE (3rd Generation)" ,
401- help = "The name of the simulator to use (default: 'iPhone SE (3rd Generation)')" ,
444+ help = (
445+ "The name of the simulator to use (eg: 'iPhone 16e'). Defaults to " ,
446+ "the most recently released 'entry level' iPhone device."
447+ )
402448 )
403449 run .add_argument (
404450 "-v" , "--verbose" ,
0 commit comments