validators_unittest.py revision a44a9531f8dbd9c61d1f940397a816d628d4b369
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, PLATFORM, VAL
19from firmware_log import MetricNameProps
20from geometry.elements import Point
21from touch_device import TouchDevice
22from validators import (CountPacketsValidator,
23                        CountTrackingIDValidator,
24                        DrumrollValidator,
25                        HysteresisValidator,
26                        LinearityValidator,
27                        NoGapValidator,
28                        NoLevelJumpValidator,
29                        NoReversedMotionValidator,
30                        PhysicalClickValidator,
31                        PinchValidator,
32                        RangeValidator,
33                        ReportRateValidator,
34                        StationaryFingerValidator,
35)
36
37
38unittest_path_lumpy = os.path.join(os.getcwd(), 'tests/logs/lumpy')
39mocked_device = create_mocked_devices()
40
41# Make short aliases for supported platforms
42alex = mocked_device[PLATFORM.ALEX]
43lumpy = mocked_device[PLATFORM.LUMPY]
44link = mocked_device[PLATFORM.LINK]
45# Some tests do not care what device is used.
46dontcare = 'dontcare'
47
48
49class CountTrackingIDValidatorTest(unittest.TestCase):
50    """Unit tests for CountTrackingIDValidator class."""
51
52    def _test_count_tracking_id(self, filename, criteria, device):
53        packets = parse_tests_data(filename)
54        validator = CountTrackingIDValidator(criteria, device=device)
55        vlog = validator.check(packets)
56        return vlog.score
57
58    def test_two_finger_id_change(self):
59        """Two two fingers id change.
60
61        Issue 7867: Cyapa : Two finger scroll, tracking ids change
62        """
63        filename = 'two_finger_id_change.dat'
64        score = self._test_count_tracking_id(filename, '== 2', lumpy)
65        self.assertTrue(score == 0)
66
67    def test_one_finger_fast_swipe_id_split(self):
68        """One finger fast swipe resulting in IDs split.
69
70        Issue: 7869: Lumpy: Tracking ID reassigned during quick-2F-swipe
71        """
72        filename = 'one_finger_fast_swipe_id_split.dat'
73        score = self._test_count_tracking_id(filename, '== 1', lumpy)
74        self.assertTrue(score == 0)
75
76    def test_two_fingers_fast_flick_id_split(self):
77        """Two figners fast flick resulting in IDs split.
78
79        Issue: 7869: Lumpy: Tracking ID reassigned during quick-2F-swipe
80        """
81        filename = 'two_finger_fast_flick_id_split.dat'
82        score = self._test_count_tracking_id(filename, '== 2', lumpy)
83        self.assertTrue(score == 0)
84
85
86class DrumrollValidatorTest(unittest.TestCase):
87    """Unit tests for DrumrollValidator class."""
88
89    def setUp(self):
90        self.criteria = conf.drumroll_criteria
91
92    def _test_drumroll(self, filename, criteria, device):
93        packets = parse_tests_data(filename)
94        validator = DrumrollValidator(criteria, device=device)
95        vlog = validator.check(packets)
96        return vlog.score
97
98    def _get_drumroll_metrics(self, filename, criteria, device):
99        packets = parse_tests_data(filename, gesture_dir=unittest_path_lumpy)
100        validator = DrumrollValidator(criteria, device=device)
101        metrics = validator.check(packets).metrics
102        return metrics
103
104    def test_drumroll_lumpy(self):
105        """Should catch the drumroll on lumpy.
106
107        Issue 7809: Lumpy: Drumroll bug in firmware
108        Max distance: 52.02 px
109        """
110        filename = 'drumroll_lumpy.dat'
111        score = self._test_drumroll(filename, self.criteria, lumpy)
112        self.assertTrue(score == 0)
113
114    def test_drumroll_lumpy_1(self):
115        """Should catch the drumroll on lumpy.
116
117        Issue 7809: Lumpy: Drumroll bug in firmware
118        Max distance: 43.57 px
119        """
120        filename = 'drumroll_lumpy_1.dat'
121        score = self._test_drumroll(filename, self.criteria, lumpy)
122        self.assertTrue(score <= 0.15)
123
124    def test_no_drumroll_link(self):
125        """Should pass (score == 1) when there is no drumroll.
126
127        Issue 7809: Lumpy: Drumroll bug in firmware
128        Max distance: 2.92 px
129        """
130        filename = 'no_drumroll_link.dat'
131        score = self._test_drumroll(filename, self.criteria, link)
132        self.assertTrue(score == 1)
133
134    def test_drumroll_metrics(self):
135        """Test the drumroll metrics."""
136        expected_max_values = {
137            '20130506_030025-fw_11.27-robot_sim/'
138            'drumroll.fast-lumpy-fw_11.27-manual-20130528_044804.dat':
139            2.29402908535,
140
141            '20130506_030025-fw_11.27-robot_sim/'
142            'drumroll.fast-lumpy-fw_11.27-manual-20130528_044820.dat':
143            0.719567771497,
144
145            '20130506_031746-fw_11.27-robot_sim/'
146            'drumroll.fast-lumpy-fw_11.27-manual-20130528_044728.dat':
147            0.833491481592,
148
149            '20130506_032458-fw_11.23-robot_sim/'
150            'drumroll.fast-lumpy-fw_11.23-manual-20130528_044856.dat':
151            1.18368539364,
152
153            '20130506_032458-fw_11.23-robot_sim/'
154            'drumroll.fast-lumpy-fw_11.23-manual-20130528_044907.dat':
155            0.851161282019,
156
157            '20130506_032659-fw_11.23-robot_sim/'
158            'drumroll.fast-lumpy-fw_11.23-manual-20130528_044933.dat':
159            2.64245519251,
160
161            '20130506_032659-fw_11.23-robot_sim/'
162            'drumroll.fast-lumpy-fw_11.23-manual-20130528_044947.dat':
163            0.910624022916,
164        }
165        criteria = self.criteria
166        for filename, expected_max_value in expected_max_values.items():
167            metrics = self._get_drumroll_metrics(filename, criteria, lumpy)
168            actual_max_value = max([m.value for m in metrics])
169            self.assertAlmostEqual(expected_max_value, actual_max_value)
170
171
172class LinearityValidatorTest(unittest.TestCase):
173    """Unit tests for LinearityValidator class."""
174
175    def setUp(self):
176        self.validator = LinearityValidator(conf.linearity_criteria,
177                                            device=lumpy, finger=0)
178        self.validator.init_check()
179
180    def test_simple_linear_regression0(self):
181        """A perfect y-t line from bottom left to top right"""
182        list_y = [20, 40, 60, 80, 100, 120, 140, 160]
183        list_t = [i * 0.1 for i in range(len(list_y))]
184        (max_err_px, rms_err_px) = self.validator._calc_errors_single_axis(
185                list_t, list_y)
186        self.assertAlmostEqual(max_err_px, 0)
187        self.assertAlmostEqual(rms_err_px, 0)
188
189    def test_simple_linear_regression0b(self):
190        """An imperfect y-t line from bottom left to top right with
191        the first and the last entries as outliers.
192
193        In this test case:
194          begin segment = [1,]
195          end segment = [188, 190]
196          middle segment = [20, 40, 60, 80, 100, 120, 140, 160]
197
198          the simple linear regression line is calculated based on the
199          middle segment, and is
200            y = 20 * t
201          the error = [1, 0, 0, 0, 0, 0, 0, 0, 0, 8, 10]
202        """
203        list_y = [1, 20, 40, 60, 80, 100, 120, 140, 160, 188, 190]
204        list_t = range(len(list_y))
205
206        expected_errs_dict = {
207            VAL.WHOLE: [1, 0, 0, 0, 0, 0, 0, 0, 0, 8, 10],
208            VAL.BEGIN: [1, ],
209            VAL.END: [8, 10],
210            VAL.BOTH_ENDS: [1, 8, 10],
211        }
212
213        for segment_flag, expected_errs in expected_errs_dict.items():
214            self.validator._segments= segment_flag
215            (max_err, rms_err) = self.validator._calc_errors_single_axis(list_t,
216                                                                         list_y)
217            expected_max_err = max(expected_errs)
218            expected_rms_err = (sum([i ** 2 for i in expected_errs]) /
219                                len(expected_errs)) ** 0.5
220            self.assertAlmostEqual(max_err, expected_max_err)
221            self.assertAlmostEqual(rms_err, expected_rms_err)
222
223    def test_log_details_and_metrics(self):
224        """Test the axes in _log_details_and_metrics"""
225        # gesture_dir: tests/data/linearity
226        gesture_dir = 'linearity'
227        filenames_axes = {
228            'two_finger_tracking.right_to_left.slow-lumpy-fw_11.27-robot-'
229                '20130227_204458.dat': [AXIS.X],
230            'one_finger_to_edge.center_to_top.slow-lumpy-fw_11.27-robot-'
231                '20130227_203228.dat': [AXIS.Y],
232            'two_finger_tracking.bottom_left_to_top_right.normal-lumpy-'
233                'fw_11.27-robot-20130227_204902.dat': [AXIS.X, AXIS.Y],
234        }
235        for filename, expected_axes in filenames_axes.items():
236            packets = parse_tests_data(filename, gesture_dir=gesture_dir)
237            # get the direction of the gesture
238            direction = [filename.split('-')[0].split('.')[1]]
239            self.validator.check(packets, direction)
240            actual_axes = sorted(self.validator.list_coords.keys())
241            self.assertEqual(actual_axes, expected_axes)
242
243    def _test_simple_linear_regression1(self):
244        """A y-t line taken from a real example.
245
246        Refer to the "Numerical example" in the wiki page:
247            http://en.wikipedia.org/wiki/Simple_linear_regression
248        """
249        list_t = [1.47, 1.50, 1.52, 1.55, 1.57, 1.60, 1.63, 1.65, 1.68, 1.70,
250                  1.73, 1.75, 1.78, 1.80, 1.83]
251        list_y = [52.21, 53.12, 54.48, 55.84, 57.20, 58.57, 59.93, 61.29,
252                  63.11, 64.47, 66.28, 68.10, 69.92, 72.19, 74.46]
253        expected_max_err = 1.3938545467809007
254        expected_rms_err = 0.70666155991311708
255        (max_err, rms_err) = self.validator._calc_errors_single_axis(
256                list_t, list_y)
257        self.assertAlmostEqual(max_err, expected_max_err)
258        self.assertAlmostEqual(rms_err, expected_rms_err)
259
260
261class NoGapValidatorTest(unittest.TestCase):
262    """Unit tests for NoGapValidator class."""
263    GAPS_SUBDIR = 'gaps'
264
265    def setUp(self):
266        self.criteria = conf.no_gap_criteria
267
268    def _test_no_gap(self, filename, criteria, device, slot):
269        file_subpath = os.path.join(self.GAPS_SUBDIR, filename)
270        packets = parse_tests_data(file_subpath)
271        validator = NoGapValidator(criteria, device=device, slot=slot)
272        vlog = validator.check(packets)
273        return vlog.score
274
275    def test_two_finger_scroll_gaps(self):
276        """Test that there are gaps in the two finger scroll gesture.
277
278        Issue 7552: Cyapa : two finger scroll motion produces gaps in tracking
279        """
280        filename = 'two_finger_gaps.horizontal.dat'
281        score0 = self._test_no_gap(filename, self.criteria, lumpy, 0)
282        score1 = self._test_no_gap(filename, self.criteria, lumpy, 1)
283        self.assertTrue(score0 <= 0.1)
284        self.assertTrue(score1 <= 0.1)
285
286    def test_gap_new_finger_arriving_or_departing(self):
287        """Test gap when new finger arriving or departing.
288
289        Issue: 8005: Cyapa : gaps appear when new finger arrives or departs
290        """
291        filename = 'gap_new_finger_arriving_or_departing.dat'
292        score = self._test_no_gap(filename, self.criteria, lumpy, 0)
293        self.assertTrue(score <= 0.3)
294
295    def test_one_stationary_finger_2nd_finger_moving_gaps(self):
296        """Test one stationary finger resulting in 2nd finger moving gaps."""
297        filename = 'one_stationary_finger_2nd_finger_moving_gaps.dat'
298        score = self._test_no_gap(filename, self.criteria, lumpy, 1)
299        self.assertTrue(score <= 0.1)
300
301    def test_resting_finger_2nd_finger_moving_gaps(self):
302        """Test resting finger resulting in 2nd finger moving gaps.
303
304        Issue 7648: Cyapa : Resting finger plus one finger move generates a gap
305        """
306        filename = 'resting_finger_2nd_finger_moving_gaps.dat'
307        score = self._test_no_gap(filename, self.criteria, lumpy, 1)
308        self.assertTrue(score <= 0.3)
309
310
311class PhysicalClickValidatorTest(unittest.TestCase):
312    """Unit tests for PhysicalClickValidator class."""
313
314    def setUp(self):
315        self.device = lumpy
316        self.criteria = '== 1'
317        self.mnprops = MetricNameProps()
318
319    def _test_physical_clicks(self, gesture_dir, files, expected_score):
320        gesture_path = os.path.join(unittest_path_lumpy, gesture_dir)
321        for filename, fingers in files.items():
322            packets = parse_tests_data(os.path.join(gesture_path, filename))
323            validator = PhysicalClickValidator(self.criteria,
324                                               fingers=fingers,
325                                               device=self.device)
326            vlog = validator.check(packets)
327            actual_score = vlog.score
328            self.assertTrue(actual_score == expected_score)
329
330    def test_physical_clicks_success(self):
331        """All physcial click files in the gesture_dir should pass."""
332        gesture_dir = '20130506_030025-fw_11.27-robot_sim'
333        gesture_path = os.path.join(unittest_path_lumpy, gesture_dir)
334
335        # Get all 1f physical click files.
336        file_prefix = 'one_finger_physical_click'
337        fingers = 1
338        files1 = [(filepath, fingers) for filepath in glob.glob(
339            os.path.join(gesture_path, file_prefix + '*.dat'))]
340
341        # Get all 2f physical click files.
342        file_prefix = 'two_fingers_physical_click'
343        fingers = 2
344        files2 = [(filepath, fingers) for filepath in glob.glob(
345            os.path.join(gesture_path, file_prefix + '*.dat'))]
346
347        # files is a dictionary of {filename: fingers}
348        files = dict(files1 + files2)
349        expected_score = 1.0
350        self._test_physical_clicks(gesture_dir, files, expected_score)
351
352    def test_physical_clicks_failure(self):
353        """All physcial click files specified below should fail."""
354        gesture_dir = '20130506_032458-fw_11.23-robot_sim'
355        # files is a dictionary of {filename: fingers}
356        files = {
357            'one_finger_physical_click.bottom_side-lumpy-fw_11.23-complete-'
358                '20130614_065744.dat': 1,
359            'one_finger_physical_click.center-lumpy-fw_11.23-complete-'
360                '20130614_065727.dat': 1,
361            'two_fingers_physical_click-lumpy-fw_11.23-complete-'
362                '20130614_065757.dat': 2,
363        }
364        expected_score = 0.0
365        self._test_physical_clicks(gesture_dir, files, expected_score)
366
367    def test_physical_clicks_by_finger_IDs(self):
368        """Test that some physical clicks may come with or without correct
369        finger IDs.
370        """
371        # files is a dictionary of {
372        #     filename: (number_fingers, (actual clicks, expected clicks))}
373        files = {
374                # An incorrect case with 1 finger: the event sequence comprises
375                #   Event: ABS_MT_TRACKING_ID, value 284
376                #   Event: ABS_MT_TRACKING_ID, value -1
377                #   Event: BTN_LEFT, value 1
378                #   Event: BTN_LEFT, value 0
379                # In this case, the BTN_LEFT occurs when there is no finger.
380                '1f_click_incorrect_behind_tid.dat': (1, (0, 1)),
381
382                # A correct case with 1 finger: the event sequence comprises
383                #   Event: ABS_MT_TRACKING_ID, value 284
384                #   Event: BTN_LEFT, value 1
385                #   Event: ABS_MT_TRACKING_ID, value -1
386                #   Event: BTN_LEFT, value 0
387                # In this case, the BTN_LEFT occurs when there is no finger.
388                '1f_click.dat': (1, (1, 1)),
389
390                # An incorrect case with 2 fingers: the event sequence comprises
391                #   Event: ABS_MT_TRACKING_ID, value 18
392                #   Event: BTN_LEFT, value 1
393                #   Event: BTN_LEFT, value 0
394                #   Event: ABS_MT_TRACKING_ID, value 19
395                #   Event: ABS_MT_TRACKING_ID, value -1
396                #   Event: ABS_MT_TRACKING_ID, value -1
397                # In this case, the BTN_LEFT occurs when there is only 1 finger.
398                '2f_clicks_incorrect_before_2nd_tid.dat': (2, (0, 1)),
399
400                # An incorrect case with 2 fingers: the event sequence comprises
401                #   Event: ABS_MT_TRACKING_ID, value 18
402                #   Event: ABS_MT_TRACKING_ID, value 19
403                #   Event: ABS_MT_TRACKING_ID, value -1
404                #   Event: ABS_MT_TRACKING_ID, value -1
405                #   Event: BTN_LEFT, value 1
406                #   Event: BTN_LEFT, value 0
407                # In this case, the BTN_LEFT occurs when there is only 1 finger.
408                '2f_clicks_incorrect_behind_2_tids.dat': (2, (0, 1)),
409
410                # A correct case with 2 fingers: the event sequence comprises
411                #   Event: ABS_MT_TRACKING_ID, value 18
412                #   Event: ABS_MT_TRACKING_ID, value 19
413                #   Event: BTN_LEFT, value 1
414                #   Event: ABS_MT_TRACKING_ID, value -1
415                #   Event: ABS_MT_TRACKING_ID, value -1
416                #   Event: BTN_LEFT, value 0
417                # In this case, the BTN_LEFT occurs when there is only 1 finger.
418                '2f_clicks.dat': (2, (1, 1)),
419        }
420        for filename, (fingers, expected_value) in files.items():
421            packets = parse_tests_data(filename)
422            validator = PhysicalClickValidator(self.criteria, fingers=fingers,
423                                               device=dontcare)
424            vlog = validator.check(packets)
425            metric_name = self.mnprops.CLICK_CHECK_TIDS.format(fingers)
426            for metric in vlog.metrics:
427                if metric.name == metric_name:
428                    self.assertEqual(metric.value, expected_value)
429
430
431class RangeValidatorTest(unittest.TestCase):
432    """Unit tests for RangeValidator class."""
433
434    def setUp(self):
435        self.device = lumpy
436
437    def _test_range(self, filename, expected_short_of_range_px):
438        filepath = os.path.join(unittest_path_lumpy, filename)
439        packets = parse_tests_data(filepath)
440        validator = RangeValidator(conf.range_criteria, device=self.device)
441
442        # Extract the gesture variation from the filename
443        variation = (filename.split('/')[-1].split('.')[1],)
444
445        # Determine the axis based on the direction in the gesture variation
446        axis = (self.device.axis_x if validator.is_horizontal(variation)
447                else self.device.axis_y if validator.is_vertical(variation)
448                else None)
449        self.assertTrue(axis is not None)
450
451        # Convert from pixels to mms.
452        expected_short_of_range_mm = self.device.pixel_to_mm_single_axis(
453                expected_short_of_range_px, axis)
454
455        vlog = validator.check(packets, variation)
456
457        # There is only one metric in the metrics list.
458        self.assertEqual(len(vlog.metrics), 1)
459        actual_short_of_range_mm = vlog.metrics[0].value
460        self.assertEqual(actual_short_of_range_mm, expected_short_of_range_mm)
461
462    def test_range(self):
463        """All physical click files specified below should fail."""
464        # files_px is a dictionary of {filename: short_of_range_px}
465        files_px = {
466            '20130506_030025-fw_11.27-robot_sim/'
467            'one_finger_to_edge.center_to_left.slow-lumpy-fw_11.27-'
468                'robot_sim-20130506_031554.dat': 0,
469
470            '20130506_030025-fw_11.27-robot_sim/'
471            'one_finger_to_edge.center_to_left.slow-lumpy-fw_11.27-'
472                'robot_sim-20130506_031608.dat': 0,
473
474            '20130506_032458-fw_11.23-robot_sim/'
475            'one_finger_to_edge.center_to_left.slow-lumpy-fw_11.23-'
476                'robot_sim-20130506_032538.dat': 1,
477
478            '20130506_032458-fw_11.23-robot_sim/'
479            'one_finger_to_edge.center_to_left.slow-lumpy-fw_11.23-'
480                'robot_sim-20130506_032549.dat': 1,
481        }
482
483        for filename, short_of_range_px in files_px.items():
484            self._test_range(filename, short_of_range_px)
485
486
487class StationaryFingerValidatorTest(unittest.TestCase):
488    """Unit tests for StationaryFingerValidator class."""
489
490    def setUp(self):
491        self.criteria = conf.stationary_finger_criteria
492
493    def _test_stationary_finger(self, filename, criteria, device):
494        packets = parse_tests_data(filename)
495        validator = StationaryFingerValidator(criteria, device=device)
496        vlog = validator.check(packets)
497        return vlog.score
498
499    def test_stationary_finger_shift(self):
500        """Test that the stationary shift due to 2nd finger tapping.
501
502        Issue 7442: Cyapa : Second finger tap events influence stationary finger
503        position
504        """
505        filename = 'stationary_finger_shift_with_2nd_finger_tap.dat'
506        score = self._test_stationary_finger(filename, self.criteria, lumpy)
507        self.assertTrue(score <= 0.1)
508
509    def test_stationary_strongly_affected_by_2nd_moving_finger(self):
510        """Test stationary finger strongly affected by 2nd moving finger with
511        gaps.
512
513        Issue 5812: [Cypress] reported positions of stationary finger strongly
514        affected by nearby moving finger
515        """
516        filename = ('stationary_finger_strongly_affected_by_2nd_moving_finger_'
517                    'with_gaps.dat')
518        score = self._test_stationary_finger(filename, self.criteria, lumpy)
519        self.assertTrue(score <= 0.1)
520
521
522class NoLevelJumpValidatorTest(unittest.TestCase):
523    """Unit tests for NoLevelJumpValidator class."""
524
525    def setUp(self):
526        self.criteria = conf.no_level_jump_criteria
527        self.gesture_dir = 'drag_edge_thumb'
528
529    def _get_score(self, filename, device):
530        validator = NoLevelJumpValidator(self.criteria, device=device,
531                                         slots=[0,])
532        packets = parse_tests_data(filename, gesture_dir=self.gesture_dir)
533        vlog = validator.check(packets)
534        score = vlog.score
535        return score
536
537    def test_level_jumps(self):
538        """Test files with level jumps."""
539        filenames = [
540            'drag_edge_thumb.horizontal.dat',
541            'drag_edge_thumb.horizontal_2.dat',
542            'drag_edge_thumb.horizontal_3.no_points.dat',
543            'drag_edge_thumb.vertical.dat',
544            'drag_edge_thumb.vertical_2.dat',
545            'drag_edge_thumb.diagonal.dat',
546        ]
547        for filename in filenames:
548            self.assertTrue(self._get_score(filename, lumpy) <= 0.6)
549
550    def test_no_level_jumps(self):
551        """Test files without level jumps."""
552        filenames = [
553            'drag_edge_thumb.horizontal.curvy.dat',
554            'drag_edge_thumb.horizontal_2.curvy.dat',
555            'drag_edge_thumb.vertical.curvy.dat',
556            'drag_edge_thumb.vertical_2.curvy.dat',
557        ]
558        for filename in filenames:
559            self.assertTrue(self._get_score(filename, lumpy) == 1.0)
560
561
562class ReportRateValidatorTest(unittest.TestCase):
563    """Unit tests for ReportRateValidator class."""
564    def setUp(self):
565        self.criteria = '>= 60'
566
567    def _get_score(self, filename, device):
568        validator = ReportRateValidator(self.criteria, device=device,
569                                        chop_off_pauses=False)
570        packets = parse_tests_data(filename)
571        vlog = validator.check(packets)
572        score = vlog.score
573        return score
574
575    def test_report_rate_scores(self):
576        """Test the score of the report rate."""
577        filename = '2f_scroll_diagonal.dat'
578        self.assertTrue(self._get_score(filename, device=lumpy) <= 0.5)
579
580        filename = 'one_finger_with_slot_0.dat'
581        self.assertTrue(self._get_score(filename, device=lumpy) >= 0.9)
582
583        filename = 'two_close_fingers_merging_changed_ids_gaps.dat'
584        self.assertTrue(self._get_score(filename, device=lumpy) <= 0.5)
585
586    def test_report_rate_without_slot(self):
587        """Test report rate without specifying any slot."""
588        filename_report_rate_pair = [
589            ('2f_scroll_diagonal.dat', 40.31),
590            ('one_finger_with_slot_0.dat', 148.65),
591            ('two_close_fingers_merging_changed_ids_gaps.dat', 53.12),
592        ]
593        for filename, expected_report_rate in filename_report_rate_pair:
594            validator = ReportRateValidator(self.criteria, device=dontcare,
595                                            chop_off_pauses=False)
596            validator.check(parse_tests_data(filename))
597            actual_report_rate = round(validator.report_rate, 2)
598            self.assertAlmostEqual(actual_report_rate, expected_report_rate)
599
600    def test_report_rate_with_slot(self):
601        """Test report rate with slot=1"""
602        # Compute actual_report_rate
603        filename = ('stationary_finger_strongly_affected_by_2nd_moving_finger_'
604                    'with_gaps.dat')
605        validator = ReportRateValidator(self.criteria, device=dontcare,
606                                        finger=1, chop_off_pauses=False)
607        validator.check(parse_tests_data(filename))
608        actual_report_rate = validator.report_rate
609        # Compute expected_report_rate
610        first_syn_time = 2597.682925
611        last_syn_time = 2604.543335
612        num_packets = 592 - 1
613        expected_report_rate = num_packets / (last_syn_time - first_syn_time)
614        self.assertAlmostEqual(actual_report_rate, expected_report_rate)
615
616    def _test_report_rate_metrics(self, filename, expected_values):
617        packets = parse_tests_data(filename)
618        validator = ReportRateValidator(self.criteria, device=lumpy,
619                                        chop_off_pauses=False)
620        vlog = validator.check(packets)
621
622        # Verify that there are 3 metrics
623        number_metrics = 3
624        self.assertEqual(len(vlog.metrics), number_metrics)
625
626        # Verify the values of the 3 metrics.
627        for i in range(number_metrics):
628            actual_value = vlog.metrics[i].value
629            if isinstance(actual_value, tuple):
630                self.assertEqual(actual_value, expected_values[i])
631            else:
632                self.assertAlmostEqual(actual_value, expected_values[i])
633
634    def test_report_rate_metrics(self):
635        """Test the metrics of the report rates."""
636        # files is a dictionary of
637        #       {filename: ((# long_intervals, # all intervals),
638        #                    ave_interval, max_interval)}
639        files = {
640            '2f_scroll_diagonal.dat':
641                ((33, 33), 24.8057272727954, 26.26600000075996),
642            'one_finger_with_slot_0.dat':
643                ((1, 12), 6.727166666678386, 20.411999998032115),
644            'two_close_fingers_merging_changed_ids_gaps.dat':
645                ((13, 58), 18.82680942272318, 40.936946868896484),
646        }
647
648        for filename, values in files.items():
649            self._test_report_rate_metrics(filename, values)
650
651    def _test_chop_off_both_ends(self, xy_pairs, distance, expected_middle):
652        """Verify if the actual middle is equal to the expected middle."""
653        points = [Point(*xy) for xy in xy_pairs]
654        validator = ReportRateValidator(self.criteria, device=dontcare)
655        actual_middle = validator._chop_off_both_ends(points, distance)
656        self.assertEqual(actual_middle, expected_middle)
657
658    def test_chop_off_both_ends0(self):
659        """Test chop_off_both_ends() with distinct distances."""
660        xy_pairs = [
661                # pauses
662                (100, 20), (100, 21), (101, 22), (102, 24), (103, 26),
663                # moving segment
664                (120, 30), (122, 29), (123, 32), (123, 33), (126, 35),
665                (126, 32), (142, 29), (148, 30), (159, 31), (162, 30),
666                (170, 32), (183, 32), (194, 32), (205, 32), (208, 32),
667                # pauses
668                (230, 30), (231, 31), (232, 30), (231, 30), (230, 30),
669        ]
670
671        distance = 20
672        expected_begin_index = 5
673        expected_end_index = 19
674        expected_middle = [expected_begin_index, expected_end_index]
675        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
676
677        distance = 0
678        expected_begin_index = 0
679        expected_end_index = len(xy_pairs) - 1
680        expected_middle = [expected_begin_index, expected_end_index]
681        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
682
683    def test_chop_off_both_ends1(self):
684        """Test chop_off_both_ends() with some corner cases"""
685        distance = 20
686        xy_pairs = [(120, 50), (120, 50)]
687        expected_middle = None
688        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
689
690        xy_pairs = [(120, 50), (150, 52), (200, 51)]
691        expected_middle = [1, 1]
692        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
693
694        xy_pairs = [(120, 50), (120, 51), (200, 52), (200, 51)]
695        expected_middle = None
696        self._test_chop_off_both_ends(xy_pairs, distance, expected_middle)
697
698
699class HysteresisValidatorTest(unittest.TestCase):
700    """Unit tests for HysteresisValidator class."""
701
702    def setUp(self):
703        self.criteria = conf.hysteresis_criteria
704
705    def test_hysteresis(self):
706        """Test that the hysteresis causes an initial jump."""
707        filenames = {'center_to_right_normal_link.dat': 4.6043458,
708                     'center_to_right_slow_link.dat': 16.8671278}
709
710        for filename, expected_value in filenames.items():
711            packets = parse_tests_data(filename)
712            validator = HysteresisValidator(self.criteria, device=link)
713            vlog = validator.check(packets)
714            self.assertAlmostEqual(vlog.metrics[0].value, expected_value)
715
716
717if __name__ == '__main__':
718  unittest.main()
719