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