Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 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 logging | 5 import logging |
| 6 import os | 6 import os |
| 7 import posixpath | 7 import posixpath |
| 8 import re | 8 import re |
| 9 import tempfile | |
| 9 import time | 10 import time |
| 10 | 11 |
| 11 from devil.android import device_errors | 12 from devil.android import device_errors |
| 12 from devil.android import flag_changer | 13 from devil.android import flag_changer |
| 13 from devil.android.sdk import shared_prefs | 14 from devil.android.sdk import shared_prefs |
| 14 from devil.utils import reraiser_thread | 15 from devil.utils import reraiser_thread |
| 15 from pylib import valgrind_tools | 16 from pylib import valgrind_tools |
| 16 from pylib.android import logdog_logcat_monitor | 17 from pylib.android import logdog_logcat_monitor |
| 17 from pylib.base import base_test_result | 18 from pylib.base import base_test_result |
| 18 from pylib.instrumentation import instrumentation_test_instance | 19 from pylib.instrumentation import instrumentation_test_instance |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 32 ('IntegrationTest', 30 * 60), | 33 ('IntegrationTest', 30 * 60), |
| 33 ('External', 10 * 60), | 34 ('External', 10 * 60), |
| 34 ('EnormousTest', 10 * 60), | 35 ('EnormousTest', 10 * 60), |
| 35 ('LargeTest', 5 * 60), | 36 ('LargeTest', 5 * 60), |
| 36 ('MediumTest', 3 * 60), | 37 ('MediumTest', 3 * 60), |
| 37 ('SmallTest', 1 * 60), | 38 ('SmallTest', 1 * 60), |
| 38 ] | 39 ] |
| 39 | 40 |
| 40 LOGCAT_FILTERS = ['*:e', 'chromium:v', 'cr_*:v'] | 41 LOGCAT_FILTERS = ['*:e', 'chromium:v', 'cr_*:v'] |
| 41 | 42 |
| 43 FEATURE_ANNOTATION = 'Feature' | |
| 44 RENDER_TEST_FEATURE_ANNOTATION = 'RenderTest' | |
| 45 | |
| 46 RE_RENDER_IMAGE_NAME = re.compile( | |
|
PEConn
2017/05/09 08:52:39
Could you please put a comment here pointing to Re
jbudorick
2017/05/09 14:33:26
Might be good to add a few tests on both sides as
mikecase (-- gone --)
2017/05/10 01:05:00
Done
| |
| 47 r'(?P<test_class>\w+)\.' | |
| 48 r'(?P<description>\w+)\.' | |
| 49 r'(?P<device_model>\w+)\.' | |
| 50 r'(?P<orientation>port|land)\.png') | |
| 51 | |
| 52 RENDER_TESTS_HTML_TEMPLATE = ''' | |
|
PEConn
2017/05/09 08:52:39
Could you move this into a separate file (or Jinja
mikecase (-- gone --)
2017/05/10 01:05:00
Done. Moved into own file so it will be easier to
| |
| 53 <html> | |
| 54 <table> | |
| 55 <tr> | |
| 56 <th>Failure</th> | |
| 57 <th>Golden</th> | |
| 58 <th>Diff</th> | |
| 59 </tr> | |
| 60 <tr> | |
| 61 <td><img src="%s"/></td> | |
|
jbudorick
2017/05/09 14:33:26
Are these ok with an empty URL? (It's been a while
mikecase (-- gone --)
2017/05/10 01:05:00
Yeah, I doubled checked, it just doesnt load anyth
| |
| 62 <td><img src="%s"/></td> | |
| 63 <td><img src="%s"/></td> | |
| 64 </tr> | |
| 65 </table> | |
| 66 </html> | |
| 67 ''' | |
| 42 | 68 |
| 43 # TODO(jbudorick): Make this private once the instrumentation test_runner is | 69 # TODO(jbudorick): Make this private once the instrumentation test_runner is |
| 44 # deprecated. | 70 # deprecated. |
| 45 def DidPackageCrashOnDevice(package_name, device): | 71 def DidPackageCrashOnDevice(package_name, device): |
| 46 # Dismiss any error dialogs. Limit the number in case we have an error | 72 # Dismiss any error dialogs. Limit the number in case we have an error |
| 47 # loop or we are failing to dismiss. | 73 # loop or we are failing to dismiss. |
| 48 try: | 74 try: |
| 49 for _ in xrange(10): | 75 for _ in xrange(10): |
| 50 package = device.DismissCrashDialogIfNeeded() | 76 package = device.DismissCrashDialogIfNeeded() |
| 51 if not package: | 77 if not package: |
| (...skipping 187 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 239 return tests | 265 return tests |
| 240 | 266 |
| 241 #override | 267 #override |
| 242 def _GetUniqueTestName(self, test): | 268 def _GetUniqueTestName(self, test): |
| 243 return instrumentation_test_instance.GetUniqueTestName(test) | 269 return instrumentation_test_instance.GetUniqueTestName(test) |
| 244 | 270 |
| 245 #override | 271 #override |
| 246 def _RunTest(self, device, test): | 272 def _RunTest(self, device, test): |
| 247 extras = {} | 273 extras = {} |
| 248 | 274 |
| 249 flags = None | 275 flags_to_add = [] |
|
jbudorick
2017/05/09 14:33:26
This change was in your other CL?
mikecase (-- gone --)
2017/05/10 01:05:00
Yeah, have to make the same change here as well. W
| |
| 276 flags_to_remove = [] | |
| 250 test_timeout_scale = None | 277 test_timeout_scale = None |
| 251 if self._test_instance.coverage_directory: | 278 if self._test_instance.coverage_directory: |
| 252 coverage_basename = '%s.ec' % ('%s_group' % test[0]['method'] | 279 coverage_basename = '%s.ec' % ('%s_group' % test[0]['method'] |
| 253 if isinstance(test, list) else test['method']) | 280 if isinstance(test, list) else test['method']) |
| 254 extras['coverage'] = 'true' | 281 extras['coverage'] = 'true' |
| 255 coverage_directory = os.path.join( | 282 coverage_directory = os.path.join( |
| 256 device.GetExternalStoragePath(), 'chrome', 'test', 'coverage') | 283 device.GetExternalStoragePath(), 'chrome', 'test', 'coverage') |
| 257 coverage_device_file = os.path.join( | 284 coverage_device_file = os.path.join( |
| 258 coverage_directory, coverage_basename) | 285 coverage_directory, coverage_basename) |
| 259 extras['coverageFile'] = coverage_device_file | 286 extras['coverageFile'] = coverage_device_file |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 286 test_display_name = self._GetUniqueTestName(test) | 313 test_display_name = self._GetUniqueTestName(test) |
| 287 if test['is_junit4']: | 314 if test['is_junit4']: |
| 288 target = '%s/%s' % ( | 315 target = '%s/%s' % ( |
| 289 self._test_instance.test_package, | 316 self._test_instance.test_package, |
| 290 self._test_instance.test_runner_junit4) | 317 self._test_instance.test_runner_junit4) |
| 291 else: | 318 else: |
| 292 target = '%s/%s' % ( | 319 target = '%s/%s' % ( |
| 293 self._test_instance.test_package, self._test_instance.test_runner) | 320 self._test_instance.test_package, self._test_instance.test_runner) |
| 294 extras['class'] = test_name | 321 extras['class'] = test_name |
| 295 if 'flags' in test: | 322 if 'flags' in test: |
| 296 flags = test['flags'] | 323 flags_to_add.extend(test['flags'].add) |
| 324 flags_to_remove.extend(test['flags'].remove) | |
| 297 timeout = self._GetTimeoutFromAnnotations( | 325 timeout = self._GetTimeoutFromAnnotations( |
| 298 test['annotations'], test_display_name) | 326 test['annotations'], test_display_name) |
| 299 | 327 |
| 300 test_timeout_scale = self._GetTimeoutScaleFromAnnotations( | 328 test_timeout_scale = self._GetTimeoutScaleFromAnnotations( |
| 301 test['annotations']) | 329 test['annotations']) |
| 302 if test_timeout_scale and test_timeout_scale != 1: | 330 if test_timeout_scale and test_timeout_scale != 1: |
| 303 valgrind_tools.SetChromeTimeoutScale( | 331 valgrind_tools.SetChromeTimeoutScale( |
| 304 device, test_timeout_scale * self._test_instance.timeout_scale) | 332 device, test_timeout_scale * self._test_instance.timeout_scale) |
| 305 | 333 |
| 306 logging.info('preparing to run %s: %s', test_display_name, test) | 334 logging.info('preparing to run %s: %s', test_display_name, test) |
| 307 | 335 |
| 308 if flags: | 336 render_tests_device_output_dir = None |
| 337 if _IsRenderTest(test): | |
| 338 # TODO(mikecase): Add DeviceTempDirectory class and use that instead. | |
| 339 render_tests_device_output_dir = posixpath.join( | |
| 340 device.GetExternalStoragePath(), | |
| 341 'render_test_output_dir') | |
| 342 flags_to_add.append('--render-test-output-dir=%s' % | |
| 343 render_tests_device_output_dir) | |
| 344 | |
| 345 if flags_to_add or flags_to_remove: | |
| 309 self._CreateFlagChangerIfNeeded(device) | 346 self._CreateFlagChangerIfNeeded(device) |
| 310 self._flag_changers[str(device)].PushFlags( | 347 self._flag_changers[str(device)].PushFlags( |
| 311 add=flags.add, remove=flags.remove) | 348 add=flags_to_add, remove=flags_to_remove) |
| 312 | 349 |
| 313 try: | 350 try: |
| 314 device.RunShellCommand( | 351 device.RunShellCommand( |
| 315 ['log', '-p', 'i', '-t', _TAG, 'START %s' % test_name], | 352 ['log', '-p', 'i', '-t', _TAG, 'START %s' % test_name], |
| 316 check_return=True) | 353 check_return=True) |
| 317 time_ms = lambda: int(time.time() * 1e3) | 354 time_ms = lambda: int(time.time() * 1e3) |
| 318 start_ms = time_ms() | 355 start_ms = time_ms() |
| 319 | 356 |
| 320 stream_name = 'logcat_%s_%s_%s' % ( | 357 stream_name = 'logcat_%s_%s_%s' % ( |
| 321 test_name.replace('#', '.'), | 358 test_name.replace('#', '.'), |
| 322 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), | 359 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), |
| 323 device.serial) | 360 device.serial) |
| 324 logmon = logdog_logcat_monitor.LogdogLogcatMonitor( | 361 logmon = logdog_logcat_monitor.LogdogLogcatMonitor( |
| 325 device.adb, stream_name, filter_specs=LOGCAT_FILTERS) | 362 device.adb, stream_name, filter_specs=LOGCAT_FILTERS) |
| 326 | 363 |
| 327 with contextlib_ext.Optional( | 364 with contextlib_ext.Optional( |
| 328 logmon, self._test_instance.should_save_logcat): | 365 logmon, self._test_instance.should_save_logcat): |
| 329 with contextlib_ext.Optional( | 366 with contextlib_ext.Optional( |
| 330 trace_event.trace(test_name), | 367 trace_event.trace(test_name), |
| 331 self._env.trace_output): | 368 self._env.trace_output): |
| 332 output = device.StartInstrumentation( | 369 output = device.StartInstrumentation( |
| 333 target, raw=True, extras=extras, timeout=timeout, retries=0) | 370 target, raw=True, extras=extras, timeout=timeout, retries=0) |
| 334 logcat_url = logmon.GetLogcatURL() | 371 logcat_url = logmon.GetLogcatURL() |
| 335 finally: | 372 finally: |
| 336 device.RunShellCommand( | 373 device.RunShellCommand( |
| 337 ['log', '-p', 'i', '-t', _TAG, 'END %s' % test_name], | 374 ['log', '-p', 'i', '-t', _TAG, 'END %s' % test_name], |
| 338 check_return=True) | 375 check_return=True) |
| 339 duration_ms = time_ms() - start_ms | 376 duration_ms = time_ms() - start_ms |
| 340 if flags: | 377 if flags_to_add or flags_to_remove: |
| 341 self._flag_changers[str(device)].Restore() | 378 self._flag_changers[str(device)].Restore() |
| 342 if test_timeout_scale: | 379 if test_timeout_scale: |
| 343 valgrind_tools.SetChromeTimeoutScale( | 380 valgrind_tools.SetChromeTimeoutScale( |
| 344 device, self._test_instance.timeout_scale) | 381 device, self._test_instance.timeout_scale) |
| 345 | 382 |
| 346 # TODO(jbudorick): Make instrumentation tests output a JSON so this | 383 # TODO(jbudorick): Make instrumentation tests output a JSON so this |
| 347 # doesn't have to parse the output. | 384 # doesn't have to parse the output. |
| 348 result_code, result_bundle, statuses = ( | 385 result_code, result_bundle, statuses = ( |
| 349 self._test_instance.ParseAmInstrumentRawOutput(output)) | 386 self._test_instance.ParseAmInstrumentRawOutput(output)) |
| 350 results = self._test_instance.GenerateTestResults( | 387 results = self._test_instance.GenerateTestResults( |
| 351 result_code, result_bundle, statuses, start_ms, duration_ms) | 388 result_code, result_bundle, statuses, start_ms, duration_ms) |
| 352 for result in results: | 389 for result in results: |
| 353 if logcat_url: | 390 if logcat_url: |
| 354 result.SetLink('logcat', logcat_url) | 391 result.SetLink('logcat', logcat_url) |
| 355 | 392 |
| 393 if _IsRenderTest(test): | |
| 394 # Render tests do not cause test failure by default. So we have to check | |
| 395 # to see if any failure images were generated even if the test does not | |
| 396 # fail. | |
| 397 self._ProcessRenderTestResults( | |
| 398 device, render_tests_device_output_dir, results) | |
| 399 | |
| 356 # Update the result name if the test used flags. | 400 # Update the result name if the test used flags. |
| 357 if flags: | 401 if flags_to_add or flags_to_remove: |
| 358 for r in results: | 402 for r in results: |
| 359 if r.GetName() == test_name: | 403 if r.GetName() == test_name: |
| 360 r.SetName(test_display_name) | 404 r.SetName(test_display_name) |
| 361 | 405 |
| 362 # Add UNKNOWN results for any missing tests. | 406 # Add UNKNOWN results for any missing tests. |
| 363 iterable_test = test if isinstance(test, list) else [test] | 407 iterable_test = test if isinstance(test, list) else [test] |
| 364 test_names = set(self._GetUniqueTestName(t) for t in iterable_test) | 408 test_names = set(self._GetUniqueTestName(t) for t in iterable_test) |
| 365 results_names = set(r.GetName() for r in results) | 409 results_names = set(r.GetName() for r in results) |
| 366 results.extend( | 410 results.extend( |
| 367 base_test_result.BaseTestResult(u, base_test_result.ResultType.UNKNOWN) | 411 base_test_result.BaseTestResult(u, base_test_result.ResultType.UNKNOWN) |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 435 include_stack_symbols=False, | 479 include_stack_symbols=False, |
| 436 wipe_tombstones=True) | 480 wipe_tombstones=True) |
| 437 stream_name = 'tombstones_%s_%s' % ( | 481 stream_name = 'tombstones_%s_%s' % ( |
| 438 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), | 482 time.strftime('%Y%m%dT%H%M%S-UTC', time.gmtime()), |
| 439 device.serial) | 483 device.serial) |
| 440 tombstones_url = logdog_helper.text( | 484 tombstones_url = logdog_helper.text( |
| 441 stream_name, '\n'.join(resolved_tombstones)) | 485 stream_name, '\n'.join(resolved_tombstones)) |
| 442 result.SetLink('tombstones', tombstones_url) | 486 result.SetLink('tombstones', tombstones_url) |
| 443 return results, None | 487 return results, None |
| 444 | 488 |
| 489 def _ProcessRenderTestResults( | |
| 490 self, device, render_tests_device_output_dir, results): | |
| 491 # Will archive test images if we are given a GS bucket to store the results | |
| 492 # in and are given a results file to output the links to. | |
| 493 if not bool(self._test_instance.gs_results_bucket): | |
| 494 return | |
| 495 | |
| 496 failure_images_device_dir = posixpath.join( | |
| 497 render_tests_device_output_dir, 'failures') | |
| 498 | |
| 499 if not device.FileExists(failure_images_device_dir): | |
| 500 return | |
| 501 | |
| 502 render_tests_bucket = ( | |
| 503 self._test_instance.gs_results_bucket + '/render_tests') | |
| 504 | |
| 505 diff_images_device_dir = posixpath.join( | |
| 506 render_tests_device_output_dir, 'diffs') | |
| 507 | |
| 508 golden_images_device_dir = posixpath.join( | |
| 509 render_tests_device_output_dir, 'goldens') | |
| 510 | |
| 511 with tempfile_ext.NamedTemporaryDirectory() as temp_dir: | |
| 512 try: | |
| 513 device.PullFile(failure_images_device_dir, temp_dir) | |
| 514 | |
| 515 if device.FileExists(diff_images_device_dir): | |
| 516 device.PullFile(diff_images_device_dir, temp_dir) | |
| 517 else: | |
| 518 logging.error('Diff images not found on device.') | |
| 519 | |
| 520 if device.FileExists(golden_images_device_dir): | |
| 521 device.PullFile(golden_images_device_dir, temp_dir) | |
| 522 else: | |
| 523 logging.error('Golden images not found on device.') | |
| 524 finally: | |
| 525 device.RemovePath(render_tests_device_output_dir, recursive=True) | |
|
jbudorick
2017/05/09 14:33:26
I think this should be the responsibility of _RunT
mikecase (-- gone --)
2017/05/10 01:05:00
Done
| |
| 526 | |
| 527 for failure_filename in os.listdir(os.path.join(temp_dir, 'failures')): | |
| 528 | |
| 529 m = RE_RENDER_IMAGE_NAME.match(failure_filename) | |
| 530 if not m: | |
| 531 logging.warning('Unexpected file in render test failures: %s', | |
| 532 failure_filename) | |
| 533 continue | |
| 534 | |
| 535 failure_filepath = os.path.join(temp_dir, 'failures', failure_filename) | |
| 536 failure_link = google_storage_helper.upload( | |
| 537 google_storage_helper.unique_name(failure_filename, device=device), | |
| 538 failure_filepath, | |
| 539 bucket=render_tests_bucket) | |
| 540 | |
| 541 golden_filepath = os.path.join(temp_dir, 'goldens', failure_filename) | |
| 542 if os.path.exists(golden_filepath): | |
| 543 golden_link = google_storage_helper.upload( | |
| 544 google_storage_helper.unique_name( | |
| 545 failure_filename, device=device), | |
| 546 golden_filepath, | |
| 547 bucket=render_tests_bucket) | |
| 548 else: | |
| 549 golden_link = '' | |
| 550 | |
| 551 diff_filepath = os.path.join(temp_dir, 'diffs', failure_filename) | |
| 552 if os.path.exists(diff_filepath): | |
| 553 diff_link = google_storage_helper.upload( | |
| 554 google_storage_helper.unique_name( | |
| 555 failure_filename, device=device), | |
| 556 diff_filepath, | |
| 557 bucket=render_tests_bucket) | |
| 558 else: | |
| 559 diff_link = '' | |
| 560 | |
| 561 with tempfile.NamedTemporaryFile(suffix='.html') as temp_html: | |
| 562 temp_html.write(RENDER_TESTS_HTML_TEMPLATE % | |
| 563 (failure_link, golden_link, diff_link)) | |
| 564 temp_html.flush() | |
| 565 html_results_link = google_storage_helper.upload( | |
| 566 google_storage_helper.unique_name('render_html', device=device), | |
| 567 temp_html.name, | |
| 568 bucket=render_tests_bucket, | |
| 569 content_type='text/html') | |
| 570 for result in results: | |
| 571 result.SetLink(failure_filename, html_results_link) | |
| 572 | |
| 445 #override | 573 #override |
| 446 def _ShouldRetry(self, test): | 574 def _ShouldRetry(self, test): |
| 447 if 'RetryOnFailure' in test.get('annotations', {}): | 575 if 'RetryOnFailure' in test.get('annotations', {}): |
| 448 return True | 576 return True |
| 449 | 577 |
| 450 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been | 578 # TODO(jbudorick): Remove this log message once @RetryOnFailure has been |
| 451 # enabled for a while. See crbug.com/619055 for more details. | 579 # enabled for a while. See crbug.com/619055 for more details. |
| 452 logging.error('Default retries are being phased out. crbug.com/619055') | 580 logging.error('Default retries are being phased out. crbug.com/619055') |
| 453 return False | 581 return False |
| 454 | 582 |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 470 if k in annotations: | 598 if k in annotations: |
| 471 timeout = v | 599 timeout = v |
| 472 break | 600 break |
| 473 else: | 601 else: |
| 474 logging.warning('Using default 1 minute timeout for %s', test_name) | 602 logging.warning('Using default 1 minute timeout for %s', test_name) |
| 475 timeout = 60 | 603 timeout = 60 |
| 476 | 604 |
| 477 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) | 605 timeout *= cls._GetTimeoutScaleFromAnnotations(annotations) |
| 478 | 606 |
| 479 return timeout | 607 return timeout |
| 608 | |
| 609 def _IsRenderTest(test): | |
| 610 """Determines if a test or list of tests has a RenderTest amongst them.""" | |
| 611 if not isinstance(test, list): | |
| 612 test = [test] | |
| 613 return any([RENDER_TEST_FEATURE_ANNOTATION in t['annotations'].get( | |
| 614 FEATURE_ANNOTATION, ())] for t in test) | |
| OLD | NEW |