@@ -60,8 +60,10 @@ async def async_check_output(*args, **kwargs):
60
60
return stdout .decode (* DECODE_ARGS )
61
61
else :
62
62
raise subprocess .CalledProcessError (
63
- process .returncode , args ,
64
- stdout .decode (* DECODE_ARGS ), stderr .decode (* DECODE_ARGS )
63
+ process .returncode ,
64
+ args ,
65
+ stdout .decode (* DECODE_ARGS ),
66
+ stderr .decode (* DECODE_ARGS ),
65
67
)
66
68
67
69
@@ -76,10 +78,9 @@ async def list_devices():
76
78
# Filter out the booted iOS simulators
77
79
return [
78
80
simulator ["udid" ]
79
- for runtime , simulators in json_data [' devices' ].items ()
81
+ for runtime , simulators in json_data [" devices" ].items ()
80
82
for simulator in simulators
81
- if runtime .split ("." )[- 1 ].startswith ("iOS" )
82
- and simulator ['state' ] == "Booted"
83
+ if runtime .split ("." )[- 1 ].startswith ("iOS" ) and simulator ["state" ] == "Booted"
83
84
]
84
85
85
86
@@ -99,7 +100,7 @@ async def find_device(initial_devices):
99
100
100
101
async def log_stream_task (initial_devices ):
101
102
# Wait up to 5 minutes for the build to complete and the simulator to boot.
102
- udid = await asyncio .wait_for (find_device (initial_devices ), 5 * 60 )
103
+ udid = await asyncio .wait_for (find_device (initial_devices ), 5 * 60 )
103
104
104
105
# Stream the iOS device's logs, filtering out messages that come from the
105
106
# XCTest test suite (catching NSLog messages from the test method), or
@@ -120,11 +121,13 @@ async def log_stream_task(initial_devices):
120
121
(
121
122
'senderImagePath ENDSWITH "/iOSTestbedTests.xctest/iOSTestbedTests"'
122
123
' OR senderImagePath ENDSWITH "/Python.framework/Python"'
123
- )
124
+ ),
124
125
]
125
126
126
127
async with async_process (
127
- * args , stdout = subprocess .PIPE , stderr = subprocess .STDOUT ,
128
+ * args ,
129
+ stdout = subprocess .PIPE ,
130
+ stderr = subprocess .STDOUT ,
128
131
) as process :
129
132
while line := (await process .stdout .readline ()).decode (* DECODE_ARGS ):
130
133
sys .stdout .write (line )
@@ -144,10 +147,12 @@ async def xcode_test(location, simulator):
144
147
"-resultBundlePath" ,
145
148
str (location / f"{ datetime .now ():%Y%m%d-%H%M%S} .xcresult" ),
146
149
"-derivedDataPath" ,
147
- str (location / "DerivedData" ,)
150
+ str (location / "DerivedData" ),
148
151
]
149
152
async with async_process (
150
- * args , stdout = subprocess .PIPE , stderr = subprocess .STDOUT ,
153
+ * args ,
154
+ stdout = subprocess .PIPE ,
155
+ stderr = subprocess .STDOUT ,
151
156
) as process :
152
157
while line := (await process .stdout .readline ()).decode (* DECODE_ARGS ):
153
158
sys .stdout .write (line )
@@ -156,34 +161,64 @@ async def xcode_test(location, simulator):
156
161
exit (status )
157
162
158
163
159
- def create_testbed (location : Path , framework : Path , apps : list [Path ]) -> None :
160
- if location .exists ():
161
- print (f"{ location } already exists; aborting without creating project." )
164
+ def clone_testbed (
165
+ source : Path ,
166
+ target : Path ,
167
+ framework : Path ,
168
+ apps : list [Path ],
169
+ ) -> None :
170
+ if target .exists ():
171
+ print (f"{ target } already exists; aborting without creating project." )
162
172
sys .exit (10 )
163
173
164
- print ("Copying template testbed project..." )
165
- shutil .copytree (Path (__file__ ).parent , location )
174
+ if framework is None :
175
+ if not (source / "Python.xcframework/ios-arm64_x86_64-simulator/bin" ).is_dir ():
176
+ print (
177
+ f"The testbed being cloned ({ source } ) does not contain "
178
+ f"a simulator framework. Re-run with --framework"
179
+ )
180
+ sys .exit (11 )
181
+ else :
182
+ if not framework .is_dir ():
183
+ print (f"{ framework } does not exist." )
184
+ sys .exit (12 )
185
+ elif not (
186
+ framework .suffix == ".xcframework"
187
+ or (framework / "Python.framework" ).is_dir ()
188
+ ):
189
+ print (
190
+ f"{ framework } is not an XCframework, "
191
+ f"or a simulator slice of a framework build."
192
+ )
193
+ sys .exit (13 )
194
+
195
+ print ("Cloning testbed project..." )
196
+ shutil .copytree (source , target )
166
197
167
- if framework .suffix == ".xcframework" :
168
- print ("Installing XCFramework..." )
169
- xc_framework_path = location / "Python.xcframework"
170
- shutil .rmtree (xc_framework_path )
171
- shutil .copytree (framework , xc_framework_path )
198
+ if framework is not None :
199
+ if framework .suffix == ".xcframework" :
200
+ print ("Installing XCFramework..." )
201
+ xc_framework_path = target / "Python.xcframework"
202
+ shutil .rmtree (xc_framework_path )
203
+ shutil .copytree (framework , xc_framework_path )
204
+ else :
205
+ print ("Installing simulator Framework..." )
206
+ sim_framework_path = (
207
+ target / "Python.xcframework" / "ios-arm64_x86_64-simulator"
208
+ )
209
+ shutil .rmtree (sim_framework_path )
210
+ shutil .copytree (framework , sim_framework_path )
172
211
else :
173
- print ("Installing simulator Framework..." )
174
- sim_framework_path = (
175
- location
176
- / "Python.xcframework"
177
- / "ios-arm64_x86_64-simulator"
178
- )
179
- shutil .rmtree (sim_framework_path )
180
- shutil .copytree (framework , sim_framework_path )
212
+ print ("Using pre-existing iOS framework." )
181
213
182
- for app in apps :
183
- print (f"Installing app { app !r} ..." )
184
- shutil .copytree (app , location / "iOSTestbed/app/{app.name}" )
214
+ for app_src in apps :
215
+ print (f"Installing app { app_src .name !r} ..." )
216
+ app_target = target / f"iOSTestbed/app/{ app_src .name } "
217
+ if app_target .is_dir ():
218
+ shutil .rmtree (app_target )
219
+ shutil .copytree (app_src , app_target )
185
220
186
- print (f"Testbed project created in { location } " )
221
+ print (f"Testbed project created in { target } " )
187
222
188
223
189
224
def update_plist (testbed_path , args ):
@@ -221,48 +256,47 @@ async def run_testbed(simulator: str, args: list[str]):
221
256
222
257
def main ():
223
258
parser = argparse .ArgumentParser (
224
- prog = "testbed" ,
225
259
description = (
226
- "Manages the process of testing a Python project in the iOS simulator"
227
- )
260
+ "Manages the process of testing a Python project in the iOS simulator. "
261
+ ),
228
262
)
229
263
230
264
subcommands = parser .add_subparsers (dest = "subcommand" )
231
265
232
- create = subcommands .add_parser (
233
- "create " ,
266
+ clone = subcommands .add_parser (
267
+ "clone " ,
234
268
description = (
235
269
"Clone the testbed project, copying in an iOS Python framework and"
236
270
"any specified application code."
237
271
),
238
- help = "Create a new testbed project"
272
+ help = "Clone a testbed project to a new location." ,
239
273
)
240
- create .add_argument (
274
+ clone .add_argument (
241
275
"--framework" ,
242
- required = True ,
243
276
help = (
244
- "The location of the XCFramework (or simulator-only slice of an XCFramework) "
245
- "to use when running the testbed"
246
- )
277
+ "The location of the XCFramework (or simulator-only slice of an "
278
+ "XCFramework) to use when running the testbed"
279
+ ),
247
280
)
248
- create .add_argument (
281
+ clone .add_argument (
249
282
"--app" ,
250
283
dest = "apps" ,
251
284
action = "append" ,
252
285
default = [],
253
286
help = "The location of any code to include in the testbed project" ,
254
287
)
255
- create .add_argument (
288
+ clone .add_argument (
256
289
"location" ,
257
- help = "The path where the testbed will be created."
290
+ help = "The path where the testbed will be cloned." ,
258
291
)
259
292
260
293
run = subcommands .add_parser (
261
294
"run" ,
262
- usage = ' %(prog)s [-h] [--simulator SIMULATOR] -- <test arg> [<test arg> ...]' ,
295
+ usage = " %(prog)s [-h] [--simulator SIMULATOR] -- <test arg> [<test arg> ...]" ,
263
296
description = (
264
- "Run a testbed project. The arguments provided after `--` will be passed to "
265
- "the running iOS process as if they were arguments to `python -m`."
297
+ "Run a testbed project. The arguments provided after `--` will be "
298
+ "passed to the running iOS process as if they were arguments to "
299
+ "`python -m`."
266
300
),
267
301
help = "Run a testbed project" ,
268
302
)
@@ -275,32 +309,43 @@ def main():
275
309
try :
276
310
pos = sys .argv .index ("--" )
277
311
testbed_args = sys .argv [1 :pos ]
278
- test_args = sys .argv [pos + 1 :]
312
+ test_args = sys .argv [pos + 1 :]
279
313
except ValueError :
280
314
testbed_args = sys .argv [1 :]
281
315
test_args = []
282
316
283
317
context = parser .parse_args (testbed_args )
284
318
285
- if context .subcommand == "create" :
286
- create_testbed (
287
- location = Path (context .location ),
288
- framework = Path (context .framework ),
319
+ if context .subcommand == "clone" :
320
+ clone_testbed (
321
+ source = Path (__file__ ).parent ,
322
+ target = Path (context .location ),
323
+ framework = Path (context .framework ) if context .framework else None ,
289
324
apps = [Path (app ) for app in context .apps ],
290
325
)
291
326
elif context .subcommand == "run" :
292
327
if test_args :
328
+ if not (
329
+ Path (__file__ ).parent / "Python.xcframework/ios-arm64_x86_64-simulator/bin"
330
+ ).is_dir ():
331
+ print (
332
+ f"Testbed does not contain a compiled iOS framework. Use "
333
+ f"`python { sys .argv [0 ]} clone ...` to create a runnable "
334
+ f"clone of this testbed."
335
+ )
336
+ sys .exit (20 )
337
+
293
338
asyncio .run (
294
339
run_testbed (
295
340
simulator = context .simulator ,
296
- args = test_args
341
+ args = test_args ,
297
342
)
298
343
)
299
344
else :
300
345
print (f"Must specify test arguments (e.g., { sys .argv [0 ]} run -- test)" )
301
346
print ()
302
347
parser .print_help (sys .stderr )
303
- sys .exit (2 )
348
+ sys .exit (21 )
304
349
else :
305
350
parser .print_help (sys .stderr )
306
351
sys .exit (1 )
0 commit comments