blob: 3acff77fa6f69be733bfa380b3ff8fe262a3d5c7 [file] [log] [blame]
# Copyright 2012 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests lid switch functionality.
Description
-----------
Test lid switch functionality by listening to lid switch event.
Test Procedure
--------------
If a BFT fixture is set:
- ask the fixture to simulate a lid close event
- confirm the event is received
- ask the fixture to simulate a lid open event
- confirm the event is received
If there is no BFT fixture, the operator should close and open the lid manually.
Dependency
----------
- Depends on 'evdev' module to monitor events.
- If ``ok_audio_path`` is set, depends on audio function to play a sound to
indicate the lid close event is received.
Examples
--------
``LidSwitch`` is defined in the generic_common test list, it calls this test
with default arguments.
"""
import datetime
import os
import time
from typing import TYPE_CHECKING, Optional, Union
from cros.factory.test import event_log # TODO(chuntsen): Deprecate event log.
# The right BFTFixture module is dynamically imported based on args.bft_fixture.
# See LidSwitchTest.setUp() for more detail.
from cros.factory.test.fixture import bft_fixture
from cros.factory.test.i18n import _
from cros.factory.test import test_case
from cros.factory.test import test_ui
from cros.factory.test.utils import audio_utils
from cros.factory.test.utils import evdev_utils
from cros.factory.testlog import testlog
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import file_utils
from cros.factory.utils import sync_utils
if not TYPE_CHECKING:
from cros.factory.external.py_lib import evdev
else:
import evdev # type: ignore[import]
_DEFAULT_TIMEOUT = 30
_BACKLIGHT_OFF_TIMEOUT = 12
_TEST_TOLERANCE = 2
_TIMESTAMP_BL_ON = _BACKLIGHT_OFF_TIMEOUT - _TEST_TOLERANCE
_TIMESTAMP_BL_OFF = _BACKLIGHT_OFF_TIMEOUT + _TEST_TOLERANCE
class LidSwitchArgs:
timeout_secs: int
ok_audio_path: str
audio_volume: int
device_filter: Union[int, str]
bft_fixture_class_name: Optional[str]
bft_fixture_params: Optional[dict]
bft_retries: int
bft_pause_secs: Union[int, float]
brightness_path: Optional[str]
brightness_when_closed: Optional[int]
check_delayed_backlight: bool
bft_control_name: str
class LidSwitchTest(test_case.TestCase):
"""Lid switch factory test."""
related_components = tuple()
ARGS = [
Arg('timeout_secs', int, 'Timeout value for the test.',
default=_DEFAULT_TIMEOUT),
Arg(
'ok_audio_path', str,
'Path to the OK audio file which is played after detecting lid close'
'signal. Defaults to play ok_*.ogg in /sounds.', default=None),
Arg('audio_volume', int,
'Percentage of audio volume to use when playing OK audio file.',
default=100),
Arg('device_filter', (int, str),
'Event ID or name for evdev. None for auto probe.', default=None),
Arg(
'bft_fixture_class_name', str,
'Fully-qualified class name of the BFTFixture implementation to use.',
default=None),
Arg(
'bft_fixture_params', dict,
'A dictionary of parameters for the BFTFixture class `Init()` '
'method.', default=None),
Arg('bft_retries', int, 'Number of retries for BFT lid open / close.',
default=3),
Arg('bft_pause_secs', (int, float),
'Pause time before issuing BFT command.', default=0.5),
Arg('brightness_path', str, 'Path to control brightness level.',
default=None),
Arg('brightness_when_closed', int,
'Value to brightness when lid switch closed.', default=None),
Arg('check_delayed_backlight', bool, 'True to check delayed backlight.',
default=False),
Arg('bft_control_name', str, 'Controller name on BFT fixture to trigger '
'Lid switch', default=bft_fixture.BFTFixture.Device.LID_MAGNET)
]
args: LidSwitchArgs
ui: test_ui.StandardUI
event_loop: test_ui.EventLoop
def AdjustBrightness(self, value):
"""Adjusts the intensity by writing targeting value to sysfs.
Args:
value: The targeted brightness value.
"""
try:
file_utils.WriteFile(self.args.brightness_path, f'{int(value)}')
except IOError:
self.FailTask(
f'Can not write {value!r} into brightness. Maybe the limit is wrong')
def GetBrightness(self): # pylint: disable=inconsistent-return-statements
"""Gets the brightness value from sysfs."""
try:
return int(file_utils.ReadFile(self.args.brightness_path))
except IOError:
self.FailTask('Can not read brightness.')
def setUp(self):
self.event_dev = evdev_utils.FindDevice(self.args.device_filter,
evdev_utils.IsLidEventDevice)
self.ui.ToggleTemplateClass('font-large', True)
self.dispatcher = evdev_utils.InputDeviceDispatcher(
self.event_dev, self.event_loop.CatchException(self.HandleEvent))
# Prepare fixture auto test if needed.
self.fixture = None
if self.args.bft_fixture_class_name:
self.fixture = bft_fixture.CreateBFTFixture(
self.args.bft_fixture_class_name, self.args.bft_fixture_params)
self.fixture_lid_closed = False
# Variables to track the time it takes to open and close the lid
self._start_waiting_sec = self.getCurrentEpochSec()
self._closed_sec = 0
self._opened_sec = 0
self._restore_brightness = None
def tearDown(self):
self.dispatcher.Close()
file_utils.TryUnlink('/run/power_manager/lid_opened')
if self.fixture:
self.BFTLid(close=False, in_tear_down=True)
self.fixture.Disconnect()
event_log.Log(
'lid_wait_sec',
time_to_close_sec=(self._closed_sec - self._start_waiting_sec),
time_to_open_sec=(self._opened_sec - self._closed_sec),
use_fixture=bool(self.fixture))
testlog.LogParam('time_to_close',
self._closed_sec - self._start_waiting_sec)
testlog.LogParam('time_to_open',
self._opened_sec - self._closed_sec)
testlog.LogParam('use_fixture', bool(self.fixture))
# Restore brightness
if self.args.brightness_path is not None:
if self._restore_brightness is not None:
self.AdjustBrightness(self._restore_brightness)
def getCurrentEpochSec(self):
"""Returns the time since epoch."""
return float(datetime.datetime.now().strftime('%s.%f'))
def CheckDelayedBacklight(self):
"""Checks delayed backlight off.
This function calls FailTask() on backlight turned off too early, or
backlight did not turn off after backlight timeout period.
Signals:
lid ---+
switch |
+-----------------------------------------------------------
fixture ---++ ++ ++-------------------+
lid || || || |
status ++ ++ ++ +--------------------------------
test skip BL_ON BL_OFF
Raises:
BFTFixtureException on fixture communication error.
"""
start_time = time.time()
timeout_time = start_time + _TIMESTAMP_BL_OFF
# Ignore leading bouncing signals
self.Sleep(_TEST_TOLERANCE)
# Check backlight power falling edge
while time.time() < timeout_time:
test_time = time.time() - start_time
if self.fixture:
backlight = self.fixture.GetSystemStatus(
bft_fixture.BFTFixture.SystemStatus.BACKLIGHT)
if backlight == bft_fixture.BFTFixture.Status.OFF:
if test_time < _TIMESTAMP_BL_ON:
self.FailTask('Backlight turned off too early.')
return
self.Sleep(0.5)
self.FailTask('Backlight does not turn off.')
def HandleEvent(self, event):
if event.type == evdev.ecodes.EV_SW and event.code == evdev.ecodes.SW_LID:
if event.value == 1: # LID_CLOSED
self._closed_sec = self.getCurrentEpochSec()
if self.fixture:
if self.args.check_delayed_backlight:
self.CheckDelayedBacklight()
self.AskForOpenLid()
if self.args.brightness_path is not None:
self._restore_brightness = self.GetBrightness()
# Close backlight
self.AdjustBrightness(self.args.brightness_when_closed)
elif event.value == 0: # LID_OPEN
self._opened_sec = self.getCurrentEpochSec()
# Restore brightness
if self.args.brightness_path is not None:
self.AdjustBrightness(self._restore_brightness)
self.PassTask()
def BFTLid(self, close, in_tear_down=False):
"""Commands BFT to close/open the lid.
It pauses for args.bft_pause_secs seconds before sending BFT command.
Also, it retries args.bft_retries times if BFT response is unexpected.
It fails the test if BFT response badly after retries.
Args:
close: True to close the lid. Otherwise, open it.
in_tear_down: True if we are in tearDown function.
"""
# if we are in tearDown function, the task is over, self.Sleep will fail
# immediately.
sleep = time.sleep if in_tear_down else self.Sleep
@sync_utils.RetryDecorator(max_attempt_count=self.args.bft_retries,
interval_sec=self.args.bft_pause_secs,
sleep=sleep, reraise=True)
def _SetDeviceEngaged():
if self.fixture:
self.fixture.SetDeviceEngaged(self.args.bft_control_name, close)
self.fixture_lid_closed = close
try:
_SetDeviceEngaged()
except bft_fixture.BFTFixtureException as e:
if not in_tear_down:
self.FailTask(f"Failed to {'close' if close else 'open'} the lid with "
f"{int(self.args.bft_retries)} retries. Reason: {e}")
def AskForOpenLid(self):
if self.fixture:
self.ui.SetState(_('Demagnetizing lid sensor'))
self.BFTLid(close=False)
else:
self.ui.SetState(_('Open the lid'))
self.PlayOkAudio()
def PlayOkAudio(self):
if self.args.ok_audio_path:
self.ui.PlayAudioFile(self.args.ok_audio_path)
else:
self.ui.PlayAudioFile(os.path.join(self.ui.GetUILocale(), 'ok.ogg'))
def runTest(self):
audio_utils.CRAS().EnableOutput()
audio_utils.CRAS().SetActiveOutputNodeVolume(self.args.audio_volume)
if self.fixture:
self.ui.SetState(_('Magnetizing lid sensor'))
else:
self.ui.SetState(_('Close then open the lid'))
self.dispatcher.StartDaemon()
self.ui.StartFailingCountdownTimer(
_DEFAULT_TIMEOUT if self.fixture else self.args.timeout_secs)
if self.fixture:
self.BFTLid(close=True)
self.WaitTaskEnd()