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