blob: ddb9022cab737836b8b07d9c6ca12c2fe2eeff96 [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 time
import utils
import Xevent
from operator import le, ge, eq, lt, gt, ne, and_
from trackpad_util import read_trackpad_test_conf
class Xcheck:
''' Check whether X events observe test criteria '''
RESULT_STR = {True : 'Pass', False : 'Fail'}
def __init__(self, dev, conf_path):
self.dev = dev
self.conf_path = conf_path
self.xbutton = Xevent.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()
self.xevent = Xevent.XEvent(self.xbutton)
self.op_dict = {'>=': ge, '<=': le, '==': eq, '=': eq, '>': gt,
'<': lt, '!=': ne, '~=': ne, 'not': ne, 'is not': ne}
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 _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 _motion_criteria(self, motion_crit):
''' Extract motion operator and value '''
if motion_crit is None:
return (None, None)
motion_op = self.op_dict[motion_crit[1]]
motion_value = motion_crit[2]
return (motion_op, motion_value)
def _button_criteria(self, button_labels, button_crit):
''' Create a list of button criteria
This supports a list of button_labels in a more flexible way.
For example,
button_labels: ('Button Horiz Wheel Left', 'Button Horiz Wheel Right')
button_crit: ('Button Wheel', '>=', 10)
And assume that button_labels = (
'Button Left', 'Button Middle', 'Button Right',
'Button Wheel Up', 'Button Wheel Down',
'Button Horiz Wheel Left', 'Button Horiz Wheel Right', ...)
The result of this method is
ops = (eq, eq, eq, eq, eq, ge, ge, ...)
values = ( 0, 0, 0, 0, 0, 10, 10, ...)
'''
len_button_labels = len(self.button_labels)
ops = [eq] * len_button_labels
values = [0] * len_button_labels
if button_crit is not None:
for button_label in button_labels:
_, button_op, button_value = button_crit
button_index = self.xbutton.get_index(button_label)
ops[button_index] = self.op_dict[button_op]
values[button_index] = button_value
return (ops, values)
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.
'''
if nop_str == 'Two Finger Touch':
dev_event_time = self.dev.get_two_finger_touch_time_list()
else:
dev_event_time = self.dev.get_finger_time(nop_str)
if dev_event_time is None:
logging.warn('Cannot get time for %s.' % nop_str)
return
# Insert NOP event into xevent data
begin_index = 0
for devent_time in dev_event_time:
for index, line in enumerate(self.xevent.xevent_data[begin_index:]):
xevent_name = line[0]
xevent_dict = line[1]
if xevent_name == 'NOP':
continue
xevent_time = float(xevent_dict.get('time', 0))
if xevent_time > devent_time:
insert_index = begin_index + index
nop_data = ('NOP', nop_str, devent_time)
self.xevent.xevent_data.insert(insert_index, nop_data)
begin_index = insert_index + 1
break
def _insert_nop_per_criteria(self, criteria_method):
''' Insert NOP based on criteria '''
for c in criteria_method:
if self.criteria.has_key(c):
if c == 'wheel_speed':
# There are a couple of times of two-finger scrolling.
# Insert NOP between them in self.xevent_seq
self._insert_nop('Two Finger Touch')
elif c == 'sequence':
crit_sequence = self.criteria[c]
# Insert NOP in self.xevent_seq if NOP is specified
# in sequence criteria.
# Example of crit_sequence below:
# ('NOP', '1st Finger Lifted')
# ('NOP', '2nd Finger Lifted')
for s in crit_sequence:
if s[0] == 'NOP':
self._insert_nop(s[1])
def _extract_func_name(self):
''' Extract functionality name from the gesture file name '''
return self.gesture_file_name.split('-')[self.func_name_pos]
def _get_direction(self):
''' Get a specific direction from functionality name '''
directions = ['up', 'down', 'left', 'right']
file_name = self._extract_func_name()
for d in directions:
if d in file_name:
return d
return None
def _get_general_direction(self):
''' Get a general direction from functionality name '''
direction = self._get_direction()
if direction is not None:
return direction
directions = ['vert', 'horiz', 'alldir']
file_name = self._extract_func_name()
for d in directions:
if d in file_name:
return d
return None
def _get_more_directions(self):
''' Get direction(s) from functionality name '''
dir_dict = {'vert': ('up', 'down'),
'horiz': ('left', 'right'),
'alldir': ('up', 'down', 'left', 'right')}
direction = self._get_direction()
if direction is not None:
return (direction,)
file_name = self._extract_func_name()
for d in dir_dict.keys():
if d in file_name:
return dir_dict[d]
def _get_button_wheel_label_per_direction(self):
''' Use the specific direction in gesture file name to get
correct button label.
Extract the scroll direction ('up', 'down', 'left', or 'right')
from the gesture file name. Use the scroll direction to derive
the correct button label.
E.g., for direction = 'up':
'Button Wheel' in config file is replaced by 'Button Wheel Up'
'''
direction = self._get_direction()
button_label = self.xbutton.wheel_label_dict[direction]
return button_label
def _get_button_wheel_labels_from_directions(self):
''' Use the direction(s) in gesture file name to get the corresponding
button wheel label(s)
'''
directions = self._get_more_directions()
button_labels = [self.xbutton.wheel_label_dict[d] for d in directions]
return button_labels
''' _verify_xxx()
Generic verification methods for various functionalities / areas
'''
def _verify_motion(self, crit_tot_movement):
''' Verify if the observed motions satisfy the criteria '''
op, val = self._motion_criteria(crit_tot_movement)
self.motion_flag = op(self.xevent.sum_move, val)
logging.info(' Verify motion: (%s)' %
Xcheck.RESULT_STR[self.motion_flag])
logging.info(' Total movement = %d' % self.xevent.sum_move)
def _verify_button(self, crit_button):
''' Verify if the observed buttons satisfy the criteria
Example of computing count_flag:
compare = ( eq, ge, eq, ...)
xevent.count_buttons = ( 0, 3, 0, ...)
crit_count = ( 0, 1, 0, ...)
result list = [True, True, True, ...]
count_flag = True (which is the AND of the result_list)
'''
if crit_button is None:
crit_button_labels = None
elif crit_button[0] == 'Button Wheel':
crit_button_labels = self._get_button_wheel_labels_from_directions()
else:
crit_button_labels = (crit_button[0],)
op, crit_count = self._button_criteria(crit_button_labels, crit_button)
compare = self._compare(tuple(op))
# Compare if all parsed button counts meet the criteria
count_flag = compare(self.xevent.count_buttons, crit_count)
# An X Button must end with a ButtonRelease
state_flags = map(lambda s: s == 'ButtonRelease',
self.xevent.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.xevent.count_buttons[idx] > 0:
logging.info(button_msg_details %
(b, self.xevent.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.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_wheel_speed(self, crit_wheel_speed):
''' Verify if the observed button wheel speed satisfies the criteria
xevent_seq for two-finger scrolling looks like:
('Motion', (0, ('Motion_x', 0), ('Motion_y', 0)))
('NOP', 'Two Finger Touch')
('Button Wheel Down', 62)
('Button Horiz Wheel Right', 1)
('Button Wheel Down', 65)
('Button Horiz Wheel Right', 1)
('Button Wheel Down', 32)
('Button Horiz Wheel Right', 1)
('Button Wheel Down', 35)
('Button Horiz Wheel Right', 2)
('Button Wheel Down', 15)
('NOP', 'Two Finger Touch')
('Button Wheel Down', 185)
('NOP', 'Two Finger Touch')
('Motion', (22.0, ('Motion_x', 11), ('Motion_y', 19)))
('Button Wheel Down', 68)
('Motion', (0, ('Motion_x', 0), ('Motion_y', 0)))
Need to accumulate the button counts partitioned by NOP (two finger
touching event). The Button Wheel event count derived in this way
should satisfy the wheel speed criteria.
'''
# Aggregate button counts partitioned by 'NOP'
button_count_list = []
init_time = [None, None]
rounds = 0
for line in self.xevent.xevent_seq:
event_name, event_count, event_time = line
if event_name == 'NOP':
button_count = self.xbutton.init_button_struct_with_time(0,
init_time)
button_count_list.append(button_count)
rounds += 1
elif rounds > 0:
if event_name.startswith('Button'):
button_value = self.xbutton.get_value(event_name)
count = button_count_list[rounds-1][button_value][0]
if count == 0:
button_count_list[rounds-1][button_value][1] = \
event_time
else:
button_count_list[rounds-1][button_value][1][1] = \
event_time[1]
# TODO(josephsih): It is hard to follow this code; It would
# be better if this used an associative array ['event']
# ['count'], dictionary or class instead of just [0] and
# [1].
button_count_list[rounds-1][button_value][0] += event_count
speed = [0] * rounds
# Calculate button wheel speed
for i, button_count in enumerate(button_count_list):
speed[i] = self.xbutton.init_button_struct(0)
for k, v in button_count.iteritems():
if v[0] > 0:
time_list = button_count[k][1]
time_interval = (time_list[1] - time_list[0]) / 1000.0
speed[i][k] = ((button_count[k][0] / time_interval)
if time_interval != 0 else 1)
# Verify if the target button satisfies wheel speed criteria
button_label = self._get_button_wheel_label_per_direction()
self.wheel_speed_flag = True
if rounds <= 1:
self.wheel_speed_flag = False
else:
target_button_value = self.xbutton.get_value(button_label)
comp_op = self.op_dict[crit_wheel_speed[1]]
multiplier = crit_wheel_speed[2]
for r in range(1, rounds):
if not comp_op(speed[r][target_button_value],
speed[r-1][target_button_value] * multiplier):
self.wheel_speed_flag = False
break
prefix_space0 = ' ' * 8
prefix_space1 = ' ' * 10
prefix_space2 = ' ' * 14
msg_title = prefix_space0 + 'Verify wheel speed: (%s)'
msg_round = prefix_space1 + 'Round %d of two-finger scroll:'
msg_speed = '{0:<25s}: {1:7.2f} times/sec ({2:4} times in [{3} {4}])'
msg_details = prefix_space2 + msg_speed
logging.info(msg_title % Xcheck.RESULT_STR[self.wheel_speed_flag])
for i, button_count in enumerate(button_count_list):
logging.info(msg_round % i)
for k, v in button_count.iteritems():
if v[0] > 0:
logging.info(msg_details.format(self.xbutton.get_label(k),
speed[i][k], v[0], str(v[1][0]), str(v[1][1])))
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'))
'''
def _get_criteria(index, crit_sequence):
if index >= len(crit_sequence):
crit_e = ''
crit_e_type = ''
else:
crit_e = crit_sequence[index]
crit_e_type = crit_e[0]
# Add Button Wheel direction
# Support only 'up', 'down', 'left', 'right' at this time in
# sequence criteria.
# May support 'vert', 'horiz', and 'alldir' later if needed.
if crit_e_type == 'Button Wheel':
crit_e_type = self._get_button_wheel_label_per_direction()
return (crit_e, crit_e_type)
op_le = self.op_dict['<=']
axis_dict = {'left': 'x', 'right': 'x', 'up': 'y', 'down': 'y',
'vert': 'y', 'horiz': 'x', 'alldir': '', None: ''}
self.seq_flag = True
crit_move_ratio = self.criteria.get('move_ratio', 0)
if '*' in crit_sequence:
work_crit_sequence = list(crit_sequence)
work_crit_sequence.reverse()
work_xevent_seq = list(self.xevent.xevent_seq)
work_xevent_seq.reverse()
else:
work_crit_sequence = crit_sequence
work_xevent_seq = self.xevent.xevent_seq
# Read some default parameters from config file
max_motion_mixed = read_trackpad_test_conf('max_motion_mixed',
self.conf_path)
max_button_wheel_mixed = read_trackpad_test_conf(
'max_button_wheel_mixed', self.conf_path)
index = -1
crit_e_type = None
keep_prev_crit = False
for e in work_xevent_seq:
e_type = e[0]
e_value = e[1]
fail_msg = None
if crit_e_type != '*':
if keep_prev_crit:
keep_prev_crit = False
else:
index += 1
(crit_e, crit_e_type) = _get_criteria(index, work_crit_sequence)
# When there is no detected motion, skip the motion criteria if any
# and get next criteria in the sequence.
if (not e_type.startswith('Motion') and
crit_e_type.startswith('Motion')):
index += 1
(crit_e, crit_e_type) = _get_criteria(index, work_crit_sequence)
# Pass this event if the criteria is a wildcard
if crit_e_type == '*':
pass
# Handle the situation that e_type not equal to crit_e_type
elif not crit_e_type.startswith(e_type):
keep_prev_crit = True
if e_type.startswith('Motion'):
motion_val = e_value[0]
if motion_val > max_motion_mixed:
fail_msg = '%s (%d) is not allowed.'
fail_para = (e_type, motion_val)
break
elif e_type.startswith('Button '):
if e_value > max_button_wheel_mixed:
fail_msg = '%s (%d) is not allowed'
fail_para = (e_type, e_value)
break
else:
fail_msg = '%s (%s) is not allowed'
fail_para = (e_type, str(e_value))
break
# Handle Motion event
elif 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 = self.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_general_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
# Handle button events for Button Left/Middle/Right
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
# Handle 'Button Wheel' and 'Button Horiz Wheel' scroll events
elif e_type.startswith('Button ') and e_type == crit_e_type:
op_str = crit_e[1]
comp_op = self.op_dict[op_str]
crit_button_count = crit_e[2]
if not comp_op(e_value, crit_button_count):
fail_msg = '%s count %d does not satisfy "%s" %d.'
fail_para = (e_type, e_value, op_str, crit_button_count)
break
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(work_crit_sequence) - 1:
# Pass if the rest of criteria are trivial ones such as
# 'Motion <= ...'
# 'Button Wheel <= ...'
index += 1
trivial_op_list = ['<', '<=']
for i in range(index, len(work_crit_sequence)):
(crit_e, crit_e_type) = _get_criteria(index, work_crit_sequence)
if (crit_e_type.startswith('Motion') or
crit_e_type.startswith('Button ')):
crit_e_op = crit_e[1]
if crit_e_op not in trivial_op_list:
fail_msg = ('Some events are missing compared to the '
'criteria: %s.')
fail_para = str(crit_sequence)
break
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.xevent.xevent_seq:
logging.info(' ' + str(e))
if not self.seq_flag:
logging.info(' ' + fail_msg % fail_para)
def _verify_all_criteria(self):
''' A general verification method for all criteria
This is the core method invoked for every functionality. What to check
is based on the criteria specified for the functionality in the
config file.
'''
# A dictionary mapping criterion to its verification method
criteria_method = {'total_movement': self._verify_motion,
'button': self._verify_button,
'delay': self._verify_select_delay,
'wheel_speed': self._verify_wheel_speed,
'sequence': self._verify_select_sequence,
}
# Insert NOP based on criteria
self._insert_nop_per_criteria(criteria_method)
# Parse X button and motion events and aggregate the results.
self.xevent.parse_button_and_motion()
# Check those criteria specified in the config file.
for c in criteria_method:
if self.criteria.has_key(c):
crit_item = self.criteria[c]
criteria_method[c](crit_item)
# AND all results of various criteria.
self._get_result()
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.xevent.parse_raw_string(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:
self._set_flags()
self._verify_all_criteria()
return self.result
else:
return False