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