1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4#
5
6"""This module contains unit tests for the classes in the validators module."""
7
8import glob
9import os.path
10import unittest
11
12import common_unittest_utils
13import common_util
14import test_conf as conf
15import validators
16
17from common_unittest_utils import create_mocked_devices, parse_tests_data
18from firmware_constants import AXIS, GV, MTB, PLATFORM, VAL
19from firmware_log import MetricNameProps
20from geometry.elements import Point
21from touch_device import TouchDevice
22from validators import (CountPacketsValidator,
23                        CountTrackingIDValidator,
24                        DiscardInitialSecondsValidator,
25                        DrumrollValidator,
26                        HysteresisValidator,
27                        LinearityValidator,
28                        MtbSanityValidator,
29                        NoGapValidator,
30                        NoLevelJumpValidator,
31                        NoReversedMotionValidator,
32                        PhysicalClickValidator,
33                        PinchValidator,
34                        RangeValidator,
35                        ReportRateValidator,
36                        StationaryFingerValidator,
37                        StationaryTapValidator,
38)
39
40
41unittest_path_lumpy = os.path.join(os.getcwd(), 'tests/logs/lumpy')
42mocked_device = create_mocked_devices()
43
44# Make short aliases for supported platforms
45alex = mocked_device[PLATFORM.ALEX]
46lumpy = mocked_device[PLATFORM.LUMPY]
47link = mocked_device[PLATFORM.LINK]
48# Some tests do not care what device is used.
49dontcare = 'dontcare'
50
51
52class CountTrackingIDValidatorTest(unittest.TestCase):
53    """Unit tests for CountTrackingIDValidator class."""
54
55    def _test_count_tracking_id(self, filename, criteria, device):
56        packets = parse_tests_data(filename)
57        validator = CountTrackingIDValidator(criteria, device=device)
58        vlog = validator.check(packets)
59        return vlog.score
60
61    def test_two_finger_id_change(self):
62        """Two two fingers id change.
63
64        Issue 7867: Cyapa : Two finger scroll, tracking ids change
65        """
66        filename = 'two_finger_id_change.dat'
67        score = self._test_count_tracking_id(filename, '== 2', lumpy)
68        self.assertTrue(score == 0)
69
70    def test_one_finger_fast_swipe_id_split(self):
71        """One finger fast swipe resulting in IDs split.
72
73        Issue: 7869: Lumpy: Tracking ID reassigned during quick-2F-swipe
74        """
75        filename = 'one_finger_fast_swipe_id_split.dat'
76        score = self._test_count_tracking_id(filename, '== 1', lumpy)
77        self.assertTrue(score == 0)
78
79    def test_two_fingers_fast_flick_id_split(self):
80        """Two figners fast flick resulting in IDs split.
81
82        Issue: 7869: Lumpy: Tracking ID reassigned during quick-2F-swipe
83        """
84        filename = 'two_finger_fast_flick_id_split.dat'
85        score = self._test_count_tracking_id(filename, '== 2', lumpy)
86        self.assertTrue(score == 0)
87
88
89class DrumrollValidatorTest(unittest.TestCase):
90    """Unit tests for DrumrollValidator class."""
91
92    def setUp(self):
93        self.criteria = conf.drumroll_criteria
94
95    def _test_drumroll(self, filename, criteria, device):
96        packets = parse_tests_data(filename)
97        validator = DrumrollValidator(criteria, device=device)
98        vlog = validator.check(packets)
99        return vlog.score
100
101    def _get_drumroll_metrics(self, filename, criteria, device):
102        packets = parse_tests_data(filename, gesture_dir=unittest_path_lumpy)
103        validator = DrumrollValidator(criteria, device=device)
104        metrics = validator.check(packets).metrics
105        return metrics
106
107    def test_drumroll_lumpy(self):
108        """Should catch the drumroll on lumpy.
109
110        Issue 7809: Lumpy: Drumroll bug in firmware
111        Max distance: 52.02 px
112        """
113        filename = 'drumroll_lumpy.dat'
114        score = self._test_drumroll(filename, self.criteria, lumpy)
115        self.assertTrue(score == 0)
116
117    def test_drumroll_lumpy_1(self):
118        """Should catch the drumroll on lumpy.
119
120        Issue 7809: Lumpy: Drumroll bug in firmware
121        Max distance: 43.57 px
122        """
123        filename = 'drumroll_lumpy_1.dat'
124        score = self._test_drumroll(filename, self.criteria, lumpy)
125        self.assertTrue(score <= 0.15)
126
127    def test_no_drumroll_link(self):
128        """Should pass (score == 1) when there is no drumroll.
129
130        Issue 7809: Lumpy: Drumroll bug in firmware
131        Max distance: 2.92 px
132        """
133        filename = 'no_drumroll_link.dat'
134        score = self._test_drumroll(filename, self.criteria, link)
135        self.assertTrue(score == 1)
136
137    def test_drumroll_metrics(self):
138        """Test the drumroll metrics."""
139        expected_max_values = {
140            '20130506_030025-fw_11.27-robot_sim/'
141            'drumroll.fast-lumpy-fw_11.27-manual-20130528_044804.dat':
142            2.29402908535,
143
144            '20130506_030025-fw_11.27-robot_sim/'
145            'drumroll.fast-lumpy-fw_11.27-manual-20130528_044820.dat':
146            0.719567771497,
147
148            '20130506_031746-fw_11.27-robot_sim/'
149            'drumroll.fast-lumpy-fw_11.27-manual-20130528_044728.dat':
150            0.833491481592,
151
152            '20130506_032458-fw_11.23-robot_sim/'
153            'drumroll.fast-lumpy-fw_11.23-manual-20130528_044856.dat':
154            1.18368539364,
155
156            '20130506_032458-fw_11.23-robot_sim/'
157            'drumroll.fast-lumpy-fw_11.23-manual-20130528_044907.dat':
158            0.851161282019,
159
160            '20130506_032659-fw_11.23-robot_sim/'
161            'drumroll.fast-lumpy-fw_11.23-manual-20130528_044933.dat':
162            2.64245519251,
163
164            '20130506_032659-fw_11.23-robot_sim/'
165            'drumroll.fast-lumpy-fw_11.23-manual-20130528_044947.dat':
166            0.910624022916,
167        }
168        criteria = self.criteria
169        for filename, expected_max_value in expected_max_values.items():
170            metrics = self._get_drumroll_metrics(filename, criteria, lumpy)
171            actual_max_value = max([m.value for m in metrics])
172            self.assertAlmostEqual(expected_max_value, actual_max_value)
173
174
175class LinearityValidatorTest(unittest.TestCase):
176    """Unit tests for LinearityValidator class."""
177
178    def setUp(self):
179        self.validator = LinearityValidator(conf.linearity_criteria,
180                                            device=lumpy, finger=0)
181        self.validator.init_check()
182
183    def test_simple_linear_regression0(self):
184        """A perfect y-t line from bottom left to top right"""
185        list_y = [20, 40, 60, 80, 100, 120, 140, 160]
186        list_t = [i * 0.1 for i in range(len(list_y))]
187        (max_err_px, rms_err_px) = self.validator._calc_errors_single_axis(
188                list_t, list_y)
189        self.assertAlmostEqual(max_err_px, 0)
190        self.assertAlmostEqual(rms_err_px, 0)
191
192    def test_simple_linear_regression0b(self):
193        """An imperfect y-t line from bottom left to top right with
194        the first and the last entries as outliers.
195
196        In this test case:
197          begin segment = [1,]
198          end segment = [188, 190]
199          middle segment = [20, 40, 60, 80, 100, 120, 140, 160]
200
201          the simple linear regression line is calculated based on the
202          middle segment, and is
203            y = 20 * t
204          the error = [1, 0, 0, 0, 0, 0, 0, 0, 0, 8, 10]
205        """
206        list_y = [1, 20, 40, 60, 80, 100, 120, 140, 160, 188, 190]
207        list_t = range(len(list_y))
208
209        expected_errs_dict = {
210            VAL.WHOLE: [1, 0, 0, 0, 0, 0, 0, 0, 0, 8, 10],
211            VAL.BEGIN: [1, ],
212            VAL.END: [8, 10],
213            VAL.BOTH_ENDS: [1, 8, 10],
214        }
215
216        for segment_flag, expected_errs in expected_errs_dict.items():
217            self.validator._segments= segment_flag
218            (max_err, rms_err) = self.validator._calc_errors_single_axis(list_t,
219                                                                         list_y)
220            expected_max_err = max(expected_errs)
221            expected_rms_err = (sum([i ** 2 for i in expected_errs]) /
222                                len(expected_errs)) ** 0.5
223            self.assertAlmostEqual(max_err, expected_max_err)
224            self.assertAlmostEqual(rms_err, expected_rms_err)
225
226    def test_log_details_and_metrics(self):
227        """Test the axes in _log_details_and_metrics"""
228        # gesture_dir: tests/data/linearity
229        gesture_dir = 'linearity'
230        filenames_axes = {
231            'two_finger_tracking.right_to_left.slow-lumpy-fw_11.27-robot-'
232                '20130227_204458.dat': [AXIS.X],
233            'one_finger_to_edge.center_to_top.slow-lumpy-fw_11.27-robot-'
234                '20130227_203228.dat': [AXIS.Y],
235            'two_finger_tracking.bottom_left_to_top_right.normal-lumpy-'
236                'fw_11.27-robot-20130227_204902.dat': [AXIS.X, AXIS.Y],
237        }
238        for filename, expected_axes in filenames_axes.items():
239            packets = parse_tests_data(filename, gesture_dir=gesture_dir)
240            # get the direction of the gesture
241            direction = [filename.split('-')[0].split('.')[1]]
242            self.validator.check(packets, direction)
243            actual_axes = sorted(self.validator.list_coords.keys())
244            self.assertEqual(actual_axes, expected_axes)
245
246    def _test_simple_linear_regression1(self):
247        """A y-t line taken from a real example.
248
249        Refer to the "Numerical example" in the wiki page:
250            http://en.wikipedia.org/wiki/Simple_linear_regression
251        """
252        list_t = [1.47, 1.50, 1.52, 1.55, 1.57, 1.60, 1.63, 1.65, 1.68, 1.70,
253                  1.73, 1.75, 1.78, 1.80, 1.83]
254        list_y = [52.21, 53.12, 54.48, 55.84, 57.20, 58.57, 59.93, 61.29,
255                  63.11, 64.47, 66.28, 68.10, 69.92, 72.19, 74.46]
256        expected_max_err = 1.3938545467809007
257        expected_rms_err = 0.70666155991311708
258        (max_err, rms_err) = self.validator._calc_errors_single_axis(
259                list_t, list_y)
260        self.assertAlmostEqual(max_err, expected_max_err)
261        self.assertAlmostEqual(rms_err, expected_rms_err)
262
263
264class NoGapValidatorTest(unittest.TestCase):
265    """Unit tests for NoGapValidator class."""
266    GAPS_SUBDIR = 'gaps'
267
268    def setUp(self):
269        self.criteria = conf.no_gap_criteria
270
271    def _test_no_gap(self, filename, criteria, device, slot):
272        file_subpath = os.path.join(self.GAPS_SUBDIR, filename)
273        packets = parse_tests_data(file_subpath)
274        validator = NoGapValidator(criteria, device=device, slot=slot)
275        vlog = validator.check(packets)
276        return vlog.score
277
278    def test_two_finger_scroll_gaps(self):
279        """Test that there are gaps in the two finger scroll gesture.
280
281        Issue 7552: Cyapa : two finger scroll motion produces gaps in tracking
282        """
283        filename = 'two_finger_gaps.horizontal.dat'
284        score0 = self._test_no_gap(filename, self.criteria, lumpy, 0)
285        score1 = self._test_no_gap(filename, self.criteria, lumpy, 1)
286        self.assertTrue(score0 <= 0.1)
287        self.assertTrue(score1 <= 0.1)
288
289    def test_gap_new_finger_arriving_or_departing(self):
290        """Test gap when new finger arriving or departing.
291
292        Issue: 8005: Cyapa : gaps appear when new finger arrives or departs
293        """
294        filename = 'gap_new_finger_arriving_or_departing.dat'
295        score = self._test_no_gap(filename, self.criteria, lumpy, 0)
296        self.assertTrue(score <= 0.3)
297
298    def test_one_stationary_finger_2nd_finger_moving_gaps(self):
299        """Test one stationary finger resulting in 2nd finger moving gaps."""
300        filename = 'one_stationary_finger_2nd_finger_moving_gaps.dat'
301        score = self._test_no_gap(filename, self.criteria, lumpy, 1)
302        self.assertTrue(score <= 0.1)
303
304    def test_resting_finger_2nd_finger_moving_gaps(self):
305        """Test resting finger resulting in 2nd finger moving gaps.
306
307        Issue 7648: Cyapa : Resting finger plus one finger move generates a gap
308        """
309        filename = 'resting_finger_2nd_finger_moving_gaps.dat'
310        score = self._test_no_gap(filename, self.criteria, lumpy, 1)
311        self.assertTrue(score <= 0.3)
312
313
314class PhysicalClickValidatorTest(unittest.TestCase):
315    """Unit tests for PhysicalClickValidator class."""
316
317    def setUp(self):
318        self.device = lumpy
319        self.criteria = '== 1'
320        self.mnprops = MetricNameProps()
321
322    def _test_physical_clicks(self, gesture_dir, files, expected_score):
323        gesture_path = os.path.join(unittest_path_lumpy, gesture_dir)
324        for filename, fingers in files.items():
325            packets = parse_tests_data(os.path.join(gesture_path, filename))
326            validator = PhysicalClickValidator(self.criteria,
327                                               fingers=fingers,
328                                               device=self.device)
329            vlog = validator.check(packets)
330            actual_score = vlog.score
331            self.assertTrue(actual_score == expected_score)
332
333    def test_physical_clicks_success(self):
334        """All physcial click files in the gesture_dir should pass."""
335        gesture_dir = '20130506_030025-fw_11.27-robot_sim'
336        gesture_path = os.path.join(unittest_path_lumpy, gesture_dir)
337
338        # Get all 1f physical click files.
339        file_prefix = 'one_finger_physical_click'
340        fingers = 1
341        files1 = [(filepath, fingers) for filepath in glob.glob(
342            os.path.join(gesture_path, file_prefix + '*.dat'))]
343
344        # Get all 2f physical click files.
345        file_prefix = 'two_fingers_physical_click'
346        fingers = 2
347        files2 = [(filepath, fingers) for filepath in glob.glob(
348            os.path.join(gesture_path, file_prefix + '*.dat'))]
349
350        # files is a dictionary of {filename: fingers}
351        files = dict(files1 + files2)
352        expected_score = 1.0
353        self._test_physical_clicks(gesture_dir, files, expected_score)
354
355    def test_physical_clicks_failure(self):
356        """All physcial click files specified below should fail."""
357        gesture_dir = '20130506_032458-fw_11.23-robot_sim'
358        # files is a dictionary of {filename: fingers}
359        files = {
360            'one_finger_physical_click.bottom_side-lumpy-fw_11.23-complete-'
361                '20130614_065744.dat': 1,
362            'one_finger_physical_click.center-lumpy-fw_11.23-complete-'
363                '20130614_065727.dat': 1,
364            'two_fingers_physical_click-lumpy-fw_11.23-complete-'
365                '20130614_065757.dat': 2,
366        }
367        expected_score = 0.0
368        self._test_physical_clicks(gesture_dir, files, expected_score)
369
370    def test_physical_clicks_by_finger_IDs(self):
371        """Test that some physical clicks may come with or without correct
372        finger IDs.
373        """
374        # files is a dictionary of {
375        #     filename: (number_fingers, (actual clicks, expected clicks))}
376        files = {
377                # An incorrect case with 1 finger: the event sequence comprises
378                #   Event: ABS_MT_TRACKING_ID, value 284
379                #   Event: ABS_MT_TRACKING_ID, value -1
380                #   Event: BTN_LEFT, value 1
381                #   Event: BTN_LEFT, value 0
382                # In this case, the BTN_LEFT occurs when there is no finger.
383                '1f_click_incorrect_behind_tid.dat': (1, (0, 1)),
384
385                # A correct case with 1 finger: the event sequence comprises
386                #   Event: ABS_MT_TRACKING_ID, value 284
387                #   Event: BTN_LEFT, value 1
388                #   Event: ABS_MT_TRACKING_ID, value -1
389                #   Event: BTN_LEFT, value 0
390                # In this case, the BTN_LEFT occurs when there is no finger.
391                '1f_click.dat': (1, (1, 1)),
392
393                # An incorrect case with 2 fingers: the event sequence comprises
394                #   Event: ABS_MT_TRACKING_ID, value 18
395                #   Event: BTN_LEFT, value 1
396                #   Event: BTN_LEFT, value 0
397                #   Event: ABS_MT_TRACKING_ID, value 19
398                #   Event: ABS_MT_TRACKING_ID, value -1
399                #   Event: ABS_MT_TRACKING_ID, value -1
400                # In this case, the BTN_LEFT occurs when there is only 1 finger.
401                '2f_clicks_incorrect_before_2nd_tid.dat': (2, (0, 1)),
402
403                # An incorrect case with 2 fingers: the event sequence comprises
404                #   Event: ABS_MT_TRACKING_ID, value 18
405                #   Event: ABS_MT_TRACKING_ID, value 19
406                #   Event: ABS_MT_TRACKING_ID, value -1
407                #   Event: ABS_MT_TRACKING_ID, value -1
408                #   Event: BTN_LEFT, value 1
409                #   Event: BTN_LEFT, value 0
410                # In this case, the BTN_LEFT occurs when there is only 1 finger.
411                '2f_clicks_incorrect_behind_2_tids.dat': (2, (0, 1)),
412
413                # A correct case with 2 fingers: the event sequence comprises
414                #   Event: ABS_MT_TRACKING_ID, value 18
415                #   Event: ABS_MT_TRACKING_ID, value 19
416                #   Event: BTN_LEFT, value 1
417                #   Event: ABS_MT_TRACKING_ID, value -1
418                #   Event: ABS_MT_TRACKING_ID, value -1
419                #   Event: BTN_LEFT, value 0
420                # In this case, the BTN_LEFT occurs when there is only 1 finger.
421                '2f_clicks.dat': (2, (1, 1)),
422        }
423        for filename, (fingers, expected_value) in files.items():
424            packets = parse_tests_data(filename)
425            validator = PhysicalClickValidator(self.criteria, fingers=fingers,
426                                               device=dontcare)
427            vlog = validator.check(packets)
428            metric_name = self.mnprops.CLICK_CHECK_TIDS.format(fingers)
429            for metric in vlog.metrics:
430                if metric.name == metric_name:
431                    self.assertEqual(metric.value, expected_value)
432
433
434class RangeValidatorTest(unittest.TestCase):
435    """Unit tests for RangeValidator class."""
436
437    def setUp(self):
438        self.device = lumpy
439
440    def _test_range(self, filename, expected_short_of_range_px):
441        filepath = os.path.join(unittest_path_lumpy, filename)
442        packets = parse_tests_data(filepath)
443        validator = RangeValidator(conf.range_criteria, device=self.device)
444
445        # Extract the gesture variation from the filename
446        variation = (filename.split('/')[-1].split('.')[1],)
447
448        # Determine the axis based on the direction in the gesture variation
449        axis = (self.device.axis_x if validator.is_horizontal(variation)
450                else self.device.axis_y if validator.is_vertical(variation)
451                else None)
452        self.assertTrue(axis is not None)
453
454        # Convert from pixels to mms.
455        expected_short_of_range_mm = self.device.pixel_to_mm_single_axis(
456                expected_short_of_range_px, axis)
457
458        vlog = validator.check(packets, variation)
459
460        # There is only one metric in the metrics list.
461        self.assertEqual(len(vlog.metrics), 1)
462        actual_short_of_range_mm = vlog.metrics[0].value
463        self.assertEqual(actual_short_of_range_mm, expected_short_of_range_mm)
464
465    def test_range(self):
466        """All physical click files specified below should fail."""
467        # files_px is a dictionary of {filename: short_of_range_px}
468        files_px = {
469            '20130506_030025-fw_11.27-robot_sim/'
470            'one_finger_to_edge.center_to_left.slow-lumpy-fw_11.27-'
471                'robot_sim-20130506_031554.dat': 0,
472
473            '20130506_030025-fw_11.27-robot_sim/'
474            'one_finger_to_edge.center_to_left.slow-lumpy-fw_11.27-'
475                'robot_sim-20130506_031608.dat': 0,
476
477            '20130506_032458-fw_11.23-robot_sim/'
478            'one_finger_to_edge.center_to_left.slow-lumpy-fw_11.23-'
479                'robot_sim-20130506_032538.dat': 1,
480
481            '20130506_032458-fw_11.23-robot_sim/'
482            'one_finger_to_edge.center_to_left.slow-lumpy-fw_11.23-'
483                'robot_sim-20130506_032549.dat': 1,
484        }
485
486        for filename, short_of_range_px in files_px.items():
487            self._test_range(filename, short_of_range_px)
488
489
490class StationaryFingerValidatorTest(unittest.TestCase):
491    """Unit tests for StationaryFingerValidator class."""
492
493    def setUp(self):
494        self.criteria = conf.stationary_finger_criteria
495
496    def _get_max_distance(self, filename, criteria, device):
497        packets = parse_tests_data(filename)
498        validator = StationaryFingerValidator(criteria, device=device)
499        vlog = validator.check(packets)
500        return vlog.metrics[0].value
501
502    def test_stationary_finger_shift(self):
503        """Test that the stationary shift due to 2nd finger tapping.
504
505        Issue 7442: Cyapa : Second finger tap events influence stationary finger
506        position
507        """
508        filename = 'stationary_finger_shift_with_2nd_finger_tap.dat'
509        max_distance = self._get_max_distance(filename, self.criteria, lumpy)
510        self.assertAlmostEqual(max_distance, 5.464430436926)
511
512    def test_stationary_strongly_affected_by_2nd_moving_finger(self):
513        """Test stationary finger strongly affected by 2nd moving finger with
514        gaps.
515
516        Issue 5812: [Cypress] reported positions of stationary finger strongly
517        affected by nearby moving finger
518        """
519        filename = ('stationary_finger_strongly_affected_by_2nd_moving_finger_'
520                    'with_gaps.dat')
521        max_distance = self._get_max_distance(filename, self.criteria, lumpy)
522        self.assertAlmostEqual(max_distance, 4.670861210146)
523
524
525class StationaryTapValidatorTest(unittest.TestCase):
526    """Unit tests for StationaryTapValidator class."""
527
528    def setUp(self):
529        self.criteria = conf.stationary_tap_criteria
530
531    def test_stationary_tap(self):
532        filenames = {'1f_click.dat': 1.718284027744,
533                     '1f_clickb.dat': 0.577590781705}
534        for filename, expected_max_distance in filenames.items():
535            packets = parse_tests_data(filename)
536            validator = StationaryTapValidator(self.criteria, device=lumpy)
537            vlog = validator.check(packets)
538            actual_max_distance = vlog.metrics[0].value
539            self.assertAlmostEqual(actual_max_distance, expected_max_distance)
540
541
542class NoLevelJumpValidatorTest(unittest.TestCase):
543    """Unit tests for NoLevelJumpValidator class."""
544
545    def setUp(self):
546        self.criteria = conf.no_level_jump_criteria
547        self.gesture_dir = 'drag_edge_thumb'
548
549    def _get_score(self, filename, device):
550        validator = NoLevelJumpValidator(self.criteria, device=device,
551                                         slots=[0,])
552        packets = parse_tests_data(filename, gesture_dir=self.gesture_dir)
553        vlog = validator.check(packets)
554        score = vlog.score
555        return score
556
557    def test_level_jumps(self):
558        """Test files with level jumps."""
559        filenames = [
560            'drag_edge_thumb.horizontal.dat',
561            'drag_edge_thumb.horizontal_2.dat',
562            'drag_edge_thumb.horizontal_3.no_points.dat',
563            'drag_edge_thumb.vertical.dat',
564            'drag_edge_thumb.vertical_2.dat',
565            'drag_edge_thumb.diagonal.dat',
566        ]
567        for filename in filenames:
568            self.assertTrue(self._get_score(filename, lumpy) <= 0.6)
569
570    def test_no_level_jumps(self):
571        """Test files without level jumps."""
572        filenames = [
573            'drag_edge_thumb.horizontal.curvy.dat',
574            'drag_edge_thumb.horizontal_2.curvy.dat',
575            'drag_edge_thumb.vertical.curvy.dat',
576            'drag_edge_thumb.vertical_2.curvy.dat',
577        ]
578        for filename in filenames:
579            self.assertTrue(self._get_score(filename, lumpy) == 1.0)
580
581
582class ReportRateValidatorTest(unittest.TestCase):
583    """Unit tests for ReportRateValidator class."""
584    def setUp(self):
585        self.criteria = '>= 60'
586
587    def _get_score(self, filename, device):
588        validator = ReportRateValidator(self.criteria, device=device,
589                                        chop_off_pauses=False)
590        packets = parse_tests_data(filename)
591        vlog = validator.check(packets)
592        score = vlog.score
593        return score
594
595    def test_report_rate_scores(self):
596        """Test the score of the report rate."""
597        filename = '2f_scroll_diagonal.dat'
598        self.assertTrue(self._get_score(filename, device=lumpy) <= 0.5)
599
600        filename = 'one_finger_with_slot_0.dat'
601        self.assertTrue(self._get_score(filename, device=lumpy) >= 0.9)
602
603        filename = 'two_close_fingers_merging_changed_ids_gaps.dat'
604        self.assertTrue(self._get_score(filename, device=lumpy) <= 0.5)
605
606    def test_report_rate_without_slot(self):
607        """Test report rate without specifying any slot."""
608        filename_report_rate_pair = [
609            ('2f_scroll_diagonal.dat', 40.31),
610            ('one_finger_with_slot_0.dat', 148.65),
611            ('two_close_fingers_merging_changed_ids_gaps.dat', 53.12),
612        ]
613        for filename, expected_report_rate in filename_report_rate_pair:
614            validator = ReportRateValidator(self.criteria, device=dontcare,
615                                            chop_off_pauses=False)
616            validator.check(parse_tests_data(filename))
617            actual_report_rate = round(validator.report_rate, 2)
618            self.assertAlmostEqual(actual_report_rate, expected_report_rate)
619
620    def test_report_rate_with_slot(self):
621        """Test report rate with slot=1"""
622        # Compute actual_report_rate
623        filename = ('stationary_finger_strongly_affected_by_2nd_moving_finger_'
624                    'with_gaps.dat')
625        validator = ReportRateValidator(self.criteria, device=dontcare,
626                                        finger=1, chop_off_pauses=False)
627        validator.check(parse_tests_data(filename))
628        actual_report_rate = validator.report_rate
629        # Compute expected_report_rate
630        first_syn_time = 2597.682925
631        last_syn_time = 2604.543335
632        num_packets = 592 - 1
633        expected_report_rate = num_packets / (last_syn_time - first_syn_time)
634        self.assertAlmostEqual(actual_report_rate, expected_report_rate)
635
636    def _test_report_rate_metrics(self, filename, expected_values):
637        packets = parse_tests_data(filename)
638        validator = ReportRateValidator(self.criteria, device=lumpy,
639                                        chop_off_pauses=False)
640        vlog = validator.check(packets)
641
642        # Verify that there are 3 metrics
643        number_metrics = 3
644        self.assertEqual(len(vlog.metrics), number_metrics)
645
646        # Verify the values of the 3 metrics.
647        for i in range(number_metrics):
648            actual_value = vlog.metrics[i].value
649            if isinstance(actual_value, tuple):
650                self.assertEqual(actual_value, expected_values[i])
651            else:
652                self.assertAlmostEqual(actual_value, expected_values[i])
653
654    def test_report_rate_metrics(self):
655        """Test the metrics of the report rates."""
656        # files is a dictionary of
657        #       {filename: ((# long_intervals, # all intervals),
658        #                    ave_interval, max_interval)}
659        files = {
660            '2f_scroll_diagonal.dat':
661                ((33, 33), 24.8057272727954, 26.26600000075996),
662            'one_finger_with_slot_0.dat':
663                ((1, 12), 6.727166666678386, 20.411999998032115),
664            'two_close_fingers_merging_changed_ids_gaps.dat':
665                ((13, 58), 18.82680942272318, 40.936946868896484),
666        }
667
668        for filename, values in files.items():
669            self._test_report_rate_metrics(filename, values)
670
671    def _test_chop_off_both_ends(self, xy_pairs, distance, expected_middle):
672        """Verify if the actual middle is equal to the expected middle."""
673        points = [Point(*xy) for xy in xy_pairs]
674        validator = ReportRateValidator(self.criteria, device=dontcare)
675        actual_middle = validator._chop_off_both_ends(points, distance)
676        self.assertEqual(actual_middle, expected_middle)
677
678    def test_chop_off_both_ends0(self):
679        """Test chop_off_both_ends() with distinct distances."""
680        xy_pairs = [
681                # pauses
682                (100, 20), (100, 21), (101, 22), (102, 24), (103, 26),
683                # moving segment
684                (120, 30), (122, 29), (123, 32), (123, 33), (126, 35),
685                (126, 32), (142, 29), (148, 30), (159, 31), (162, 30),
686                (170, 32), (183, 32), (194, 32), (205, 32), (208, 32),
687                # pauses
688                (230, 30), (231, 31), (232, 30), (231, 30), (230, 30),
689        ]
690
691        distance = 20
692        expected_begin_index = 5
693        expected_end_index = 19
694        expected_middle = [expected_begin_index, expected_end_index]
695        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
696
697        distance = 0
698        expected_begin_index = 0
699        expected_end_index = len(xy_pairs) - 1
700        expected_middle = [expected_begin_index, expected_end_index]
701        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
702
703    def test_chop_off_both_ends1(self):
704        """Test chop_off_both_ends() with some corner cases"""
705        distance = 20
706        xy_pairs = [(120, 50), (120, 50)]
707        expected_middle = None
708        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
709
710        xy_pairs = [(120, 50), (150, 52), (200, 51)]
711        expected_middle = [1, 1]
712        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
713
714        xy_pairs = [(120, 50), (120, 51), (200, 52), (200, 51)]
715        expected_middle = None
716        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
717
718
719class HysteresisValidatorTest(unittest.TestCase):
720    """Unit tests for HysteresisValidator class."""
721
722    def setUp(self):
723        self.criteria = conf.hysteresis_criteria
724
725    def test_hysteresis(self):
726        """Test that the hysteresis causes an initial jump."""
727        filenames = {'center_to_right_normal_link.dat': 4.6043458,
728                     'center_to_right_slow_link.dat': 16.8671278}
729
730        for filename, expected_value in filenames.items():
731            packets = parse_tests_data(filename)
732            validator = HysteresisValidator(self.criteria, device=link)
733            vlog = validator.check(packets)
734            self.assertAlmostEqual(vlog.metrics[0].value, expected_value)
735
736    def test_click_data(self):
737        """Test that the validator handles None distances well.
738
739        In this test, distance1 = None and distance2 = None.
740        This results in ratio = infinity. There should be no error incurred.
741        """
742        packets = parse_tests_data('2f_clicks_test_hysteresis.dat')
743        validator = HysteresisValidator(self.criteria, device=link)
744        vlog = validator.check(packets)
745        self.assertEqual(vlog.metrics[0].value, float('infinity'))
746
747
748class MtbSanityValidatorTest(unittest.TestCase):
749    """Unit tests for MtbSanityValidator class."""
750
751    def setUp(self):
752        import fake_input_device
753        self.fake_device_info = fake_input_device.FakeInputDevice()
754
755    def _get_number_errors(self, filename):
756        packets = parse_tests_data(filename)
757        validator = MtbSanityValidator(device=link,
758                                       device_info=self.fake_device_info)
759        vlog = validator.check(packets)
760        number_errors, _ = vlog.metrics[1].value
761        return number_errors
762
763    def test_sanity_found_errors(self):
764        """Test that the tracking id is set to -1 before being assigned a
765        positive value.
766        """
767        filenames = ['finger_crossing.top_right_to_bottom_left.slow.dat',
768                     'two_finger_tap.vertical.dat']
769        for filename in filenames:
770            number_errors = self._get_number_errors(filename)
771            self.assertTrue(number_errors > 0)
772
773    def test_sanity_pass(self):
774        """Test that the MTB format is correct."""
775        filenames = ['2f_scroll_diagonal.dat',
776                     'drumroll_lumpy.dat']
777        for filename in filenames:
778            number_errors = self._get_number_errors(filename)
779            self.assertTrue(number_errors == 0)
780
781
782class DiscardInitialSecondsValidatorTest(unittest.TestCase):
783    """Unit tests for DiscardInitialSecondsValidator class."""
784
785    def setUp(self):
786        import fake_input_device
787        self.fake_device_info = fake_input_device.FakeInputDevice()
788
789    def _get_score(self, filename, criteria_str):
790        packets = parse_tests_data(filename)
791        validator = DiscardInitialSecondsValidator(
792            validator=CountTrackingIDValidator(criteria_str, device=link),
793            device=link)
794        vlog = validator.check(packets)
795        return vlog.score
796
797    def test_single_finger_hold(self):
798        """Test that the state machine reads one finger if
799        only one finger was held for over a second."""
800
801        filename = 'one_finger_long_hold.dat'
802        score = self._get_score(filename, '== 1')
803        self.assertTrue(score == 1)
804
805    def test_double_finger_hold(self):
806        """Test that the state machine reads two fingers if
807        two fingers were held for over a second."""
808
809        filename = 'two_finger_long_hold.dat'
810        score = self._get_score(filename, '== 2')
811        self.assertTrue(score == 1)
812
813    def test_double_tap_single_hold(self):
814        """Test that the validator discards the initial double tap and only
815        validates on the single finger long hold at the end.
816        """
817
818        filename = 'two_finger_tap_one_finger_hold.dat'
819        score = self._get_score(filename, '== 1')
820        self.assertTrue(score == 1)
821
822    def test_discard_initial_seconds(self):
823        """Test that discard_initial_seconds() cuts at the proper packet.
824
825        Note: to print the final_state_packet, use the following statements:
826            import mtb
827            print mtb.make_pretty_packet(final_state_packet)
828        """
829        packets = parse_tests_data('noise_stationary_extended.dat')
830        validator = DiscardInitialSecondsValidator(
831            validator=CountTrackingIDValidator('== 1', device=link),
832            device=link)
833        validator.init_check(packets)
834        packets = validator._discard_initial_seconds(packets, 1)
835        final_state_packet = packets[0]
836
837        self.assertTrue(len(final_state_packet) == 11)
838        # Assert the correctness of the 1st finger data in the order of
839        #     SLOT, TRACKING_ID, POSITION_X, POSITION_Y, PRESSURE
840        self.assertTrue(final_state_packet[0][MTB.EV_VALUE] == 2)
841        self.assertTrue(final_state_packet[1][MTB.EV_VALUE] == 2427)
842        self.assertTrue(final_state_packet[2][MTB.EV_VALUE] == 670)
843        self.assertTrue(final_state_packet[3][MTB.EV_VALUE] == 361)
844        self.assertTrue(final_state_packet[4][MTB.EV_VALUE] == 26)
845        # Assert the correctness of the 2nd finger data in the order of
846        #     SLOT, TRACKING_ID, POSITION_X, POSITION_Y, PRESSURE
847        self.assertTrue(final_state_packet[5][MTB.EV_VALUE] == 3)
848        self.assertTrue(final_state_packet[6][MTB.EV_VALUE] == 2426)
849        self.assertTrue(final_state_packet[7][MTB.EV_VALUE] == 670)
850        self.assertTrue(final_state_packet[8][MTB.EV_VALUE] == 368)
851        self.assertTrue(final_state_packet[9][MTB.EV_VALUE] == 21)
852        # EVENT TIME
853        self.assertTrue(final_state_packet[0][MTB.EV_TIME] == 1412021965.723953)
854
855    def test_get_snapshot_after_discarding_init_packets(self):
856        """Test that get_snapshot() handles non-ready packet properly
857        after discard_initial_seconds(). A non-ready packet is one that
858        the attributes such as X, Y, and Z are not all ready.
859        """
860        packets = parse_tests_data('non_ready_events_in_final_state_packet.dat')
861        validator = DiscardInitialSecondsValidator(
862            validator=CountTrackingIDValidator('== 1', device=link),
863            device=link)
864        validator.init_check(packets)
865        packets = validator._discard_initial_seconds(packets, 1)
866        final_state_packet = packets[0]
867
868        self.assertTrue(len(final_state_packet) == 4)
869        # Assert the correctness of the finger data in the order of
870        #     SLOT, TRACKING_ID, and POSITION_Y
871        self.assertTrue(final_state_packet[0][MTB.EV_VALUE] == 0)
872        self.assertTrue(final_state_packet[1][MTB.EV_VALUE] == 102)
873        self.assertTrue(final_state_packet[2][MTB.EV_VALUE] == 1316)
874        # EVENT TIME
875        self.assertTrue(final_state_packet[0][MTB.EV_TIME] == 1412888977.716634)
876
877    def test_noise_line_with_all_fingers_left(self):
878        """In this test case, all fingers left. The final_state_packet is []."""
879        packets=parse_tests_data('noise_line.dat')
880        validator = DiscardInitialSecondsValidator(ReportRateValidator('>= 60'))
881        validator.init_check(packets)
882        packets = validator._discard_initial_seconds(packets, 1)
883        validator.validator.init_check(packets)
884        list_syn_time = validator.validator.packets.get_list_syn_time([])
885        self.assertEqual(len(packets), 84)
886        self.assertEqual(len(list_syn_time), 84)
887
888
889if __name__ == '__main__':
890  unittest.main()
891