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