77import sys
88from contextlib import asynccontextmanager
99from datetime import datetime
10+ from itertools import chain
1011from pathlib import Path
1112
1213
@@ -141,10 +142,12 @@ async def log_stream_task(initial_devices):
141142 else :
142143 suppress_dupes = False
143144 sys .stdout .write (line )
145+ sys .stdout .flush ()
144146
145147
146- async def xcode_test (location , simulator ):
148+ async def xcode_test (location , simulator , verbose ):
147149 # Run the test suite on the named simulator
150+ print ("Starting xcodebuild..." )
148151 args = [
149152 "xcodebuild" ,
150153 "test" ,
@@ -159,18 +162,33 @@ async def xcode_test(location, simulator):
159162 "-derivedDataPath" ,
160163 str (location / "DerivedData" ),
161164 ]
165+ if not verbose :
166+ args += ["-quiet" ]
167+
162168 async with async_process (
163169 * args ,
164170 stdout = subprocess .PIPE ,
165171 stderr = subprocess .STDOUT ,
166172 ) as process :
167173 while line := (await process .stdout .readline ()).decode (* DECODE_ARGS ):
168174 sys .stdout .write (line )
175+ sys .stdout .flush ()
169176
170177 status = await asyncio .wait_for (process .wait (), timeout = 1 )
171178 exit (status )
172179
173180
181+ # A backport of Path.relative_to(*, walk_up=True)
182+ def relative_to (target , other ):
183+ for step , path in enumerate (chain ([other ], other .parents )):
184+ if path == target or path in target .parents :
185+ break
186+ else :
187+ raise ValueError (f"{ str (target )!r} and { str (other )!r} have different anchors" )
188+ parts = ['..' ] * step + list (target .parts [len (path .parts ):])
189+ return Path ("/" .join (parts ))
190+
191+
174192def clone_testbed (
175193 source : Path ,
176194 target : Path ,
@@ -182,7 +200,9 @@ def clone_testbed(
182200 sys .exit (10 )
183201
184202 if framework is None :
185- if not (source / "Python.xcframework/ios-arm64_x86_64-simulator/bin" ).is_dir ():
203+ if not (
204+ source / "Python.xcframework/ios-arm64_x86_64-simulator/bin"
205+ ).is_dir ():
186206 print (
187207 f"The testbed being cloned ({ source } ) does not contain "
188208 f"a simulator framework. Re-run with --framework"
@@ -202,33 +222,48 @@ def clone_testbed(
202222 )
203223 sys .exit (13 )
204224
205- print ("Cloning testbed project..." )
206- shutil .copytree (source , target )
225+ print ("Cloning testbed project:" )
226+ print (f" Cloning { source } ..." , end = "" , flush = True )
227+ shutil .copytree (source , target , symlinks = True )
228+ print (" done" )
207229
208230 if framework is not None :
209231 if framework .suffix == ".xcframework" :
210- print ("Installing XCFramework..." )
211- xc_framework_path = target / "Python.xcframework"
212- shutil .rmtree (xc_framework_path )
213- shutil .copytree (framework , xc_framework_path )
232+ print (" Installing XCFramework..." , end = "" , flush = True )
233+ xc_framework_path = (target / "Python.xcframework" ).resolve ()
234+ if xc_framework_path .is_dir ():
235+ shutil .rmtree (xc_framework_path )
236+ else :
237+ xc_framework_path .unlink ()
238+ xc_framework_path .symlink_to (
239+ relative_to (framework , xc_framework_path .parent )
240+ )
241+ print (" done" )
214242 else :
215- print ("Installing simulator Framework ..." )
243+ print (" Installing simulator framework ..." , end = "" , flush = True )
216244 sim_framework_path = (
217245 target / "Python.xcframework" / "ios-arm64_x86_64-simulator"
246+ ).resolve ()
247+ if sim_framework_path .is_dir ():
248+ shutil .rmtree (sim_framework_path )
249+ else :
250+ sim_framework_path .unlink ()
251+ sim_framework_path .symlink_to (
252+ relative_to (framework , sim_framework_path .parent )
218253 )
219- shutil .rmtree (sim_framework_path )
220- shutil .copytree (framework , sim_framework_path )
254+ print (" done" )
221255 else :
222- print ("Using pre-existing iOS framework." )
256+ print (" Using pre-existing iOS framework." )
223257
224258 for app_src in apps :
225- print (f"Installing app { app_src .name !r} ..." )
259+ print (f" Installing app { app_src .name !r} ..." , end = "" , flush = True )
226260 app_target = target / f"iOSTestbed/app/{ app_src .name } "
227261 if app_target .is_dir ():
228262 shutil .rmtree (app_target )
229263 shutil .copytree (app_src , app_target )
264+ print (" done" )
230265
231- print (f"Testbed project created in { target } " )
266+ print (f"Successfully cloned testbed: { target . resolve () } " )
232267
233268
234269def update_plist (testbed_path , args ):
@@ -243,10 +278,11 @@ def update_plist(testbed_path, args):
243278 plistlib .dump (info , f )
244279
245280
246- async def run_testbed (simulator : str , args : list [str ]):
281+ async def run_testbed (simulator : str , args : list [str ], verbose : bool = False ):
247282 location = Path (__file__ ).parent
248- print ("Updating plist..." )
283+ print ("Updating plist..." , end = "" , flush = True )
249284 update_plist (location , args )
285+ print (" done." )
250286
251287 # Get the list of devices that are booted at the start of the test run.
252288 # The simulator started by the test suite will be detected as the new
@@ -256,10 +292,10 @@ async def run_testbed(simulator: str, args: list[str]):
256292 try :
257293 async with asyncio .TaskGroup () as tg :
258294 tg .create_task (log_stream_task (initial_devices ))
259- tg .create_task (xcode_test (location , simulator ))
260- except* MySystemExit as e :
295+ tg .create_task (xcode_test (location , simulator = simulator , verbose = verbose ))
296+ except MySystemExit as e :
261297 raise SystemExit (* e .exceptions [0 ].args ) from None
262- except* subprocess .CalledProcessError as e :
298+ except subprocess .CalledProcessError as e :
263299 # Extract it from the ExceptionGroup so it can be handled by `main`.
264300 raise e .exceptions [0 ]
265301
@@ -315,6 +351,11 @@ def main():
315351 default = "iPhone SE (3rd Generation)" ,
316352 help = "The name of the simulator to use (default: 'iPhone SE (3rd Generation)')" ,
317353 )
354+ run .add_argument (
355+ "-v" , "--verbose" ,
356+ action = "store_true" ,
357+ help = "Enable verbose output" ,
358+ )
318359
319360 try :
320361 pos = sys .argv .index ("--" )
@@ -330,7 +371,7 @@ def main():
330371 clone_testbed (
331372 source = Path (__file__ ).parent ,
332373 target = Path (context .location ),
333- framework = Path (context .framework ) if context .framework else None ,
374+ framework = Path (context .framework ). resolve () if context .framework else None ,
334375 apps = [Path (app ) for app in context .apps ],
335376 )
336377 elif context .subcommand == "run" :
@@ -348,6 +389,7 @@ def main():
348389 asyncio .run (
349390 run_testbed (
350391 simulator = context .simulator ,
392+ verbose = context .verbose ,
351393 args = test_args ,
352394 )
353395 )
0 commit comments