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

Side by Side Diff: build/android/pylib/local/device/local_device_perf_test_run.py

Issue 2200193002: [Android] Add ability to run deviceless tests on hosts without devices for perf tests. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: [Android] Add ability to run deviceless tests on hosts without devices for perf tests. Created 4 years, 4 months 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 | « no previous file | no next file » | 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 import io 5 import io
6 import json 6 import json
7 import logging 7 import logging
8 import os 8 import os
9 import pickle 9 import pickle
10 import shutil 10 import shutil
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
54 self._timer.cancel() 54 self._timer.cancel()
55 self._running = False 55 self._running = False
56 56
57 def _LogMessage(self): 57 def _LogMessage(self):
58 logging.info('Currently working on test %s', self._shard.current_test) 58 logging.info('Currently working on test %s', self._shard.current_test)
59 self._timer = threading.Timer(self._wait_time, self._LogMessage) 59 self._timer = threading.Timer(self._wait_time, self._LogMessage)
60 self._timer.start() 60 self._timer.start()
61 61
62 62
63 class TestShard(object): 63 class TestShard(object):
64 def __init__( 64 def __init__(self, env, test_instance, tests, retries=3, timeout=None):
65 self, env, test_instance, device, index, tests, retries=3, timeout=None): 65 logging.info('Create shard for the following tests:')
66 logging.info('Create shard %s for device %s to run the following tests:',
67 index, device)
68 for t in tests: 66 for t in tests:
69 logging.info(' %s', t) 67 logging.info(' %s', t)
70 self._battery = battery_utils.BatteryUtils(device)
71 self._current_test = None 68 self._current_test = None
72 self._device = device
73 self._env = env 69 self._env = env
74 self._index = index 70 self._heart_beat = HeartBeat(self)
71 self._index = None
75 self._output_dir = None 72 self._output_dir = None
76 self._retries = retries 73 self._retries = retries
77 self._test_instance = test_instance 74 self._test_instance = test_instance
78 self._tests = tests 75 self._tests = tests
79 self._timeout = timeout 76 self._timeout = timeout
80 self._heart_beat = HeartBeat(self)
81
82 @local_device_environment.handle_shard_failures
83 def RunTestsOnShard(self):
84 results = base_test_result.TestRunResults()
85 for test in self._tests:
86 tries_left = self._retries
87 result_type = None
88 while (result_type != base_test_result.ResultType.PASS
89 and tries_left > 0):
90 try:
91 self._TestSetUp(test)
92 result_type = self._RunSingleTest(test)
93 except device_errors.CommandTimeoutError:
94 result_type = base_test_result.ResultType.TIMEOUT
95 except device_errors.CommandFailedError:
96 logging.exception('Exception when executing %s.', test)
97 result_type = base_test_result.ResultType.FAIL
98 finally:
99 self._TestTearDown()
100 if result_type != base_test_result.ResultType.PASS:
101 try:
102 device_recovery.RecoverDevice(self._device, self._env.blacklist)
103 except device_errors.CommandTimeoutError:
104 logging.exception(
105 'Device failed to recover after failing %s.', test)
106 tries_left = tries_left - 1
107
108 results.AddResult(base_test_result.BaseTestResult(test, result_type))
109 return results
110 77
111 def _TestSetUp(self, test): 78 def _TestSetUp(self, test):
112 if not self._device.IsOnline():
113 msg = 'Device %s is unresponsive.' % str(self._device)
114 raise device_errors.DeviceUnreachableError(msg)
115
116 logging.info('Charge level: %s%%',
117 str(self._battery.GetBatteryInfo().get('level')))
118 if self._test_instance.min_battery_level:
119 self._battery.ChargeDeviceToLevel(self._test_instance.min_battery_level)
120
121 logging.info('temperature: %s (0.1 C)',
122 str(self._battery.GetBatteryInfo().get('temperature')))
123 if self._test_instance.max_battery_temp:
124 self._battery.LetBatteryCoolToTemperature(
125 self._test_instance.max_battery_temp)
126
127 if not self._device.IsScreenOn():
128 self._device.SetScreen(True)
129
130 if (self._test_instance.collect_chartjson_data 79 if (self._test_instance.collect_chartjson_data
131 or self._tests[test].get('archive_output_dir')): 80 or self._tests[test].get('archive_output_dir')):
132 self._output_dir = tempfile.mkdtemp() 81 self._output_dir = tempfile.mkdtemp()
133 82
134 self._current_test = test 83 self._current_test = test
135 self._heart_beat.Start() 84 self._heart_beat.Start()
136 85
137 def _RunSingleTest(self, test): 86 def _RunSingleTest(self, test):
138 self._test_instance.WriteBuildBotJson(self._output_dir) 87 self._test_instance.WriteBuildBotJson(self._output_dir)
139 88
140 timeout = self._tests[test].get('timeout', self._timeout) 89 timeout = self._tests[test].get('timeout', self._timeout)
141 cmd = self._CreateCmd(test) 90 cmd = self._CreateCmd(test)
142 cwd = os.path.abspath(host_paths.DIR_SOURCE_ROOT) 91 cwd = os.path.abspath(host_paths.DIR_SOURCE_ROOT)
143 92
144 logging.debug("Running %s with command '%s' on shard %d with timeout %d", 93 self._LogTest(test, cmd, timeout)
145 test, cmd, self._index, timeout)
146 94
147 try: 95 try:
148 start_time = time.time() 96 start_time = time.time()
149 exit_code, output = cmd_helper.GetCmdStatusAndOutputWithTimeout( 97 exit_code, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
150 cmd, timeout, cwd=cwd, shell=True) 98 cmd, timeout, cwd=cwd, shell=True)
151 end_time = time.time() 99 end_time = time.time()
152 json_output = self._test_instance.ReadChartjsonOutput(self._output_dir) 100 json_output = self._test_instance.ReadChartjsonOutput(self._output_dir)
153 if exit_code == 0: 101 if exit_code == 0:
154 result_type = base_test_result.ResultType.PASS 102 result_type = base_test_result.ResultType.PASS
155 else: 103 else:
156 result_type = base_test_result.ResultType.FAIL 104 result_type = base_test_result.ResultType.FAIL
157 except cmd_helper.TimeoutError as e: 105 except cmd_helper.TimeoutError as e:
158 end_time = time.time() 106 end_time = time.time()
159 exit_code = -1 107 exit_code = -1
160 output = e.output 108 output = e.output
161 json_output = '' 109 json_output = ''
162 result_type = base_test_result.ResultType.TIMEOUT 110 result_type = base_test_result.ResultType.TIMEOUT
163 111
164 return self._ProcessTestResult(test, cmd, start_time, end_time, exit_code, 112 return self._ProcessTestResult(test, cmd, start_time, end_time, exit_code,
165 output, json_output, result_type) 113 output, json_output, result_type)
166 114
167 def _CreateCmd(self, test): 115 def _CreateCmd(self, test):
168 cmd = '%s --device %s' % (self._tests[test]['cmd'], str(self._device)) 116 cmd = []
117 if self._test_instance.dry_run:
118 cmd.append('echo')
119 cmd.append(self._tests[test]['cmd'])
169 if self._output_dir: 120 if self._output_dir:
170 cmd = cmd + ' --output-dir=%s' % self._output_dir 121 cmd.append('--output-dir=%s' % self._output_dir)
171 if self._test_instance.dry_run: 122 return ' '.join(self._ExtendCmd(cmd))
172 cmd = 'echo %s' % cmd 123
124 def _ExtendCmd(self, cmd): # pylint: disable=no-self-use
173 return cmd 125 return cmd
174 126
127 def _LogTest(self, _test, _cmd, _timeout):
128 raise NotImplementedError
129
130 def _LogTestExit(self, test, exit_code, duration):
131 # pylint: disable=no-self-use
132 logging.info('%s : exit_code=%d in %d secs.', test, exit_code, duration)
133
134 def _ExtendPersistedResult(self, persisted_result):
135 raise NotImplementedError
136
175 def _ProcessTestResult(self, test, cmd, start_time, end_time, exit_code, 137 def _ProcessTestResult(self, test, cmd, start_time, end_time, exit_code,
176 output, json_output, result_type): 138 output, json_output, result_type):
177 if exit_code is None: 139 if exit_code is None:
178 exit_code = -1 140 exit_code = -1
179 logging.info('%s : exit_code=%d in %d secs on device %s', 141
180 test, exit_code, end_time - start_time, 142 self._LogTestExit(test, exit_code, end_time - start_time)
181 str(self._device))
182 143
183 actual_exit_code = exit_code 144 actual_exit_code = exit_code
184 if (self._test_instance.flaky_steps 145 if (self._test_instance.flaky_steps
185 and test in self._test_instance.flaky_steps): 146 and test in self._test_instance.flaky_steps):
186 exit_code = 0 147 exit_code = 0
187 archive_bytes = (self._ArchiveOutputDir() 148 archive_bytes = (self._ArchiveOutputDir()
188 if self._tests[test].get('archive_output_dir') 149 if self._tests[test].get('archive_output_dir')
189 else None) 150 else None)
190 persisted_result = { 151 persisted_result = {
191 'name': test, 152 'name': test,
192 'output': [output], 153 'output': [output],
193 'chartjson': json_output, 154 'chartjson': json_output,
194 'archive_bytes': archive_bytes, 155 'archive_bytes': archive_bytes,
195 'exit_code': exit_code, 156 'exit_code': exit_code,
196 'actual_exit_code': actual_exit_code, 157 'actual_exit_code': actual_exit_code,
197 'result_type': result_type, 158 'result_type': result_type,
198 'start_time': start_time, 159 'start_time': start_time,
199 'end_time': end_time, 160 'end_time': end_time,
200 'total_time': end_time - start_time, 161 'total_time': end_time - start_time,
201 'device': str(self._device),
202 'cmd': cmd, 162 'cmd': cmd,
203 } 163 }
164 self._ExtendPersistedResult(persisted_result)
204 self._SaveResult(persisted_result) 165 self._SaveResult(persisted_result)
205 return result_type 166 return result_type
206 167
207 def _ArchiveOutputDir(self): 168 def _ArchiveOutputDir(self):
208 """Archive all files in the output dir, and return as compressed bytes.""" 169 """Archive all files in the output dir, and return as compressed bytes."""
209 with io.BytesIO() as archive: 170 with io.BytesIO() as archive:
210 with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as contents: 171 with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as contents:
211 num_files = 0 172 num_files = 0
212 for absdir, _, files in os.walk(self._output_dir): 173 for absdir, _, files in os.walk(self._output_dir):
213 reldir = os.path.relpath(absdir, self._output_dir) 174 reldir = os.path.relpath(absdir, self._output_dir)
(...skipping 16 matching lines...) Expand all
230 with file(pickled, 'r') as f: 191 with file(pickled, 'r') as f:
231 previous = pickle.loads(f.read()) 192 previous = pickle.loads(f.read())
232 result['output'] = previous['output'] + result['output'] 193 result['output'] = previous['output'] + result['output']
233 with file(pickled, 'w') as f: 194 with file(pickled, 'w') as f:
234 f.write(pickle.dumps(result)) 195 f.write(pickle.dumps(result))
235 196
236 def _TestTearDown(self): 197 def _TestTearDown(self):
237 if self._output_dir: 198 if self._output_dir:
238 shutil.rmtree(self._output_dir, ignore_errors=True) 199 shutil.rmtree(self._output_dir, ignore_errors=True)
239 self._output_dir = None 200 self._output_dir = None
201 self._heart_beat.Stop()
202 self._current_test = None
203
204 @property
205 def current_test(self):
206 return self._current_test
207
208
209 class DeviceTestShard(TestShard):
210 def __init__(
211 self, env, test_instance, device, index, tests, retries=3, timeout=None):
212 super(DeviceTestShard, self).__init__(
213 env, test_instance, tests, retries, timeout)
214 self._battery = battery_utils.BatteryUtils(device) if device else None
215 self._device = device
216 self._index = index
217
218 @local_device_environment.handle_shard_failures
219 def RunTestsOnShard(self):
220 results = base_test_result.TestRunResults()
221 for test in self._tests:
222 tries_left = self._retries
223 result_type = None
224 while (result_type != base_test_result.ResultType.PASS
225 and tries_left > 0):
226 try:
227 self._TestSetUp(test)
228 result_type = self._RunSingleTest(test)
229 except device_errors.CommandTimeoutError:
230 result_type = base_test_result.ResultType.TIMEOUT
231 except device_errors.CommandFailedError:
232 logging.exception('Exception when executing %s.', test)
233 result_type = base_test_result.ResultType.FAIL
234 finally:
235 self._TestTearDown()
236 if result_type != base_test_result.ResultType.PASS:
237 try:
238 device_recovery.RecoverDevice(self._device, self._env.blacklist)
239 except device_errors.CommandTimeoutError:
240 logging.exception(
241 'Device failed to recover after failing %s.', test)
242 tries_left = tries_left - 1
243
244 results.AddResult(base_test_result.BaseTestResult(test, result_type))
245 return results
246
247 def _LogTestExit(self, test, exit_code, duration):
248 logging.info('%s : exit_code=%d in %d secs on device %s',
249 test, exit_code, duration, str(self._device))
250
251 def _TestSetUp(self, test):
252 if not self._device.IsOnline():
253 msg = 'Device %s is unresponsive.' % str(self._device)
254 raise device_errors.DeviceUnreachableError(msg)
255
256 logging.info('Charge level: %s%%',
257 str(self._battery.GetBatteryInfo().get('level')))
258 if self._test_instance.min_battery_level:
259 self._battery.ChargeDeviceToLevel(self._test_instance.min_battery_level)
260
261 logging.info('temperature: %s (0.1 C)',
262 str(self._battery.GetBatteryInfo().get('temperature')))
263 if self._test_instance.max_battery_temp:
264 self._battery.LetBatteryCoolToTemperature(
265 self._test_instance.max_battery_temp)
266
267 if not self._device.IsScreenOn():
268 self._device.SetScreen(True)
269
270 super(DeviceTestShard, self)._TestSetUp(test)
271
272 def _LogTest(self, test, cmd, timeout):
273 logging.debug("Running %s with command '%s' on shard %s with timeout %d",
274 test, cmd, str(self._index), timeout)
275
276 def _ExtendCmd(self, cmd):
277 cmd.extend(['--device=%s' % str(self._device)])
278 return cmd
279
280 def _ExtendPersistedResult(self, persisted_result):
281 persisted_result['host_test'] = False
282 persisted_result['device'] = str(self._device)
283
284 def _TestTearDown(self):
240 try: 285 try:
241 logging.info('Unmapping device ports for %s.', self._device) 286 logging.info('Unmapping device ports for %s.', self._device)
242 forwarder.Forwarder.UnmapAllDevicePorts(self._device) 287 forwarder.Forwarder.UnmapAllDevicePorts(self._device)
243 except Exception: # pylint: disable=broad-except 288 except Exception: # pylint: disable=broad-except
244 logging.exception('Exception when resetting ports.') 289 logging.exception('Exception when resetting ports.')
245 finally: 290 finally:
246 self._heart_beat.Stop() 291 super(DeviceTestShard, self)._TestTearDown()
247 self._current_test = None
248 292
249 @property 293 class HostTestShard(TestShard):
250 def current_test(self): 294 def __init__(self, env, test_instance, tests, retries=3, timeout=None):
251 return self._current_test 295 super(HostTestShard, self).__init__(
296 env, test_instance, tests, retries, timeout)
297
298 @local_device_environment.handle_shard_failures
299 def RunTestsOnShard(self):
300 results = base_test_result.TestRunResults()
301 for test in self._tests:
302 tries_left = self._retries
303 result_type = None
304 while (result_type != base_test_result.ResultType.PASS
305 and tries_left > 0):
306 try:
307 self._TestSetUp(test)
308 result_type = self._RunSingleTest(test)
309 finally:
310 self._TestTearDown()
311 results.AddResult(base_test_result.BaseTestResult(test, result_type))
312 return results
313
314 def _LogTest(self, test, cmd, timeout):
315 logging.debug("Running %s with command '%s' on host shard with timeout %d",
316 test, cmd, timeout)
317
318 def _ExtendPersistedResult(self, persisted_result):
319 persisted_result['host_test'] = True
320
252 321
253 class LocalDevicePerfTestRun(local_device_test_run.LocalDeviceTestRun): 322 class LocalDevicePerfTestRun(local_device_test_run.LocalDeviceTestRun):
254 323
255 _DEFAULT_TIMEOUT = 60 * 60 324 _DEFAULT_TIMEOUT = 60 * 60
256 _CONFIG_VERSION = 1 325 _CONFIG_VERSION = 1
257 326
258 def __init__(self, env, test_instance): 327 def __init__(self, env, test_instance):
259 super(LocalDevicePerfTestRun, self).__init__(env, test_instance) 328 super(LocalDevicePerfTestRun, self).__init__(env, test_instance)
260 self._devices = None 329 self._devices = None
261 self._env = env 330 self._env = env
331 self._no_device_tests = {}
262 self._test_buckets = [] 332 self._test_buckets = []
263 self._test_instance = test_instance 333 self._test_instance = test_instance
264 self._timeout = None if test_instance.no_timeout else self._DEFAULT_TIMEOUT 334 self._timeout = None if test_instance.no_timeout else self._DEFAULT_TIMEOUT
265 335
266 def SetUp(self): 336 def SetUp(self):
267 self._devices = self._GetAllDevices(self._env.devices, 337 self._devices = self._GetAllDevices(self._env.devices,
268 self._test_instance.known_devices_file) 338 self._test_instance.known_devices_file)
269 339
270 if os.path.exists(constants.PERF_OUTPUT_DIR): 340 if os.path.exists(constants.PERF_OUTPUT_DIR):
271 shutil.rmtree(constants.PERF_OUTPUT_DIR) 341 shutil.rmtree(constants.PERF_OUTPUT_DIR)
(...skipping 25 matching lines...) Expand all
297 raise PerfTestRunGetStepsError( 367 raise PerfTestRunGetStepsError(
298 'Neither single_step or steps set in test_instance.') 368 'Neither single_step or steps set in test_instance.')
299 369
300 def _SplitTestsByAffinity(self): 370 def _SplitTestsByAffinity(self):
301 # This splits tests by their device affinity so that the same tests always 371 # This splits tests by their device affinity so that the same tests always
302 # run on the same devices. This is important for perf tests since different 372 # run on the same devices. This is important for perf tests since different
303 # devices might yield slightly different performance results. 373 # devices might yield slightly different performance results.
304 test_dict = self._GetStepsFromDict() 374 test_dict = self._GetStepsFromDict()
305 for test, test_config in test_dict['steps'].iteritems(): 375 for test, test_config in test_dict['steps'].iteritems():
306 try: 376 try:
307 affinity = test_config['device_affinity'] 377 affinity = test_config.get('device_affinity')
308 if len(self._test_buckets) < affinity + 1: 378 if affinity is None:
309 while len(self._test_buckets) != affinity + 1: 379 self._no_device_tests[test] = test_config
310 self._test_buckets.append({}) 380 else:
311 self._test_buckets[affinity][test] = test_config 381 if len(self._test_buckets) < affinity + 1:
382 while len(self._test_buckets) != affinity + 1:
383 self._test_buckets.append({})
384 self._test_buckets[affinity][test] = test_config
312 except KeyError: 385 except KeyError:
313 logging.exception( 386 logging.exception(
314 'Test config for %s is bad.\n Config:%s', test, str(test_config)) 387 'Test config for %s is bad.\n Config:%s', test, str(test_config))
315 388
316 @staticmethod 389 @staticmethod
317 def _GetAllDevices(active_devices, devices_path): 390 def _GetAllDevices(active_devices, devices_path):
318 try: 391 try:
319 if devices_path: 392 if devices_path:
320 devices = [device_utils.DeviceUtils(s) 393 devices = [device_utils.DeviceUtils(s)
321 for s in device_list.GetPersistentDeviceList(devices_path)] 394 for s in device_list.GetPersistentDeviceList(devices_path)]
322 if not devices and active_devices: 395 if not devices and active_devices:
323 logging.warning('%s is empty. Falling back to active devices.', 396 logging.warning('%s is empty. Falling back to active devices.',
324 devices_path) 397 devices_path)
325 devices = active_devices 398 devices = active_devices
326 else: 399 else:
327 logging.warning('Known devices file path not being passed. For device ' 400 logging.warning('Known devices file path not being passed. For device '
328 'affinity to work properly, it must be passed.') 401 'affinity to work properly, it must be passed.')
329 devices = active_devices 402 devices = active_devices
330 except IOError as e: 403 except IOError as e:
331 logging.error('Unable to find %s [%s]', devices_path, e) 404 logging.error('Unable to find %s [%s]', devices_path, e)
332 devices = active_devices 405 devices = active_devices
333 return sorted(devices) 406 return sorted(devices)
334 407
335 #override 408 #override
336 def RunTests(self): 409 def RunTests(self):
337 # Affinitize the tests. 410 # Affinitize the tests.
338 self._SplitTestsByAffinity() 411 self._SplitTestsByAffinity()
339 if not self._test_buckets: 412 if not self._test_buckets and not self._no_device_tests:
340 raise local_device_test_run.NoTestsError() 413 raise local_device_test_run.NoTestsError()
341 414
342 def run_perf_tests(shard_id): 415 def run_perf_tests(shard_id):
343 if device_status.IsBlacklisted( 416 if shard_id is None:
344 str(self._devices[shard_id]), self._env.blacklist): 417 s = HostTestShard(self._env, self._test_instance, self._no_device_tests,
345 logging.warning('Device %s is not active. Will not create shard %s.', 418 retries=3, timeout=self._timeout)
346 str(self._devices[shard_id]), shard_id) 419 else:
347 return None 420 if device_status.IsBlacklisted(
348 s = TestShard(self._env, self._test_instance, self._devices[shard_id], 421 str(self._devices[shard_id]), self._env.blacklist):
349 shard_id, self._test_buckets[shard_id], 422 logging.warning('Device %s is not active. Will not create shard %s.',
350 retries=self._env.max_tries, timeout=self._timeout) 423 str(self._devices[shard_id]), shard_id)
424 return None
425 s = DeviceTestShard(self._env, self._test_instance,
426 self._devices[shard_id], shard_id,
427 self._test_buckets[shard_id],
428 retries=self._env.max_tries, timeout=self._timeout)
351 return s.RunTestsOnShard() 429 return s.RunTestsOnShard()
352 430
353 device_indices = range(min(len(self._devices), len(self._test_buckets))) 431 device_indices = range(min(len(self._devices), len(self._test_buckets)))
432 if self._no_device_tests:
433 device_indices.append(None)
354 shards = parallelizer.Parallelizer(device_indices).pMap(run_perf_tests) 434 shards = parallelizer.Parallelizer(device_indices).pMap(run_perf_tests)
355 return [x for x in shards.pGet(self._timeout) if x is not None] 435 return [x for x in shards.pGet(self._timeout) if x is not None]
356 436
357 # override 437 # override
358 def TestPackage(self): 438 def TestPackage(self):
359 return 'perf' 439 return 'perf'
360 440
361 # override 441 # override
362 def _CreateShards(self, _tests): 442 def _CreateShards(self, _tests):
363 raise NotImplementedError 443 raise NotImplementedError
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after
417 # override 497 # override
418 def _RunTest(self, _device, _test): 498 def _RunTest(self, _device, _test):
419 raise NotImplementedError 499 raise NotImplementedError
420 500
421 501
422 class TestDictVersionError(Exception): 502 class TestDictVersionError(Exception):
423 pass 503 pass
424 504
425 class PerfTestRunGetStepsError(Exception): 505 class PerfTestRunGetStepsError(Exception):
426 pass 506 pass
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698