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