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