| 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 errno |
| 10 import os | 10 import os |
| 11 import shutil | 11 import shutil |
| 12 import subprocess | 12 import subprocess |
| 13 import sys | 13 import sys |
| 14 import tempfile | 14 import tempfile |
| 15 import time | 15 import time |
| 16 | 16 |
| 17 import find_xcode | 17 import find_xcode |
| 18 import gtest_utils | 18 import gtest_utils |
| 19 import xctest_utils |
| 19 | 20 |
| 20 | 21 |
| 21 class Error(Exception): | 22 class Error(Exception): |
| 22 """Base class for errors.""" | 23 """Base class for errors.""" |
| 23 pass | 24 pass |
| 24 | 25 |
| 25 | 26 |
| 26 class TestRunnerError(Error): | 27 class TestRunnerError(Error): |
| 27 """Base class for TestRunner-related errors.""" | 28 """Base class for TestRunner-related errors.""" |
| 28 pass | 29 pass |
| (...skipping 11 matching lines...) Expand all Loading... |
| 40 'App does not exist: %s' % app_path) | 41 'App does not exist: %s' % app_path) |
| 41 | 42 |
| 42 | 43 |
| 43 class DeviceDetectionError(TestRunnerError): | 44 class DeviceDetectionError(TestRunnerError): |
| 44 """Unexpected number of devices detected.""" | 45 """Unexpected number of devices detected.""" |
| 45 def __init__(self, udids): | 46 def __init__(self, udids): |
| 46 super(DeviceDetectionError, self).__init__( | 47 super(DeviceDetectionError, self).__init__( |
| 47 'Expected one device, found %s:\n%s' % (len(udids), '\n'.join(udids))) | 48 'Expected one device, found %s:\n%s' % (len(udids), '\n'.join(udids))) |
| 48 | 49 |
| 49 | 50 |
| 51 class PlugInsNotFoundError(TestRunnerError): |
| 52 """The PlugIns directory was not found.""" |
| 53 def __init__(self, plugins_dir): |
| 54 super(PlugInsNotFoundError, self).__init__( |
| 55 'PlugIns directory does not exist: %s' % plugins_dir) |
| 56 |
| 57 |
| 50 class SimulatorNotFoundError(TestRunnerError): | 58 class SimulatorNotFoundError(TestRunnerError): |
| 51 """The given simulator binary was not found.""" | 59 """The given simulator binary was not found.""" |
| 52 def __init__(self, iossim_path): | 60 def __init__(self, iossim_path): |
| 53 super(SimulatorNotFoundError, self).__init__( | 61 super(SimulatorNotFoundError, self).__init__( |
| 54 'Simulator does not exist: %s' % iossim_path) | 62 'Simulator does not exist: %s' % iossim_path) |
| 55 | 63 |
| 56 | 64 |
| 57 class XcodeVersionNotFound(TestRunnerError): | 65 class XcodeVersionNotFoundError(TestRunnerError): |
| 58 """The requested version of Xcode was not found.""" | 66 """The requested version of Xcode was not found.""" |
| 59 def __init__(self, xcode_version): | 67 def __init__(self, xcode_version): |
| 60 super(XcodeVersionNotFoundError, self).__init__( | 68 super(XcodeVersionNotFoundError, self).__init__( |
| 61 'Xcode version not found: %s', xcode_version) | 69 'Xcode version not found: %s', xcode_version) |
| 62 | 70 |
| 63 | 71 |
| 72 class XCTestPlugInNotFoundError(TestRunnerError): |
| 73 """The .xctest PlugIn was not found.""" |
| 74 def __init__(self, xctest_path): |
| 75 super(XCTestPlugInNotFoundError, self).__init__( |
| 76 'XCTest not found: %s', xctest_path) |
| 77 |
| 78 |
| 64 def get_kif_test_filter(tests, invert=False): | 79 def get_kif_test_filter(tests, invert=False): |
| 65 """Returns the KIF test filter to filter the given test cases. | 80 """Returns the KIF test filter to filter the given test cases. |
| 66 | 81 |
| 67 Args: | 82 Args: |
| 68 tests: List of test cases to filter. | 83 tests: List of test cases to filter. |
| 69 invert: Whether to invert the filter or not. Inverted, the filter will match | 84 invert: Whether to invert the filter or not. Inverted, the filter will match |
| 70 everything except the given test cases. | 85 everything except the given test cases. |
| 71 | 86 |
| 72 Returns: | 87 Returns: |
| 73 A string which can be supplied to GKIF_SCENARIO_FILTER. | 88 A string which can be supplied to GKIF_SCENARIO_FILTER. |
| (...skipping 24 matching lines...) Expand all Loading... |
| 98 test_filter = ':'.join(test for test in tests) | 113 test_filter = ':'.join(test for test in tests) |
| 99 if invert: | 114 if invert: |
| 100 return '-%s' % test_filter | 115 return '-%s' % test_filter |
| 101 return test_filter | 116 return test_filter |
| 102 | 117 |
| 103 | 118 |
| 104 class TestRunner(object): | 119 class TestRunner(object): |
| 105 """Base class containing common functionality.""" | 120 """Base class containing common functionality.""" |
| 106 | 121 |
| 107 def __init__( | 122 def __init__( |
| 108 self, app_path, xcode_version, out_dir, env_vars=None, test_args=None): | 123 self, |
| 124 app_path, |
| 125 xcode_version, |
| 126 out_dir, |
| 127 env_vars=None, |
| 128 test_args=None, |
| 129 xctest=False, |
| 130 ): |
| 109 """Initializes a new instance of this class. | 131 """Initializes a new instance of this class. |
| 110 | 132 |
| 111 Args: | 133 Args: |
| 112 app_path: Path to the compiled .app to run. | 134 app_path: Path to the compiled .app to run. |
| 113 xcode_version: Version of Xcode to use when running the test. | 135 xcode_version: Version of Xcode to use when running the test. |
| 114 out_dir: Directory to emit test data into. | 136 out_dir: Directory to emit test data into. |
| 115 env_vars: List of environment variables to pass to the test itself. | 137 env_vars: List of environment variables to pass to the test itself. |
| 116 test_args: List of strings to pass as arguments to the test when | 138 test_args: List of strings to pass as arguments to the test when |
| 117 launching. | 139 launching. |
| 140 xctest: Whether or not this is an XCTest. |
| 118 | 141 |
| 119 Raises: | 142 Raises: |
| 120 AppNotFoundError: If the given app does not exist. | 143 AppNotFoundError: If the given app does not exist. |
| 144 PlugInsNotFoundError: If the PlugIns directory does not exist for XCTests. |
| 121 XcodeVersionNotFoundError: If the given Xcode version does not exist. | 145 XcodeVersionNotFoundError: If the given Xcode version does not exist. |
| 146 XCTestPlugInNotFoundError: If the .xctest PlugIn does not exist. |
| 122 """ | 147 """ |
| 123 if not os.path.exists(app_path): | 148 if not os.path.exists(app_path): |
| 124 raise AppNotFoundError(app_path) | 149 raise AppNotFoundError(app_path) |
| 125 | 150 |
| 126 if not find_xcode.find_xcode(xcode_version)['found']: | 151 if not find_xcode.find_xcode(xcode_version)['found']: |
| 127 raise XcodeVersionNotFoundError(xcode_version) | 152 raise XcodeVersionNotFoundError(xcode_version) |
| 128 | 153 |
| 129 if not os.path.exists(out_dir): | 154 if not os.path.exists(out_dir): |
| 130 os.makedirs(out_dir) | 155 os.makedirs(out_dir) |
| 131 | 156 |
| 132 self.app_name = os.path.splitext(os.path.split(app_path)[-1])[0] | 157 self.app_name = os.path.splitext(os.path.split(app_path)[-1])[0] |
| 133 self.app_path = app_path | 158 self.app_path = app_path |
| 134 self.cfbundleid = subprocess.check_output([ | 159 self.cfbundleid = subprocess.check_output([ |
| 135 '/usr/libexec/PlistBuddy', | 160 '/usr/libexec/PlistBuddy', |
| 136 '-c', 'Print:CFBundleIdentifier', | 161 '-c', 'Print:CFBundleIdentifier', |
| 137 os.path.join(app_path, 'Info.plist'), | 162 os.path.join(app_path, 'Info.plist'), |
| 138 ]).rstrip() | 163 ]).rstrip() |
| 139 self.env_vars = env_vars or [] | 164 self.env_vars = env_vars or [] |
| 140 self.logs = collections.OrderedDict() | 165 self.logs = collections.OrderedDict() |
| 141 self.out_dir = out_dir | 166 self.out_dir = out_dir |
| 142 self.test_args = test_args or [] | 167 self.test_args = test_args or [] |
| 143 self.xcode_version = xcode_version | 168 self.xcode_version = xcode_version |
| 169 self.xctest_path = '' |
| 170 |
| 171 if xctest: |
| 172 plugins_dir = os.path.join(self.app_path, 'PlugIns') |
| 173 if not os.path.exists(plugins_dir): |
| 174 raise PlugInsNotFoundError(plugins_dir) |
| 175 for plugin in os.listdir(plugins_dir): |
| 176 if plugin.endswith('.xctest'): |
| 177 self.xctest_path = os.path.join(plugins_dir, plugin) |
| 178 if not os.path.exists(self.xctest_path): |
| 179 raise XCTestPlugInNotFoundError(self.xctest_path) |
| 144 | 180 |
| 145 def get_launch_command(self, test_filter=None, invert=False): | 181 def get_launch_command(self, test_filter=None, invert=False): |
| 146 """Returns the command that can be used to launch the test app. | 182 """Returns the command that can be used to launch the test app. |
| 147 | 183 |
| 148 Args: | 184 Args: |
| 149 test_filter: List of test cases to filter. | 185 test_filter: List of test cases to filter. |
| 150 invert: Whether to invert the filter or not. Inverted, the filter will | 186 invert: Whether to invert the filter or not. Inverted, the filter will |
| 151 match everything except the given test cases. | 187 match everything except the given test cases. |
| 152 | 188 |
| 153 Returns: | 189 Returns: |
| 154 A list of strings forming the command to launch the test. | 190 A list of strings forming the command to launch the test. |
| 155 """ | 191 """ |
| 156 raise NotImplementedError | 192 raise NotImplementedError |
| 157 | 193 |
| 158 def set_up(self): | 194 def set_up(self): |
| 159 """Performs setup actions which must occur prior to every test launch.""" | 195 """Performs setup actions which must occur prior to every test launch.""" |
| 160 raise NotImplementedError | 196 raise NotImplementedError |
| 161 | 197 |
| 162 def tear_down(self): | 198 def tear_down(self): |
| 163 """Performs cleanup actions which must occur after every test launch.""" | 199 """Performs cleanup actions which must occur after every test launch.""" |
| 164 raise NotImplementedError | 200 raise NotImplementedError |
| 165 | 201 |
| 166 def screenshot_desktop(self): | 202 def screenshot_desktop(self): |
| 167 """Saves a screenshot of the desktop in the output directory.""" | 203 """Saves a screenshot of the desktop in the output directory.""" |
| 168 subprocess.check_call([ | 204 subprocess.check_call([ |
| 169 'screencapture', | 205 'screencapture', |
| 170 os.path.join(self.out_dir, 'desktop_%s.png' % time.time()), | 206 os.path.join(self.out_dir, 'desktop_%s.png' % time.time()), |
| 171 ]) | 207 ]) |
| 172 | 208 |
| 173 @staticmethod | 209 def _run(self, cmd): |
| 174 def _run(cmd): | |
| 175 """Runs the specified command, parsing GTest output. | 210 """Runs the specified command, parsing GTest output. |
| 176 | 211 |
| 177 Args: | 212 Args: |
| 178 cmd: List of strings forming the command to run. | 213 cmd: List of strings forming the command to run. |
| 179 | 214 |
| 180 Returns: | 215 Returns: |
| 181 GTestResult instance. | 216 GTestResult instance. |
| 182 """ | 217 """ |
| 183 print ' '.join(cmd) | 218 print ' '.join(cmd) |
| 184 print | 219 print |
| 185 | 220 |
| 186 parser = gtest_utils.GTestLogParser() | |
| 187 result = gtest_utils.GTestResult(cmd) | 221 result = gtest_utils.GTestResult(cmd) |
| 222 if self.xctest_path: |
| 223 parser = xctest_utils.XCTestLogParser() |
| 224 else: |
| 225 parser = gtest_utils.GTestLogParser() |
| 188 | 226 |
| 189 proc = subprocess.Popen( | 227 proc = subprocess.Popen( |
| 190 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | 228 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
| 191 | 229 |
| 192 while True: | 230 while True: |
| 193 line = proc.stdout.readline() | 231 line = proc.stdout.readline() |
| 194 if not line: | 232 if not line: |
| 195 break | 233 break |
| 196 line = line.rstrip() | 234 line = line.rstrip() |
| 197 parser.ProcessLine(line) | 235 parser.ProcessLine(line) |
| (...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 231 result = self._run(cmd) | 269 result = self._run(cmd) |
| 232 | 270 |
| 233 if result.crashed and not result.crashed_test: | 271 if result.crashed and not result.crashed_test: |
| 234 raise AppLaunchError | 272 raise AppLaunchError |
| 235 | 273 |
| 236 passed = result.passed_tests | 274 passed = result.passed_tests |
| 237 failed = result.failed_tests | 275 failed = result.failed_tests |
| 238 flaked = result.flaked_tests | 276 flaked = result.flaked_tests |
| 239 | 277 |
| 240 try: | 278 try: |
| 241 while result.crashed and result.crashed_test: | 279 # XCTests cannot currently be resumed at the next test case. |
| 280 while not self.xctest_path and result.crashed and result.crashed_test: |
| 242 # If the app crashes during a specific test case, then resume at the | 281 # If the app crashes during a specific test case, then resume at the |
| 243 # next test case. This is achieved by filtering out every test case | 282 # next test case. This is achieved by filtering out every test case |
| 244 # which has already run. | 283 # which has already run. |
| 245 print 'Crashed during %s, resuming...' % result.crashed_test | 284 print 'Crashed during %s, resuming...' % result.crashed_test |
| 246 print | 285 print |
| 247 result = self._run(self.get_launch_command( | 286 result = self._run(self.get_launch_command( |
| 248 test_filter=passed + failed.keys() + flaked.keys(), invert=True, | 287 test_filter=passed + failed.keys() + flaked.keys(), invert=True, |
| 249 )) | 288 )) |
| 250 passed.extend(result.passed_tests) | 289 passed.extend(result.passed_tests) |
| 251 failed.update(result.failed_tests) | 290 failed.update(result.failed_tests) |
| (...skipping 22 matching lines...) Expand all Loading... |
| 274 def __init__( | 313 def __init__( |
| 275 self, | 314 self, |
| 276 app_path, | 315 app_path, |
| 277 iossim_path, | 316 iossim_path, |
| 278 platform, | 317 platform, |
| 279 version, | 318 version, |
| 280 xcode_version, | 319 xcode_version, |
| 281 out_dir, | 320 out_dir, |
| 282 env_vars=None, | 321 env_vars=None, |
| 283 test_args=None, | 322 test_args=None, |
| 323 xctest=False, |
| 284 ): | 324 ): |
| 285 """Initializes a new instance of this class. | 325 """Initializes a new instance of this class. |
| 286 | 326 |
| 287 Args: | 327 Args: |
| 288 app_path: Path to the compiled .app or .ipa to run. | 328 app_path: Path to the compiled .app or .ipa to run. |
| 289 iossim_path: Path to the compiled iossim binary to use. | 329 iossim_path: Path to the compiled iossim binary to use. |
| 290 platform: Name of the platform to simulate. Supported values can be found | 330 platform: Name of the platform to simulate. Supported values can be found |
| 291 by running "iossim -l". e.g. "iPhone 5s", "iPad Retina". | 331 by running "iossim -l". e.g. "iPhone 5s", "iPad Retina". |
| 292 version: Version of iOS the platform should be running. Supported values | 332 version: Version of iOS the platform should be running. Supported values |
| 293 can be found by running "iossim -l". e.g. "9.3", "8.2", "7.1". | 333 can be found by running "iossim -l". e.g. "9.3", "8.2", "7.1". |
| 294 xcode_version: Version of Xcode to use when running the test. | 334 xcode_version: Version of Xcode to use when running the test. |
| 295 out_dir: Directory to emit test data into. | 335 out_dir: Directory to emit test data into. |
| 296 env_vars: List of environment variables to pass to the test itself. | 336 env_vars: List of environment variables to pass to the test itself. |
| 297 test_args: List of strings to pass as arguments to the test when | 337 test_args: List of strings to pass as arguments to the test when |
| 298 launching. | 338 launching. |
| 339 xctest: Whether or not this is an XCTest. |
| 299 | 340 |
| 300 Raises: | 341 Raises: |
| 301 AppNotFoundError: If the given app does not exist. | 342 AppNotFoundError: If the given app does not exist. |
| 343 PlugInsNotFoundError: If the PlugIns directory does not exist for XCTests. |
| 302 XcodeVersionNotFoundError: If the given Xcode version does not exist. | 344 XcodeVersionNotFoundError: If the given Xcode version does not exist. |
| 345 XCTestPlugInNotFoundError: If the .xctest PlugIn does not exist. |
| 303 """ | 346 """ |
| 304 super(SimulatorTestRunner, self).__init__( | 347 super(SimulatorTestRunner, self).__init__( |
| 305 app_path, | 348 app_path, |
| 306 xcode_version, | 349 xcode_version, |
| 307 out_dir, | 350 out_dir, |
| 308 env_vars=env_vars, | 351 env_vars=env_vars, |
| 309 test_args=test_args, | 352 test_args=test_args, |
| 353 xctest=xctest, |
| 310 ) | 354 ) |
| 311 | 355 |
| 312 if not os.path.exists(iossim_path): | 356 if not os.path.exists(iossim_path): |
| 313 raise SimulatorNotFoundError(iossim_path) | 357 raise SimulatorNotFoundError(iossim_path) |
| 314 | 358 |
| 315 self.homedir = '' | 359 self.homedir = '' |
| 316 self.iossim_path = iossim_path | 360 self.iossim_path = iossim_path |
| 317 self.platform = platform | 361 self.platform = platform |
| 318 self.start_time = None | 362 self.start_time = None |
| 319 self.version = version | 363 self.version = version |
| (...skipping 119 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 439 | 483 |
| 440 if self.xcode_version == '8.0': | 484 if self.xcode_version == '8.0': |
| 441 args.extend(['-c', '--gtest_filter=%s' % gtest_filter]) | 485 args.extend(['-c', '--gtest_filter=%s' % gtest_filter]) |
| 442 else: | 486 else: |
| 443 args.append('--gtest_filter=%s' % gtest_filter) | 487 args.append('--gtest_filter=%s' % gtest_filter) |
| 444 | 488 |
| 445 for env_var in self.env_vars: | 489 for env_var in self.env_vars: |
| 446 cmd.extend(['-e', env_var]) | 490 cmd.extend(['-e', env_var]) |
| 447 | 491 |
| 448 cmd.append(self.app_path) | 492 cmd.append(self.app_path) |
| 493 if self.xctest_path: |
| 494 cmd.append(self.xctest_path) |
| 449 cmd.extend(self.test_args) | 495 cmd.extend(self.test_args) |
| 450 cmd.extend(args) | 496 cmd.extend(args) |
| 451 return cmd | 497 return cmd |
| 452 | 498 |
| 453 | 499 |
| 454 class DeviceTestRunner(TestRunner): | 500 class DeviceTestRunner(TestRunner): |
| 455 """Class for running tests on devices.""" | 501 """Class for running tests on devices.""" |
| 456 | 502 |
| 457 def __init__( | 503 def __init__( |
| 458 self, app_path, xcode_version, out_dir, env_vars=None, test_args=None): | 504 self, app_path, xcode_version, out_dir, env_vars=None, test_args=None): |
| (...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 536 | 582 |
| 537 for env_var in self.env_vars: | 583 for env_var in self.env_vars: |
| 538 cmd.extend(['-D', env_var]) | 584 cmd.extend(['-D', env_var]) |
| 539 | 585 |
| 540 if args or self.test_args: | 586 if args or self.test_args: |
| 541 cmd.append('--args') | 587 cmd.append('--args') |
| 542 cmd.extend(self.test_args) | 588 cmd.extend(self.test_args) |
| 543 cmd.extend(args) | 589 cmd.extend(args) |
| 544 | 590 |
| 545 return cmd | 591 return cmd |
| OLD | NEW |