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