7
7
import sys
8
8
from contextlib import asynccontextmanager
9
9
from datetime import datetime
10
+ from itertools import chain
10
11
from pathlib import Path
11
12
12
13
@@ -141,10 +142,12 @@ async def log_stream_task(initial_devices):
141
142
else :
142
143
suppress_dupes = False
143
144
sys .stdout .write (line )
145
+ sys .stdout .flush ()
144
146
145
147
146
- async def xcode_test (location , simulator ):
148
+ async def xcode_test (location , simulator , verbose ):
147
149
# Run the test suite on the named simulator
150
+ print ("Starting xcodebuild..." )
148
151
args = [
149
152
"xcodebuild" ,
150
153
"test" ,
@@ -159,18 +162,33 @@ async def xcode_test(location, simulator):
159
162
"-derivedDataPath" ,
160
163
str (location / "DerivedData" ),
161
164
]
165
+ if not verbose :
166
+ args += ["-quiet" ]
167
+
162
168
async with async_process (
163
169
* args ,
164
170
stdout = subprocess .PIPE ,
165
171
stderr = subprocess .STDOUT ,
166
172
) as process :
167
173
while line := (await process .stdout .readline ()).decode (* DECODE_ARGS ):
168
174
sys .stdout .write (line )
175
+ sys .stdout .flush ()
169
176
170
177
status = await asyncio .wait_for (process .wait (), timeout = 1 )
171
178
exit (status )
172
179
173
180
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
+
174
192
def clone_testbed (
175
193
source : Path ,
176
194
target : Path ,
@@ -182,7 +200,9 @@ def clone_testbed(
182
200
sys .exit (10 )
183
201
184
202
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 ():
186
206
print (
187
207
f"The testbed being cloned ({ source } ) does not contain "
188
208
f"a simulator framework. Re-run with --framework"
@@ -202,33 +222,48 @@ def clone_testbed(
202
222
)
203
223
sys .exit (13 )
204
224
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" )
207
229
208
230
if framework is not None :
209
231
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" )
214
242
else :
215
- print ("Installing simulator Framework ..." )
243
+ print (" Installing simulator framework ..." , end = "" , flush = True )
216
244
sim_framework_path = (
217
245
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 )
218
253
)
219
- shutil .rmtree (sim_framework_path )
220
- shutil .copytree (framework , sim_framework_path )
254
+ print (" done" )
221
255
else :
222
- print ("Using pre-existing iOS framework." )
256
+ print (" Using pre-existing iOS framework." )
223
257
224
258
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 )
226
260
app_target = target / f"iOSTestbed/app/{ app_src .name } "
227
261
if app_target .is_dir ():
228
262
shutil .rmtree (app_target )
229
263
shutil .copytree (app_src , app_target )
264
+ print (" done" )
230
265
231
- print (f"Testbed project created in { target } " )
266
+ print (f"Successfully cloned testbed: { target . resolve () } " )
232
267
233
268
234
269
def update_plist (testbed_path , args ):
@@ -243,10 +278,11 @@ def update_plist(testbed_path, args):
243
278
plistlib .dump (info , f )
244
279
245
280
246
- async def run_testbed (simulator : str , args : list [str ]):
281
+ async def run_testbed (simulator : str , args : list [str ], verbose : bool = False ):
247
282
location = Path (__file__ ).parent
248
- print ("Updating plist..." )
283
+ print ("Updating plist..." , end = "" , flush = True )
249
284
update_plist (location , args )
285
+ print (" done." )
250
286
251
287
# Get the list of devices that are booted at the start of the test run.
252
288
# 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]):
256
292
try :
257
293
async with asyncio .TaskGroup () as tg :
258
294
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 :
261
297
raise SystemExit (* e .exceptions [0 ].args ) from None
262
- except* subprocess .CalledProcessError as e :
298
+ except subprocess .CalledProcessError as e :
263
299
# Extract it from the ExceptionGroup so it can be handled by `main`.
264
300
raise e .exceptions [0 ]
265
301
@@ -315,6 +351,11 @@ def main():
315
351
default = "iPhone SE (3rd Generation)" ,
316
352
help = "The name of the simulator to use (default: 'iPhone SE (3rd Generation)')" ,
317
353
)
354
+ run .add_argument (
355
+ "-v" , "--verbose" ,
356
+ action = "store_true" ,
357
+ help = "Enable verbose output" ,
358
+ )
318
359
319
360
try :
320
361
pos = sys .argv .index ("--" )
@@ -330,7 +371,7 @@ def main():
330
371
clone_testbed (
331
372
source = Path (__file__ ).parent ,
332
373
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 ,
334
375
apps = [Path (app ) for app in context .apps ],
335
376
)
336
377
elif context .subcommand == "run" :
@@ -348,6 +389,7 @@ def main():
348
389
asyncio .run (
349
390
run_testbed (
350
391
simulator = context .simulator ,
392
+ verbose = context .verbose ,
351
393
args = test_args ,
352
394
)
353
395
)
0 commit comments