blob: 21eb387e9814403672a207f3a71bde55c501cfd0 [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.
"""Tests Raiden DP function with Plankton-Raiden, which links/unlinks DUT Raiden
port to DP sink. And with Plankton-HDMI as DP sunk to capture DP output to
verify.
"""
import evdev
import logging
import os
import time
import unittest
import xmlrpclib
import factory_common # pylint: disable=unused-import
from cros.factory.device import device_utils
from cros.factory.test import factory
from cros.factory.test.fixture import bft_fixture
from cros.factory.test.fixture.dolphin import plankton_hdmi
from cros.factory.test.i18n import test_ui as i18n_test_ui
from cros.factory.test import test_ui
from cros.factory.test import ui_templates
from cros.factory.test.utils import evdev_utils
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import file_utils
from cros.factory.utils import sync_utils
_TEST_TITLE = i18n_test_ui.MakeI18nLabel('Raiden Display Test')
_BLACKSCREEN_STR = i18n_test_ui.MakeI18nLabel(
'Caution: monitor may turn black for a short time.')
_ID_CONTAINER = 'raiden-display-container'
# The style is in raiden_display.css
# The layout contains one div for display.
_HTML_DISPLAY = (
'<link rel="stylesheet" type="text/css" href="raiden_display.css">'
'<div id="%s"></div>\n' % _ID_CONTAINER)
_WAIT_DISPLAY_SIGNAL_SECS = 3
_WAIT_RETEST_SECS = 2
def _GetConnectStr(device):
return i18n_test_ui.MakeI18nLabel(
'Connecting BFT display: {device}', device=device)
def _GetVideoStr(device):
return i18n_test_ui.MakeI18nLabel(
'BFT display {device} is connected. Sending image...', device=device)
def _GetDisconnectStr(device):
return i18n_test_ui.MakeI18nLabel(
'Disconnecting BFT display: {device}', device=device)
class RaidenDisplayTest(unittest.TestCase):
"""Tests Raiden ports display functionality."""
ARGS = [
Arg('bft_fixture', dict, bft_fixture.TEST_ARG_HELP),
Arg('raiden_index', int, 'Index of DUT raiden port'),
Arg('bft_media_device', str,
'Device name of BFT used to insert/remove the media.'),
Arg('display_id', str,
'Display ID used to identify display in xrandr/modeprint.'),
Arg('capture_resolution', tuple,
'A tuple (x-res, y-res) indicating the'
'image capture resolution to use.',
default=(1920, 1080)),
Arg('capture_fps', int, 'Camera capture rate in frames per second.',
default=30),
Arg('uvc_video_dev_index', int, 'index of video device (-1 for default)',
default=-1),
Arg('uvc_video_dev_port', str, 'port of video device (ex. 3-1)',
optional=True),
Arg('corr_value_threshold', tuple,
'A tuple of (b, g, r) channel histogram '
'correlation pass/fail threshold. '
'Should be int/float type, ex. (0.8, 0.8, 0.8)',
default=(0.8, 0.8, 0.8)),
Arg('dp_verify_server', str,
'Server URL for verifying DP output, e.g. "http://192.168.0.1:9999". '
'Default None means verifying locally.',
optional=True),
Arg('verify_display_switch', bool,
'Set False to test without display switch, and compare default '
'wallpaper only (can save more testing time).',
default=True),
Arg('force_dp_renegotiated', bool,
'Force DP to renegotiate with dolphin by disconnecting TypeC port',
default=False),
Arg('fire_hpd_manually', bool, 'Fire HPD manually.', default=False)
]
def setUp(self):
self._dut = device_utils.CreateDUTInterface()
self._ui = test_ui.UI()
self._template = ui_templates.TwoSections(self._ui)
self._template.SetTitle(_TEST_TITLE)
self._ui.AppendHTML(_HTML_DISPLAY)
self._static_dir = self._ui.GetStaticDirectoryPath()
self._display_image_path = os.path.join(self._static_dir, 'template.png')
self._golden_image_path = os.path.join(self._static_dir, 'golden.png')
self.ExtractTestImage()
self._ui.CallJSFunction('setupDisplayTest', _ID_CONTAINER)
self._total_tests = 0
self._finished_tests = 0
self._finished = False
self._image_matched = True
self._testing_display = self.args.display_id
self._bft_fixture = bft_fixture.CreateBFTFixture(**self.args.bft_fixture)
self._bft_media_device = self.args.bft_media_device
if self._bft_media_device not in self._bft_fixture.Device:
self.Fail('Invalid args.bft_media_device: ' + self._bft_media_device)
self._original_primary_display = self._GetPrimaryScreenId()
self._verify_server = None
self._server_camera_enabled = False
self._camera_device = None
self._verify_locally = not self.args.dp_verify_server
if self.args.dp_verify_server:
# allow_none is necessary as most of the methods return None.
self._verify_server = xmlrpclib.ServerProxy(
self.args.dp_verify_server, allow_none=True)
else:
uvc_video_index = (None if self.args.uvc_video_dev_index < 0 else
self.args.uvc_video_dev_index)
self._camera_device = plankton_hdmi.PlanktonHDMI(
uvc_video_index=uvc_video_index,
uvc_video_port=self.args.uvc_video_dev_port,
capture_resolution=self.args.capture_resolution,
capture_fps=self.args.capture_fps)
def tearDown(self):
# Make sure to disable camera of dp_verify_server in the end of test.
if self._server_camera_enabled:
self._verify_server.DisableCamera()
self._bft_fixture.Disconnect()
self.RemoveTestImage()
return
def ExtractTestImage(self):
"""Extracts selected test images from zipped files."""
filename = ('template.tar.gz' if self.args.verify_display_switch else
'wallpaper.tar.gz')
file_utils.ExtractFile(os.path.join(self._static_dir, filename),
self._static_dir)
def RemoveTestImage(self):
"""Removes extracted image files after test finished."""
file_utils.TryUnlink(self._display_image_path)
file_utils.TryUnlink(self._golden_image_path)
def TestConnectivity(self, connect):
"""Tests connectivity of bft media device.
It uses BFT fixture to engage / disengage the display device.
Args:
connect: True if testing engagement, False if testing disengagement.
"""
if connect:
self._template.SetInstruction(_GetConnectStr(self._bft_media_device))
self._bft_fixture.SetDeviceEngaged(self._bft_media_device, engage=True)
if self.args.force_dp_renegotiated:
self._bft_fixture.SetFakeDisconnection(1)
time.sleep(1) # diconnetion by software for re-negotiation.
else:
time.sleep(0.5)
if self.args.fire_hpd_manually:
self._dut.usb_c.SetHPD(self.args.raiden_index)
self._dut.usb_c.SetPortFunction(self.args.raiden_index, 'dp')
sync_utils.WaitFor(self._PollDisplayConnected, timeout_secs=10)
else:
self._template.SetInstruction(_GetDisconnectStr(self._bft_media_device))
if self.args.fire_hpd_manually:
self._dut.usb_c.ResetHPD(self.args.raiden_index)
self._bft_fixture.SetDeviceEngaged(self._bft_media_device, engage=False)
if self.args.force_dp_renegotiated:
self._bft_fixture.SetFakeDisconnection(1)
time.sleep(1) # diconnetion by software for re-negotiation.
sync_utils.WaitFor(lambda: not self._PollDisplayConnected(),
timeout_secs=10)
if not self.args.verify_display_switch:
self.AdvanceProgress()
return
time.sleep(_WAIT_DISPLAY_SIGNAL_SECS) # need a delay for display_info
display_info = factory.get_state_instance().DeviceGetDisplayInfo()
logging.info('Get display info %r', display_info)
# In the case of connecting an external display, make sure there
# is an item in display_info with 'isInternal' False.
# 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 not connect or any(x['isInternal'] is False for x in display_info):
self.AdvanceProgress()
else:
self.Fail('Get the wrong display info')
def TestDisplayPlayback(self):
"""Projects the screen to external display, make the display to show an
image by JS function.
"""
if self.args.verify_display_switch:
self._template.SetInstruction(_GetVideoStr(self._bft_media_device))
self._ui.CallJSFunction('switchDisplayOnOff')
self.SetMainDisplay(recover_original=False)
time.sleep(_WAIT_DISPLAY_SIGNAL_SECS) # wait for display signal stable
self.AdvanceProgress()
def TestCaptureImage(self):
"""Tests and compares loopback image.
Link to camera device and capture an image from camera. Compare to
the image projected to external display by bgr-channel histogram
comparisons to judge DP functionality.
Raises:
BFTFixtureException: If it failed to detect camera.
"""
if self._verify_locally:
self._image_matched = self._camera_device.CaptureCompare(
self._golden_image_path, self.args.corr_value_threshold)
else:
corr_values = self._verify_server.VerifyDP(True)
self._image_matched = all(
c >= t for c, t in zip(corr_values, self.args.corr_value_threshold))
logging.info('CompareHist correlation result = b: %.4f, g: %.4f, r: %.4f',
corr_values[0], corr_values[1], corr_values[2])
if self.args.verify_display_switch:
self._ui.CallJSFunction('switchDisplayOnOff')
self.SetMainDisplay(recover_original=True)
time.sleep(_WAIT_DISPLAY_SIGNAL_SECS) # wait for display signal stable
self.AdvanceProgress()
def SetMainDisplay(self, recover_original=True):
"""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:
recover_original: True to set the original display as main; False to
set the other (external) display as main.
"""
display_info = factory.get_state_instance().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.Fail('Fail to detect external display')
else:
return
# Try to switch main display for at most 5 times.
tries_left = 5
while tries_left:
if not (recover_original ^ (self._GetPrimaryScreenId() ==
self._original_primary_display)):
# Stop the loop if these two conditions are either both True or
# both False.
break
evdev_utils.SendKeys([evdev.ecodes.KEY_LEFTALT, evdev.ecodes.KEY_F4])
tries_left -= 1
time.sleep(_WAIT_RETEST_SECS)
if tries_left == 0:
self.Fail('Fail to switch main display')
def Fail(self, msg):
"""Fails the test."""
self._ui.Fail(msg)
raise factory.FactoryTestFailure(msg)
def AdvanceProgress(self, value=1):
"""Advances the progess bar.
Args:
value: The amount of progress to advance.
"""
self._finished_tests += value
if self._finished_tests > self._total_tests:
self._finished_tests = self._total_tests
self._template.SetProgressBarValue(
100 * self._finished_tests / self._total_tests)
def _GetPrimaryScreenId(self):
"""Gets ID of primary screen.
Returns:
Integer for screen ID.
"""
for info in factory.get_state_instance().DeviceGetDisplayInfo():
if info['isPrimary']:
return info['id']
self.Fail('Fail to get primary display ID')
def _PollDisplayConnected(self):
"""Event for polling display connected.
Returns:
True if connected; otherwise False.
"""
return self._dut.display.GetPortInfo()[self._testing_display].connected
def runTest(self):
"""Runs display test."""
# Sanity check
if self.args.verify_display_switch:
self.assertTrue(os.path.isfile(self._display_image_path))
self.assertTrue(os.path.isfile(self._golden_image_path))
self._template.DrawProgressBar()
# Connect, video playback, capture, disconnect
self._total_tests = 4
self._template.SetProgressBarValue(0)
self._template.SetState(_BLACKSCREEN_STR)
logging.info('Testing device: %s', self._bft_media_device)
if self._verify_locally:
self._camera_device.EnableCamera()
else:
self._verify_server.EnableCamera()
self._server_camera_enabled = True
self.TestConnectivity(connect=True)
self.TestDisplayPlayback()
self.TestCaptureImage()
self.TestConnectivity(connect=False)
if self._verify_locally:
self._camera_device.DisableCamera()
else:
self._verify_server.DisableCamera()
self._server_camera_enabled = False
self._finished = True
if not self._image_matched:
self.Fail('DP Loopback image correlation is below threshold.')