blob: a622ab8af0ac5c679633b297c8b6b42b59d57e36 [file] [log] [blame]
# -*- coding: utf-8 -*-
#
# Copyright 2015 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 screen rotation through ChromeOS and accelerometer data.
Description
-----------
Tests that ChromeOS properly rotates the screen when the device is physically
rotated in tablet mode.
If ``check_accelerometer`` is set, also checks that the orientation matches up
with accelerometer data.
Test Procedure
--------------
1. If ``prompt_flip_tablet`` is set, operator would be prompted to flip the
Chromebook to tablet mode.
2. A picture would be shown on the screen. Operator should rotate the tablet to
align with the image. This would repeat four times with each orientations,
and the test automatically pass when the desired orientation is reached.
If ``check_accelerometer`` is set, the test would also check if the value of
accelerometer is within acceptable range for each orientation.
3. If ``prompt_flip_notebook`` is set, operator would be prompted to flip the
Chromebook back to notebook mode.
Dependency
----------
``chrome.display.system.getInfo`` in Chrome extension to get screen orientation.
If ``check_accelerometer`` is set, also depends on device API
``cros.factory.device.accelerometer``.
Examples
--------
To test screen rotation for Chrome and prompt operator to flip before and after
the test (The default), add this in test list::
{
"pytest_name": "tablet_rotation"
}
To test screen rotation, assume that the Chromebook is already in tablet mode,
and have a timeout of 10 minutes::
{
"pytest_name": "tablet_rotation",
"args": {
"prompt_flip_tablet": false,
"timeout_secs": 600
}
}
To also check accelerometer when testing screen rotation::
{
"pytest_name": "tablet_rotation",
"args": {
"check_accelerometer": true,
"degrees_to_orientations": [
[
0,
{
"in_accel_x": 0,
"in_accel_y": 1,
"in_accel_z": 0
}
],
[
90,
{
"in_accel_x": 1,
"in_accel_y": 0,
"in_accel_z": 0
}
],
[
180,
{
"in_accel_x": 0,
"in_accel_y": -1,
"in_accel_z": 0
}
],
[
270,
{
"in_accel_x": -1,
"in_accel_y": 0,
"in_accel_z": 0
}
]
],
"spec_offset": [0.5, 0.5]
}
}
"""
import random
import threading
import time
import unittest
import factory_common # pylint: disable=unused-import
from cros.factory.device import device_utils
from cros.factory.test import countdown_timer
from cros.factory.test.i18n import test_ui as i18n_test_ui
from cros.factory.test.pytests import tablet_mode_ui
from cros.factory.test import state
from cros.factory.test import test_ui
from cros.factory.test import ui_templates
from cros.factory.utils.arg_utils import Arg
_DEFAULT_TIMEOUT = 30
_UNICODE_PICTURES = u'☃☺☎'
_TEST_DEGREES = [90, 180, 270, 0]
_POLL_ROTATION_INTERVAL = 0.1
_MSG_PROMPT_ROTATE_TABLET = i18n_test_ui.MakeI18nLabel(
'Rotate the tablet to correctly align the picture, holding it at an '
'upright 90-degree angle.')
_ID_COUNTDOWN_TIMER = 'countdown-timer'
_ID_PROMPT = 'prompt'
_ID_PICTURE = 'picture'
_HTML_COUNTDOWN_TIMER = '<div id="%s" class="countdown-timer"></div>' % (
_ID_COUNTDOWN_TIMER)
_HTML_BUILD_PICTURE = lambda degrees, picture: (
'<span style="-webkit-transform: rotate(%sdeg);">%s</span>'
% (degrees, picture))
_HTML = """
<div class="prompt" id="%s"></div>
<div class="picture" id="%s"></div>
""" % (_ID_PROMPT, _ID_PICTURE)
_CSS_COUNTDOWN_TIMER = """
.countdown-timer {
font-size: 2em;
align-self: flex-end;
}
"""
_CSS = """
.prompt {
font-size: 2em;
margin-bottom: 1em;
}
.picture span {
font-size: 15em;
display: block;
}
"""
class TabletRotationTest(unittest.TestCase):
"""Tablet rotation factory test."""
ARGS = [
Arg('timeout_secs', int, 'Timeout value for the test.',
default=_DEFAULT_TIMEOUT, optional=True),
Arg('prompt_flip_tablet', bool,
'Assume the notebook is not yet in tablet mode, and operator should '
'first be instructed to flip it as such. (This is useful to unset if '
'the previous test finished in tablet mode.)',
default=True, optional=True),
Arg('prompt_flip_notebook', bool,
'After the test, prompt the operator to flip back into notebook '
'mode. (This is useful to unset if the next test requires tablet '
'mode.)',
default=True, optional=True),
Arg('check_accelerometer', bool,
'In addition to checking the ChromeOS screen orientation, also check '
'accelerometer data to ensure it reports the same orientation.',
default=False, optional=True),
Arg('accelerometer_location', str,
'If check_accelerometer is true, the location of accelerometer that '
'should be checked. Should be either "lid" or "base"',
default='lid', optional=True),
Arg('degrees_to_orientations', list,
'A list of [key, value] pairs. '
'Keys: degree of the orientation, limited to [0, 90, 180, 270]. '
'Values: a dictionary containing orientation configuration. Keys '
'should be the name of the accelerometer signal. The possible keys '
'are "in_accel_(x|y|z)". Values should be one of [0, 1, -1], '
'representing the ideal value for gravity under such orientation.',
default=[], optional=True),
Arg('spec_offset', tuple,
'A tuple of two numbers, ex: (1.5, 1.5) '
'indicating the tolerance for the digital output of sensors under '
'zero gravity and one gravity.', optional=True),
Arg('sample_rate_hz', int,
'The sample rate in Hz to get raw data from '
'accelerometers.', default=20, optional=True),
]
def setUp(self):
# args.check_accelerometer implies the following required arguments:
# degrees_to_orientations
# spec_offset
self.dut = device_utils.CreateDUTInterface()
self.accel_controller = None
if self.args.check_accelerometer:
if not all([self.args.degrees_to_orientations,
self.args.spec_offset]):
self.fail('If running in check_accelerometer mode, please provide '
'arguments degrees_to_orientations and spec_offset.')
return
self.accel_controller = self.dut.accelerometer.GetController(
location=self.args.accelerometer_location)
self.degrees_to_orientations = dict(self.args.degrees_to_orientations)
self.ui = test_ui.UI()
self.state = state.get_instance()
self.tablet_mode_ui = tablet_mode_ui.TabletModeUI(self.ui,
_HTML_COUNTDOWN_TIMER,
_CSS_COUNTDOWN_TIMER)
def TestRotationUIFlow(self, degrees_targets=None):
if degrees_targets is None:
degrees_targets = _TEST_DEGREES
for degrees_target in degrees_targets:
# Initialize UI template, HTML and CSS.
template = ui_templates.OneSection(self.ui)
template.SetState(_HTML + _HTML_COUNTDOWN_TIMER)
self.ui.AppendCSS(_CSS + _CSS_COUNTDOWN_TIMER)
self._PromptAndWaitForRotation(degrees_target)
self.tablet_mode_ui.FlashSuccess()
if self.args.prompt_flip_notebook:
wait_event = threading.Event()
self.tablet_mode_ui.AskForNotebookMode(
lambda unused_event: wait_event.set())
wait_event.wait()
def _PromptAndWaitForRotation(self, degrees_target):
# Choose a new picture and set the prompt message.
rand_int = random.randint(0, len(_UNICODE_PICTURES) - 1)
picture = _UNICODE_PICTURES[rand_int]
self.ui.SetHTML(_MSG_PROMPT_ROTATE_TABLET, id=_ID_PROMPT)
degrees_previous = None
while True:
# Get current rotation.
degrees_current = self._GetCurrentDegrees()
# TODO(kitching): Research disabling ChromeOS screen rotation when in
# factory mode.
#
# When the device is physically rotated, ChromeOS rotates the screen
# accordingly. If this wasn't the case, we could simply paint the
# picture in the desired orientation, and have the operator rotate the
# device appropriately:
#
# | > | | ^ | | < |
#
# But, because ChromeOS automatically rotates the screen, if we keep the
# picture's orientation the same, it would *always* face the same
# direction, regardless of how the tablet is rotated. (This makes
# perfect sense. We always want the UI to face the user in the correct
# orientation.) The picture would look like this:
#
# | > | | > | | > |
#
# Thus, instructing the operator to align the picture to an upright
# position becomes a fruitless effort. So, we need to offset this
# change by also rotating our picture every time the screen is rotated.
# We set the rotation via CSS using degrees_delta as the angle.
degrees_delta = degrees_target - degrees_current
success = (degrees_delta == 0)
# Check accelerometer if necessary.
if (success and
self.accel_controller and
degrees_target in self.degrees_to_orientations):
orientations = self.degrees_to_orientations[degrees_target]
cal_data = self.accel_controller.GetData(
sample_rate=self.args.sample_rate_hz)
if not self.accel_controller.IsWithinOffsetRange(
cal_data, orientations, self.args.spec_offset):
success = False
# Are we currently at our target?
if success:
return True
# If the device has been rotated, we also need to update our picture's
# orientation accordingly (see comment above describing degrees_delta).
elif degrees_previous != degrees_current:
self.ui.SetHTML(_HTML_BUILD_PICTURE(degrees_delta, picture),
id=_ID_PICTURE)
# Target has still not been reached. Sleep and continue.
degrees_previous = degrees_current
time.sleep(_POLL_ROTATION_INTERVAL)
def _GetCurrentDegrees(self):
display_info = None
try:
display_info = self.state.DeviceGetDisplayInfo()
except Exception:
pass
if not display_info:
raise Exception('Failed to get display_info')
display_info = [info for info in display_info if info['isPrimary']]
if len(display_info) != 1:
raise Exception('Failed to get internal display')
return display_info[0]['rotation']
def runTest(self):
# Create a thread to run countdown timer.
countdown_timer.StartCountdownTimer(
self.args.timeout_secs,
lambda: self.ui.Fail('Tablet rotation test failed due to timeout.'),
self.ui,
_ID_COUNTDOWN_TIMER)
# Create a thread to control UI flow.
def _UIFlow():
if self.args.prompt_flip_tablet:
wait_event = threading.Event()
self.tablet_mode_ui.AskForTabletMode(
lambda unused_event: wait_event.set())
wait_event.wait()
self.TestRotationUIFlow()
self.ui.RunInBackground(_UIFlow)
self.ui.Run()