Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(275)

Side by Side Diff: ios/build/bots/scripts/test_runner.py

Issue 2437953002: Support xctests on simulators (Closed)
Patch Set: Fix Created 4 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « ios/build/bots/scripts/run.py ('k') | ios/build/bots/scripts/xctest_utils.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
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
OLDNEW
« no previous file with comments | « ios/build/bots/scripts/run.py ('k') | ios/build/bots/scripts/xctest_utils.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698