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