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