blob: 6d03bdc6c8dc569470c6864d359479e0fe2175e4 [file] [log] [blame]
# Copyright (c) 2011 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.
''' A module verifying whether X events satisfy specified criteria '''
import logging
import math
import os
import re
import time
import utils
import trackpad_device
import trackpad_util
from operator import le, ge, eq, lt, gt, ne, and_
class XButton:
''' Manipulation of X Button labels and values '''
# Define some common X button values
Left = 1
Middle = 2
Right = 3
Wheel_Up = 4
Wheel_Down = 5
Wheel_Left = 6
Wheel_Right = 7
Mouse_Wheel_Up = 8
Mouse_Wheel_Down = 9
def __init__(self):
self.display_environ = trackpad_util.Display().get_environ()
self.xinput_list_cmd = ' '.join([self.display_environ, 'xinput --list'])
self.xinput_dev_cmd = ' '.join([self.display_environ,
'xinput --list --long %s'])
self.trackpad_dev_id = self._get_trackpad_dev_id()
def _get_trackpad_dev_id(self):
trackpad_dev_id = None
if os.system('which xinput') == 0:
input_dev_str = utils.system_output(self.xinput_list_cmd)
for dev_str in input_dev_str.splitlines():
res = re.search(r'(t(ouch|rack)pad\s+id=)(\d+)', dev_str, re.I)
if res is not None:
trackpad_dev_id = res.group(3)
break
return trackpad_dev_id
def get_supported_buttons(self):
''' Get supported button labels from xinput
a device returned from 'xinput --list' looks like:
| SynPS/2 Synaptics TouchPad id=11 [slave pointer (2)]
Button labels returned from 'xinput --list <device_id>' looks like:
Button labels: Button Left Button Middle Button Right Button Wheel Up
Button Wheel Down Button Horiz Wheel Left Button Horiz Wheel Right
Button 0 Button 1 Button 2 Button 3 Button 4 Button 5 Button 6
Button 7
'''
DEFAULT_BUTTON_LABELS = (
'Button Left', 'Button Middle', 'Button Right',
'Button Wheel Up', 'Button Wheel Down',
'Button Horiz Wheel Left', 'Button Horiz Wheel Right',
'Button 0', 'Button 1', 'Button 2', 'Button 3',
'Button 4', 'Button 5', 'Button 6', 'Button 7')
if self.trackpad_dev_id is not None:
features = utils.system_output(self.xinput_dev_cmd %
self.trackpad_dev_id)
button_labels_str = [line for line in features.splitlines()
if line.lstrip().startswith('Button labels:')]
strip_str = button_labels_str[0].lstrip().lstrip('Button labels:')
self.button_labels = tuple(['Button ' + b.strip() for b in
strip_str.split('Button')
if len(b) > 0])
else:
logging.warn('Cannot find trackpad device in xinput. '
'Using default Button Labels instead.')
self.button_labels = DEFAULT_BUTTON_LABELS
logging.info('Button Labels (%d) in the trackpad: %s' %
(len(self.button_labels), self.button_labels))
return self.button_labels
def get_value(self, button_label):
''' Mapping an X button label to an X button value
For example, 'Button Left' returns 1
'Button Wheel Up' returns 4
'Button Wheel Down' returns 5
'''
return self.button_labels.index(button_label) + 1
def get_label(self, button_value):
''' Mapping an X button value to an X button label
For example, 1 returns 'Button Left'
4 returns 'Button Wheel Up'
5 returns 'Button Wheel Down'
'''
return self.button_labels[button_value - 1]
def get_index(self, button_label):
''' Mapping an X button label to its index in a button tuple
Generally, a button index is equal to its button value decreased by 1.
For example, 'Button Left' returns 0
'Button Wheel Up' returns 3
'Button Wheel Down' returns 4
'''
return self.button_labels.index(button_label)
def init_button_struct(self, value):
''' Initialize a button dictionary to the given values. '''
return dict(map(lambda b: (self.get_value(b), value),
self.button_labels))
class Xcheck:
''' Check whether X events observe test criteria '''
RESULT_STR = {True : 'Pass', False : 'Fail'}
def __init__(self, dev):
self.dev = dev
self.xevent_data = None
self.xbutton = XButton()
self.button_labels = self.xbutton.get_supported_buttons()
# Create a dictionary to look up button label
# e.g., {1: 'Button Left', ...}
self.button_dict = dict(map(lambda b:
(self.xbutton.get_value(b), b),
self.button_labels))
self._get_boot_time()
def _get_boot_time(self):
''' Get the system boot up time
Boot time can be used to convert the elapsed time since booting up
to that since Epoch.
'''
stat_cmd = 'cat /proc/stat'
stat = utils.system_output(stat_cmd)
boot_time_tuple = tuple(int(line.split()[1])
for line in stat.splitlines()
if line.startswith('btime'))
if len(boot_time_tuple) == 0:
raise error.TestError('Fail to extract boot time by "%s"' %
stat_cmd)
self.boot_time = boot_time_tuple[0]
def _calc_distance(self, x0, y0, x1, y1):
''' A simple Manhattan distance '''
delta_x = abs(x1 - x0)
delta_y = abs(y1 - y0)
dist = round(math.sqrt(delta_x * delta_x + delta_y * delta_y))
return [dist, [delta_x, delta_y]]
def _parse_button_and_motion(self):
''' Parse button events and motion events
The variable seg_move accumulates the motions of the contiguous events
segmented by some boundary events such as Button events and other
NOP events.
A NOP (no operation) event is a fake X event which is
used to indicate the occurrence of some related device events.
'''
# Define some functions for seg_move
def _reset_seg_move():
return [0, [0, 0]]
def _append_motion_event(seg_move):
self.event_seq.append(('Motion', (seg_move[0],
('Motion_x', seg_move[1][0]),
('Motion_y', seg_move[1][1]))))
def _add_seg_move(seg_move, move):
list_add = lambda list1, list2: map(sum, zip(list1, list2))
return [seg_move[0] + move[0], list_add(seg_move[1], move[1])]
self.count_buttons = self.xbutton.init_button_struct(0)
self.count_buttons_press = self.xbutton.init_button_struct(0)
self.count_buttons_release = self.xbutton.init_button_struct(0)
self.button_states = self.xbutton.init_button_struct('ButtonRelease')
pre_xy = [None, None]
self.event_seq = []
seg_move = _reset_seg_move()
self.sum_move = 0
indent1 = ' ' * 8
indent2 = ' ' * 14
log_msg = {True: indent2 + '{0} (button %d)',
False: indent2 + '{0} mis-matched (button %d)'}
precede_state = {'ButtonPress': 'ButtonRelease',
'ButtonRelease': 'ButtonPress',}
logging.info(indent1 + 'X button events detected:')
for line in self.xevent_data:
event_name = line[0]
if event_name != 'NOP':
event_dict = line[1]
if event_dict.has_key('coord'):
event_coord = list(eval(event_dict['coord']))
if event_dict.has_key('button'):
event_button = eval(event_dict['button'])
if event_name == 'EnterNotify':
if pre_xy == [None, None]:
pre_xy = event_coord
elif event_name == 'MotionNotify':
if pre_xy == [None, None]:
pre_xy = event_coord
else:
cur_xy = event_coord
move = self._calc_distance(*(pre_xy + cur_xy))
pre_xy = cur_xy
seg_move = _add_seg_move(seg_move, move)
self.sum_move += move[0]
elif event_name.startswith('Button'):
_append_motion_event(seg_move)
seg_move = _reset_seg_move()
button_label = self.xbutton.get_label(event_button)
self.event_seq.append((event_name, button_label))
prev_button_state = self.button_states[event_button]
self.button_states[event_button] = event_name
# A ButtonRelease should precede ButtonPress
# A ButtonPress should precede ButtonRelease
precede_flag = prev_button_state == precede_state[event_name]
if event_name == 'ButtonPress':
self.count_buttons_press[event_button] += 1
elif event_name == 'ButtonRelease':
self.count_buttons_release[event_button] += 1
self.count_buttons[event_button] += precede_flag
logging.info(log_msg[precede_flag].format(event_name) %
event_button)
elif event_name == 'NOP':
_append_motion_event(seg_move)
seg_move = _reset_seg_move()
self.event_seq.append(('NOP', line[1]))
_append_motion_event(seg_move)
# Convert dictionary to tuple
self.button_states = tuple(self.button_states.values())
self.count_buttons= tuple(self.count_buttons.values())
def _set_flags(self):
''' Set all flags to True before invoking check function '''
self.motion_flag = True
self.button_flag = True
self.seq_flag = True
self.delay_flag = True
def _get_result(self):
''' Get the final result from various check flags '''
flags = (self.motion_flag, self.button_flag, self.seq_flag,
self.delay_flag)
self.result = flags[0] if len(flags) == 1 else reduce(and_, flags)
logging.info(' --> Result: %s' % Xcheck.RESULT_STR[self.result])
def _compare(self, ops):
''' Compare function generator
Generate a function to compare two sequences using the specified
operators.
'''
return lambda seq1, seq2: reduce(and_, map(lambda op, s1, s2:
op(s1, s2), ops, seq1, seq2))
def _button_criteria(self, button):
''' Convert the key of the button tuple from label to index '''
crit_button_count = [0,] * len(self.button_labels)
if button is not None:
button_label, button_value = button
button_index = self.xbutton.get_index(button_label)
crit_button_count[button_index] = button_value
return tuple(crit_button_count)
def _insert_nop(self, nop_str):
''' Insert a 'NOP' fake event into the xevent_data
NOP is not an X event. It is inserted to indicate the occurrence of
related device events.
'''
lifted_time = self.dev.get_2nd_finger_lifted_time()
if lifted_time is None:
logging.warn('Cannot get time for %s.' % nop_str)
else:
for index, line in enumerate(self.xevent_data):
event_name = line[0]
event_dict = line[1]
if event_name == 'MotionNotify':
event_time = float(event_dict['time'])
if event_time > lifted_time:
self.xevent_data.insert(index, ('NOP', nop_str))
break
def _get_direction(self):
directions = ['left', 'right', 'up', 'down']
file_name = self.gesture_file_name.split('-')[self.func_name_pos]
for d in directions:
if d in file_name:
return d
return None
''' _verify_xxx()
Generic verification methods for various functionalities / areas
'''
def _verify_motion(self, compare, crit_max_movement):
''' Verify if the observed motions satisfy the criteria '''
self.motion_flag = compare(self.sum_move, crit_max_movement)
logging.info(' Verify motion: (%s)' %
Xcheck.RESULT_STR[self.motion_flag])
logging.info(' Total movement = %d' % self.sum_move)
def _verify_button(self, compare, crit_button_count):
''' Verify if the observed buttons satisfy the criteria '''
count_flag = compare(self.count_buttons, crit_button_count)
state_flags = map(lambda s: s == 'ButtonRelease', self.button_states)
state_flag = reduce(and_, state_flags)
self.button_flag = state_flag and count_flag
logging.info(' Verify button: (%s)' %
Xcheck.RESULT_STR[self.button_flag])
button_msg_details = ' %s %d times'
count_flag = False
for idx, b in enumerate(self.button_labels):
if self.count_buttons[idx] > 0:
logging.info(button_msg_details % (b, self.count_buttons[idx]))
count_flag = True
if not count_flag:
logging.info(' No Button events detected.')
def _verify_select_delay(self, crit_delay):
''' Verify if the delay time satisfy the criteria
The delay time is defined to be the time interval between the time
the 2nd finger touching the trackpad and the time of the corresponding
X Motion event.
'''
# Extract scroll direction, i.e., 'up' or 'down', from the file name
# We do not support scrolling 'left' or 'right' at this time.
pos = self.func_name_pos
direction = self._get_direction()
# Derive the device event playback time when the 2nd finger touches
dev_event_time = self.dev.get_2nd_finger_touch_time(direction)
# Derive the motion event time of the 2nd finger
found_ButtonPress = False
event_time = None
for line in self.xevent_data:
event_name = line[0]
event_dict = line[1]
if not found_ButtonPress and event_name == 'ButtonPress':
found_ButtonPress = True
elif found_ButtonPress and event_name == 'MotionNotify':
event_time = float(event_dict['time'])
break
if dev_event_time is None or event_time is None:
delay = 'Not found'
self.delay_flag = False
else:
delay = (event_time - dev_event_time) * 0.001
self.delay_flag = delay < crit_delay
logging.info(' Verify delay: (%s)' %
Xcheck.RESULT_STR[self.delay_flag])
logging.info(' Delay time = %s (criteria = %f)' %
(str(delay), crit_delay))
def _verify_select_sequence(self, crit_sequence):
''' Verify event sequence against criteria sequence
For example, the following event_sequence matches crit_sequence.
event_sequence: [('ButtonPress', 'Button Left'),
('Motion', 68),
('ButtonRelease', 'Button Left')]
crit_sequence: (('ButtonPress', 'Button Left'),
('Motion', '>=', 20),
('ButtonRelease', 'Button Left'))
'''
op_dict = {'>=': ge, '<=': le, '==': eq, '=': eq, '>': gt, '<': lt,
'!=': ne, '~=': ne, 'not': ne, 'is not': ne}
op_le = op_dict['<=']
axis_dict = {'left': 'x', 'right': 'x', 'up': 'y', 'down': 'y',
None: ''}
self.seq_flag = True
crit_move_ratio = self.criteria.get('move_ratio', 0)
index = -1
for e in self.event_seq:
e_type, e_value = e
fail_msg = None
index += 1
if index >= len(crit_sequence):
fail_msg = 'Event (%s, %s) is extra compared to the criteria.'
fail_para = (e_type, str(e_value))
break
crit_e = crit_sequence[index]
crit_e_type = crit_e[0]
if e_type.startswith('Motion'):
motion_val = e_value[0]
motion_x_val = e_value[1][1]
motion_y_val = e_value[2][1]
if crit_e_type.startswith('Motion'):
crit_e_op = crit_e[1]
crit_e_val = crit_e[2]
op = op_dict[crit_e_op]
if crit_e_type == 'Motion':
crit_check = op(motion_val, crit_e_val)
if not crit_check:
fail_msg = '%s %s does not satisfy %s. '
fail_para = (crit_e_type, str(e_value), str(crit_e))
break
elif crit_e_type == 'Motion_x_or_y':
axis = axis_dict[self._get_direction()]
motion_axis_dict = {'x': {'this': motion_x_val,
'other': motion_y_val},
'y': {'this': motion_y_val,
'other': motion_x_val}}
motion_axis_val = motion_axis_dict[axis]['this']
motion_other_val = motion_axis_dict[axis]['other']
check_this_axis = op(motion_axis_val, crit_e_val)
# If the criteria requests that one axis move more
# than a threshold value, the other axis should move
# much less. This is to confirm that the movement is
# in the right direction.
other_axis_cond = crit_e_op == '>=' or crit_e_op == '>'
bound_other_axis = motion_axis_val * crit_move_ratio
check_other_axis = (not other_axis_cond or
op_le(motion_other_val, bound_other_axis))
crit_check = check_this_axis and check_other_axis
if not crit_check:
fail_msg = '%s %s does not satisfy %s. ' \
'Check motion for this axis = %s. ' \
'Check motion for the other axis = %s'
fail_para = (crit_e_type, str(e_value), str(crit_e),
check_this_axis, check_other_axis)
break
else:
fail_msg = '%s does not conform to the format.'
fail_para = crit_e_type
break
else:
# No motion allowed
if e_value > 0:
fail_msg = 'Motion %d is not allowed.'
fail_para = e_value
break
elif e_type == crit_e_type == 'ButtonPress' or \
e_type == crit_e_type == 'ButtonRelease':
# Check if the button label matches criteria
if e_value != crit_e[1]:
fail_msg = 'Button %s does not match %s.'
fail_para = (e_value, crit_e[1])
break
elif e_type == crit_e_type == 'NOP':
pass
else:
fail_msg = 'Event %s does not match criteria %s.'
fail_para = (e_type, crit_e_type)
break
# Check if the criteria has been fully matched
if fail_msg is None and index < len(crit_sequence) - 1:
fail_msg = 'Some events are missing compared to the criteria: %s.'
fail_para = str(crit_sequence)
if fail_msg is not None:
self.seq_flag = False
logging.info(' Verify select sequence: (%s)' %
Xcheck.RESULT_STR[self.seq_flag])
logging.info(' Detected event sequence')
for e in self.event_seq:
logging.info(' ' + str(e))
if not self.seq_flag:
logging.info(' ' + fail_msg % fail_para)
''' _verify_area_xxx()
The following methods are generally used for the group of functionalities
in the same area.
'''
def _verify_all_criteria(self):
''' A general verification method for all criteria '''
self._parse_button_and_motion()
if self.criteria.has_key('max_movement'):
crit_max_movement = self.criteria['max_movement']
self._verify_motion(le, crit_max_movement)
if self.criteria.has_key('button'):
crit_button_count = self._button_criteria(self.criteria['button'])
self._verify_button(eq, crit_button_count)
if self.criteria.has_key('delay'):
crit_delay = self.criteria['delay']
self._verify_select_delay(crit_delay)
if self.criteria.has_key('sequence'):
crit_sequence = self.criteria['sequence']
self._verify_select_sequence(crit_sequence)
self._get_result()
''' _check_xxx()
For each functionality xxx, there is a corresponding _check_xxx() method
which is executed by run() automatically.
'''
''' area 0: 1 finger point & click '''
def _check_any_finger_click(self):
''' Any finger, including thumb, can click '''
self._verify_all_criteria()
def _check_any_angle_click(self):
''' Finger can be oriented at any angle relative to trackpad '''
self._verify_all_criteria()
def _check_any_location_click(self):
''' Click can occur at any location on trackpad (no hot zones) '''
self._verify_all_criteria()
def _check_no_min_width_click(self):
''' First finger should not have any minimum width defined for it
(i.e., point and/or click with finger tip. E.g., click with fingernail)
'''
self._verify_all_criteria()
def _check_no_cursor_wobble(self):
''' No cursor wobble, creep, or jumping (or jump back) during clicking
'''
self._verify_all_criteria()
def _check_drum_roll(self):
''' Drum roll: One finger (including thumb) touches trackpad followed
shortly (<500ms) by a second finger touching trackpad should not result
in cursor jumping
'''
self._verify_all_criteria()
''' area 1: Click & select/drag '''
def _check_single_finger_select(self):
''' (Single finger) Finger physical click or tap & a half, then finger -
remaining in contact with trackpad - drags along surface of trackpad
'''
self._verify_all_criteria()
def _check_single_finger_lifted(self):
''' (Single finger) If finger leaves trackpad for only 800ms-1s
(Synaptics UX should know value), select/drag should continue
'''
self._verify_all_criteria()
def _check_two_fingers_select(self):
''' (Two fingers) 1st finger click or tap & a half, 2nd finger's
movement selects/drags
'''
self._verify_all_criteria()
def _check_two_fingers_lifted(self):
''' (Two fingers) Continues to drag when second finger is lifted then
placed again
'''
self._insert_nop('2nd Finger Lifted')
self._verify_all_criteria()
def _check_two_fingers_no_delay(self):
''' (Two fingers) Drag should be immediate (no delay between movement
of finger and movement of selection/drag)
'''
self._verify_all_criteria()
''' area 2: 2 finger alternate/right click '''
def _check_x_seconds_interval(self):
''' 1st use case: 1st finger touches trackpad, X seconds pass, 2nd
finger touches trackpad, physical click = right click, where X is any
number of seconds
'''
self._verify_all_criteria()
def _check_roll_case(self):
''' 2nd use case ("roll case"): If first finger generates a click and
second finger "rolls on" within 300ms of first finger click (again,
rely on Synaptics UX to know value), an alternate/right click results
'''
self._verify_all_criteria()
def _check_one_finger_tracking(self):
''' 1 finger tracking, never leaves trackpad, 2f arrives and there is
a click. right click results.
'''
self._verify_all_criteria()
''' area 3: 2 finger scroll '''
def _check_two_finger_scroll(self):
''' Vertical scroll, reflecting movement of finger(s)
Criteria:
1. sum of movement is less than crit_max_movement
2. if subname in the file name is up:
A number of button 4 (Wheel Up) events should be observed
without other button events.
elif subname in the file name is down:
A number of button 5 (Wheel Down) events should be observed
without other button events.
'''
# Extract scroll direction, i.e., 'up' or 'down', from the file name
pos = self.func_name_pos
direction = self._get_direction()
# Get criteria for max movement and wheel up/down
crit_max_movement = self.criteria['max_movement']
ops = [eq,] * len(self.button_labels)
if direction == 'up':
button_label = self.xbutton.get_label(self.xbutton.Wheel_Up)
ops[self.xbutton.get_index(button_label)] = ge
crit_up = self.criteria['button'][0]
crit_button_count = self._button_criteria(crit_up)
elif direction == 'down':
button_label = self.xbutton.get_label(self.xbutton.Wheel_Down)
ops[self.xbutton.get_index(button_label)] = ge
crit_down = self.criteria['button'][1]
crit_button_count = self._button_criteria(crit_down)
else:
msg = ' scroll direction in the file name is not correct: (%s)'
logging.info(msg % direction)
self.result = False
return
self._parse_button_and_motion()
self._verify_motion(le, crit_max_movement)
self._verify_button(self._compare(tuple(ops)), crit_button_count)
self._get_result()
''' area 4: Palm/thumb detection '''
''' parse and run below '''
def _extract_prop(self, event_name, line, prop_key):
''' Extract property from X events '''
if line is None:
logging.warn(' X event format may not be correct.')
return None
# Declare the format to extract information from X event structures
format_dict = {
'Motion_coord' : '{6}',
'Motion_time' : '{5}',
'Motion_tv' : '{7}',
'Button_coord' : '{6}',
'Button_button' : '{3}',
'Button_time' : '{5}',
'Button_tv' : '{7}',
}
event_format_str = format_dict[event_name]
try:
prop_val = event_format_str.format(*line.strip().split()).strip(',')
except IndexError, err:
logging.warn(' %s in X event data.' % str(err))
return None
return (prop_key, prop_val)
def _parse(self, xevent_str):
''' Parse all X events
The event information of a single X event may span across multiple
lines. This function extracts the important event information of
an event into a dictionary so that it is easier to process in
subsequent stages.
For example:
A MotionNotify event looks like:
MotionNotify event, serial 25, synthetic NO, window 0xa00001,
root 0xab, subw 0x0, time 925196, (750,395), root:(750,395),
state 0x0, is_hint 0, same_screen YES
A ButtonPress event looks like:
ButtonPress event, serial 25, synthetic NO, window 0xa00001,
root 0xab, subw 0x0, time 1098904, (770,422), root:(770,422),
state 0x0, button 1, same_screen YES
The property extracted for the MotionNotify event looks like:
['MotionNotify', {'coord': (150,200), 'time': ...]
The property extracted for the ButtonPress event looks like:
['ButtonPress', {'coord': (150,200), 'button': 5}, 'time': ...]
'''
if len(xevent_str) == 0:
logging.warn(' No X events were captured.')
return False
xevent_iter = iter(xevent_str)
self.xevent_data = []
while True:
line = next(xevent_iter, None)
if line is None:
break
line_words = line.split()
if len(line_words) > 0:
event_name = line_words[0]
else:
continue
# Extract event information for important event types
if event_name == 'MotionNotify' or event_name == 'EnterNotify':
line1 = next(xevent_iter, None)
line2 = next(xevent_iter, None)
prop_coord = self._extract_prop('Motion_coord', line1, 'coord')
prop_time = self._extract_prop('Motion_time', line1, 'time')
if prop_coord is not None and prop_time is not None:
event_dict = dict([prop_coord, prop_time])
self.xevent_data.append([event_name, event_dict])
elif line.startswith('Button'):
line1 = next(xevent_iter, None)
line2 = next(xevent_iter, None)
prop_coord = self._extract_prop('Button_coord', line1, 'coord')
prop_time = self._extract_prop('Button_time', line1, 'time')
prop_button = self._extract_prop('Button_button', line2,
'button')
if (prop_coord is not None and prop_button is not None
and prop_time is not None):
event_dict = dict([prop_coord, prop_button, prop_time])
self.xevent_data.append([event_name, event_dict])
return True
def run(self, tp_func, tp_data, xevent_str):
''' Parse the x events and invoke a proper check function
Invoke the corresponding check function based on its functionality name.
For example, tp_func.name == 'no_cursor_wobble' will result in the
invocation of self._check_no_cursor_wobble()
'''
parse_result = self._parse(xevent_str)
self.gesture_file_name = tp_data.file_basename
self.func_name_pos = 0 if tp_data.prefix is None else 1
self.criteria = tp_func.criteria
if parse_result:
check_function = eval('self._check_' + tp_func.name)
self._set_flags()
check_function()
return self.result
else:
return False