blob: 732bbeb06e7ef5b022ad5c5d18b505a0520e7c00 [file] [log] [blame]
# Copyright 2012 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 keyboard functionality.
Description
-----------
This test check basic keyboard functionality by asking operator to press each
keys on keyboard once at a time.
The layout of the keyboard is derived from vpd 'region' value, and can be
overwritten by argument ``layout``.
If ``allow_multi_keys`` is True, the operator can press multiple keys at once
to speed up the testing.
If ``sequential_press`` or ``strict_sequential_press`` is True, the operator
have to press each key in order from top-left to bottom-right. Additionally, if
``strict_sequential_press`` is True, the test would fail if the operator press
the wrong key.
A dict ``repeat_times`` can be specified to indicate number of times each key
have to be pressed before the key is marked as checked.
The test would fail after ``timeout_secs`` seconds.
Test Procedure
--------------
1. The test shows an image of the keyboard, and each key labeled with how many
times it need to be pressed.
2. Operator press each key the number of times needed, and keys on UI would be
marked as such.
3. The test pass when all keys have been pressed for the number of times
needed, or fail after ``timeout_secs`` seconds.
Dependency
----------
Depends on 'evdev' module to monitor key presses.
Examples
--------
To test keyboard functionality, add this into test list::
{
"pytest_name": "keyboard"
}
To test keyboard functionality, allow multiple keys to be pressed at once, and
have a timeout of 10 seconds, add this into test list::
{
"pytest_name": "keyboard",
"args": {
"allow_multi_keys": true,
"timeout_secs": 10
}
}
To test keyboard functionality, ask operator to press keys in order, skip
keycode [4, 5, 6], have keycode 3 be pressed 5 times, and other keys be pressed
2 times to pass, add this into test list::
{
"pytest_name": "keyboard",
"args": {
"sequential_press": true,
"skip_keycodes": [4, 5, 6],
"repeat_times": {
"3": 5,
"default": 2
}
}
}
To test keyboard functionality, ask operator to press keys in order (and fail
the test if wrong key is pressed), and set keyboard layout to ISO, add this
into test list::
{
"pytest_name": "keyboard",
"args": {
"strict_sequential_press": true,
"layout": "ISO"
}
}
"""
from __future__ import print_function
import ast
import os
import re
import factory_common # pylint: disable=unused-import
from cros.factory.external import evdev
from cros.factory.test.l10n import regions
from cros.factory.test import session
from cros.factory.test import test_ui
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 process_utils
_RE_EVTEST_EVENT = re.compile(
r'^Event: time .*?, type .*? \((.*?)\), code (.*?) \(.*?\), value (.*?)$')
_POWER_KEY_CODE = 116
class KeyboardTest(test_ui.TestCaseWithUI):
"""Tests if all the keys on a keyboard are functioning. The test checks for
keydown and keyup events for each key, following certain order if required,
and passes if both events of all keys are received.
Among the args are two related arguments:
- sequential_press: a keycode is simply ignored if the key is not pressed
in order
- strict_sequential_press: the test failed immediately if a key is skipped.
"""
ARGS = [
Arg('allow_multi_keys', bool, 'Allow multiple keys pressed '
'simultaneously. (Less strictly checking '
'with shorter cycle time)', default=False),
Arg('layout', basestring,
'Use specified layout other than derived from VPD. '
'If None, the layout from the VPD is used.',
default=None),
Arg('timeout_secs', int, 'Timeout for the test.', default=30),
Arg('sequential_press', bool, 'Indicate whether keycodes need to be '
'pressed sequentially or not.', default=False),
Arg('strict_sequential_press', bool, 'Indicate whether keycodes need to '
'be pressed strictly sequentially or not.',
default=False),
Arg('board', str,
'If presents, in filename, the board name is appended after layout.',
default=''),
Arg('device_filter', (int, str),
'If present, the input event ID or a substring of the input device '
'name specifying which keyboard to test.',
default=None),
Arg('skip_power_key', bool, 'Skip power button testing', default=False),
Arg('skip_keycodes', list, 'Keycodes to skip', default=[]),
Arg('replacement_keymap', dict, 'Dictionary mapping key codes to '
'replacement key codes', default={}),
Arg('detect_long_press', bool, 'Detect long press event. Usually for '
'detecting bluetooth keyboard disconnection.', default=False),
Arg('repeat_times', dict, 'A dict object {key_code: times} to specify '
'number of presses required for keys specified in key code, e.g. '
'``{"28": 3, "57": 5}``, then ENTER (28) shall be pressed 3 times '
'while SPACE (57) shall be pressed 5 times. If you want all keys to '
'be pressed twice, you can do: ``{"default": 2}``. '
'You can find keycode mappings in /usr/include/linux/input.h',
default=None),
]
def setUp(self):
self.assertTrue(not (self.args.allow_multi_keys and
self.args.sequential_press),
'Sequential press requires one key at a time.')
self.assertTrue(not (self.args.allow_multi_keys and
self.args.strict_sequential_press),
'Strict sequential press requires one key at a time.')
# Get the keyboard input device.
self.keyboard_device = evdev_utils.FindDevice(
self.args.device_filter, evdev_utils.IsKeyboardDevice)
# Initialize keyboard layout and bindings
self.layout = self.GetKeyboardLayout()
if self.args.board:
self.layout += '_%s' % self.args.board
self.bindings = self.ReadBindings(self.layout)
# Apply any replacement keymap
for old_key, new_key in self.args.replacement_keymap.iteritems():
if old_key in self.bindings:
self.bindings[new_key] = self.bindings[old_key]
del self.bindings[old_key]
self.all_keys = set(self.bindings.keys())
self.frontend_proxy = self.ui.InitJSTestObject('KeyboardTest', self.layout,
self.bindings)
keycodes_to_skip = set(self.args.skip_keycodes)
if self.args.skip_power_key:
keycodes_to_skip.add(_POWER_KEY_CODE)
keycodes_to_skip &= self.all_keys
if self.args.sequential_press or self.args.strict_sequential_press:
self.key_order_list = [
key for key in self.ReadKeyOrder(self.layout) if key in self.all_keys
]
else:
self.key_order_list = None
self.ui.HideElement('instruction-sequential')
if self.args.allow_multi_keys:
self.ui.HideElement('instruction-single-key')
self.down_keys = set()
self.ignored_down_keys = set()
self.number_to_press = {}
repeat_times = self.args.repeat_times or {}
default_number_to_press = repeat_times.get('default', 1)
for key in self.all_keys:
if key in keycodes_to_skip:
self.number_to_press[key] = 0
self.MarkKeyState(key, 'skipped')
else:
self.number_to_press[key] = repeat_times.get(
str(key), default_number_to_press)
self.MarkKeyState(key, 'untested')
self.dispatcher = evdev_utils.InputDeviceDispatcher(
self.keyboard_device, self.event_loop.CatchException(self.HandleEvent))
def tearDown(self):
"""Terminates the running process or we'll have trouble stopping the test.
"""
self.dispatcher.close()
self.keyboard_device.ungrab()
def GetKeyboardLayout(self):
"""Uses the given keyboard layout or auto-detect from VPD."""
if self.args.layout:
return self.args.layout
# Use the primary keyboard_layout for testing.
region = process_utils.CheckOutput(['vpd', '-g', 'region']).strip()
return regions.REGIONS[region].keyboard_mechanical_layout
def ReadBindings(self, layout):
"""Reads in key bindings and their associates figure regions."""
bindings_filename = os.path.join(self.ui.GetStaticDirectoryPath(),
layout + '.bindings')
bindings = ast.literal_eval(file_utils.ReadFile(bindings_filename))
for k in bindings:
# Convert single tuple to list of tuples
if not isinstance(bindings[k], list):
bindings[k] = [bindings[k]]
return bindings
def ReadKeyOrder(self, layout):
"""Reads in key order that must be followed when press key."""
key_order_list_filename = os.path.join(self.ui.GetStaticDirectoryPath(),
layout + '.key_order')
return ast.literal_eval(file_utils.ReadFile(key_order_list_filename))
def MarkKeyState(self, keycode, state):
"""Call frontend JavaScript to update UI."""
self.frontend_proxy.MarkKeyState(keycode, state,
self.number_to_press[keycode])
def HandleEvent(self, event):
"""Handler for evdev events."""
if event.type != evdev.ecodes.EV_KEY:
return
if event.value == 1:
self.OnKeydown(event.code)
elif event.value == 0:
self.OnKeyup(event.code)
elif self.args.detect_long_press and event.value == 2:
fail_msg = 'Got events on keycode %d pressed too long.' % event.code
session.console.error(fail_msg)
self.FailTask(fail_msg)
def OnKeydown(self, keycode):
"""Callback when got a keydown event from evdev."""
if keycode not in self.all_keys:
return
if not self.args.allow_multi_keys and self.down_keys:
self.FailTask(
'Got key down event on keycode %d but there are other key pressed: %d'
% (keycode, next(iter(self.down_keys))))
self.down_keys.add(keycode)
if self.key_order_list and keycode in self.key_order_list:
first_untested_key = next(key for key in self.key_order_list
if self.number_to_press[key] > 0)
if keycode != first_untested_key:
if self.args.strict_sequential_press:
self.FailTask('Expect keycode %d but get %d' % (first_untested_key,
keycode))
else:
self.ignored_down_keys.add(keycode)
return
if self.number_to_press[keycode] > 0:
self.MarkKeyState(keycode, 'down')
def OnKeyup(self, keycode):
"""Callback when got a keyup event from evdev."""
if keycode not in self.all_keys:
return
if keycode not in self.down_keys:
self.FailTask(
'Got key up event for keycode %d but did not get key down event' %
keycode)
self.down_keys.remove(keycode)
if keycode in self.ignored_down_keys:
self.ignored_down_keys.remove(keycode)
return
if self.number_to_press[keycode] > 0:
self.number_to_press[keycode] -= 1
if self.number_to_press[keycode] > 0:
self.MarkKeyState(keycode, 'untested')
else:
self.MarkKeyState(keycode, 'tested')
if all(num_left == 0 for num_left in self.number_to_press.itervalues()):
self.PassTask()
def FailTestTimeout(self):
"""Fail the test due to timeout, and log untested keys."""
failed_keys = [
key for key, num_left in self.number_to_press.iteritems() if num_left
]
self.FailTask('Keyboard test timed out. Malfunction keys: %r' % failed_keys)
def runTest(self):
self.keyboard_device.grab()
self.dispatcher.StartDaemon()
self.ui.StartCountdownTimer(self.args.timeout_secs, self.FailTestTimeout)
self.WaitTaskEnd()