blob: f8f7e4a308e8408a649c81ffd3776f98f96a6995 [file] [log] [blame]
# -*- coding: utf-8 -*-
#
# Copyright (c) 2010 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.
# DESCRIPTION :
#
# Intended for use during manufacturing to validate that all keyboard
# keys function properly. This program will display a keyboard image
# and keys will be highlighted as they are pressed and released.
# After the first key is hit, a countdown will begin. If not all keys
# are used in time, the test will fail.
import cairo
import gobject
import gtk
import logging
import time
import os
import sys
import utils
import re
import subprocess
from gtk import gdk
from autotest_lib.client.bin import test
from autotest_lib.client.common_lib import error
from autotest_lib.client.cros import factory_setup_modules
from cros.factory.test import factory
from cros.factory.test import ui as ful
# The keycodes from the GTK keyboard event have a +8 offset
# from the real one, hence the constant here
_GTK_KB_KEYCODE_OFFSET = 8
# GTK keycode for left Ctrl, Alt and Shift.
_LCTRL = 37
_LALT = 64
_LSHIFT = 50
# GTK state mask for key combination.
_MOD_MASK = gtk.gdk.CONTROL_MASK | gtk.gdk.MOD1_MASK | gtk.gdk.SHIFT_MASK
def GenerateKeycodeBinding(old_bindings):
'''Offsets the bindings keycodes for GTK.'''
key_to_geom = {}
for item in old_bindings.items():
key_to_geom[item[0] + _GTK_KB_KEYCODE_OFFSET] = (
item[1] if isinstance(item[1], list) else [item[1],])
return key_to_geom
class KeyboardTest:
'''Keyboard test.
Args:
kbd_image: Keyboard image file
binding: Keyboard binding file
scale: image scaling factor
accept_combi_key: (bool) True to accept combination key.
'''
def __init__(self, kbd_image, bindings, scale, accept_combi_key):
self._kbd_image = kbd_image
self._bindings = bindings
self._scale = scale
self._pressed_keys = set()
self._deadline = None
self.successful_keys = set()
self._accept_combi_key = accept_combi_key
def calc_missing_string(self):
missing_keys = sorted((gdk.keyval_name(k) or '<0x%x>' % k)for k in
set(self._bindings) - self.successful_keys)
if not missing_keys:
return ''
return ('Missing following keys\n' +
'没有侦测到下列按键,键盘可能故障,请检修: %s' %
', '.join(missing_keys))
def timer_event(self, countdown_label):
if not self._deadline: # Ignore timer with no countdown in progress.
return True
time_remaining = max(0, self._deadline - time.time())
if time_remaining == 0:
factory.log('deadline reached')
gtk.main_quit()
countdown_label.set_text('%d' % time_remaining)
countdown_label.queue_draw()
return True
def expose_event(self, widget, event):
context = widget.window.cairo_create()
# Show keyboard image as the background.
context.scale(self._scale, self._scale)
context.set_source_surface(self._kbd_image, 0, 0)
context.paint()
for key in self.successful_keys:
for coords in self._bindings[key]:
context.rectangle(*coords)
context.set_source_rgba(*ful.RGBA_GREEN_OVERLAY)
context.fill()
for key in self._pressed_keys:
for coords in self._bindings[key]:
context.rectangle(*coords)
context.set_source_rgba(*ful.RGBA_YELLOW_OVERLAY)
context.fill()
return True
def get_combined_keycode(self, event):
'''Gets combined keycode.
If Ctrl/Alt/Shift is also presented in key event, returns
hardware_keycode + offset:
Shift: 256
Ctrl: 1024
Alt: 2048
Args:
event: GTK event.
Returns:
Combined keycode explained above.
'''
return event.hardware_keycode + ((event.state & _MOD_MASK) << 8)
def process_combi_key_press_event(self, event):
'''Processes key-press event with combination key.
Args:
event: GTK event.
Returns:
(accept, keycode):
accept == True if the key is in _bindings.
keycode: combined keycode. Refer get_combined_keycode().
'''
keycode = self.get_combined_keycode(event)
if keycode in self._bindings:
# Known issue: if you press left Ctrl then "New Tab (Ctrl-T)", and
# then release them, the left Ctrl will be discarded unexpectedly.
# However, as it only hapeends when an operator sweeps keyboard
# and presses left Ctrl first then "New Tab", which is rare. And
# if it happends, operator can hit left Ctrl again to fix the
# issue. So I will leave the issue open until we found a nice fix.
if event.state & gtk.gdk.CONTROL_MASK:
self._pressed_keys.discard(_LCTRL)
if event.state & gtk.gdk.MOD1_MASK:
self._pressed_keys.discard(_LALT)
if event.state & gtk.gdk.SHIFT_MASK:
self._pressed_keys.discard(_LSHIFT)
elif event.hardware_keycode in self._bindings:
# For those unbinded key combinations, treat them as only
# base keys are pressed.
keycode = event.hardware_keycode
else:
return False, 0
return True, keycode
def process_simple_key_press_event(self, event):
'''Processes key-press event regardless key combination.
Args:
event: GTK event.
Returns:
(accept, keycode):
accept == True if the key is in _bindings.
keycode: hardware_keycode.
'''
keycode = event.hardware_keycode
return keycode in self._bindings, keycode
def key_press_event(self, widget, event):
accept = False
if self._accept_combi_key:
accept, keycode = self.process_combi_key_press_event(event)
else:
accept, keycode = self.process_simple_key_press_event(event)
if not accept:
factory.log('key (0x%x) ignored because not in bindings'
% event.keyval)
return True
self._pressed_keys.add(keycode)
widget.queue_draw()
# The first keypress starts test countdown.
if self._deadline is None:
self._deadline = int(time.time()) + ful.FAIL_TIMEOUT
return True
def key_release_event(self, widget, event):
hardware_keycode_check = True
if self._accept_combi_key:
keycode = self.get_combined_keycode(event)
# If combined keycode is not pressed before, fall back to check
# hardware_keycode instead.
hardware_keycode_check = keycode not in self._pressed_keys
if hardware_keycode_check:
keycode = event.hardware_keycode
if keycode not in self._pressed_keys:
# Ignore releases for keys not previously accepted as pressed.
return False
self._pressed_keys.remove(keycode)
self.successful_keys.add(keycode)
widget.queue_draw()
if not self.calc_missing_string():
factory.log('completed successfully')
gtk.main_quit()
return True
def register_callbacks(self, window):
window.connect('key-press-event', self.key_press_event)
window.connect('key-release-event', self.key_release_event)
window.add_events(gdk.KEY_PRESS_MASK | gdk.KEY_RELEASE_MASK)
class factory_Keyboard(test.test):
version = 1
preserve_srcdir = True
def get_layout_from_vpd(self):
""" vpd should contain
"initial_locale"="en-US"
"keyboard_layout"="xkb:us::eng"
or similar. """
cmd = 'vpd -l | grep initial_locale | cut -f4 -d\'"\''
layout = utils.system_output(cmd).strip()
if layout != '':
return layout
return None
def run_once(self, layout=None, combi_key=False, config_dir=''):
'''
Args:
layout: use specified layout other than derived from VPD.
combi_key: True to handle key combination.
config_dir: specify directory to read keyboard image and binding
from. If unspeified, read from default directory.
'''
factory.log('%s run_once' % self.__class__)
os.chdir(self.srcdir)
# Autodetect from VPD.
if not layout:
layout = self.get_layout_from_vpd()
# Default to United States.
if not layout:
layout = 'en-US'
factory.log("Using keyboard layout %s" % layout)
layout_filename = os.path.join(config_dir, '%s.png' % layout)
try:
kbd_image = cairo.ImageSurface.create_from_png(layout_filename)
image_size = (kbd_image.get_width(), kbd_image.get_height())
except cairo.Error as e:
raise error.TestNAError('Error while opening %s: %s' %
(layout_filename, e.message))
bindings_filename = os.path.join(config_dir, '%s.bindings' % layout)
try:
with open(bindings_filename, 'r') as file:
bindings = eval(file.read())
bindings = GenerateKeycodeBinding(bindings)
except IOError as e:
raise error.TestNAError('Error while opening %s: %s [Errno %d]' %
(e.filename, e.strerror, e.errno))
scale = ful.calc_scale(*image_size)
test = KeyboardTest(kbd_image, bindings, scale, combi_key)
scaled_image_size = (image_size[0] * scale, image_size[1] * scale)
drawing_area = gtk.DrawingArea()
drawing_area.set_size_request(*scaled_image_size)
drawing_area.connect('expose_event', test.expose_event)
drawing_area.add_events(gdk.EXPOSURE_MASK)
countdown_widget, countdown_label = ful.make_countdown_widget()
gobject.timeout_add(1000, test.timer_event, countdown_label)
test_widget = gtk.VBox()
test_widget.set_spacing(20)
test_widget.pack_start(drawing_area, False, False)
test_widget.pack_start(countdown_widget, False, False)
ful.run_test_widget(self.job, test_widget,
window_registration_callback=test.register_callbacks)
missing = test.calc_missing_string()
if missing:
raise error.TestFail(missing)
factory.log('%s run_once finished' % self.__class__)