| # 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 copy |
| import logging |
| |
| import common_util |
| import constants |
| import trackpad_util |
| import Xevent |
| |
| from operator import le, ge, eq, lt, gt, ne, and_ |
| from trackpad_util import read_trackpad_test_conf, debug |
| |
| |
| # Declare NOP as a instance containing NOP related constants |
| NOP = constants.NOP() |
| |
| |
| 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} |
| self.motion_list = ['Motion', 'Motion_x', 'Motion_y'] |
| |
| 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 = common_util.simple_system_output(stat_cmd) |
| boot_time_tuple = tuple(int(line.split()[1]) |
| for line in stat.splitlines() |
| if line.startswith('btime')) |
| if not boot_time_tuple: |
| raise error.TestError('Fail to extract boot time by "%s"' % |
| stat_cmd) |
| self.boot_time = boot_time_tuple[0] |
| |
| def _set_result_flags(self): |
| ''' Set all result flags to True before invoking check function ''' |
| self.motion_flag = True |
| self.button_flag = True |
| self.button_dev_flag = True |
| self.delay_flag = True |
| self.wheel_speed_flag = True |
| self.seq_flag = True |
| self.button_seg_flag = True |
| self.result_flags = ('self.motion_flag', |
| 'self.button_flag', |
| 'self.button_dev_flag', |
| 'self.delay_flag', |
| 'self.wheel_speed_flag', |
| 'self.seq_flag', |
| 'self.button_seg_flag') |
| |
| def _get_result(self): |
| ''' Get the final result from various check flags ''' |
| # Evaluate the result_flags |
| flags = map(eval, self.result_flags) |
| 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_fake_event(self, criterion, fake_xevent_value, |
| fake_xevent_name=NOP.NOP): |
| ''' Insert a fake X event into the xevent_data |
| |
| A NOP event is not an X event. It is inserted to indicate the |
| occurrence of related device events. |
| ''' |
| if fake_xevent_value == NOP.TWO_FINGER_TOUCH: |
| dev_event_time = self.dev.get_two_finger_touch_time_list() |
| elif (criterion == 'button_dev'): |
| dev_event_time = self.dev.find_all_event_time(fake_xevent_value) |
| else: |
| dev_event_time = self.dev.get_finger_time(fake_xevent_value) |
| |
| if not dev_event_time: |
| logging.warn('Fail to get time for "%s" in device file.' % |
| fake_xevent_value) |
| return |
| |
| # Insert fake_xevent_name event into xevent data |
| begin_index = 0 |
| for devent_time in dev_event_time: |
| found_insert_index = False |
| fake_event = [fake_xevent_name, {'event': fake_xevent_value, |
| 'time': devent_time}] |
| for index, line in enumerate(self.xevent.xevent_data[begin_index:]): |
| xevent_dict = line[1] |
| xevent_time = float(xevent_dict.get('time', 0)) |
| if xevent_time > devent_time: |
| insert_index = begin_index + index |
| self.xevent.xevent_data.insert(insert_index, fake_event) |
| begin_index = insert_index + 1 |
| found_insert_index = True |
| break |
| if not found_insert_index: |
| self.xevent.xevent_data.append(fake_event) |
| |
| def _insert_nop_per_criteria(self, criteria_method): |
| ''' Insert NOP events based on criteria ''' |
| for c in criteria_method: |
| crit = self._match_criteria_with_subname(c) |
| if crit is not None: |
| if c == 'wheel_speed': |
| # There are a couple of times of two-finger scrolling. |
| # Insert NOP between them in self.xevent_seq |
| self._insert_fake_event(c, NOP.TWO_FINGER_TOUCH) |
| elif c in ['sequence', 'button_segment', 'button_dev']: |
| crit_item = self.criteria[crit] |
| # crit_item could be either 'sequence' or 'button_segment' |
| # |
| # 'sequence' |
| # Insert NOP in self.xevent_seq if NOP is specified |
| # in sequence criteria. |
| # Example of criteria of 'sequence' below: |
| # ('NOP', '1st Finger Lifted') |
| # ('NOP', '2nd Finger Lifted') |
| # |
| # 'button_segment' |
| # Insert NOP (device) event into self.xevent_seq if |
| # 'NOP' is specified in button_segment criteria. |
| # Example of criteria of 'button_segment' below: |
| # ('NOP', ('Device Mouse Click Press', 'before', True)) |
| # ('NOP', ('Device Mouse Click Release', 'between', True)) |
| # |
| # 'button_dev' |
| # Insert NOP (device) event into self.xevent_seq if |
| # 'NOP' is specified in button criteria. |
| # Example of criteria of 'button_segment' below: |
| # ('NOP', ('Finger On', None)) |
| # ('NOP', ('Device Mouse Click Release', True)) |
| # ('NOP', ('Finger Off', None)) |
| # |
| for s in crit_item: |
| if s[0] == NOP.NOP: |
| _, value = s |
| dev_ev = value if c == 'sequence' else value[0] |
| self._insert_fake_event(c, dev_ev) |
| |
| def _extract_func_name(self): |
| ''' Extract functionality name plus subname from the gesture file name |
| |
| E.g., A file name looks like: |
| palm-palm_presence.static.both-alex-jane-20111215_001214.dat |
| Return value in this case: palm_presence.static.both |
| ''' |
| return self.gesture_file_name.split('-')[self.func_name_pos] |
| |
| def _extract_subname(self): |
| ''' Extract subname from the gesture file name |
| |
| E.g., A file name looks like: |
| palm-palm_presence.static.both-alex-jane-20111215_001214.dat |
| Return value in this case: static.both |
| ''' |
| full_name = self._extract_func_name() |
| name_seg = full_name.split('.', 1) |
| return name_seg[1] if len(name_seg) > 1 else None |
| |
| 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 |
| |
| def _match_criteria_with_subname(self, crit): |
| ''' Determine if a given criterion crit could apply to a file with a |
| particular subname |
| |
| E.g., |
| A file with subname of 'physical_click' could match the criteria: |
| 'button_segment' |
| 'button_segment(physical_click)' |
| but not the criteria: |
| 'button_segment(tap_and_half)' |
| ''' |
| subname = self._extract_subname() |
| for c in self.criteria: |
| if c == crit: |
| return c |
| elif subname is not None: |
| crit_with_subname = '%s(%s)' % (crit, subname) |
| if c == crit_with_subname: |
| return c |
| |
| ''' _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) |
| self.vlog.verify_motion_log(self.motion_flag, 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 |
| self.vlog.verify_button_log(self.button_flag, self.xevent.count_buttons) |
| |
| def _verify_button_with_device_events(self, crit_button_dev): |
| ''' Verify if the observed button satisfy the criteria |
| |
| E.g., the critieria for |
| 'button_dev(physical_click)': ( |
| ('NOP', ('Finger On', None)), |
| ('NOP', ('Device Mouse Click Press', True)), |
| ('NOP', ('Device Mouse Click Release', True)), |
| ('NOP', ('One Finger On', True)), |
| ('Motion', '<=', 0), |
| ('Button', 'Button Left'), |
| ('NOP', ('Finger Off', None)), |
| ) |
| |
| The rules above make sure that |
| (1) It only counts Button Left with a physical mouse click |
| (2) Exactly one finger is observed. |
| (3) Should match the number of physical mouse clicks. |
| (4) Ignore finger tracking. |
| |
| Refer to configuration files (*.conf) for more details about the |
| criteria. |
| ''' |
| |
| def _reset_button_dev_motion(self, button_dev_events): |
| ''' Initialize button_dev motion events ''' |
| for m in self.motion_list: |
| button_dev_events[m] = 0 |
| |
| def _init_button_dev_events(self, motion_values=True): |
| ''' Initialize button_dev_events ''' |
| button_dev_events = {} |
| # Initialize 'NOP' |
| button_dev_events['NOP'] = {} |
| |
| # Initialize 'Button' |
| button_dev_events['Button'] = {} |
| for b in self.button_labels: |
| button_dev_events['Button'][b] = 0 |
| |
| # Initialize 'Motion' |
| if motion_values: |
| for m in self.motion_list: |
| button_dev_events[m] = 0 |
| return button_dev_events |
| |
| def _check_button_dev_criteria(self, button_dev_events, |
| crit_button_dev_events): |
| ''' Check if the button_dev_events conform to the criteria ''' |
| fail_cause = [] |
| # Check all observed NOP device events match the corresponding |
| # criteria |
| crit_dup = copy.deepcopy(crit_button_dev_events) |
| for e in button_dev_events['NOP']: |
| # check if this NOP event is specified in the criteria |
| if crit_button_dev_events['NOP'].has_key(e): |
| this_ev = button_dev_events['NOP'][e] |
| crit = crit_button_dev_events['NOP'][e] |
| crit_dup['NOP'].pop(e) |
| if this_ev == crit or crit == 'DONTCARE': |
| continue |
| msg = 'NOP[%s]: %s' % (e, this_ev) |
| fail_cause.append(msg) |
| |
| # Check if there are any NOP criteria not matched yet. |
| if crit_dup['NOP']: |
| for e in crit_dup['NOP']: |
| if (crit_dup['NOP'][e] != 'DONTCARE' and |
| crit_dup['NOP'][e]): |
| msg = ('NOP[%s]: %s is missing' % |
| (e, crit_dup['NOP'][e])) |
| fail_cause.append(msg) |
| |
| debug(' check Button: %s' % str(button_dev_events['Button'])) |
| debug(' check Button crit: %s' % |
| str(crit_button_dev_events['Button'])) |
| |
| # Check Button event |
| for b in self.button_labels: |
| crit_button_count = crit_button_dev_events['Button'][b] |
| button_count = button_dev_events['Button'][b] |
| if button_count != crit_button_count: |
| msg = ('Count of Button[%s]: %d. It should be %d' % |
| (b, button_count, crit_button_count)) |
| fail_cause.append(msg) |
| |
| # Check Motion events |
| for m in self.motion_list: |
| if crit_button_dev_events.has_key(m): |
| op_str, val = crit_button_dev_events[m] |
| op = self.op_dict[op_str] |
| if not op(button_dev_events[m], val): |
| msg = '%s: %s' % (m, button_dev_events[m]) |
| fail_cause.append(msg) |
| |
| result = (len(fail_cause) == 0) |
| return (result, fail_cause) |
| |
| def _parse_button_dev_criteria(self): |
| ''' Parse the button_dev criteria ''' |
| crit_button_dev_events = _init_button_dev_events(self, |
| motion_values=False) |
| |
| for c in crit_button_dev: |
| name = c[0] |
| if name == 'NOP': |
| # E.g., ('NOP', ('Finger On', True)), |
| # ('NOP', ('Device Mouse Click Release', True)), |
| name, (dev_event, value) = c |
| crit_button_dev_events[name][dev_event] = value |
| elif name == 'Button': |
| # E.g., ('Button', 'Button Left') |
| # No need to specifiy the count in the criteria |
| # Will count it based on the gesture file. |
| name, button = c |
| self.button_dev_target = button |
| crit_button_dev_events[name][button] = 0 |
| elif name.startswith('Motion'): |
| # E.g., ('Motion', '<=', 0), |
| name, op, value = c |
| crit_button_dev_events[name] = (op, int(value)) |
| |
| return crit_button_dev_events |
| |
| def _check_device_events(button_dev_events, crit_button_dev_events): |
| ''' Check if all NOP device events except Finger On/Off |
| are matched. |
| |
| If any NOP device event does not match, the user had made |
| a wrong gesture. For example, a user may make tap-to-clicks |
| when physical clicks are expected. |
| ''' |
| flag_match = True |
| debug(' button_dev_events: %s' % str(button_dev_events['NOP'])) |
| debug(' crit_button_dev_events: %s' % |
| str(crit_button_dev_events['NOP'])) |
| if len(crit_button_dev_events['NOP']) > 0: |
| for ev in crit_button_dev_events['NOP']: |
| if not ev.startswith('Finger'): |
| if button_dev_events['NOP'].has_key(ev): |
| flag_match = (button_dev_events['NOP'][ev] == |
| crit_button_dev_events['NOP'][ev]) |
| else: |
| flag_match = not crit_button_dev_events['NOP'][ev] |
| if not flag_match: |
| debug(' NOP[%s] violation' % ev) |
| break |
| return flag_match |
| |
| def _init_file_accu_motion(self, file_accu_motion): |
| for m in self.motion_list: |
| file_accu_motion[m] = 0 |
| |
| def _update_file_accu_motion(self, file_accu_motion, button_dev_events): |
| for m in self.motion_list: |
| file_accu_motion[m] += button_dev_events[m] |
| |
| def _check_button_dev(self, dev_event, button_dev_events, |
| crit_button_dev_events, file_accu_motion): |
| # Check if there is a 'Finger On' event observed. |
| if not button_dev_events['NOP'].has_key('Finger On'): |
| msg = ' Warning: There is no Finger On before %s.' % dev_event |
| logging.info(msg) |
| return |
| |
| device_events_matched = _check_device_events(button_dev_events, |
| crit_button_dev_events) |
| debug(' check device events: matched = %s' % |
| str(device_events_matched)) |
| if device_events_matched: |
| result = _check_button_dev_criteria(self, button_dev_events, |
| crit_button_dev_events) |
| debug(' *** result: %s' % str(result)) |
| result_flag, fail_cause = result |
| self.fail_causes += fail_cause |
| if self.button_dev_flag is None: |
| self.button_dev_flag = result_flag |
| else: |
| self.button_dev_flag &= result_flag |
| _update_file_accu_motion(self, file_accu_motion, |
| button_dev_events) |
| else: |
| msg = ' check device events: not matched. Skip.' |
| logging.info(msg) |
| |
| # Some initialization |
| self.button_dev_flag = None |
| self.fail_causes = [] |
| button_dev_events = _init_button_dev_events(self) |
| state_button = {} |
| for b in self.button_labels: |
| state_button[b] = None |
| state_click = None |
| |
| file_crit_button_count = 0 |
| file_button_count = 0 |
| file_accu_motion = {} |
| _init_file_accu_motion(self, file_accu_motion) |
| |
| # Parse the criteria into a dictionary |
| crit_button_dev_events = _parse_button_dev_criteria(self) |
| target_button = self.button_dev_target |
| |
| # Match the xevent sequence against the criteria dictionary |
| # For a normal Button Left resulting from a mouse click: |
| # (1) Finger On |
| # (2) One Finger On |
| # (3) Mouse Click Press |
| # (4) ButtonPress |
| # (5) Mouse Click Release |
| # (6) ButtonRelease |
| # . (Optional: repeat Steps (3) ~ (6)) |
| # (7) Finger Off |
| xevent_seq = self.xevent.xevent_seq |
| for ev in xevent_seq: |
| name = ev[0] |
| if name == 'ButtonPress': |
| # E.g., ('ButtonPress', 'Button Left', 443854733) |
| name, button, timestamp = ev |
| if state_button[button] in ['ButtonRelease', None]: |
| state_button[button] = 'ButtonPress' |
| button_dev_events['Button'][button] += 0.5 |
| debug(' %s(%s): state=%s count=%s '% |
| (name, button, state_button[button], |
| button_dev_events['Button'][button])) |
| |
| elif name == 'ButtonRelease': |
| # E.g, ('ButtonRelease', 'Button Left', 443854884) |
| name, button_released, timestamp = ev |
| if state_button[button] == 'ButtonPress': |
| state_button[button] = 'ButtonRelease' |
| button_dev_events['Button'][button_released] += 0.5 |
| debug(' %s(%s): state=%s count=%s '% |
| (name, button, state_button[button], |
| button_dev_events['Button'][button])) |
| |
| elif name == 'NOP': |
| # E.g., ('NOP', 'Device Mouse Click Press') |
| # ('NOP', 'Device Mouse Click Release') |
| name, dev_event, timestamp = ev |
| button_dev_events[name][dev_event] = True |
| |
| # When finger off, check if all NOP device criteria are matched. |
| if dev_event == 'Finger Off': |
| # Check button_dev criteria |
| _check_button_dev(self, dev_event, button_dev_events, |
| crit_button_dev_events, file_accu_motion) |
| # Accumulate file-wise button counts |
| file_button_count += \ |
| button_dev_events['Button'][target_button] |
| file_crit_button_count += \ |
| crit_button_dev_events['Button'][target_button] |
| # Reset |
| button_dev_events = _init_button_dev_events(self) |
| crit_button_dev_events = _parse_button_dev_criteria(self) |
| _reset_button_dev_motion(self, button_dev_events) |
| |
| elif dev_event == 'Device Mouse Click Press': |
| # _reset_button_dev_motion(self, button_dev_events) |
| if state_click in ['Click Release', None]: |
| state_click = 'Click Press' |
| crit_button_dev_events['Button'][target_button] += 0.5 |
| debug(' %s: target_button count = %s' % (dev_event, |
| crit_button_dev_events['Button'][target_button])) |
| |
| elif dev_event == 'Device Mouse Click Release': |
| if state_click == 'Click Press': |
| state_click = 'Click Release' |
| crit_button_dev_events['Button'][target_button] += 0.5 |
| debug(' %s: target_button count = %s' % (dev_event, |
| crit_button_dev_events['Button'][target_button])) |
| |
| elif name == 'Motion': |
| # E.g., ('Motion', (655.0, ('Motion_x', 605), ('Motion_y', 20)), |
| # [443855715, 443858536]) |
| (name, (motion_val, (_, motion_x_val), (_, motion_y_val)), |
| timestamp) = ev |
| button_dev_events['Motion'] += int(motion_val) |
| button_dev_events['Motion_x'] += int(motion_x_val) |
| button_dev_events['Motion_y'] += int(motion_y_val) |
| |
| debug(' *** vlog: button_dev_flag = %s' % self.button_dev_flag) |
| self.vlog.verify_button_dev_log(self.button_dev_flag, |
| xevent_seq, |
| self.fail_causes, |
| target_button, |
| file_button_count, |
| file_crit_button_count) |
| |
| 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. |
| 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.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': 'xy', None: 'xy'} |
| 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 |
| # Handle boundary condition when work_xevent_seq is empty |
| fail_msg = '%s' |
| fail_para = '(empty work_xevent_seq)' |
| 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] |
| motion_xy_val = motion_x_val + motion_y_val |
| 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}, |
| 'xy': {'this': motion_xy_val, |
| 'other': motion_xy_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)) |
| |
| # If this axis is 'x', movement in 'y' should be small. |
| # If this axis is 'y', movement in 'x' should be small. |
| # If this axis is 'xy', no need to check the other axis. |
| crit_check = (check_this_axis and |
| (axis == 'xy' or 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.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 |
| |
| self.vlog.verify_sequence_log(self.seq_flag, self.xevent.xevent_seq, |
| fail_msg, fail_para) |
| |
| def _verify_button_segment(self, crit_button_segment): |
| ''' Verify if a button event segment satisfies criteria |
| |
| This button_segment criteria allows to specify the interleaving of |
| various device events in a bracketing X Button events between |
| ButtonPress and ButtonRelease. This criteria is usually used |
| for select and drag gesture with or without trackpad clicking. |
| |
| For example, the following event subsequence matches |
| crit_button_segment. |
| event sequence looks like: [ |
| ... |
| ('NOP', 'Device Mouse Click Press'), |
| ('Motion', 10), |
| ('ButtonPress', 'Button Left'), |
| ('Motion', 68), |
| ('NOP', 'Device Mouse Click Release'), |
| ('ButtonRelease', 'Button Left')] |
| ... |
| 'crit_button_segment' : ( |
| ('NOP', ('Device Mouse Click Press', 'before', True)), |
| ('NOP', ('Device Mouse Click Press', 'between', False)), |
| ('NOP', ('Device Mouse Click Release', 'between', True)), |
| ('Button', 'Button Left'), |
| ('Motion', '>=', select_drag_distance), |
| ), |
| ''' |
| |
| def _init_button_seg_events(self, nop_init_value=False, |
| button_init_value=None, |
| motion_init_value=0): |
| ''' Initialize button_seg_events ''' |
| button_seg_events = {} |
| # Initialize device event flag |
| button_seg_events['accept_1st_gesture_only'] = False |
| button_seg_events[NOP.NOP] = {} |
| for d in self.button_segment_dev_event_list: |
| button_seg_events[NOP.NOP][d] = {} |
| for w in self.where_list: |
| button_seg_events[NOP.NOP][d][w] = nop_init_value |
| button_seg_events['Button'] = button_init_value |
| for m in self.motion_list: |
| button_seg_events[m] = motion_init_value |
| return button_seg_events |
| |
| def _check_button_segment(self, button_seg_events, |
| crit_button_seg_events): |
| ''' Check if the button_seg_events conform to the criteria ''' |
| flag = _init_button_seg_events(self, nop_init_value=False, |
| button_init_value=False, |
| motion_init_value=False) |
| result = True |
| fail_causes = [] |
| |
| # Check device events |
| for d in self.button_segment_dev_event_list: |
| for w in self.where_list: |
| this_ev = button_seg_events[NOP.NOP][d][w] |
| flag[NOP.NOP][d][w] = (this_ev == |
| crit_button_seg_events[NOP.NOP][d][w]) |
| if not flag[NOP.NOP][d][w]: |
| msg = 'NOP[%s][%s]: %s' % (d, w, this_ev) |
| fail_causes.append(msg) |
| |
| # Check button event |
| flag['Button'] = (button_seg_events['Button'] == |
| crit_button_seg_events['Button']) |
| if not flag['Button']: |
| msg = 'button: %s' % button_seg_events['Button'] |
| fail_causes.append(msg) |
| |
| # Check Motion events |
| for m in self.motion_list: |
| op_str, val = crit_button_seg_events[m] |
| op = self.op_dict[op_str] |
| flag[m] = op(button_seg_events[m], val) |
| if not flag[m]: |
| msg = '%s: %s' % (m, button_seg_events[m]) |
| fail_causes.append(msg) |
| |
| result = (len(fail_causes) == 0) |
| return (result, ' Check Button Segment: %s', str(fail_causes)) |
| |
| def _parse_button_seg_criteria(self): |
| ''' Parse the button_segment criteria ''' |
| crit_button_seg_events = _init_button_seg_events(self, |
| motion_init_value=('>=', 0)) |
| |
| for c in crit_button_segment: |
| name = c[0] |
| if name == NOP.NOP: |
| # E.g.,('NOP', ('Device Mouse Click Press', 'before', True)) |
| name, (dev_event, where, value) = c |
| crit_button_seg_events[name][dev_event][where] = value |
| elif name == 'Button': |
| # E.g., ('bracket', 'Button Left') |
| name, button = c |
| crit_button_seg_events[name] = button |
| elif name.startswith('Motion'): |
| # E.g., ('Motion', '>=', 20), |
| # E.g., ('Motion_x', '>=', 20), |
| # E.g., ('Motion_y', '<=', 0), |
| name, op, value = c |
| crit_button_seg_events[name] = (op, int(value)) |
| elif name == 'accept_1st_gesture_only': |
| name, value = c |
| crit_button_seg_events[name] = value |
| |
| return crit_button_seg_events |
| |
| # Some initialization |
| where = 'before' |
| fail_msg = '%s' |
| fail_para = None |
| self.button_seg_flag = False |
| self.button_segment_dev_event_list = ['Device Mouse Click Press', |
| 'Device Mouse Click Release'] |
| self.where_list = ['before', 'between'] |
| button_seg_events = _init_button_seg_events(self) |
| |
| # Parse the criteria into a dictionary |
| crit_button_seg_events = _parse_button_seg_criteria(self) |
| |
| # Match the xevent sequence against the criteria dictionary |
| xevent_seq = self.xevent.xevent_seq |
| for ev in xevent_seq: |
| name = ev[0] |
| if name == 'ButtonPress': |
| # E.g., ('ButtonPress', 'Button Left', 443854733) |
| name, button, timestamp = ev |
| button_seg_events['Button'] = button |
| where = 'between' |
| for m in self.motion_list: |
| button_seg_events[m] = 0 |
| |
| elif name == 'ButtonRelease': |
| # E.g, ('ButtonRelease', 'Button Left', 443854884) |
| name, button_release, timestamp = ev |
| if button_release == button_seg_events['Button']: |
| check_results = _check_button_segment(self, |
| button_seg_events, crit_button_seg_events) |
| self.button_seg_flag, fail_msg, fail_para = check_results |
| if (self.button_seg_flag or |
| crit_button_seg_events['accept_1st_gesture_only']): |
| break |
| where = 'before' |
| button_seg_events = _init_button_seg_events(self) |
| else: |
| fail_msg = ('ButtonRelease of "%s" is not consistent with ' |
| 'ButtonPress of "%s".') |
| fail_para = (button_release, button) |
| break |
| |
| elif name == NOP.NOP: |
| # E.g., ('NOP', 'Device Mouse Click Press') |
| # ('NOP', 'Device Mouse Click Release') |
| name, dev_event, timestamp = ev |
| button_seg_events[name][dev_event][where] = True |
| |
| elif name == 'Motion': |
| # E.g., ('Motion', (655.0, ('Motion_x', 605), ('Motion_y', 20)), |
| # [443855715, 443858536]) |
| (name, (motion_val, (_, motion_x_val), (_, motion_y_val)), |
| timestamp) = ev |
| button_seg_events['Motion'] = int(motion_val) |
| button_seg_events['Motion_x'] = int(motion_x_val) |
| button_seg_events['Motion_y'] = int(motion_y_val) |
| |
| self.vlog.verify_button_segment_log(self.button_seg_flag, xevent_seq, |
| 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, |
| 'button_dev': self._verify_button_with_device_events, |
| 'delay': self._verify_select_delay, |
| 'wheel_speed': self._verify_wheel_speed, |
| 'sequence': self._verify_select_sequence, |
| 'button_segment': self._verify_button_segment, |
| } |
| |
| # The result flags of performing the above verification methods |
| self._set_result_flags() |
| |
| # Insert NOP based on criteria |
| self._insert_nop_per_criteria(criteria_method) |
| |
| debug(' xevent_data:', level=1) |
| for x in self.xevent.xevent_data: |
| debug(' %s' % x, level=1) |
| |
| # 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: |
| crit = self._match_criteria_with_subname(c) |
| if crit is not None: |
| crit_item = self.criteria[crit] |
| 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 |
| self.vlog = trackpad_util.VerificationLog() |
| if parse_result: |
| self._verify_all_criteria() |
| return {'result': self.result, 'vlog': self.vlog.log} |
| else: |
| return False |