OLD | NEW |
1 # Copyright 2016 The Chromium Authors. All rights reserved. | 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Test runners for iOS.""" | 5 """Test runners for iOS.""" |
6 | 6 |
7 import argparse | 7 import argparse |
8 import collections | 8 import collections |
| 9 import errno |
9 import os | 10 import os |
10 import shutil | 11 import shutil |
11 import subprocess | 12 import subprocess |
12 import sys | 13 import sys |
13 import tempfile | 14 import tempfile |
14 import time | 15 import time |
15 | 16 |
16 import find_xcode | 17 import find_xcode |
17 import gtest_utils | 18 import gtest_utils |
18 | 19 |
(...skipping 10 matching lines...) Expand all Loading... |
29 | 30 |
30 class AppLaunchError(TestRunnerError): | 31 class AppLaunchError(TestRunnerError): |
31 """The app failed to launch.""" | 32 """The app failed to launch.""" |
32 pass | 33 pass |
33 | 34 |
34 | 35 |
35 class AppNotFoundError(TestRunnerError): | 36 class AppNotFoundError(TestRunnerError): |
36 """The requested app was not found.""" | 37 """The requested app was not found.""" |
37 def __init__(self, app_path): | 38 def __init__(self, app_path): |
38 super(AppNotFoundError, self).__init__( | 39 super(AppNotFoundError, self).__init__( |
39 'App does not exist: %s' % app_path) | 40 'App does not exist: %s' % app_path) |
| 41 |
| 42 |
| 43 class DeviceDetectionError(TestRunnerError): |
| 44 """Unexpected number of devices detected.""" |
| 45 def __init__(self, udids): |
| 46 super(DeviceDetectionError, self).__init__( |
| 47 'Expected one device, found %s:\n%s' % (len(udids), '\n'.join(udids))) |
40 | 48 |
41 | 49 |
42 class SimulatorNotFoundError(TestRunnerError): | 50 class SimulatorNotFoundError(TestRunnerError): |
43 """The given simulator binary was not found.""" | 51 """The given simulator binary was not found.""" |
44 def __init__(self, iossim_path): | 52 def __init__(self, iossim_path): |
45 super(SimulatorNotFoundError, self).__init__( | 53 super(SimulatorNotFoundError, self).__init__( |
46 'Simulator does not exist: %s' % iossim_path) | 54 'Simulator does not exist: %s' % iossim_path) |
47 | 55 |
48 | 56 |
49 class XcodeVersionNotFound(TestRunnerError): | 57 class XcodeVersionNotFound(TestRunnerError): |
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
89 # e.g. -a:b:c matches everything except a, b, c. | 97 # e.g. -a:b:c matches everything except a, b, c. |
90 test_filter = ':'.join(test for test in tests) | 98 test_filter = ':'.join(test for test in tests) |
91 if invert: | 99 if invert: |
92 return '-%s' % test_filter | 100 return '-%s' % test_filter |
93 return test_filter | 101 return test_filter |
94 | 102 |
95 | 103 |
96 class TestRunner(object): | 104 class TestRunner(object): |
97 """Base class containing common functionality.""" | 105 """Base class containing common functionality.""" |
98 | 106 |
99 def __init__(self, app_path, xcode_version, out_dir, test_args=None): | 107 def __init__( |
| 108 self, app_path, xcode_version, out_dir, env_vars=None, test_args=None): |
100 """Initializes a new instance of this class. | 109 """Initializes a new instance of this class. |
101 | 110 |
102 Args: | 111 Args: |
103 app_path: Path to the compiled .app to run. | 112 app_path: Path to the compiled .app to run. |
104 xcode_version: Version of Xcode to use when running the test. | 113 xcode_version: Version of Xcode to use when running the test. |
105 out_dir: Directory to emit test data into. | 114 out_dir: Directory to emit test data into. |
| 115 env_vars: List of environment variables to pass to the test itself. |
106 test_args: List of strings to pass as arguments to the test when | 116 test_args: List of strings to pass as arguments to the test when |
107 launching. | 117 launching. |
108 | 118 |
109 Raises: | 119 Raises: |
110 AppNotFoundError: If the given app does not exist. | 120 AppNotFoundError: If the given app does not exist. |
111 XcodeVersionNotFoundError: If the given Xcode version does not exist. | 121 XcodeVersionNotFoundError: If the given Xcode version does not exist. |
112 """ | 122 """ |
113 if not os.path.exists(app_path): | 123 if not os.path.exists(app_path): |
114 raise AppNotFoundError(app_path) | 124 raise AppNotFoundError(app_path) |
115 | 125 |
116 if not find_xcode.find_xcode(xcode_version)['found']: | 126 if not find_xcode.find_xcode(xcode_version)['found']: |
117 raise XcodeVersionNotFoundError(xcode_version) | 127 raise XcodeVersionNotFoundError(xcode_version) |
118 | 128 |
119 if not os.path.exists(out_dir): | 129 if not os.path.exists(out_dir): |
120 os.makedirs(out_dir) | 130 os.makedirs(out_dir) |
121 | 131 |
122 self.app_name = os.path.splitext(os.path.split(app_path)[-1])[0] | 132 self.app_name = os.path.splitext(os.path.split(app_path)[-1])[0] |
123 self.app_path = app_path | 133 self.app_path = app_path |
124 self.cfbundleid = subprocess.check_output([ | 134 self.cfbundleid = subprocess.check_output([ |
125 '/usr/libexec/PlistBuddy', | 135 '/usr/libexec/PlistBuddy', |
126 '-c', 'Print:CFBundleIdentifier', | 136 '-c', 'Print:CFBundleIdentifier', |
127 os.path.join(app_path, 'Info.plist'), | 137 os.path.join(app_path, 'Info.plist'), |
128 ]).rstrip() | 138 ]).rstrip() |
| 139 self.env_vars = env_vars or [] |
129 self.logs = collections.OrderedDict() | 140 self.logs = collections.OrderedDict() |
130 self.out_dir = out_dir | 141 self.out_dir = out_dir |
131 self.test_args = test_args or [] | 142 self.test_args = test_args or [] |
| 143 self.xcode_version = xcode_version |
132 | 144 |
133 def get_launch_command(self, test_filter=None, invert=False): | 145 def get_launch_command(self, test_filter=None, invert=False): |
134 """Returns the command that can be used to launch the test app. | 146 """Returns the command that can be used to launch the test app. |
135 | 147 |
136 Args: | 148 Args: |
137 test_filter: List of test cases to filter. | 149 test_filter: List of test cases to filter. |
138 invert: Whether to invert the filter or not. Inverted, the filter will | 150 invert: Whether to invert the filter or not. Inverted, the filter will |
139 match everything except the given test cases. | 151 match everything except the given test cases. |
140 | 152 |
141 Returns: | 153 Returns: |
(...skipping 118 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
260 """Class for running tests on iossim.""" | 272 """Class for running tests on iossim.""" |
261 | 273 |
262 def __init__( | 274 def __init__( |
263 self, | 275 self, |
264 app_path, | 276 app_path, |
265 iossim_path, | 277 iossim_path, |
266 platform, | 278 platform, |
267 version, | 279 version, |
268 xcode_version, | 280 xcode_version, |
269 out_dir, | 281 out_dir, |
| 282 env_vars=None, |
270 test_args=None, | 283 test_args=None, |
271 ): | 284 ): |
272 """Initializes a new instance of this class. | 285 """Initializes a new instance of this class. |
273 | 286 |
274 Args: | 287 Args: |
275 app_path: Path to the compiled .app or .ipa to run. | 288 app_path: Path to the compiled .app or .ipa to run. |
276 iossim_path: Path to the compiled iossim binary to use. | 289 iossim_path: Path to the compiled iossim binary to use. |
277 platform: Name of the platform to simulate. Supported values can be found | 290 platform: Name of the platform to simulate. Supported values can be found |
278 by running "iossim -l". e.g. "iPhone 5s", "iPad Retina". | 291 by running "iossim -l". e.g. "iPhone 5s", "iPad Retina". |
279 version: Version of iOS the platform should be running. Supported values | 292 version: Version of iOS the platform should be running. Supported values |
280 can be found by running "iossim -l". e.g. "9.3", "8.2", "7.1". | 293 can be found by running "iossim -l". e.g. "9.3", "8.2", "7.1". |
281 xcode_version: Version of Xcode to use when running the test. | 294 xcode_version: Version of Xcode to use when running the test. |
282 out_dir: Directory to emit test data into. | 295 out_dir: Directory to emit test data into. |
| 296 env_vars: List of environment variables to pass to the test itself. |
283 test_args: List of strings to pass as arguments to the test when | 297 test_args: List of strings to pass as arguments to the test when |
284 launching. | 298 launching. |
285 | 299 |
286 Raises: | 300 Raises: |
287 AppNotFoundError: If the given app does not exist. | 301 AppNotFoundError: If the given app does not exist. |
288 XcodeVersionNotFoundError: If the given Xcode version does not exist. | 302 XcodeVersionNotFoundError: If the given Xcode version does not exist. |
289 """ | 303 """ |
290 super(SimulatorTestRunner, self).__init__( | 304 super(SimulatorTestRunner, self).__init__( |
291 app_path, xcode_version, out_dir, test_args=test_args) | 305 app_path, |
| 306 xcode_version, |
| 307 out_dir, |
| 308 env_vars=env_vars, |
| 309 test_args=test_args, |
| 310 ) |
292 | 311 |
293 if not os.path.exists(iossim_path): | 312 if not os.path.exists(iossim_path): |
294 raise SimulatorNotFoundError(iossim_path) | 313 raise SimulatorNotFoundError(iossim_path) |
295 | 314 |
296 self.homedir = '' | 315 self.homedir = '' |
297 self.iossim_path = iossim_path | 316 self.iossim_path = iossim_path |
298 self.platform = platform | 317 self.platform = platform |
299 self.start_time = None | 318 self.start_time = None |
300 self.version = version | 319 self.version = version |
301 | 320 |
(...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
410 '-s', self.version, | 429 '-s', self.version, |
411 '-t', '120', | 430 '-t', '120', |
412 '-u', self.homedir, | 431 '-u', self.homedir, |
413 ] | 432 ] |
414 args = [] | 433 args = [] |
415 | 434 |
416 if test_filter: | 435 if test_filter: |
417 kif_filter = get_kif_test_filter(test_filter, invert=invert) | 436 kif_filter = get_kif_test_filter(test_filter, invert=invert) |
418 gtest_filter = get_gtest_filter(test_filter, invert=invert) | 437 gtest_filter = get_gtest_filter(test_filter, invert=invert) |
419 cmd.extend(['-e', 'GKIF_SCENARIO_FILTER=%s' % kif_filter]) | 438 cmd.extend(['-e', 'GKIF_SCENARIO_FILTER=%s' % kif_filter]) |
420 args.append('--gtest_filter=%s' % gtest_filter) | 439 |
| 440 if self.xcode_version == '8.0': |
| 441 args.extend(['-c', '--gtest_filter=%s' % gtest_filter]) |
| 442 else: |
| 443 args.append('--gtest_filter=%s' % gtest_filter) |
| 444 |
| 445 for env_var in self.env_vars: |
| 446 cmd.extend(['-e', env_var]) |
421 | 447 |
422 cmd.append(self.app_path) | 448 cmd.append(self.app_path) |
423 cmd.extend(self.test_args) | 449 cmd.extend(self.test_args) |
424 cmd.extend(args) | 450 cmd.extend(args) |
425 return cmd | 451 return cmd |
| 452 |
| 453 |
| 454 class DeviceTestRunner(TestRunner): |
| 455 """Class for running tests on devices.""" |
| 456 |
| 457 def __init__( |
| 458 self, app_path, xcode_version, out_dir, env_vars=None, test_args=None): |
| 459 """Initializes a new instance of this class. |
| 460 |
| 461 Args: |
| 462 app_path: Path to the compiled .app to run. |
| 463 xcode_version: Version of Xcode to use when running the test. |
| 464 out_dir: Directory to emit test data into. |
| 465 env_vars: List of environment variables to pass to the test itself. |
| 466 test_args: List of strings to pass as arguments to the test when |
| 467 launching. |
| 468 |
| 469 Raises: |
| 470 AppNotFoundError: If the given app does not exist. |
| 471 XcodeVersionNotFoundError: If the given Xcode version does not exist. |
| 472 """ |
| 473 super(DeviceTestRunner, self).__init__( |
| 474 app_path, xcode_version, out_dir, env_vars=env_vars, test_args=test_args) |
| 475 |
| 476 self.udid = subprocess.check_output(['idevice_id', '--list']).rstrip() |
| 477 if len(self.udid.splitlines()) != 1: |
| 478 raise DeviceDetectionError(self.udid) |
| 479 |
| 480 def uninstall_apps(self): |
| 481 """Uninstalls all apps found on the device.""" |
| 482 for app in subprocess.check_output( |
| 483 ['idevicefs', '--udid', self.udid, 'ls', '@']).splitlines(): |
| 484 subprocess.check_call( |
| 485 ['ideviceinstaller', '--udid', self.udid, '--uninstall', app]) |
| 486 |
| 487 def install_app(self): |
| 488 """Installs the app.""" |
| 489 subprocess.check_call( |
| 490 ['ideviceinstaller', '--udid', self.udid, '--install', self.app_path]) |
| 491 |
| 492 def set_up(self): |
| 493 """Performs setup actions which must occur prior to every test launch.""" |
| 494 self.uninstall_apps() |
| 495 self.install_app() |
| 496 |
| 497 def extract_test_data(self): |
| 498 """Extracts data emitted by the test.""" |
| 499 subprocess.check_call([ |
| 500 'idevicefs', |
| 501 '--udid', self.udid, |
| 502 'pull', |
| 503 '@%s/Documents' % self.cfbundleid, |
| 504 os.path.join(self.out_dir, 'Documents'), |
| 505 ]) |
| 506 |
| 507 def tear_down(self): |
| 508 """Performs cleanup actions which must occur after every test launch.""" |
| 509 self.extract_test_data() |
| 510 self.screenshot_desktop() |
| 511 self.uninstall_apps() |
| 512 |
| 513 def get_launch_command(self, test_filter=None, invert=False): |
| 514 """Returns the command that can be used to launch the test app. |
| 515 |
| 516 Args: |
| 517 test_filter: List of test cases to filter. |
| 518 invert: Whether to invert the filter or not. Inverted, the filter will |
| 519 match everything except the given test cases. |
| 520 |
| 521 Returns: |
| 522 A list of strings forming the command to launch the test. |
| 523 """ |
| 524 cmd = [ |
| 525 'idevice-app-runner', |
| 526 '--udid', self.udid, |
| 527 '--start', self.cfbundleid, |
| 528 ] |
| 529 args = [] |
| 530 |
| 531 if test_filter: |
| 532 kif_filter = get_kif_test_filter(test_filter, invert=invert) |
| 533 gtest_filter = get_gtest_filter(test_filter, invert=invert) |
| 534 cmd.extend(['-D', 'GKIF_SCENARIO_FILTER=%s' % kif_filter]) |
| 535 args.append('--gtest-filter=%s' % gtest_filter) |
| 536 |
| 537 for env_var in self.env_vars: |
| 538 cmd.extend(['-D', env_var]) |
| 539 |
| 540 if args or self.test_args: |
| 541 cmd.append('--args') |
| 542 cmd.extend(self.test_args) |
| 543 cmd.extend(args) |
| 544 |
| 545 return cmd |
OLD | NEW |