blob: eadc0b8cf43d77caae2e23b5d87b5a8cc6c253a7 [file] [log] [blame]
# Copyright 2017 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""A factory test to test the functionality of touchpad.
Description
-----------
A touchpad test for touching, clicking, and multi-contact.
Test Procedure
--------------
1. Take off all your fingers from the touchpad.
2. Press spacebar to start.
3. Touch all `x_segments` * `y_segments` areas of touchpad with one finger.
The corresponding regions in screen will become green once you touch them.
4. Scroll up and down with two fingers to make all cells in the right side of
screen green.
5. Click left-top, right-top, left-bottom, and right-bottom corner of touchpad
with one finger for `number_to_quadrant` times.
6. Click anywhere of touchpad with one finger until you have already done that
`number_to_click` times.
7. Click anywhere of touchpad with two fingers for `number_to_click` times.
If you don't pass the test in `timeout_secs` seconds, the test will fail.
Dependency
----------
- Based on Linux evdev.
Examples
--------
To test touchpad with default parameters, add this in test list:
.. test_list::
generic_touchpad_examples:Touchpad
If you want to change the time limit to 100 seconds::
.. test_list::
generic_touchpad_examples:Touchpad100Seconds
"""
import logging
from cros.factory.test.i18n import _
from cros.factory.test import session
from cros.factory.test import test_case
from cros.factory.test import test_ui
from cros.factory.test.utils import evdev_utils
from cros.factory.test.utils import touch_monitor
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import process_utils
from cros.factory.external.py_lib import evdev
class TouchpadMonitor(touch_monitor.MultiTouchMonitor):
def __init__(self, device, test):
super().__init__(device)
self.test = test
def OnKey(self, key_event_code):
"""See TouchMonitorBase.OnKey."""
state = self.GetState()
key_event_value = state.keys[key_event_code]
# yapf: disable
if key_event_code == evdev.ecodes.BTN_LEFT and state.num_fingers == 1: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.test.OnSingleClick(key_event_value)
else:
if self.test.touchpad_has_right_btn:
# yapf: disable
if key_event_code != evdev.ecodes.BTN_RIGHT: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
return
else:
# yapf: disable
if key_event_code != evdev.ecodes.BTN_LEFT or state.num_fingers != 2: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
return
self.test.OnDoubleClick(key_event_value)
def OnNew(self, slot_id):
"""See MultiTouchMonitor.OnNew."""
state = self.GetState()
if state.num_fingers <= 2:
self.OnMove(slot_id)
elif not self.test.already_alerted:
self.test.already_alerted = True
msg = f'number_fingers = {int(state.num_fingers)}'
logging.error(msg)
session.console.error(msg)
self.test.ui.Alert(_(
"Please don't put your third finger on the touchpad.\n"
"If you didn't do that,\n"
"treat this touch panel as a problematic one!!"))
self.test.FailWithMessage()
def OnMove(self, slot_id):
"""See MultiTouchMonitor.OnMove."""
state = self.GetState()
slot = state.slots[slot_id]
self.test.OnMoveEvent(slot.x, slot.y, state.num_fingers)
class Quadrant:
"""The class is to update quadrant information.
Update quadrant information according to x_ratio and y_ratio:
Quadrant 1 is Right-Top Corner
Quadrant 2 is Left-Top Corner
Quadrant 3 is Left-Bottom Corner
Quadrant 4 is Right-Bottom Corner
"""
def __init__(self):
self.quadrant = 0
def UpdateQuadrant(self, x_ratio, y_ratio):
if y_ratio < 0.5 <= x_ratio:
self.quadrant = 1
elif x_ratio < 0.5 and y_ratio < 0.5:
self.quadrant = 2
elif x_ratio < 0.5 <= y_ratio:
self.quadrant = 3
elif x_ratio >= 0.5 and y_ratio >= 0.5:
self.quadrant = 4
class TouchpadTest(test_case.TestCase):
"""Tests the function of touchpad.
The test checks the following function:
1. Detect finger on every sector of touchpad.
2. Two finger scrolling.
3. Single click.
4. Either double click or right click.
Properties:
self.touchpad_device_name: This can be probed from evdev.
self.touchpad_has_right_btn: for touchpad with right button, we don't want
to process double click. We will only process right_btn and left_btn.
self.quadrant: This represents the current quadrant of mouse.
"""
related_components = (test_case.TestCategory.TRACKPAD, )
ARGS = [
Arg('device_filter', (int, str),
'Touchpad input event id or evdev name. The test will probe'
' for event id if it is not given.', default=None),
Arg('timeout_secs', int, 'Timeout for the test.', default=20),
Arg('number_to_click', int, 'Target number to click.', default=10),
Arg('number_to_quadrant', int,
'Target number to click for each quadrant.', default=3),
Arg('x_segments', int, 'Number of X axis segments to test.', default=5),
Arg('y_segments', int, 'Number of Y axis segments to test.', default=5)]
def setUp(self):
# Initialize properties
self.touchpad_device_name = None
self.touchpad_has_right_btn = False
self.quadrant = Quadrant()
# yapf: disable
self.touchpad_device = evdev_utils.FindDevice(self.args.device_filter, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
evdev_utils.IsTouchpadDevice)
self.monitor = None
self.dispatcher = None
self.already_alerted = False
self.frontend_proxy = None
# yapf: disable
self.x_segments = self.args.x_segments # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.y_segments = self.args.y_segments # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.scroll_tested = [False] * self.y_segments
self.touch_tested = [[False] * self.y_segments
for unused_i in range(self.x_segments)]
# Quadrant has index 1 to 4.
self.quadrant_count = [None, 0, 0, 0, 0]
self.single_click_count = 0
self.double_click_count = 0
# Disable lid function since lid open|close will trigger button up event.
process_utils.CheckOutput(['ectool', 'forcelidopen', '1'])
def tearDown(self):
"""Clean-up stuff.
Terminates the running process or we'll have trouble stopping the
test.
"""
if self.dispatcher is not None:
self.dispatcher.Close()
# Enable lid function.
process_utils.CheckOutput(['ectool', 'forcelidopen', '0'])
def GetSpec(self):
"""Gets device name, btn_right."""
self.touchpad_device_name = self.touchpad_device.name
# yapf: disable
if evdev.ecodes.BTN_RIGHT in self.monitor.GetState().keys: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.touchpad_has_right_btn = True
logging.info('get device %s spec right_btn = %s',
self.touchpad_device_name, self.touchpad_has_right_btn)
def OnMoveEvent(self, x, y, num_fingers):
"""Marks a scroll sector as tested or a move sector as tested."""
self.quadrant.UpdateQuadrant(x, y)
if num_fingers == 2:
self.MarkScrollSectorTested(y)
else:
self.MarkSectorTested(x, y)
self.CheckTestPassed()
def OnSingleClick(self, down):
"""Draws single click event by calling javascript function.
Args:
down: bool
"""
if not down:
quadrant = self.quadrant.quadrant
logging.info('mark single click up quadrant = %d', quadrant)
# yapf: disable
self.frontend_proxy.MarkCircleTested('left') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.single_click_count < self.args.number_to_click: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.single_click_count += 1
# yapf: disable
self.frontend_proxy.UpdateCircleCountText(self.single_click_count, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.double_click_count)
# yapf: disable
if self.quadrant_count[quadrant] < self.args.number_to_quadrant: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.quadrant_count[quadrant] += 1 # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.frontend_proxy.UpdateQuadrantCountText( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
quadrant, self.quadrant_count[quadrant])
# yapf: disable
if self.quadrant_count[quadrant] == self.args.number_to_quadrant: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.frontend_proxy.MarkQuadrantSectorTested(quadrant) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
else:
logging.info('mark single click down')
# yapf: disable
self.frontend_proxy.MarkCircleDown('left') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.CheckTestPassed()
def OnDoubleClick(self, down):
"""Draws double click event by calling javascript function.
Args:
down: bool
"""
if not down:
logging.info('mark double click up')
# yapf: disable
self.frontend_proxy.MarkCircleTested('right') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.double_click_count < self.args.number_to_click: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.double_click_count += 1
# yapf: disable
self.frontend_proxy.UpdateCircleCountText(self.single_click_count, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.double_click_count)
else:
logging.info('mark double click down')
# yapf: disable
self.frontend_proxy.MarkCircleDown('right') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.CheckTestPassed()
def MarkScrollSectorTested(self, y_ratio):
"""Marks a scroll sector tested.
Gets the scroll sector from y_ratio then calls Javascript to mark the sector
as tested.
"""
y_segment = int(y_ratio * self.y_segments)
if 0 <= y_segment < self.y_segments:
logging.debug('mark %d scroll segment tested', y_segment)
self.scroll_tested[y_segment] = True
# yapf: disable
self.frontend_proxy.MarkScrollSectorTested(y_segment) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def MarkSectorTested(self, x_ratio, y_ratio):
"""Marks a touch sector tested.
Gets the segment from x_ratio and y_ratio then calls Javascript to
mark the sector as tested.
"""
x_segment = int(x_ratio * self.x_segments)
y_segment = int(y_ratio * self.y_segments)
if 0 <= x_segment < self.x_segments and 0 <= y_segment < self.y_segments:
logging.debug('mark x-%d y-%d sector tested', x_segment, y_segment)
self.touch_tested[x_segment][y_segment] = True
# yapf: disable
self.frontend_proxy.MarkSectorTested(x_segment, y_segment) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
def CheckTestPassed(self):
"""Check if all items have been tested."""
# yapf: disable
if (self.single_click_count >= self.args.number_to_click and # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.double_click_count >= self.args.number_to_click and # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
min(self.quadrant_count[1:]) >= self.args.number_to_quadrant and # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
all(self.scroll_tested) and all(all(r) for r in self.touch_tested)):
self.PassTask()
def FailWithMessage(self):
"""Fail the test with untested items."""
# yapf: disable
fail_items = [] # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
for x, row in enumerate(self.touch_tested):
fail_items.extend(
f'touch-x-{int(x)}-y-{int(y)}' for y, tested in enumerate(row)
if not tested)
fail_items.extend(
f'scroll-y-{int(y)}' for y, tested in enumerate(self.scroll_tested)
if not tested)
fail_items.extend(
f'quadrant-{int(i)}' for i, c in enumerate(self.quadrant_count[1:], 1)
# yapf: disable
if c < self.args.number_to_quadrant) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.single_click_count < self.args.number_to_click: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
fail_items.append(f'left click count: {int(self.single_click_count)}')
# yapf: disable
if self.double_click_count < self.args.number_to_click: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
fail_items.append(f'right click count: {int(self.double_click_count)}')
self.FailTask(
f"Touchpad test failed. Malfunction sectors: {', '.join(fail_items)}")
def runTest(self):
"""Start the test if the touchpad is clear.
This function ask operator to press SPACE key and run the test. It will
first check whether the touchpad is clear or not. If not, it will notice
the operator and fail the test. Else, it will clear the event buffer and
start the test.
"""
# yapf: disable
self.ui.WaitKeysOnce(test_ui.SPACE_KEY) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.ui.HideElement('prompt') # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.ui.StartCountdownTimer(self.args.timeout_secs, self.FailWithMessage) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.touchpad_device = evdev_utils.DeviceReopen(self.touchpad_device)
with self.touchpad_device.grab_context():
self.monitor = TouchpadMonitor(self.touchpad_device, self)
if self.monitor.GetState().num_fingers != 0:
logging.error('Ghost finger detected.')
# yapf: disable
self.ui.Alert(_( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
'Ghost finger detected!!\n'
'Please treat this touch panel as a problematic one!!'))
self.FailTask('Ghost finger detected.')
# yapf: disable
self.frontend_proxy = self.ui.InitJSTestObject( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
'TouchpadTest', self.x_segments, self.y_segments,
# yapf: disable
self.args.number_to_click, self.args.number_to_quadrant) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.GetSpec()
self.dispatcher = evdev_utils.InputDeviceDispatcher(
self.touchpad_device,
# yapf: disable
self.event_loop.CatchException(self.monitor.Handler)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
logging.info('start monitor daemon thread')
self.dispatcher.StartDaemon()
self.WaitTaskEnd()