Skip to content

Commit 0b9baa1

Browse files
committed
Modifications to make testbed runner more flexible and robust.
1 parent 3dc0d71 commit 0b9baa1

File tree

2 files changed

+103
-58
lines changed

2 files changed

+103
-58
lines changed

Makefile.pre.in

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2165,8 +2165,8 @@ testios:
21652165
exit 1;\
21662166
fi
21672167

2168-
# Create the testbed project in the XCFOLDER
2169-
$(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed create --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)"
2168+
# Clone the testbed project into the XCFOLDER
2169+
$(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)"
21702170

21712171
# Run the testbed project
21722172
$(PYTHON_FOR_BUILD) "$(XCFOLDER)" run -- test -uall --single-process --rerun -W

iOS/testbed/__main__.py

Lines changed: 101 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ async def async_check_output(*args, **kwargs):
6060
return stdout.decode(*DECODE_ARGS)
6161
else:
6262
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),
6567
)
6668

6769

@@ -76,10 +78,9 @@ async def list_devices():
7678
# Filter out the booted iOS simulators
7779
return [
7880
simulator["udid"]
79-
for runtime, simulators in json_data['devices'].items()
81+
for runtime, simulators in json_data["devices"].items()
8082
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"
8384
]
8485

8586

@@ -99,7 +100,7 @@ async def find_device(initial_devices):
99100

100101
async def log_stream_task(initial_devices):
101102
# 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)
103104

104105
# Stream the iOS device's logs, filtering out messages that come from the
105106
# XCTest test suite (catching NSLog messages from the test method), or
@@ -120,11 +121,13 @@ async def log_stream_task(initial_devices):
120121
(
121122
'senderImagePath ENDSWITH "/iOSTestbedTests.xctest/iOSTestbedTests"'
122123
' OR senderImagePath ENDSWITH "/Python.framework/Python"'
123-
)
124+
),
124125
]
125126

126127
async with async_process(
127-
*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
128+
*args,
129+
stdout=subprocess.PIPE,
130+
stderr=subprocess.STDOUT,
128131
) as process:
129132
while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
130133
sys.stdout.write(line)
@@ -144,10 +147,12 @@ async def xcode_test(location, simulator):
144147
"-resultBundlePath",
145148
str(location / f"{datetime.now():%Y%m%d-%H%M%S}.xcresult"),
146149
"-derivedDataPath",
147-
str(location / "DerivedData",)
150+
str(location / "DerivedData"),
148151
]
149152
async with async_process(
150-
*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
153+
*args,
154+
stdout=subprocess.PIPE,
155+
stderr=subprocess.STDOUT,
151156
) as process:
152157
while line := (await process.stdout.readline()).decode(*DECODE_ARGS):
153158
sys.stdout.write(line)
@@ -156,34 +161,64 @@ async def xcode_test(location, simulator):
156161
exit(status)
157162

158163

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.")
162172
sys.exit(10)
163173

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)
166197

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)
172211
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.")
181213

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)
185220

186-
print(f"Testbed project created in {location}")
221+
print(f"Testbed project created in {target}")
187222

188223

189224
def update_plist(testbed_path, args):
@@ -221,48 +256,47 @@ async def run_testbed(simulator: str, args: list[str]):
221256

222257
def main():
223258
parser = argparse.ArgumentParser(
224-
prog="testbed",
225259
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+
),
228262
)
229263

230264
subcommands = parser.add_subparsers(dest="subcommand")
231265

232-
create = subcommands.add_parser(
233-
"create",
266+
clone = subcommands.add_parser(
267+
"clone",
234268
description=(
235269
"Clone the testbed project, copying in an iOS Python framework and"
236270
"any specified application code."
237271
),
238-
help="Create a new testbed project"
272+
help="Clone a testbed project to a new location.",
239273
)
240-
create.add_argument(
274+
clone.add_argument(
241275
"--framework",
242-
required=True,
243276
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+
),
247280
)
248-
create.add_argument(
281+
clone.add_argument(
249282
"--app",
250283
dest="apps",
251284
action="append",
252285
default=[],
253286
help="The location of any code to include in the testbed project",
254287
)
255-
create.add_argument(
288+
clone.add_argument(
256289
"location",
257-
help="The path where the testbed will be created."
290+
help="The path where the testbed will be cloned.",
258291
)
259292

260293
run = subcommands.add_parser(
261294
"run",
262-
usage='%(prog)s [-h] [--simulator SIMULATOR] -- <test arg> [<test arg> ...]',
295+
usage="%(prog)s [-h] [--simulator SIMULATOR] -- <test arg> [<test arg> ...]",
263296
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`."
266300
),
267301
help="Run a testbed project",
268302
)
@@ -275,32 +309,43 @@ def main():
275309
try:
276310
pos = sys.argv.index("--")
277311
testbed_args = sys.argv[1:pos]
278-
test_args = sys.argv[pos+1:]
312+
test_args = sys.argv[pos + 1 :]
279313
except ValueError:
280314
testbed_args = sys.argv[1:]
281315
test_args = []
282316

283317
context = parser.parse_args(testbed_args)
284318

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,
289324
apps=[Path(app) for app in context.apps],
290325
)
291326
elif context.subcommand == "run":
292327
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+
293338
asyncio.run(
294339
run_testbed(
295340
simulator=context.simulator,
296-
args=test_args
341+
args=test_args,
297342
)
298343
)
299344
else:
300345
print(f"Must specify test arguments (e.g., {sys.argv[0]} run -- test)")
301346
print()
302347
parser.print_help(sys.stderr)
303-
sys.exit(2)
348+
sys.exit(21)
304349
else:
305350
parser.print_help(sys.stderr)
306351
sys.exit(1)

0 commit comments

Comments
 (0)