Skip to content

Commit c3601eb

Browse files
[3.10] pythongh-127845: Minor improvements to iOS test runner script (pythonGH-127846) (python#127892)
Uses symlinks to install iOS framework into testbed clone, adds a verbose mode to the iOS runner to hide most Xcode output, adds another mechanism to disable terminal colors, and ensures that stdout is flushed after every write. (cherry picked from commit ba2d2fd) Co-authored-by: Russell Keith-Magee <[email protected]>
1 parent 94b7be9 commit c3601eb

File tree

3 files changed

+67
-22
lines changed

3 files changed

+67
-22
lines changed

Makefile.pre.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1301,7 +1301,7 @@ testios:
13011301
$(PYTHON_FOR_BUILD) $(srcdir)/iOS/testbed clone --framework $(PYTHONFRAMEWORKPREFIX) "$(XCFOLDER)"
13021302

13031303
# Run the testbed project
1304-
$(PYTHON_FOR_BUILD) "$(XCFOLDER)" run -- test -uall --single-process --rerun -W
1304+
$(PYTHON_FOR_BUILD) "$(XCFOLDER)" run --verbose -- test -uall --single-process --rerun -W
13051305

13061306
# Like testall, but with only one pass and without multiple processes.
13071307
# Run an optional script to include information about the build environment.

iOS/testbed/__main__.py

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
from contextlib import asynccontextmanager
99
from datetime import datetime
10+
from itertools import chain
1011
from 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+
174192
def 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

234269
def 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
)

iOS/testbed/iOSTestbedTests/iOSTestbedTests.m

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ - (void)testPython {
2424

2525
NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
2626

27-
// Disable all color, as the Xcode log can't display color
27+
// Set some other common environment indicators to disable color, as the
28+
// Xcode log can't display color. Stdout will report that it is *not* a
29+
// TTY.
2830
setenv("NO_COLOR", "1", true);
31+
setenv("PY_COLORS", "0", true);
2932

3033
// Arguments to pass into the test suite runner.
3134
// argv[0] must identify the process; any subsequent arg

0 commit comments

Comments
 (0)