forked from chromium/chromium
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrun.py
executable file
·490 lines (453 loc) · 16.9 KB
/
run.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
#!/usr/bin/env vpython
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Run a test.
Sample usage:
./run.py \
-a src/xcodebuild/Release-iphoneos/base_unittests.app \
-o /tmp/out \
-p iPhone 5s \
-v 9.3 \
-b 9b46
Installs base_unittests.app in an iPhone 5s simulator running iOS 9.3 under
Xcode build version 9b46, runs it, and captures all test data in /tmp/out.
"""
import argparse
import json
import logging
import os
import subprocess
import sys
import traceback
import shard_util
import test_runner
import wpr_runner
import xcodebuild_runner
import xcode_util as xcode
class Runner():
"""
Object to encapsulate iOS test runner execution coordination. Parses
arguments and invokes underlying test runners accordingly.
"""
def __init__(self, args=None):
"""
args = argparse Namespace object.
test_args = string list of args.
"""
self.args = argparse.Namespace()
self.test_args = []
if args:
self.parse_args(args)
def install_xcode(self):
"""Installs the requested Xcode build version.
Returns:
(bool, bool)
First bool: True if installation was successful. False otherwise.
Second bool: True if Xcode is legacy package. False if it's new.
"""
try:
if not self.args.mac_toolchain_cmd:
raise test_runner.MacToolchainNotFoundError(self.args.mac_toolchain_cmd)
# Guard against incorrect install paths. On swarming, this path
# should be a requested named cache, and it must exist.
if not os.path.exists(self.args.xcode_path):
raise test_runner.XcodePathNotFoundError(self.args.xcode_path)
runtime_cache_folder = None
# Runner script only utilizes runtime cache when it's a simulator task.
if self.args.version:
runtime_cache_folder = xcode.construct_runtime_cache_folder(
self.args.runtime_cache_prefix, self.args.version)
if not os.path.exists(runtime_cache_folder):
# Depending on infra project, runtime named cache might not be
# deployed. Create the dir if it doesn't exist since xcode_util
# assumes it exists.
# TODO(crbug.com/1191260): Raise error instead of creating dirs after
# runtime named cache is deployed everywhere.
os.makedirs(runtime_cache_folder)
# xcode.install() installs the Xcode & iOS runtime, and returns a bool
# indicating if the Xcode version in CIPD is a legacy Xcode package (which
# includes iOS runtimes).
is_legacy_xcode = xcode.install(
self.args.mac_toolchain_cmd,
self.args.xcode_build_version,
self.args.xcode_path,
runtime_cache_folder=runtime_cache_folder,
ios_version=self.args.version)
xcode.select(self.args.xcode_path)
except subprocess.CalledProcessError as e:
# Flush buffers to ensure correct output ordering.
sys.stdout.flush()
sys.stderr.write('Xcode build version %s failed to install: %s\n' %
(self.args.xcode_build_version, e))
sys.stderr.flush()
return (False, False)
else:
return (True, is_legacy_xcode)
def run(self, args):
"""
Main coordinating function.
"""
self.parse_args(args)
# This logic is run by default before the otool command is invoked such that
# otool has the correct Xcode selected for command line dev tools.
install_success, is_legacy_xcode = self.install_xcode()
if not install_success:
raise test_runner.XcodeVersionNotFoundError(self.args.xcode_build_version)
# GTEST_SHARD_INDEX and GTEST_TOTAL_SHARDS are additional test environment
# variables, set by Swarming, that are only set for a swarming task
# shard count is > 1.
#
# For a given test on a given run, otool should return the same total
# counts and thus, should generate the same sublists. With the shard index,
# each shard would then know the exact test case to run.
gtest_shard_index = os.getenv('GTEST_SHARD_INDEX', 0)
gtest_total_shards = os.getenv('GTEST_TOTAL_SHARDS', 0)
if gtest_shard_index and gtest_total_shards:
self.args.test_cases = shard_util.shard_test_cases(
self.args, gtest_shard_index, gtest_total_shards)
summary = {}
tr = None
if not os.path.exists(self.args.out_dir):
os.makedirs(self.args.out_dir)
try:
if self.args.xcode_parallelization:
tr = xcodebuild_runner.SimulatorParallelTestRunner(
self.args.app,
self.args.host_app,
self.args.iossim,
self.args.version,
self.args.platform,
out_dir=self.args.out_dir,
release=self.args.release,
retries=self.args.retries,
shards=self.args.shards,
test_cases=self.args.test_cases,
test_args=self.test_args,
use_clang_coverage=self.args.use_clang_coverage,
env_vars=self.args.env_var)
elif self.args.replay_path != 'NO_PATH':
tr = wpr_runner.WprProxySimulatorTestRunner(
self.args.app,
self.args.host_app,
self.args.iossim,
self.args.replay_path,
self.args.platform,
self.args.version,
self.args.wpr_tools_path,
self.args.out_dir,
env_vars=self.args.env_var,
retries=self.args.retries,
shards=self.args.shards,
test_args=self.test_args,
test_cases=self.args.test_cases,
xctest=self.args.xctest,
)
elif self.args.iossim and self.args.platform and self.args.version:
tr = test_runner.SimulatorTestRunner(
self.args.app,
self.args.iossim,
self.args.platform,
self.args.version,
self.args.out_dir,
env_vars=self.args.env_var,
retries=self.args.retries,
shards=self.args.shards,
test_args=self.test_args,
test_cases=self.args.test_cases,
use_clang_coverage=self.args.use_clang_coverage,
wpr_tools_path=self.args.wpr_tools_path,
xctest=self.args.xctest,
)
elif self.args.xcodebuild_device_runner and self.args.xctest:
tr = xcodebuild_runner.DeviceXcodeTestRunner(
app_path=self.args.app,
host_app_path=self.args.host_app,
out_dir=self.args.out_dir,
release=self.args.release,
retries=self.args.retries,
test_cases=self.args.test_cases,
test_args=self.test_args,
env_vars=self.args.env_var)
else:
tr = test_runner.DeviceTestRunner(
self.args.app,
self.args.out_dir,
env_vars=self.args.env_var,
restart=self.args.restart,
retries=self.args.retries,
test_args=self.test_args,
test_cases=self.args.test_cases,
xctest=self.args.xctest,
)
logging.info("Using test runner %s" % type(tr).__name__)
return 0 if tr.launch() else 1
except test_runner.DeviceError as e:
sys.stderr.write(traceback.format_exc())
summary['step_text'] = '%s%s' % (e.__class__.__name__,
': %s' % e.args[0] if e.args else '')
# Swarming infra marks device status unavailable for any device related
# issue using this return code.
return 3
except test_runner.TestRunnerError as e:
sys.stderr.write(traceback.format_exc())
summary['step_text'] = '%s%s' % (e.__class__.__name__,
': %s' % e.args[0] if e.args else '')
# test_runner.Launch returns 0 on success, 1 on failure, so return 2
# on exception to distinguish between a test failure, and a failure
# to launch the test at all.
return 2
finally:
if tr:
summary['logs'] = tr.logs
with open(os.path.join(self.args.out_dir, 'summary.json'), 'w') as f:
json.dump(summary, f)
if tr:
with open(os.path.join(self.args.out_dir, 'full_results.json'),
'w') as f:
json.dump(tr.test_results, f)
# The value of test-launcher-summary-output is set by the recipe
# and passed here via swarming.py. This argument defaults to
# ${ISOLATED_OUTDIR}/output.json. out-dir is set to ${ISOLATED_OUTDIR}
# TODO(crbug.com/1031338) - the content of this output.json will
# work with Chromium recipe because we use the noop_merge merge script,
# but will require structural changes to support the default gtest
# merge script (ref: //testing/merge_scripts/standard_gtest_merge.py)
output_json_path = (
self.args.test_launcher_summary_output or
os.path.join(self.args.out_dir, 'output.json'))
with open(output_json_path, 'w') as f:
json.dump(tr.test_results, f)
# Move the iOS runtime back to cache dir if the Xcode package is not
# legacy (i.e. Xcode program & runtimes are in different CIPD packages.)
# and it's a simulator task.
if not is_legacy_xcode and self.args.version:
runtime_cache_folder = xcode.construct_runtime_cache_folder(
self.args.runtime_cache_prefix, self.args.version)
xcode.move_runtime(runtime_cache_folder, self.args.xcode_path, False)
test_runner.defaults_delete('com.apple.CoreSimulator',
'FramebufferServerRendererPolicy')
def parse_args(self, args):
"""
Parse the args into args and test_args.
"""
parser = argparse.ArgumentParser()
parser.add_argument(
'-x',
'--xcode-parallelization',
help='Run tests using xcodebuild\'s parallelization.',
action='store_true',
)
parser.add_argument(
'-a',
'--app',
help='Compiled .app to run for EG1, Compiled -Runner.app for EG2',
metavar='app',
)
parser.add_argument(
'-b',
'--xcode-build-version',
help='Xcode build version to install.',
required=True,
metavar='build_id',
)
parser.add_argument(
'-e',
'--env-var',
action='append',
help='Environment variable to pass to the test itself.',
metavar='ENV=val',
)
parser.add_argument(
'--host-app',
help='Compiled host .app to run.',
default='NO_PATH',
metavar='host_app',
)
parser.add_argument(
'-i',
'--iossim',
help='Compiled iossim to run the app on.',
metavar='iossim',
)
parser.add_argument(
'-j',
'--args-json',
default='{}',
help=
'Specify "env_var": [...] and "test_args": [...] using a JSON dict.',
metavar='{}',
)
parser.add_argument(
'--mac-toolchain-cmd',
help='Command to run mac_toolchain tool. Default: %(default)s.',
default='mac_toolchain',
metavar='mac_toolchain',
)
parser.add_argument(
'-o',
'--out-dir',
help='Directory to store all test data in.',
metavar='dir',
required=True,
)
parser.add_argument(
'-p',
'--platform',
help='Platform to simulate.',
metavar='sim',
)
#TODO(crbug.com/1056887): Implement this arg in infra.
parser.add_argument(
'--release',
help='Indicates if this is a release build.',
action='store_true',
)
parser.add_argument(
'--replay-path',
help=('Path to a directory containing WPR replay and recipe files, for '
'use with WprProxySimulatorTestRunner to replay a test suite '
'against multiple saved website interactions. '
'Default: %(default)s'),
default='NO_PATH',
metavar='replay-path',
)
parser.add_argument(
'--restart',
action='store_true',
help=argparse.SUPPRESS,
)
parser.add_argument(
'-r',
'--retries',
help='Number of times to retry failed test cases.',
metavar='n',
type=int,
)
parser.add_argument(
'--runtime-cache-prefix',
metavar='PATH',
help=(
'Path prefix for runtime cache folder. The prefix will be appended '
'with iOS version to construct the path. iOS simulator will be '
'installed to the path and further copied into Xcode. Default: '
'%(default)s. WARNING: this folder will be overwritten! This '
'folder is intended to be a cached CIPD installation.'),
default='Runtime-ios-',
)
parser.add_argument(
'-s',
'--shards',
help='Number of shards to split test cases.',
metavar='n',
type=int,
)
parser.add_argument(
'-t',
'--test-cases',
action='append',
help=('Tests that should be included in the test run. All other tests '
'will be excluded from this run. If unspecified, run all tests.'),
metavar='testcase',
)
parser.add_argument(
'--use-clang-coverage',
help='Enable code coverage related steps in test runner scripts.',
action='store_true',
)
parser.add_argument(
'--use-trusted-cert',
action='store_true',
help=('Whether to install a cert to the simulator to allow for local '
'HTTPS testing.'),
)
parser.add_argument(
'-v',
'--version',
help='Version of iOS the simulator should run.',
metavar='ver',
)
parser.add_argument(
'--wpr-tools-path',
help=(
'Location of WPR test tools (should be preinstalled, e.g. as part '
'of a swarming task requirement). Default: %(default)s.'),
default='NO_PATH',
metavar='wpr-tools-path',
)
parser.add_argument(
'--xcode-path',
metavar='PATH',
help=('Path to <Xcode>.app folder where contents of the app will be '
'installed. Default: %(default)s. WARNING: this folder will be '
'overwritten! This folder is intended to be a cached CIPD '
'installation.'),
default='Xcode.app',
)
parser.add_argument(
'--xcodebuild-device-runner',
help='Run tests using xcodebuild\'s on real device.',
action='store_true',
)
parser.add_argument(
'--xctest',
action='store_true',
help='Whether or not the given app should be run as an XCTest.',
)
parser.add_argument(
'--test-launcher-summary-output',
default=None,
help='Full path to output.json file. output.json is consumed by both '
'collect_task.py and merge scripts.')
def load_from_json(args):
"""
Load and set arguments from args_json
"""
args_json = json.loads(args.args_json)
args.env_var = args.env_var or []
args.env_var.extend(args_json.get('env_var', []))
args.restart = args_json.get('restart', args.restart)
args.test_cases = args.test_cases or []
args.test_cases.extend(args_json.get('test_cases', []))
args.xctest = args_json.get('xctest', args.xctest)
args.xcode_parallelization = args_json.get('xcode_parallelization',
args.xcode_parallelization)
args.xcodebuild_device_runner = (
args_json.get('xcodebuild_device_runner',
args.xcodebuild_device_runner))
args.shards = args_json.get('shards', args.shards)
test_args.extend(args_json.get('test_args', []))
def validate(args):
"""
Runs argument validation
"""
if (not (args.xcode_parallelization or args.xcodebuild_device_runner) and
(args.iossim or args.platform or args.version)):
# If any of --iossim, --platform, or --version
# are specified then they must all be specified.
if not (args.iossim and args.platform and args.version):
parser.error('must specify all or none of '
'-i/--iossim, -p/--platform, -v/--version')
if args.xcode_parallelization and not (args.platform and args.version):
parser.error('--xcode-parallelization also requires '
'both -p/--platform and -v/--version')
args, test_args = parser.parse_known_args(args)
load_from_json(args)
validate(args)
# TODO(crbug.com/1056820): |app| won't contain "Debug" or "Release" after
# recipe migrations.
args.release = args.release or (args.app and "Release" in args.app)
self.args = args
self.test_args = test_args
def main(args):
logging.basicConfig(
format='[%(asctime)s:%(levelname)s] %(message)s',
level=logging.DEBUG,
datefmt='%I:%M:%S')
test_runner.defaults_delete('com.apple.CoreSimulator',
'FramebufferServerRendererPolicy')
runner = Runner()
logging.debug("Arg values passed for this run: %s" % args)
return runner.run(args)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))