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