• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# vim: set expandtab shiftwidth=4:
3# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
4#
5# Copyright © 2018 Red Hat, Inc.
6#
7# Permission is hereby granted, free of charge, to any person obtaining a
8# copy of this software and associated documentation files (the "Software"),
9# to deal in the Software without restriction, including without limitation
10# the rights to use, copy, modify, merge, publish, distribute, sublicense,
11# and/or sell copies of the Software, and to permit persons to whom the
12# Software is furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice (including the next
15# paragraph) shall be included in all copies or substantial portions of the
16# Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
21# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24# DEALINGS IN THE SOFTWARE.
25
26import argparse
27import os
28import sys
29import unittest
30import yaml
31import re
32
33from pkg_resources import parse_version
34
35
36class TestYaml(unittest.TestCase):
37    filename = ''
38
39    @classmethod
40    def setUpClass(cls):
41        with open(cls.filename) as f:
42            cls.yaml = yaml.safe_load(f)
43
44    def dict_key_crosscheck(self, d, keys):
45        '''Check that each key in d is in keys, and that each key is in d'''
46        self.assertEqual(sorted(d.keys()), sorted(keys))
47
48    def libinput_events(self, filter=None):
49        '''Returns all libinput events in the recording, regardless of the
50        device'''
51        devices = self.yaml['devices']
52        for d in devices:
53            events = d['events']
54            if not events:
55                raise unittest.SkipTest()
56            for e in events:
57                try:
58                    libinput = e['libinput']
59                except KeyError:
60                    continue
61
62                for ev in libinput:
63                    if (filter is None or ev['type'] == filter or
64                            isinstance(filter, list) and ev['type'] in filter):
65                        yield ev
66
67    def test_sections_exist(self):
68        sections = ['version', 'ndevices', 'libinput', 'system', 'devices']
69        for section in sections:
70            self.assertIn(section, self.yaml)
71
72    def test_version(self):
73        version = self.yaml['version']
74        self.assertTrue(isinstance(version, int))
75        self.assertEqual(version, 1)
76
77    def test_ndevices(self):
78        ndevices = self.yaml['ndevices']
79        self.assertTrue(isinstance(ndevices, int))
80        self.assertGreaterEqual(ndevices, 1)
81        self.assertEqual(ndevices, len(self.yaml['devices']))
82
83    def test_libinput(self):
84        libinput = self.yaml['libinput']
85        version = libinput['version']
86        self.assertTrue(isinstance(version, str))
87        self.assertGreaterEqual(parse_version(version),
88                                parse_version('1.10.0'))
89        git = libinput['git']
90        self.assertTrue(isinstance(git, str))
91        self.assertNotEqual(git, 'unknown')
92
93    def test_system(self):
94        system = self.yaml['system']
95        kernel = system['kernel']
96        self.assertTrue(isinstance(kernel, str))
97        self.assertEqual(kernel, os.uname().release)
98
99        dmi = system['dmi']
100        self.assertTrue(isinstance(dmi, str))
101        with open('/sys/class/dmi/id/modalias') as f:
102            sys_dmi = f.read()[:-1]  # trailing newline
103            self.assertEqual(dmi, sys_dmi)
104
105    def test_devices_sections_exist(self):
106        devices = self.yaml['devices']
107        for d in devices:
108            self.assertIn('node', d)
109            self.assertIn('evdev', d)
110            self.assertIn('udev', d)
111
112    def test_evdev_sections_exist(self):
113        sections = ['name', 'id', 'codes', 'properties']
114        devices = self.yaml['devices']
115        for d in devices:
116            evdev = d['evdev']
117            for s in sections:
118                self.assertIn(s, evdev)
119
120    def test_evdev_name(self):
121        devices = self.yaml['devices']
122        for d in devices:
123            evdev = d['evdev']
124            name = evdev['name']
125            self.assertTrue(isinstance(name, str))
126            self.assertGreaterEqual(len(name), 5)
127
128    def test_evdev_id(self):
129        devices = self.yaml['devices']
130        for d in devices:
131            evdev = d['evdev']
132            id = evdev['id']
133            self.assertTrue(isinstance(id, list))
134            self.assertEqual(len(id), 4)
135            self.assertGreater(id[0], 0)
136            self.assertGreater(id[1], 0)
137
138    def test_evdev_properties(self):
139        devices = self.yaml['devices']
140        for d in devices:
141            evdev = d['evdev']
142            properties = evdev['properties']
143            self.assertTrue(isinstance(properties, list))
144
145    def test_hid(self):
146        devices = self.yaml['devices']
147        for d in devices:
148            hid = d['hid']
149            self.assertTrue(isinstance(hid, list))
150            for byte in hid:
151                self.assertGreaterEqual(byte, 0)
152                self.assertLessEqual(byte, 255)
153
154    def test_udev_sections_exist(self):
155        sections = ['properties']
156        devices = self.yaml['devices']
157        for d in devices:
158            udev = d['udev']
159            for s in sections:
160                self.assertIn(s, udev)
161
162    def test_udev_properties(self):
163        devices = self.yaml['devices']
164        for d in devices:
165            udev = d['udev']
166            properties = udev['properties']
167            self.assertTrue(isinstance(properties, list))
168            self.assertGreater(len(properties), 0)
169
170            self.assertIn('ID_INPUT=1', properties)
171            for p in properties:
172                self.assertTrue(re.match('[A-Z0-9_]+=.+', p))
173
174    def test_udev_id_inputs(self):
175        devices = self.yaml['devices']
176        for d in devices:
177            udev = d['udev']
178            properties = udev['properties']
179            id_inputs = [p for p in properties if p.startswith('ID_INPUT')]
180            # We expect ID_INPUT and ID_INPUT_something, but might get more
181            # than one of the latter
182            self.assertGreaterEqual(len(id_inputs), 2)
183
184    def test_events_have_section(self):
185        devices = self.yaml['devices']
186        for d in devices:
187            events = d['events']
188            if not events:
189                raise unittest.SkipTest()
190            for e in events:
191                self.assertTrue('evdev' in e or 'libinput' in e)
192
193    def test_events_evdev(self):
194        devices = self.yaml['devices']
195        for d in devices:
196            events = d['events']
197            if not events:
198                raise unittest.SkipTest()
199            for e in events:
200                try:
201                    evdev = e['evdev']
202                except KeyError:
203                    continue
204
205                for ev in evdev:
206                    self.assertEqual(len(ev), 5)
207
208                # Last event in each frame is SYN_REPORT
209                ev_syn = evdev[-1]
210                self.assertEqual(ev_syn[2], 0)
211                self.assertEqual(ev_syn[3], 0)
212                # SYN_REPORT value is 1 in case of some key repeats
213                self.assertLessEqual(ev_syn[4], 1)
214
215    def test_events_evdev_syn_report(self):
216        devices = self.yaml['devices']
217        for d in devices:
218            events = d['events']
219            if not events:
220                raise unittest.SkipTest()
221            for e in events:
222                try:
223                    evdev = e['evdev']
224                except KeyError:
225                    continue
226                for ev in evdev[:-1]:
227                    self.assertFalse(ev[2] == 0 and ev[3] == 0)
228
229    def test_events_libinput(self):
230        devices = self.yaml['devices']
231        for d in devices:
232            events = d['events']
233            if not events:
234                raise unittest.SkipTest()
235            for e in events:
236                try:
237                    libinput = e['libinput']
238                except KeyError:
239                    continue
240
241                self.assertTrue(isinstance(libinput, list))
242                for ev in libinput:
243                    self.assertTrue(isinstance(ev, dict))
244
245    def test_events_libinput_type(self):
246        types = ['POINTER_MOTION', 'POINTER_MOTION_ABSOLUTE', 'POINTER_AXIS',
247                 'POINTER_BUTTON', 'DEVICE_ADDED', 'KEYBOARD_KEY',
248                 'TOUCH_DOWN', 'TOUCH_MOTION', 'TOUCH_UP', 'TOUCH_FRAME',
249                 'GESTURE_SWIPE_BEGIN', 'GESTURE_SWIPE_UPDATE',
250                 'GESTURE_SWIPE_END', 'GESTURE_PINCH_BEGIN',
251                 'GESTURE_PINCH_UPDATE', 'GESTURE_PINCH_END',
252                 'TABLET_TOOL_AXIS', 'TABLET_TOOL_PROXIMITY',
253                 'TABLET_TOOL_BUTTON', 'TABLET_TOOL_TIP',
254                 'TABLET_PAD_STRIP', 'TABLET_PAD_RING',
255                 'TABLET_PAD_BUTTON', 'SWITCH_TOGGLE',
256                 ]
257        for e in self.libinput_events():
258            self.assertIn('type', e)
259            self.assertIn(e['type'], types)
260
261    def test_events_libinput_time(self):
262        # DEVICE_ADDED has no time
263        # first event may have 0.0 time if the first frame generates a
264        # libinput event.
265        try:
266            for e in list(self.libinput_events())[2:]:
267                self.assertIn('time', e)
268                self.assertGreater(e['time'], 0.0)
269                self.assertLess(e['time'], 60.0)
270        except IndexError:
271            pass
272
273    def test_events_libinput_device_added(self):
274        keys = ['type', 'seat', 'logical_seat']
275        for e in self.libinput_events('DEVICE_ADDED'):
276            self.dict_key_crosscheck(e, keys)
277            self.assertEqual(e['seat'], 'seat0')
278            self.assertEqual(e['logical_seat'], 'default')
279
280    def test_events_libinput_pointer_motion(self):
281        keys = ['type', 'time', 'delta', 'unaccel']
282        for e in self.libinput_events('POINTER_MOTION'):
283            self.dict_key_crosscheck(e, keys)
284            delta = e['delta']
285            self.assertTrue(isinstance(delta, list))
286            self.assertEqual(len(delta), 2)
287            for d in delta:
288                self.assertTrue(isinstance(d, float))
289            unaccel = e['unaccel']
290            self.assertTrue(isinstance(unaccel, list))
291            self.assertEqual(len(unaccel), 2)
292            for d in unaccel:
293                self.assertTrue(isinstance(d, float))
294
295    def test_events_libinput_pointer_button(self):
296        keys = ['type', 'time', 'button', 'state', 'seat_count']
297        for e in self.libinput_events('POINTER_BUTTON'):
298            self.dict_key_crosscheck(e, keys)
299            button = e['button']
300            self.assertGreater(button, 0x100)  # BTN_0
301            self.assertLess(button, 0x160)  # KEY_OK
302            state = e['state']
303            self.assertIn(state, ['pressed', 'released'])
304            scount = e['seat_count']
305            self.assertGreaterEqual(scount, 0)
306
307    def test_events_libinput_pointer_absolute(self):
308        keys = ['type', 'time', 'point', 'transformed']
309        for e in self.libinput_events('POINTER_MOTION_ABSOLUTE'):
310            self.dict_key_crosscheck(e, keys)
311            point = e['point']
312            self.assertTrue(isinstance(point, list))
313            self.assertEqual(len(point), 2)
314            for p in point:
315                self.assertTrue(isinstance(p, float))
316                self.assertGreater(p, 0.0)
317                self.assertLess(p, 300.0)
318
319            transformed = e['transformed']
320            self.assertTrue(isinstance(transformed, list))
321            self.assertEqual(len(transformed), 2)
322            for t in transformed:
323                self.assertTrue(isinstance(t, float))
324                self.assertGreater(t, 0.0)
325                self.assertLess(t, 100.0)
326
327    def test_events_libinput_touch(self):
328        keys = ['type', 'time', 'slot', 'seat_slot']
329        for e in self.libinput_events():
330            if (not e['type'].startswith('TOUCH_') or
331                    e['type'] == 'TOUCH_FRAME'):
332                continue
333
334            for k in keys:
335                self.assertIn(k, e.keys())
336            slot = e['slot']
337            seat_slot = e['seat_slot']
338
339            self.assertGreaterEqual(slot, 0)
340            self.assertGreaterEqual(seat_slot, 0)
341
342    def test_events_libinput_touch_down(self):
343        keys = ['type', 'time', 'slot', 'seat_slot', 'point', 'transformed']
344        for e in self.libinput_events('TOUCH_DOWN'):
345            self.dict_key_crosscheck(e, keys)
346            point = e['point']
347            self.assertTrue(isinstance(point, list))
348            self.assertEqual(len(point), 2)
349            for p in point:
350                self.assertTrue(isinstance(p, float))
351                self.assertGreater(p, 0.0)
352                self.assertLess(p, 300.0)
353
354            transformed = e['transformed']
355            self.assertTrue(isinstance(transformed, list))
356            self.assertEqual(len(transformed), 2)
357            for t in transformed:
358                self.assertTrue(isinstance(t, float))
359                self.assertGreater(t, 0.0)
360                self.assertLess(t, 100.0)
361
362    def test_events_libinput_touch_motion(self):
363        keys = ['type', 'time', 'slot', 'seat_slot', 'point', 'transformed']
364        for e in self.libinput_events('TOUCH_MOTION'):
365            self.dict_key_crosscheck(e, keys)
366            point = e['point']
367            self.assertTrue(isinstance(point, list))
368            self.assertEqual(len(point), 2)
369            for p in point:
370                self.assertTrue(isinstance(p, float))
371                self.assertGreater(p, 0.0)
372                self.assertLess(p, 300.0)
373
374            transformed = e['transformed']
375            self.assertTrue(isinstance(transformed, list))
376            self.assertEqual(len(transformed), 2)
377            for t in transformed:
378                self.assertTrue(isinstance(t, float))
379                self.assertGreater(t, 0.0)
380                self.assertLess(t, 100.0)
381
382    def test_events_libinput_touch_frame(self):
383        devices = self.yaml['devices']
384        for d in devices:
385            events = d['events']
386            if not events:
387                raise unittest.SkipTest()
388            for e in events:
389                try:
390                    evdev = e['libinput']
391                except KeyError:
392                    continue
393
394                need_frame = False
395                for ev in evdev:
396                    t = ev['type']
397                    if not t.startswith('TOUCH_'):
398                        self.assertFalse(need_frame)
399                        continue
400
401                    if t == 'TOUCH_FRAME':
402                        self.assertTrue(need_frame)
403                        need_frame = False
404                    else:
405                        need_frame = True
406
407                self.assertFalse(need_frame)
408
409    def test_events_libinput_gesture_pinch(self):
410        keys = ['type', 'time', 'nfingers', 'delta',
411                'unaccel', 'angle_delta', 'scale']
412        for e in self.libinput_events(['GESTURE_PINCH_BEGIN',
413                                       'GESTURE_PINCH_UPDATE',
414                                       'GESTURE_PINCH_END']):
415            self.dict_key_crosscheck(e, keys)
416            delta = e['delta']
417            self.assertTrue(isinstance(delta, list))
418            self.assertEqual(len(delta), 2)
419            for d in delta:
420                self.assertTrue(isinstance(d, float))
421            unaccel = e['unaccel']
422            self.assertTrue(isinstance(unaccel, list))
423            self.assertEqual(len(unaccel), 2)
424            for d in unaccel:
425                self.assertTrue(isinstance(d, float))
426
427            adelta = e['angle_delta']
428            self.assertTrue(isinstance(adelta, list))
429            self.assertEqual(len(adelta), 2)
430            for d in adelta:
431                self.assertTrue(isinstance(d, float))
432
433            scale = e['scale']
434            self.assertTrue(isinstance(scale, list))
435            self.assertEqual(len(scale), 2)
436            for d in scale:
437                self.assertTrue(isinstance(d, float))
438
439    def test_events_libinput_gesture_swipe(self):
440        keys = ['type', 'time', 'nfingers', 'delta',
441                'unaccel']
442        for e in self.libinput_events(['GESTURE_SWIPE_BEGIN',
443                                       'GESTURE_SWIPE_UPDATE',
444                                       'GESTURE_SWIPE_END']):
445            self.dict_key_crosscheck(e, keys)
446            delta = e['delta']
447            self.assertTrue(isinstance(delta, list))
448            self.assertEqual(len(delta), 2)
449            for d in delta:
450                self.assertTrue(isinstance(d, float))
451            unaccel = e['unaccel']
452            self.assertTrue(isinstance(unaccel, list))
453            self.assertEqual(len(unaccel), 2)
454            for d in unaccel:
455                self.assertTrue(isinstance(d, float))
456
457    def test_events_libinput_tablet_pad_button(self):
458        keys = ['type', 'time', 'button', 'state', 'mode', 'is-toggle']
459
460        for e in self.libinput_events('TABLET_PAD_BUTTON'):
461            self.dict_key_crosscheck(e, keys)
462
463            b = e['button']
464            self.assertTrue(isinstance(b, int))
465            self.assertGreaterEqual(b, 0)
466            self.assertLessEqual(b, 16)
467
468            state = e['state']
469            self.assertIn(state, ['pressed', 'released'])
470
471            m = e['mode']
472            self.assertTrue(isinstance(m, int))
473            self.assertGreaterEqual(m, 0)
474            self.assertLessEqual(m, 3)
475
476            t = e['is-toggle']
477            self.assertTrue(isinstance(t, bool))
478
479    def test_events_libinput_tablet_pad_ring(self):
480        keys = ['type', 'time', 'number', 'position', 'source', 'mode']
481
482        for e in self.libinput_events('TABLET_PAD_RING'):
483            self.dict_key_crosscheck(e, keys)
484
485            n = e['number']
486            self.assertTrue(isinstance(n, int))
487            self.assertGreaterEqual(n, 0)
488            self.assertLessEqual(n, 4)
489
490            p = e['position']
491            self.assertTrue(isinstance(p, float))
492            if p != -1.0:  # special 'end' case
493                self.assertGreaterEqual(p, 0.0)
494                self.assertLess(p, 360.0)
495
496            m = e['mode']
497            self.assertTrue(isinstance(m, int))
498            self.assertGreaterEqual(m, 0)
499            self.assertLessEqual(m, 3)
500
501            s = e['source']
502            self.assertIn(s, ['finger', 'unknown'])
503
504    def test_events_libinput_tablet_pad_strip(self):
505        keys = ['type', 'time', 'number', 'position', 'source', 'mode']
506
507        for e in self.libinput_events('TABLET_PAD_STRIP'):
508            self.dict_key_crosscheck(e, keys)
509
510            n = e['number']
511            self.assertTrue(isinstance(n, int))
512            self.assertGreaterEqual(n, 0)
513            self.assertLessEqual(n, 4)
514
515            p = e['position']
516            self.assertTrue(isinstance(p, float))
517            if p != -1.0:  # special 'end' case
518                self.assertGreaterEqual(p, 0.0)
519                self.assertLessEqual(p, 1.0)
520
521            m = e['mode']
522            self.assertTrue(isinstance(m, int))
523            self.assertGreaterEqual(m, 0)
524            self.assertLessEqual(m, 3)
525
526            s = e['source']
527            self.assertIn(s, ['finger', 'unknown'])
528
529    def test_events_libinput_tablet_tool_proximity(self):
530        keys = ['type', 'time', 'proximity', 'tool-type', 'serial', 'axes']
531
532        for e in self.libinput_events('TABLET_TOOL_PROXIMITY'):
533            for k in keys:
534                self.assertIn(k, e)
535
536            p = e['proximity']
537            self.assertIn(p, ['in', 'out'])
538
539            p = e['tool-type']
540            self.assertIn(p, ['pen', 'eraser', 'brush', 'airbrush', 'mouse',
541                              'lens', 'unknown'])
542
543            s = e['serial']
544            self.assertTrue(isinstance(s, int))
545            self.assertGreaterEqual(s, 0)
546
547            a = e['axes']
548            for ax in e['axes']:
549                self.assertIn(a, 'pdtrsw')
550
551    def test_events_libinput_tablet_tool(self):
552        keys = ['type', 'time', 'tip']
553
554        for e in self.libinput_events(['TABLET_TOOL_AXIS',
555                                       'TABLET_TOOL_TIP']):
556            for k in keys:
557                self.assertIn(k, e)
558
559            t = e['tip']
560            self.assertIn(t, ['down', 'up'])
561
562    def test_events_libinput_tablet_tool_button(self):
563        keys = ['type', 'time', 'button', 'state']
564
565        for e in self.libinput_events('TABLET_TOOL_BUTTON'):
566            self.dict_key_crosscheck(e, keys)
567
568            b = e['button']
569            # STYLUS, STYLUS2, STYLUS3
570            self.assertIn(b, [0x14b, 0x14c, 0x139])
571
572            s = e['state']
573            self.assertIn(s, ['pressed', 'released'])
574
575    def test_events_libinput_tablet_tool_axes(self):
576        for e in self.libinput_events(['TABLET_TOOL_PROXIMITY',
577                                       'TABLET_TOOL_AXIS',
578                                       'TABLET_TOOL_TIP']):
579
580            point = e['point']
581            self.assertTrue(isinstance(point, list))
582            self.assertEqual(len(point), 2)
583            for p in point:
584                self.assertTrue(isinstance(p, float))
585                self.assertGreater(p, 0.0)
586
587            try:
588                tilt = e['tilt']
589                self.assertTrue(isinstance(tilt, list))
590                self.assertEqual(len(tilt), 2)
591                for t in tilt:
592                    self.assertTrue(isinstance(t, float))
593            except KeyError:
594                pass
595
596            try:
597                d = e['distance']
598                self.assertTrue(isinstance(d, float))
599                self.assertGreaterEqual(d, 0.0)
600                self.assertNotIn('pressure', e)
601            except KeyError:
602                pass
603
604            try:
605                p = e['pressure']
606                self.assertTrue(isinstance(p, float))
607                self.assertGreaterEqual(p, 0.0)
608                self.assertNotIn('distance', e)
609            except KeyError:
610                pass
611
612            try:
613                r = e['rotation']
614                self.assertTrue(isinstance(r, float))
615                self.assertGreaterEqual(r, 0.0)
616            except KeyError:
617                pass
618
619            try:
620                s = e['slider']
621                self.assertTrue(isinstance(s, float))
622                self.assertGreaterEqual(s, 0.0)
623            except KeyError:
624                pass
625
626            try:
627                w = e['wheel']
628                self.assertTrue(isinstance(w, float))
629                self.assertGreaterEqual(w, 0.0)
630                self.assertIn('wheel-discrete', e)
631                wd = e['wheel-discrete']
632                self.assertTrue(isinstance(wd, 1))
633                self.assertGreaterEqual(wd, 0.0)
634
635                def sign(x):
636                    (1, -1)[x < 0]
637                self.assertTrue(sign(w), sign(wd))
638            except KeyError:
639                pass
640
641    def test_events_libinput_switch(self):
642        keys = ['type', 'time', 'switch', 'state']
643
644        for e in self.libinput_events('SWITCH_TOGGLE'):
645            self.dict_key_crosscheck(e, keys)
646
647            s = e['switch']
648            self.assertTrue(isinstance(s, int))
649            self.assertIn(s, [0x00, 0x01])
650
651            # yaml converts on/off to true/false
652            state = e['state']
653            self.assertTrue(isinstance(state, bool))
654
655
656if __name__ == '__main__':
657    parser = argparse.ArgumentParser(description='Verify a YAML recording')
658    parser.add_argument('recording', metavar='recorded-file.yaml',
659                        type=str, help='Path to device recording')
660    parser.add_argument('--verbose', action='store_true')
661    args, remainder = parser.parse_known_args()
662    TestYaml.filename = args.recording
663    verbosity = 1
664    if args.verbose:
665        verbosity = 3
666
667    argv = [sys.argv[0], *remainder]
668    unittest.main(argv=argv, verbosity=verbosity)
669