blob: 482bf40fbc4bca00912f94d6cb1fc9951741b874 [file] [log] [blame]
# Copyright 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Test external display with optional audio playback test.
Description
-----------
Verify the external display is functional.
The test is defined by a list ``[display_label, display_id,
audio_info, usbpd_port]``. Each item represents an external port:
- ``display_label``: I18n display name seen by operator, e.g. ``_('VGA')``.
- ``display_id``: (str) ID used to identify display in xrandr or modeprint,
e.g. VGA1.
- ``audio_info``: A list of ``[audio_card, audio_device, init_actions]``,
or None:
- ``audio_card`` is either the card's name (str), or the card's index (int).
- ``audio_device`` is the device's index (int).
- ``init_actions`` is a list of ``[card_name, action]`` (list).
action is a dict key defined in audio.json (ref: audio.py) to be passed
into dut.audio.ApplyAudioConfig.
e.g. ``[["rt5650", "init_audio"], ["rt5650", "enable_hdmi"]]``.
This argument is optional. If set, the audio playback test is added.
- ``usbpd_port``: (int) Verify the USB PD TypeC port status, or None.
It can also be configured to run automatically by specifying ``bft_fixture``
argument, and skip some steps by setting ``connect_only``,
``start_output_only`` and ``stop_output_only``.
Test Procedure
--------------
This test can be manual or automated depends on whether ``bft_fixture``
is specified. The test loops through all items in ``display_info`` and:
1. Plug an external monitor to the port specified in dargs.
2. (Optional) If ``audio_info.usbpd_port`` is specified, verify usbpd port
status automatically.
3. Main display will automatically switch to the external one.
4. Press the number shown on the display to verify display works.
5. (Optional) If ``audio_info`` is specified, the speaker will play a random
number, and operator has to press the number to verify audio functionality.
6. Unplug the external monitor to finish the test.
Dependency
----------
- ``display`` component in device API.
- Optional ``audio`` and ``usb_c`` components in device API.
- Optional fixture can be used to support automated test.
Examples
--------
To manual checking external display at USB Port 0, add this in test list::
{
"pytest_name": "external_display",
"args": {
"display_info": [
["i18n! Left HDMI External Display", "HDMI-A-1", null, 0]
]
}
}
"""
from __future__ import print_function
import collections
import logging
import random
import factory_common # pylint: disable=unused-import
from cros.factory.device import device_utils
from cros.factory.test.fixture import bft_fixture
from cros.factory.test.i18n import _
from cros.factory.test.pytests import audio
from cros.factory.test import state
from cros.factory.test import test_case
from cros.factory.utils.arg_utils import Arg
# Interval (seconds) of probing connection state.
_CONNECTION_CHECK_PERIOD_SECS = 1
ExtDisplayTaskArg = collections.namedtuple('ExtDisplayTaskArg', [
'display_label', 'display_id', 'audio_card', 'audio_device', 'init_actions',
'usbpd_port'
])
class ExtDisplayTest(test_case.TestCase):
"""Main class for external display test."""
ARGS = [
Arg('main_display', str,
"xrandr/modeprint ID for ChromeBook's main display."),
Arg('display_info', list,
'A list of tuples (display_label, display_id, audio_info, '
'usbpd_port) represents an external port to test.'),
Arg('bft_fixture', dict, bft_fixture.TEST_ARG_HELP,
default=None),
Arg('connect_only', bool,
'Just detect ext display connection. This is for a hack that DUT '
'needs reboot after connect to prevent X crash.',
default=False),
Arg('start_output_only', bool,
'Only start output of external display. This is for bringing up '
'the external display for other tests that need it.',
default=False),
Arg('stop_output_only', bool,
'Only stop output of external display. This is for bringing down '
'the external display that other tests have finished using.',
default=False),
Arg('already_connect', bool,
'Also for the reboot hack with fixture. With it set to True, DUT '
'does not issue plug ext display command.',
default=False)
]
def setUp(self):
self._dut = device_utils.CreateDUTInterface()
self._fixture = None
if self.args.bft_fixture:
self._fixture = bft_fixture.CreateBFTFixture(**self.args.bft_fixture)
self.assertLessEqual(
[self.args.start_output_only, self.args.connect_only,
self.args.stop_output_only].count(True),
1,
'Only one of start_output_only, connect_only '
'and stop_output_only can be true.')
self.do_connect, self.do_output, self.do_disconnect = False, False, False
if self.args.start_output_only:
self.do_connect = True
self.do_output = True
elif self.args.connect_only:
self.do_connect = True
elif self.args.stop_output_only:
self.do_disconnect = True
else:
self.do_connect = True
self.do_output = True
self.do_disconnect = True
self._toggle_timestamp = 0
# Setup tasks
for info in self.args.display_info:
args = self.ParseDisplayInfo(info)
if self.do_connect:
self.AddTask(self.WaitConnect, args)
if self.do_output:
self.AddTask(self.CheckVideo, args)
if args.audio_card:
self.AddTask(self.SetupAudio, args)
audio_label = _(
'{display_label} Audio', display_label=args.display_label)
self.AddTask(
audio.TestAudioDigitPlayback, self.ui, self._dut, audio_label,
card=args.audio_card, device=args.audio_device)
if self.do_disconnect:
self.AddTask(self.WaitDisconnect, args)
def ParseDisplayInfo(self, info):
"""Parses lists from args.display_info.
Args:
info: a list in args.display_info. Refer display_info definition.
Returns:
Parsed ExtDisplayTaskArg.
Raises:
ValueError if parse error.
"""
# Sanity check
if len(info) not in [2, 3, 4]:
raise ValueError('ERROR: invalid display_info item: ' + str(info))
display_label, display_id = info[:2]
audio_card, audio_device, init_actions, usbpd_port = None, None, None, None
if len(info) >= 3 and info[2] is not None:
if (not isinstance(info[2], list) or
not isinstance(info[2][2], list)):
raise ValueError('ERROR: invalid display_info item: ' + str(info))
audio_card = self._dut.audio.GetCardIndexByName(info[2][0])
audio_device = info[2][1]
init_actions = info[2][2]
if len(info) == 4:
if not isinstance(info[3], int):
raise ValueError('USB PD Port should be an integer')
usbpd_port = info[3]
return ExtDisplayTaskArg(
display_label=display_label,
display_id=display_id,
audio_card=audio_card,
audio_device=audio_device,
init_actions=init_actions,
usbpd_port=usbpd_port)
def CheckVideo(self, args):
self.ui.BindStandardFailKeys()
self.SetMainDisplay(False)
try:
if self._fixture:
self.CheckVideoFixture(args)
else:
self.CheckVideoManual(args)
finally:
self.SetMainDisplay(True)
def CheckVideoManual(self, args):
pass_digit = random.randrange(10)
self.ui.SetState([
_('Do you see video on {display}?', display=args.display_label),
_('Press {key} to pass the test.',
key=('<span id="pass_key">%s</span>' % pass_digit))
])
key = int(self.ui.WaitKeysOnce([str(i) for i in xrange(10)]))
if key != pass_digit:
self.FailTask('Wrong key pressed. pressed: %d, correct: %d' %
(key, pass_digit))
def CheckVideoFixture(self, args):
"""Use fixture to check display.
When expected connection state is observed, it pass the task.
It probes display state every second.
Args:
args: ExtDisplayTaskArg instance.
"""
check_interval_secs = 1
retry_times = 10
# Show light green background for Fixture's light sensor checking.
self.ui.RunJS(
'window.template.classList.add("green-background")')
self.ui.SetState(
_('Fixture is checking if video is displayed on {display}?',
display=args.display_label))
for num_tries in xrange(1, retry_times + 1):
try:
self._fixture.CheckExtDisplay()
self.PassTask()
except bft_fixture.BFTFixtureException:
if num_tries < retry_times:
logging.info(
'Cannot see screen on external display. Wait for %.1f seconds.',
check_interval_secs)
self.Sleep(check_interval_secs)
else:
self.FailTask(
'Failed to see screen on external display after %d retries.' %
retry_times)
def SetMainDisplay(self, to_internal):
"""Sets the main display.
If there are two displays, this method can switch main display based on
recover_original. If there is only one display, it returns if the only
display is an external display (e.g. on a chromebox).
Args:
to_internal: Set the internal display to the main display or not.
"""
display_info = state.GetInstance().DeviceGetDisplayInfo()
if len(display_info) == 1:
# Fail the test if we see only one display and it's the internal one.
if display_info[0]['isInternal']:
self.FailTask('Fail to detect external display')
return
# In regular case, we assume and only accept to find 2 displays, one
# internal and one external.
self.assertEqual(len(display_info), 2,
'At most 1 external display is allowed.')
self.assertNotEqual(display_info[0]['isInternal'],
display_info[1]['isInternal'],
'Should be 1 internal and 1 external display.')
if display_info[0]['isInternal']:
internal_display_id = display_info[0]['id']
external_display_id = display_info[1]['id']
else:
internal_display_id = display_info[1]['id']
external_display_id = display_info[0]['id']
primary_display_id = (internal_display_id
if to_internal else external_display_id)
err = state.GetInstance().DeviceSetDisplayProperties(primary_display_id,
{'isPrimary': True})
self.assertIsNone(err, 'Failed to set the main display: %s' % err)
def SetupAudio(self, args):
for card, action in args.init_actions:
card = self._dut.audio.GetCardIndexByName(card)
self._dut.audio.ApplyAudioConfig(action, card)
def WaitConnect(self, args):
self.ui.BindStandardFailKeys()
self.ui.SetState(_('Connect external display: {display} and wait until '
'it becomes primary.',
display=args.display_label))
self._WaitDisplayConnection(args, True)
def WaitDisconnect(self, args):
self.ui.BindStandardFailKeys()
self.ui.SetState(
_('Disconnect external display: {display}', display=args.display_label))
self._WaitDisplayConnection(args, False)
def _WaitDisplayConnection(self, args, connect):
if self._fixture and not (connect and self.args.already_connect):
try:
self._fixture.SetDeviceEngaged(
bft_fixture.BFTFixture.Device.EXT_DISPLAY, connect)
except bft_fixture.BFTFixtureException as e:
self.FailTask('Detect display failed: %s' % e)
while True:
# Check USBPD status before display info
if (args.usbpd_port is None or
self._dut.usb_c.GetPDStatus(args.usbpd_port)['connected'] == connect):
port_info = self._dut.display.GetPortInfo()
if port_info[args.display_id].connected == connect:
display_info = state.GetInstance().DeviceGetDisplayInfo()
# In the case of connecting an external display, make sure there
# is an item in display_info with 'isInternal' False. If no such
# display_info item, we assume the device's default mode is mirror
# mode and try to turn off mirror mode.
# On the other hand, in the case of disconnecting an external display,
# we can not check display info has no display with 'isInternal' False
# because any display for chromebox has 'isInternal' False.
if connect and all(x['isInternal'] for x in display_info):
err = state.GetInstance().DeviceSetDisplayMirrorMode(
{'mode': 'off'})
if err is not None:
logging.warning('Failed to turn off the mirror mode: %s', err)
else:
logging.info('Get display info %r', display_info)
break
self.Sleep(_CONNECTION_CHECK_PERIOD_SECS)