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