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 |