1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5""" 6Provides graphics related utils, like capturing screenshots or checking on 7the state of the graphics driver. 8""" 9 10import collections 11import contextlib 12import glob 13import logging 14import os 15import re 16import sys 17import time 18#import traceback 19# Please limit the use of the uinput library to this file. Try not to spread 20# dependencies and abstract as much as possible to make switching to a different 21# input library in the future easier. 22import uinput 23 24from autotest_lib.client.bin import test 25from autotest_lib.client.bin import utils 26from autotest_lib.client.common_lib import error 27from autotest_lib.client.common_lib import test as test_utils 28from autotest_lib.client.cros import power_utils 29from autotest_lib.client.cros.graphics import gbm 30from functools import wraps 31 32 33class GraphicsTest(test.test): 34 """Base class for graphics test. 35 36 GraphicsTest is the base class for graphics tests. 37 Every subclass of GraphicsTest should call GraphicsTests initialize/cleanup 38 method as they will do GraphicsStateChecker as well as report states to 39 Chrome Perf dashboard. 40 41 Attributes: 42 _test_failure_description(str): Failure name reported to chrome perf 43 dashboard. (Default: Failures) 44 _test_failure_report_enable(bool): Enable/Disable reporting 45 failures to chrome perf dashboard 46 automatically. (Default: True) 47 """ 48 version = 1 49 _GSC = None 50 51 _test_failure_description = "Failures" 52 _test_failure_report_enable = True 53 54 def __init__(self, *args, **kwargs): 55 """Initialize flag setting.""" 56 super(GraphicsTest, self).__init__(*args, **kwargs) 57 self._failures = [] 58 59 def initialize(self, raise_error_on_hang=False, *args, **kwargs): 60 """Initial state checker and report initial value to perf dashboard.""" 61 self._GSC = GraphicsStateChecker(raise_error_on_hang) 62 63 self.output_perf_value( 64 description='Timeout_Reboot', 65 value=1, 66 units='count', 67 higher_is_better=False 68 ) 69 70 if hasattr(super(GraphicsTest, self), "initialize"): 71 test_utils._cherry_pick_call(super(GraphicsTest, self).initialize, 72 *args, **kwargs) 73 74 def cleanup(self, *args, **kwargs): 75 """Finalize state checker and report values to perf dashboard.""" 76 if self._GSC: 77 self._GSC.finalize() 78 79 self.output_perf_value( 80 description='Timeout_Reboot', 81 value=0, 82 units='count', 83 higher_is_better=False, 84 replace_existing_values=True 85 ) 86 87 logging.debug('GraphicsTest recorded failures: %s', self.get_failures()) 88 if self._test_failure_report_enable: 89 self.output_perf_value( 90 description=self._test_failure_description, 91 value=len(self._failures), 92 units='count', 93 higher_is_better=False 94 ) 95 96 if hasattr(super(GraphicsTest, self), "cleanup"): 97 test_utils._cherry_pick_call(super(GraphicsTest, self).cleanup, 98 *args, **kwargs) 99 100 @contextlib.contextmanager 101 def failure_report(self, name): 102 """Record the failure of an operation to the self._failures. 103 104 Records if the operation taken inside executed normally or not. 105 If the operation taken inside raise unexpected failure, failure named 106 |name|, will be added to the self._failures list and reported to the 107 chrome perf dashboard in the cleanup stage. 108 109 Usage: 110 # Record failure of doSomething 111 with failure_report('doSomething'): 112 doSomething() 113 """ 114 # Assume failed at the beginning 115 self.add_failures(name) 116 yield {} 117 self.remove_failures(name) 118 119 @classmethod 120 def failure_report_decorator(cls, name): 121 """Record the failure if the function failed to finish. 122 This method should only decorate to functions of GraphicsTest. 123 In addition, functions with this decorator should be called with no 124 unnamed arguments. 125 Usage: 126 @GraphicsTest.test_run_decorator('graphics_test') 127 def Foo(self, bar='test'): 128 return doStuff() 129 130 is equivalent to 131 132 def Foo(self, bar): 133 with failure_reporter('graphics_test'): 134 return doStuff() 135 136 # Incorrect usage. 137 @GraphicsTest.test_run_decorator('graphics_test') 138 def Foo(self, bar='test'): 139 pass 140 self.Foo('test_name', bar='test_name') # call Foo with named args 141 142 # Incorrect usage. 143 @GraphicsTest.test_run_decorator('graphics_test') 144 def Foo(self, bar='test'): 145 pass 146 self.Foo('test_name') # call Foo with unnamed args 147 """ 148 def decorator(fn): 149 @wraps(fn) 150 def wrapper(*args, **kwargs): 151 if len(args) > 1: 152 raise error.TestError('Unnamed arguments is not accepted. ' 153 'Please apply this decorator to ' 154 'function without unnamed args.') 155 # A member function of GraphicsTest is decorated. The first 156 # argument is the instance itself. 157 instance = args[0] 158 with instance.failure_report(name): 159 # Cherry pick the arguments for the wrapped function. 160 d_args, d_kwargs = test_utils._cherry_pick_args(fn, args, 161 kwargs) 162 return fn(instance, *d_args, **d_kwargs) 163 return wrapper 164 return decorator 165 166 def add_failures(self, failure_description): 167 """ 168 Add a record to failures list which will report back to chrome perf 169 dashboard at the cleanup stage. 170 """ 171 self._failures.append(failure_description) 172 173 def remove_failures(self, failure_description): 174 """ 175 Remove a record from failures list which will report back to chrome perf 176 dashboard at the cleanup stage. 177 """ 178 self._failures.remove(failure_description) 179 180 def get_failures(self): 181 """ 182 Get currently recorded failures list. 183 """ 184 return list(self._failures) 185 186 187def screen_disable_blanking(): 188 """ Called from power_Backlight to disable screen blanking. """ 189 # We don't have to worry about unexpected screensavers or DPMS here. 190 return 191 192 193def screen_disable_energy_saving(): 194 """ Called from power_Consumption to immediately disable energy saving. """ 195 # All we need to do here is enable displays via Chrome. 196 power_utils.set_display_power(power_utils.DISPLAY_POWER_ALL_ON) 197 return 198 199 200def screen_toggle_fullscreen(): 201 """Toggles fullscreen mode.""" 202 press_keys(['KEY_F11']) 203 204 205def screen_toggle_mirrored(): 206 """Toggles the mirrored screen.""" 207 press_keys(['KEY_LEFTCTRL', 'KEY_F4']) 208 209 210def hide_cursor(): 211 """Hides mouse cursor.""" 212 # Send a keystroke to hide the cursor. 213 press_keys(['KEY_UP']) 214 215 216def hide_typing_cursor(): 217 """Hides typing cursor.""" 218 # Press the tab key to move outside the typing bar. 219 press_keys(['KEY_TAB']) 220 221 222def screen_wakeup(): 223 """Wake up the screen if it is dark.""" 224 # Move the mouse a little bit to wake up the screen. 225 device = _get_uinput_device_mouse_rel() 226 _uinput_emit(device, 'REL_X', 1) 227 _uinput_emit(device, 'REL_X', -1) 228 229 230def switch_screen_on(on): 231 """ 232 Turn the touch screen on/off. 233 234 @param on: On or off. 235 """ 236 raise error.TestFail('switch_screen_on is not implemented.') 237 238 239# Don't create a device during build_packages or for tests that don't need it. 240uinput_device_keyboard = None 241uinput_device_touch = None 242uinput_device_mouse_rel = None 243 244# Don't add more events to this list than are used. For a complete list of 245# available events check python2.7/site-packages/uinput/ev.py. 246UINPUT_DEVICE_EVENTS_KEYBOARD = [ 247 uinput.KEY_F4, 248 uinput.KEY_F11, 249 uinput.KEY_KPPLUS, 250 uinput.KEY_KPMINUS, 251 uinput.KEY_LEFTCTRL, 252 uinput.KEY_TAB, 253 uinput.KEY_UP, 254 uinput.KEY_DOWN, 255 uinput.KEY_LEFT, 256 uinput.KEY_RIGHT 257] 258# TODO(ihf): Find an ABS sequence that actually works. 259UINPUT_DEVICE_EVENTS_TOUCH = [ 260 uinput.BTN_TOUCH, 261 uinput.ABS_MT_SLOT, 262 uinput.ABS_MT_POSITION_X + (0, 2560, 0, 0), 263 uinput.ABS_MT_POSITION_Y + (0, 1700, 0, 0), 264 uinput.ABS_MT_TRACKING_ID + (0, 10, 0, 0), 265 uinput.BTN_TOUCH 266] 267UINPUT_DEVICE_EVENTS_MOUSE_REL = [ 268 uinput.REL_X, 269 uinput.REL_Y, 270 uinput.BTN_MOUSE, 271 uinput.BTN_LEFT, 272 uinput.BTN_RIGHT 273] 274 275 276def _get_uinput_device_keyboard(): 277 """ 278 Lazy initialize device and return it. We don't want to create a device 279 during build_packages or for tests that don't need it, hence init with None. 280 """ 281 global uinput_device_keyboard 282 if uinput_device_keyboard is None: 283 uinput_device_keyboard = uinput.Device(UINPUT_DEVICE_EVENTS_KEYBOARD) 284 return uinput_device_keyboard 285 286 287def _get_uinput_device_mouse_rel(): 288 """ 289 Lazy initialize device and return it. We don't want to create a device 290 during build_packages or for tests that don't need it, hence init with None. 291 """ 292 global uinput_device_mouse_rel 293 if uinput_device_mouse_rel is None: 294 uinput_device_mouse_rel = uinput.Device(UINPUT_DEVICE_EVENTS_MOUSE_REL) 295 return uinput_device_mouse_rel 296 297 298def _get_uinput_device_touch(): 299 """ 300 Lazy initialize device and return it. We don't want to create a device 301 during build_packages or for tests that don't need it, hence init with None. 302 """ 303 global uinput_device_touch 304 if uinput_device_touch is None: 305 uinput_device_touch = uinput.Device(UINPUT_DEVICE_EVENTS_TOUCH) 306 return uinput_device_touch 307 308 309def _uinput_translate_name(event_name): 310 """ 311 Translates string |event_name| to uinput event. 312 """ 313 return getattr(uinput, event_name) 314 315 316def _uinput_emit(device, event_name, value, syn=True): 317 """ 318 Wrapper for uinput.emit. Emits event with value. 319 Example: ('REL_X', 20), ('BTN_RIGHT', 1) 320 """ 321 event = _uinput_translate_name(event_name) 322 device.emit(event, value, syn) 323 324 325def _uinput_emit_click(device, event_name, syn=True): 326 """ 327 Wrapper for uinput.emit_click. Emits click event. Only KEY and BTN events 328 are accepted, otherwise ValueError is raised. Example: 'KEY_A' 329 """ 330 event = _uinput_translate_name(event_name) 331 device.emit_click(event, syn) 332 333 334def _uinput_emit_combo(device, event_names, syn=True): 335 """ 336 Wrapper for uinput.emit_combo. Emits sequence of events. 337 Example: ['KEY_LEFTCTRL', 'KEY_LEFTALT', 'KEY_F5'] 338 """ 339 events = [_uinput_translate_name(en) for en in event_names] 340 device.emit_combo(events, syn) 341 342 343def press_keys(key_list): 344 """Presses the given keys as one combination. 345 346 Please do not leak uinput dependencies outside of the file. 347 348 @param key: A list of key strings, e.g. ['LEFTCTRL', 'F4'] 349 """ 350 _uinput_emit_combo(_get_uinput_device_keyboard(), key_list) 351 352 353def click_mouse(): 354 """Just click the mouse. 355 Presumably only hacky tests use this function. 356 """ 357 logging.info('click_mouse()') 358 # Move a little to make the cursor appear. 359 device = _get_uinput_device_mouse_rel() 360 _uinput_emit(device, 'REL_X', 1) 361 # Some sleeping is needed otherwise events disappear. 362 time.sleep(0.1) 363 # Move cursor back to not drift. 364 _uinput_emit(device, 'REL_X', -1) 365 time.sleep(0.1) 366 # Click down. 367 _uinput_emit(device, 'BTN_LEFT', 1) 368 time.sleep(0.2) 369 # Release click. 370 _uinput_emit(device, 'BTN_LEFT', 0) 371 372 373# TODO(ihf): this function is broken. Make it work. 374def activate_focus_at(rel_x, rel_y): 375 """Clicks with the mouse at screen position (x, y). 376 377 This is a pretty hacky method. Using this will probably lead to 378 flaky tests as page layout changes over time. 379 @param rel_x: relative horizontal position between 0 and 1. 380 @param rel_y: relattive vertical position between 0 and 1. 381 """ 382 width, height = get_internal_resolution() 383 device = _get_uinput_device_touch() 384 _uinput_emit(device, 'ABS_MT_SLOT', 0, syn=False) 385 _uinput_emit(device, 'ABS_MT_TRACKING_ID', 1, syn=False) 386 _uinput_emit(device, 'ABS_MT_POSITION_X', int(rel_x * width), syn=False) 387 _uinput_emit(device, 'ABS_MT_POSITION_Y', int(rel_y * height), syn=False) 388 _uinput_emit(device, 'BTN_TOUCH', 1, syn=True) 389 time.sleep(0.2) 390 _uinput_emit(device, 'BTN_TOUCH', 0, syn=True) 391 392 393def take_screenshot(resultsdir, fname_prefix, extension='png'): 394 """Take screenshot and save to a new file in the results dir. 395 Args: 396 @param resultsdir: Directory to store the output in. 397 @param fname_prefix: Prefix for the output fname. 398 @param extension: String indicating file format ('png', 'jpg', etc). 399 Returns: 400 the path of the saved screenshot file 401 """ 402 403 old_exc_type = sys.exc_info()[0] 404 405 next_index = len(glob.glob( 406 os.path.join(resultsdir, '%s-*.%s' % (fname_prefix, extension)))) 407 screenshot_file = os.path.join( 408 resultsdir, '%s-%d.%s' % (fname_prefix, next_index, extension)) 409 logging.info('Saving screenshot to %s.', screenshot_file) 410 411 try: 412 image = gbm.crtcScreenshot() 413 image.save(screenshot_file) 414 except Exception as err: 415 # Do not raise an exception if the screenshot fails while processing 416 # another exception. 417 if old_exc_type is None: 418 raise 419 logging.error(err) 420 421 return screenshot_file 422 423 424def take_screenshot_crop_by_height(fullpath, final_height, x_offset_pixels, 425 y_offset_pixels): 426 """ 427 Take a screenshot, crop to final height starting at given (x, y) coordinate. 428 Image width will be adjusted to maintain original aspect ratio). 429 430 @param fullpath: path, fullpath of the file that will become the image file. 431 @param final_height: integer, height in pixels of resulting image. 432 @param x_offset_pixels: integer, number of pixels from left margin 433 to begin cropping. 434 @param y_offset_pixels: integer, number of pixels from top margin 435 to begin cropping. 436 """ 437 image = gbm.crtcScreenshot() 438 image.crop() 439 width, height = image.size 440 # Preserve aspect ratio: Wf / Wi == Hf / Hi 441 final_width = int(width * (float(final_height) / height)) 442 box = (x_offset_pixels, y_offset_pixels, 443 x_offset_pixels + final_width, y_offset_pixels + final_height) 444 cropped = image.crop(box) 445 cropped.save(fullpath) 446 return fullpath 447 448 449def take_screenshot_crop_x(fullpath, box=None): 450 """ 451 Take a screenshot using import tool, crop according to dim given by the box. 452 @param fullpath: path, full path to save the image to. 453 @param box: 4-tuple giving the upper left and lower right pixel coordinates. 454 """ 455 456 if box: 457 img_w, img_h, upperx, uppery = box 458 cmd = ('/usr/local/bin/import -window root -depth 8 -crop ' 459 '%dx%d+%d+%d' % (img_w, img_h, upperx, uppery)) 460 else: 461 cmd = ('/usr/local/bin/import -window root -depth 8') 462 463 old_exc_type = sys.exc_info()[0] 464 try: 465 utils.system('%s %s' % (cmd, fullpath)) 466 except Exception as err: 467 # Do not raise an exception if the screenshot fails while processing 468 # another exception. 469 if old_exc_type is None: 470 raise 471 logging.error(err) 472 473 474def take_screenshot_crop(fullpath, box=None, crtc_id=None): 475 """ 476 Take a screenshot using import tool, crop according to dim given by the box. 477 @param fullpath: path, full path to save the image to. 478 @param box: 4-tuple giving the upper left and lower right pixel coordinates. 479 """ 480 if crtc_id is not None: 481 image = gbm.crtcScreenshot(crtc_id) 482 else: 483 image = gbm.crtcScreenshot(get_internal_crtc()) 484 if box: 485 image = image.crop(box) 486 image.save(fullpath) 487 return fullpath 488 489 490_MODETEST_CONNECTOR_PATTERN = re.compile( 491 r'^(\d+)\s+\d+\s+(connected|disconnected)\s+(\S+)\s+\d+x\d+\s+\d+\s+\d+') 492 493_MODETEST_MODE_PATTERN = re.compile( 494 r'\s+.+\d+\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+\d+\s+\d+\s+\d+\s+flags:.+type:' 495 r' preferred') 496 497_MODETEST_CRTCS_START_PATTERN = re.compile(r'^id\s+fb\s+pos\s+size') 498 499_MODETEST_CRTC_PATTERN = re.compile( 500 r'^(\d+)\s+(\d+)\s+\((\d+),(\d+)\)\s+\((\d+)x(\d+)\)') 501 502Connector = collections.namedtuple( 503 'Connector', [ 504 'cid', # connector id (integer) 505 'ctype', # connector type, e.g. 'eDP', 'HDMI-A', 'DP' 506 'connected', # boolean 507 'size', # current screen size, e.g. (1024, 768) 508 'encoder', # encoder id (integer) 509 # list of resolution tuples, e.g. [(1920,1080), (1600,900), ...] 510 'modes', 511 ]) 512 513CRTC = collections.namedtuple( 514 'CRTC', [ 515 'id', # crtc id 516 'fb', # fb id 517 'pos', # position, e.g. (0,0) 518 'size', # size, e.g. (1366,768) 519 ]) 520 521 522def get_display_resolution(): 523 """ 524 Parses output of modetest to determine the display resolution of the dut. 525 @return: tuple, (w,h) resolution of device under test. 526 """ 527 connectors = get_modetest_connectors() 528 for connector in connectors: 529 if connector.connected: 530 return connector.size 531 return None 532 533 534def _get_num_outputs_connected(): 535 """ 536 Parses output of modetest to determine the number of connected displays 537 @return: The number of connected displays 538 """ 539 connected = 0 540 connectors = get_modetest_connectors() 541 for connector in connectors: 542 if connector.connected: 543 connected = connected + 1 544 545 return connected 546 547 548def get_num_outputs_on(): 549 """ 550 Retrieves the number of connected outputs that are on. 551 552 Return value: integer value of number of connected outputs that are on. 553 """ 554 555 return _get_num_outputs_connected() 556 557 558def get_modetest_connectors(): 559 """ 560 Retrieves a list of Connectors using modetest. 561 562 Return value: List of Connectors. 563 """ 564 connectors = [] 565 modetest_output = utils.system_output('modetest -c') 566 for line in modetest_output.splitlines(): 567 # First search for a new connector. 568 connector_match = re.match(_MODETEST_CONNECTOR_PATTERN, line) 569 if connector_match is not None: 570 cid = int(connector_match.group(1)) 571 connected = False 572 if connector_match.group(2) == 'connected': 573 connected = True 574 ctype = connector_match.group(3) 575 size = (-1, -1) 576 encoder = -1 577 modes = None 578 connectors.append( 579 Connector(cid, ctype, connected, size, encoder, modes)) 580 else: 581 # See if we find corresponding line with modes, sizes etc. 582 mode_match = re.match(_MODETEST_MODE_PATTERN, line) 583 if mode_match is not None: 584 size = (int(mode_match.group(1)), int(mode_match.group(2))) 585 # Update display size of last connector in list. 586 c = connectors.pop() 587 connectors.append( 588 Connector( 589 c.cid, c.ctype, c.connected, size, c.encoder, 590 c.modes)) 591 return connectors 592 593 594def get_modetest_crtcs(): 595 """ 596 Returns a list of CRTC data. 597 598 Sample: 599 [CRTC(id=19, fb=50, pos=(0, 0), size=(1366, 768)), 600 CRTC(id=22, fb=54, pos=(0, 0), size=(1920, 1080))] 601 """ 602 crtcs = [] 603 modetest_output = utils.system_output('modetest -p') 604 found = False 605 for line in modetest_output.splitlines(): 606 if found: 607 crtc_match = re.match(_MODETEST_CRTC_PATTERN, line) 608 if crtc_match is not None: 609 crtc_id = int(crtc_match.group(1)) 610 fb = int(crtc_match.group(2)) 611 x = int(crtc_match.group(3)) 612 y = int(crtc_match.group(4)) 613 width = int(crtc_match.group(5)) 614 height = int(crtc_match.group(6)) 615 # CRTCs with fb=0 are disabled, but lets skip anything with 616 # trivial width/height just in case. 617 if not (fb == 0 or width == 0 or height == 0): 618 crtcs.append(CRTC(crtc_id, fb, (x, y), (width, height))) 619 elif line and not line[0].isspace(): 620 return crtcs 621 if re.match(_MODETEST_CRTCS_START_PATTERN, line) is not None: 622 found = True 623 return crtcs 624 625 626def get_modetest_output_state(): 627 """ 628 Reduce the output of get_modetest_connectors to a dictionary of connector/active states. 629 """ 630 connectors = get_modetest_connectors() 631 outputs = {} 632 for connector in connectors: 633 # TODO(ihf): Figure out why modetest output needs filtering. 634 if connector.connected: 635 outputs[connector.ctype] = connector.connected 636 return outputs 637 638 639def get_output_rect(output): 640 """Gets the size and position of the given output on the screen buffer. 641 642 @param output: The output name as a string. 643 644 @return A tuple of the rectangle (width, height, fb_offset_x, 645 fb_offset_y) of ints. 646 """ 647 connectors = get_modetest_connectors() 648 for connector in connectors: 649 if connector.ctype == output: 650 # Concatenate two 2-tuples to 4-tuple. 651 return connector.size + (0, 0) # TODO(ihf): Should we use CRTC.pos? 652 return (0, 0, 0, 0) 653 654 655def get_internal_resolution(): 656 if has_internal_display(): 657 crtcs = get_modetest_crtcs() 658 if len(crtcs) > 0: 659 return crtcs[0].size 660 return (-1, -1) 661 662 663def has_internal_display(): 664 """Checks whether the DUT is equipped with an internal display. 665 666 @return True if internal display is present; False otherwise. 667 """ 668 return bool(get_internal_connector_name()) 669 670 671def get_external_resolution(): 672 """Gets the resolution of the external display. 673 674 @return A tuple of (width, height) or None if no external display is 675 connected. 676 """ 677 offset = 1 if has_internal_display() else 0 678 crtcs = get_modetest_crtcs() 679 if len(crtcs) > offset and crtcs[offset].size != (0, 0): 680 return crtcs[offset].size 681 return None 682 683 684def get_display_output_state(): 685 """ 686 Retrieves output status of connected display(s). 687 688 Return value: dictionary of connected display states. 689 """ 690 return get_modetest_output_state() 691 692 693def set_modetest_output(output_name, enable): 694 # TODO(ihf): figure out what to do here. Don't think this is the right command. 695 # modetest -s <connector_id>[,<connector_id>][@<crtc_id>]:<mode>[-<vrefresh>][@<format>] set a mode 696 pass 697 698 699def set_display_output(output_name, enable): 700 """ 701 Sets the output given by |output_name| on or off. 702 """ 703 set_modetest_output(output_name, enable) 704 705 706# TODO(ihf): Fix this for multiple external connectors. 707def get_external_crtc(index=0): 708 offset = 1 if has_internal_display() else 0 709 crtcs = get_modetest_crtcs() 710 if len(crtcs) > offset + index: 711 return crtcs[offset + index].id 712 return -1 713 714 715def get_internal_crtc(): 716 if has_internal_display(): 717 crtcs = get_modetest_crtcs() 718 if len(crtcs) > 0: 719 return crtcs[0].id 720 return -1 721 722 723# TODO(ihf): Fix this for multiple external connectors. 724def get_external_connector_name(): 725 """Gets the name of the external output connector. 726 727 @return The external output connector name as a string, if any. 728 Otherwise, return False. 729 """ 730 outputs = get_display_output_state() 731 for output in outputs.iterkeys(): 732 if outputs[output] and (output.startswith('HDMI') 733 or output.startswith('DP') 734 or output.startswith('DVI') 735 or output.startswith('VGA')): 736 return output 737 return False 738 739 740def get_internal_connector_name(): 741 """Gets the name of the internal output connector. 742 743 @return The internal output connector name as a string, if any. 744 Otherwise, return False. 745 """ 746 outputs = get_display_output_state() 747 for output in outputs.iterkeys(): 748 # reference: chromium_org/chromeos/display/output_util.cc 749 if (output.startswith('eDP') 750 or output.startswith('LVDS') 751 or output.startswith('DSI')): 752 return output 753 return False 754 755 756def wait_output_connected(output): 757 """Wait for output to connect. 758 759 @param output: The output name as a string. 760 761 @return: True if output is connected; False otherwise. 762 """ 763 def _is_connected(output): 764 """Helper function.""" 765 outputs = get_display_output_state() 766 if output not in outputs: 767 return False 768 return outputs[output] 769 770 return utils.wait_for_value(lambda: _is_connected(output), 771 expected_value=True) 772 773 774def set_content_protection(output_name, state): 775 """ 776 Sets the content protection to the given state. 777 778 @param output_name: The output name as a string. 779 @param state: One of the states 'Undesired', 'Desired', or 'Enabled' 780 781 """ 782 raise error.TestFail('freon: set_content_protection not implemented') 783 784 785def get_content_protection(output_name): 786 """ 787 Gets the state of the content protection. 788 789 @param output_name: The output name as a string. 790 @return: A string of the state, like 'Undesired', 'Desired', or 'Enabled'. 791 False if not supported. 792 793 """ 794 raise error.TestFail('freon: get_content_protection not implemented') 795 796 797def is_sw_rasterizer(): 798 """Return true if OpenGL is using a software rendering.""" 799 cmd = utils.wflinfo_cmd() + ' | grep "OpenGL renderer string"' 800 output = utils.run(cmd) 801 result = output.stdout.splitlines()[0] 802 logging.info('wflinfo: %s', result) 803 # TODO(ihf): Find exhaustive error conditions (especially ARM). 804 return 'llvmpipe' in result.lower() or 'soft' in result.lower() 805 806 807def get_gles_version(): 808 cmd = utils.wflinfo_cmd() 809 wflinfo = utils.system_output(cmd, retain_output=False, ignore_status=False) 810 # OpenGL version string: OpenGL ES 3.0 Mesa 10.5.0-devel 811 version = re.findall(r'OpenGL version string: ' 812 r'OpenGL ES ([0-9]+).([0-9]+)', wflinfo) 813 if version: 814 version_major = int(version[0][0]) 815 version_minor = int(version[0][1]) 816 return (version_major, version_minor) 817 return (None, None) 818 819 820def get_egl_version(): 821 cmd = 'eglinfo' 822 eglinfo = utils.system_output(cmd, retain_output=False, ignore_status=False) 823 # EGL version string: 1.4 (DRI2) 824 version = re.findall(r'EGL version string: ([0-9]+).([0-9]+)', eglinfo) 825 if version: 826 version_major = int(version[0][0]) 827 version_minor = int(version[0][1]) 828 return (version_major, version_minor) 829 return (None, None) 830 831 832class GraphicsKernelMemory(object): 833 """ 834 Reads from sysfs to determine kernel gem objects and memory info. 835 """ 836 # These are sysfs fields that will be read by this test. For different 837 # architectures, the sysfs field paths are different. The "paths" are given 838 # as lists of strings because the actual path may vary depending on the 839 # system. This test will read from the first sysfs path in the list that is 840 # present. 841 # e.g. ".../memory" vs ".../gpu_memory" -- if the system has either one of 842 # these, the test will read from that path. 843 amdgpu_fields = { 844 'gem_objects': ['/sys/kernel/debug/dri/0/amdgpu_gem_info'], 845 'memory': ['/sys/kernel/debug/dri/0/amdgpu_gtt_mm'], 846 } 847 arm_fields = {} 848 exynos_fields = { 849 'gem_objects': ['/sys/kernel/debug/dri/?/exynos_gem_objects'], 850 'memory': ['/sys/class/misc/mali0/device/memory', 851 '/sys/class/misc/mali0/device/gpu_memory'], 852 } 853 mediatek_fields = {} # TODO(crosbug.com/p/58189) add nodes 854 # TODO Add memory nodes once the GPU patches landed. 855 rockchip_fields = {} 856 tegra_fields = { 857 'memory': ['/sys/kernel/debug/memblock/memory'], 858 } 859 i915_fields = { 860 'gem_objects': ['/sys/kernel/debug/dri/0/i915_gem_objects'], 861 'memory': ['/sys/kernel/debug/dri/0/i915_gem_gtt'], 862 } 863 864 arch_fields = { 865 'amdgpu': amdgpu_fields, 866 'arm': arm_fields, 867 'exynos5': exynos_fields, 868 'i915': i915_fields, 869 'mediatek': mediatek_fields, 870 'rockchip': rockchip_fields, 871 'tegra': tegra_fields, 872 } 873 874 num_errors = 0 875 876 def __init__(self): 877 self._initial_memory = self.get_memory_keyvals() 878 879 def get_memory_difference_keyvals(self): 880 """ 881 Reads the graphics memory values and return the difference between now 882 and the memory usage at initialization stage as keyvals. 883 """ 884 current_memory = self.get_memory_keyvals() 885 return {key: self._initial_memory[key] - current_memory[key] 886 for key in self._initial_memory} 887 888 def get_memory_keyvals(self): 889 """ 890 Reads the graphics memory values and returns them as keyvals. 891 """ 892 keyvals = {} 893 894 # Get architecture type and list of sysfs fields to read. 895 soc = utils.get_cpu_soc_family() 896 897 arch = utils.get_cpu_arch() 898 if arch == 'x86_64' or arch == 'i386': 899 pci_vga_device = utils.run("lspci | grep VGA").stdout.rstrip('\n') 900 if "Advanced Micro Devices" in pci_vga_device: 901 soc = 'amdgpu' 902 elif "Intel Corporation" in pci_vga_device: 903 soc = 'i915' 904 905 if not soc in self.arch_fields: 906 raise error.TestFail('Error: Architecture "%s" not yet supported.' % soc) 907 fields = self.arch_fields[soc] 908 909 for field_name in fields: 910 possible_field_paths = fields[field_name] 911 field_value = None 912 for path in possible_field_paths: 913 if utils.system('ls %s' % path): 914 continue 915 field_value = utils.system_output('cat %s' % path) 916 break 917 918 if not field_value: 919 logging.error('Unable to find any sysfs paths for field "%s"', 920 field_name) 921 self.num_errors += 1 922 continue 923 924 parsed_results = GraphicsKernelMemory._parse_sysfs(field_value) 925 926 for key in parsed_results: 927 keyvals['%s_%s' % (field_name, key)] = parsed_results[key] 928 929 if 'bytes' in parsed_results and parsed_results['bytes'] == 0: 930 logging.error('%s reported 0 bytes', field_name) 931 self.num_errors += 1 932 933 keyvals['meminfo_MemUsed'] = (utils.read_from_meminfo('MemTotal') - 934 utils.read_from_meminfo('MemFree')) 935 keyvals['meminfo_SwapUsed'] = (utils.read_from_meminfo('SwapTotal') - 936 utils.read_from_meminfo('SwapFree')) 937 return keyvals 938 939 @staticmethod 940 def _parse_sysfs(output): 941 """ 942 Parses output of graphics memory sysfs to determine the number of 943 buffer objects and bytes. 944 945 Arguments: 946 output Unprocessed sysfs output 947 Return value: 948 Dictionary containing integer values of number bytes and objects. 949 They may have the keys 'bytes' and 'objects', respectively. However 950 the result may not contain both of these values. 951 """ 952 results = {} 953 labels = ['bytes', 'objects'] 954 955 for line in output.split('\n'): 956 # Strip any commas to make parsing easier. 957 line_words = line.replace(',', '').split() 958 959 prev_word = None 960 for word in line_words: 961 # When a label has been found, the previous word should be the 962 # value. e.g. "3200 bytes" 963 if word in labels and word not in results and prev_word: 964 logging.info(prev_word) 965 results[word] = int(prev_word) 966 967 prev_word = word 968 969 # Once all values has been parsed, return. 970 if len(results) == len(labels): 971 return results 972 973 return results 974 975 976class GraphicsStateChecker(object): 977 """ 978 Analyzes the state of the GPU and log history. Should be instantiated at the 979 beginning of each graphics_* test. 980 """ 981 crash_blacklist = [] 982 dirty_writeback_centisecs = 0 983 existing_hangs = {} 984 985 _BROWSER_VERSION_COMMAND = '/opt/google/chrome/chrome --version' 986 _HANGCHECK = ['drm:i915_hangcheck_elapsed', 'drm:i915_hangcheck_hung', 987 'Hangcheck timer elapsed...'] 988 _HANGCHECK_WARNING = ['render ring idle'] 989 _MESSAGES_FILE = '/var/log/messages' 990 991 def __init__(self, raise_error_on_hang=True): 992 """ 993 Analyzes the initial state of the GPU and log history. 994 """ 995 # Attempt flushing system logs every second instead of every 10 minutes. 996 self.dirty_writeback_centisecs = utils.get_dirty_writeback_centisecs() 997 utils.set_dirty_writeback_centisecs(100) 998 self._raise_error_on_hang = raise_error_on_hang 999 logging.info(utils.get_board_with_frequency_and_memory()) 1000 self.graphics_kernel_memory = GraphicsKernelMemory() 1001 1002 if utils.get_cpu_arch() != 'arm': 1003 if is_sw_rasterizer(): 1004 raise error.TestFail('Refusing to run on SW rasterizer.') 1005 logging.info('Initialize: Checking for old GPU hangs...') 1006 messages = open(self._MESSAGES_FILE, 'r') 1007 for line in messages: 1008 for hang in self._HANGCHECK: 1009 if hang in line: 1010 logging.info(line) 1011 self.existing_hangs[line] = line 1012 messages.close() 1013 1014 def finalize(self): 1015 """ 1016 Analyzes the state of the GPU, log history and emits warnings or errors 1017 if the state changed since initialize. Also makes a note of the Chrome 1018 version for later usage in the perf-dashboard. 1019 """ 1020 utils.set_dirty_writeback_centisecs(self.dirty_writeback_centisecs) 1021 new_gpu_hang = False 1022 new_gpu_warning = False 1023 if utils.get_cpu_arch() != 'arm': 1024 logging.info('Cleanup: Checking for new GPU hangs...') 1025 messages = open(self._MESSAGES_FILE, 'r') 1026 for line in messages: 1027 for hang in self._HANGCHECK: 1028 if hang in line: 1029 if not line in self.existing_hangs.keys(): 1030 logging.info(line) 1031 for warn in self._HANGCHECK_WARNING: 1032 if warn in line: 1033 new_gpu_warning = True 1034 logging.warning( 1035 'Saw GPU hang warning during test.') 1036 else: 1037 logging.warning('Saw GPU hang during test.') 1038 new_gpu_hang = True 1039 messages.close() 1040 1041 if is_sw_rasterizer(): 1042 logging.warning('Finished test on SW rasterizer.') 1043 raise error.TestFail('Finished test on SW rasterizer.') 1044 if self._raise_error_on_hang and new_gpu_hang: 1045 raise error.TestError('Detected GPU hang during test.') 1046 if new_gpu_hang: 1047 raise error.TestWarn('Detected GPU hang during test.') 1048 if new_gpu_warning: 1049 raise error.TestWarn('Detected GPU warning during test.') 1050 1051 def get_memory_access_errors(self): 1052 """ Returns the number of errors while reading memory stats. """ 1053 return self.graphics_kernel_memory.num_errors 1054 1055 def get_memory_difference_keyvals(self): 1056 return self.graphics_kernel_memory.get_memory_difference_keyvals() 1057 1058 def get_memory_keyvals(self): 1059 """ Returns memory stats. """ 1060 return self.graphics_kernel_memory.get_memory_keyvals() 1061 1062class GraphicsApiHelper(object): 1063 """ 1064 Report on the available graphics APIs. 1065 Ex. gles2, gles3, gles31, and vk 1066 """ 1067 _supported_apis = [] 1068 1069 DEQP_BASEDIR = os.path.join('/usr', 'local', 'deqp') 1070 DEQP_EXECUTABLE = { 1071 'gles2': os.path.join('modules', 'gles2', 'deqp-gles2'), 1072 'gles3': os.path.join('modules', 'gles3', 'deqp-gles3'), 1073 'gles31': os.path.join('modules', 'gles31', 'deqp-gles31'), 1074 'vk': os.path.join('external', 'vulkancts', 'modules', 1075 'vulkan', 'deqp-vk') 1076 } 1077 1078 def __init__(self): 1079 # Determine which executable should be run. Right now never egl. 1080 major, minor = get_gles_version() 1081 logging.info('Found gles%d.%d.', major, minor) 1082 if major is None or minor is None: 1083 raise error.TestFail( 1084 'Failed: Could not get gles version information (%d, %d).' % 1085 (major, minor) 1086 ) 1087 if major >= 2: 1088 self._supported_apis.append('gles2') 1089 if major >= 3: 1090 self._supported_apis.append('gles3') 1091 if major > 3 or minor >= 1: 1092 self._supported_apis.append('gles31') 1093 1094 # If libvulkan is installed, then assume the board supports vulkan. 1095 has_libvulkan = False 1096 for libdir in ('/usr/lib', '/usr/lib64', 1097 '/usr/local/lib', '/usr/local/lib64'): 1098 if os.path.exists(os.path.join(libdir, 'libvulkan.so')): 1099 has_libvulkan = True 1100 1101 if has_libvulkan: 1102 executable_path = os.path.join( 1103 self.DEQP_BASEDIR, 1104 self.DEQP_EXECUTABLE['vk'] 1105 ) 1106 if os.path.exists(executable_path): 1107 self._supported_apis.append('vk') 1108 else: 1109 logging.warning('Found libvulkan.so but did not find deqp-vk ' 1110 'binary for testing.') 1111 1112 def get_supported_apis(self): 1113 """Return the list of supported apis. eg. gles2, gles3, vk etc. 1114 @returns: a copy of the supported api list will be returned 1115 """ 1116 return list(self._supported_apis) 1117 1118 def get_deqp_executable(self, api): 1119 """Return the path to the api executable.""" 1120 if api not in self.DEQP_EXECUTABLE: 1121 raise KeyError( 1122 "%s is not a supported api for GraphicsApiHelper." % api 1123 ) 1124 1125 executable = os.path.join( 1126 self.DEQP_BASEDIR, 1127 self.DEQP_EXECUTABLE[api] 1128 ) 1129 return executable 1130