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