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 os 27import resource 28import sys 29import subprocess 30import logging 31 32try: 33 import pytest 34except ImportError: 35 print('Failed to import pytest. Skipping.', file=sys.stderr) 36 sys.exit(77) 37 38 39logger = logging.getLogger('test') 40logger.setLevel(logging.DEBUG) 41 42if '@DISABLE_WARNING@' != 'yes': 43 print('This is the source file, run the one in the meson builddir instead') 44 sys.exit(1) 45 46 47def _disable_coredump(): 48 resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) 49 50 51def run_command(args): 52 logger.debug('run command: {}'.format(' '.join(args))) 53 with subprocess.Popen(args, preexec_fn=_disable_coredump, 54 stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p: 55 try: 56 p.wait(0.7) 57 except subprocess.TimeoutExpired: 58 p.send_signal(3) # SIGQUIT 59 stdout, stderr = p.communicate(timeout=5) 60 if p.returncode == -3: 61 p.returncode = 0 62 return p.returncode, stdout.decode('UTF-8'), stderr.decode('UTF-8') 63 64 65class LibinputTool(object): 66 libinput_tool = 'libinput' 67 subtool = None 68 69 def __init__(self, subtool=None): 70 self.libinput_tool = "@TOOL_PATH@" 71 self.subtool = subtool 72 73 def run_command(self, args): 74 args = [self.libinput_tool] + args 75 if self.subtool is not None: 76 args.insert(1, self.subtool) 77 78 return run_command(args) 79 80 def run_command_success(self, args): 81 rc, stdout, stderr = self.run_command(args) 82 # if we're running as user, we might fail the command but we should 83 # never get rc 2 (invalid usage) 84 assert rc in [0, 1], (stdout, stderr) 85 return stdout, stderr 86 87 def run_command_invalid(self, args): 88 rc, stdout, stderr = self.run_command(args) 89 assert rc == 2, (rc, stdout, stderr) 90 return rc, stdout, stderr 91 92 def run_command_unrecognized_option(self, args): 93 rc, stdout, stderr = self.run_command(args) 94 assert rc == 2, (rc, stdout, stderr) 95 assert stdout.startswith('Usage') or stdout == '' 96 assert 'unrecognized option' in stderr 97 98 def run_command_missing_arg(self, args): 99 rc, stdout, stderr = self.run_command(args) 100 assert rc == 2, (rc, stdout, stderr) 101 assert stdout.startswith('Usage') or stdout == '' 102 assert 'requires an argument' in stderr 103 104 def run_command_unrecognized_tool(self, args): 105 rc, stdout, stderr = self.run_command(args) 106 assert rc == 2, (rc, stdout, stderr) 107 assert stdout.startswith('Usage') or stdout == '' 108 assert 'is not installed' in stderr 109 110 111class LibinputDebugGui(LibinputTool): 112 def __init__(self, subtool='debug-gui'): 113 assert subtool == 'debug-gui' 114 super().__init__(subtool) 115 116 debug_gui_enabled = '@MESON_ENABLED_DEBUG_GUI@' == 'True' 117 if not debug_gui_enabled: 118 pytest.skip() 119 120 if not os.getenv('DISPLAY') and not os.getenv('WAYLAND_DISPLAY'): 121 pytest.skip() 122 123 # 77 means gtk_init() failed, which is probably because you can't 124 # connect to the display server. 125 rc, _, _ = self.run_command(['--help']) 126 if rc == 77: 127 pytest.skip() 128 129 130def get_tool(subtool=None): 131 if subtool == 'debug-gui': 132 return LibinputDebugGui() 133 else: 134 return LibinputTool(subtool) 135 136 137@pytest.fixture 138def libinput(): 139 return get_tool() 140 141 142@pytest.fixture(params=['debug-events', 'debug-gui']) 143def libinput_debug_tool(request): 144 yield get_tool(request.param) 145 146 147@pytest.fixture 148def libinput_debug_events(): 149 return get_tool('debug-events') 150 151 152@pytest.fixture 153def libinput_debug_gui(): 154 return get_tool('debug-gui') 155 156 157@pytest.fixture 158def libinput_record(): 159 return get_tool('record') 160 161 162def test_help(libinput): 163 stdout, stderr = libinput.run_command_success(['--help']) 164 assert stdout.startswith('Usage:') 165 assert stderr == '' 166 167 168def test_version(libinput): 169 stdout, stderr = libinput.run_command_success(['--version']) 170 assert stdout.startswith('1') 171 assert stderr == '' 172 173 174@pytest.mark.parametrize('argument', ['--banana', '--foo', '--quiet', '--verbose']) 175def test_invalid_arguments(libinput, argument): 176 libinput.run_command_unrecognized_option([argument]) 177 178 179@pytest.mark.parametrize('tool', [['foo'], ['debug'], ['foo', '--quiet']]) 180def test_invalid_tool(libinput, tool): 181 libinput.run_command_unrecognized_tool(tool) 182 183 184def test_udev_seat(libinput_debug_tool): 185 libinput_debug_tool.run_command_missing_arg(['--udev']) 186 libinput_debug_tool.run_command_success(['--udev', 'seat0']) 187 libinput_debug_tool.run_command_success(['--udev', 'seat1']) 188 189 190@pytest.mark.skipif(os.environ.get('UDEV_NOT_AVAILABLE'), reason='udev required') 191def test_device_arg(libinput_debug_tool): 192 libinput_debug_tool.run_command_missing_arg(['--device']) 193 libinput_debug_tool.run_command_success(['--device', '/dev/input/event0']) 194 libinput_debug_tool.run_command_success(['--device', '/dev/input/event1']) 195 libinput_debug_tool.run_command_success(['/dev/input/event0']) 196 197 198options = { 199 'pattern': ['sendevents'], 200 # enable/disable options 201 'enable-disable': [ 202 'tap', 203 'drag', 204 'drag-lock', 205 'middlebutton', 206 'natural-scrolling', 207 'left-handed', 208 'dwt' 209 ], 210 # options with distinct values 211 'enums': { 212 'set-click-method': ['none', 'clickfinger', 'buttonareas'], 213 'set-scroll-method': ['none', 'twofinger', 'edge', 'button'], 214 'set-profile': ['adaptive', 'flat'], 215 'set-tap-map': ['lrm', 'lmr'], 216 }, 217 # options with a range 218 'ranges': { 219 'set-speed': (float, -1.0, +1.0), 220 } 221} 222 223 224# Options that allow for glob patterns 225@pytest.mark.parametrize('option', options['pattern']) 226def test_options_pattern(libinput_debug_tool, option): 227 libinput_debug_tool.run_command_success(['--disable-{}'.format(option), '*']) 228 libinput_debug_tool.run_command_success(['--disable-{}'.format(option), 'abc*']) 229 230 231@pytest.mark.parametrize('option', options['enable-disable']) 232def test_options_enable_disable(libinput_debug_tool, option): 233 libinput_debug_tool.run_command_success(['--enable-{}'.format(option)]) 234 libinput_debug_tool.run_command_success(['--disable-{}'.format(option)]) 235 236 237@pytest.mark.parametrize('option', options['enums'].items()) 238def test_options_enums(libinput_debug_tool, option): 239 name, values = option 240 for v in values: 241 libinput_debug_tool.run_command_success(['--{}'.format(name), v]) 242 libinput_debug_tool.run_command_success(['--{}={}'.format(name, v)]) 243 244 245@pytest.mark.parametrize('option', options['ranges'].items()) 246def test_options_ranges(libinput_debug_tool, option): 247 name, values = option 248 range_type, minimum, maximum = values 249 assert range_type == float 250 step = (maximum - minimum) / 10.0 251 value = minimum 252 while value < maximum: 253 libinput_debug_tool.run_command_success(['--{}'.format(name), str(value)]) 254 libinput_debug_tool.run_command_success(['--{}={}'.format(name, value)]) 255 value += step 256 libinput_debug_tool.run_command_success(['--{}'.format(name), str(maximum)]) 257 libinput_debug_tool.run_command_success(['--{}={}'.format(name, maximum)]) 258 259 260def test_apply_to(libinput_debug_tool): 261 libinput_debug_tool.run_command_missing_arg(['--apply-to']) 262 libinput_debug_tool.run_command_success(['--apply-to', '*foo*']) 263 libinput_debug_tool.run_command_success(['--apply-to', 'foobar']) 264 libinput_debug_tool.run_command_success(['--apply-to', 'any']) 265 266 267@pytest.mark.parametrize('args', [['--verbose'], ['--quiet'], 268 ['--verbose', '--quiet'], 269 ['--quiet', '--verbose']]) 270def test_debug_events_verbose_quiet(libinput_debug_events, args): 271 libinput_debug_events.run_command_success(args) 272 273 274@pytest.mark.parametrize('arg', ['--banana', '--foo', '--version']) 275def test_invalid_args(libinput_debug_tool, arg): 276 libinput_debug_tool.run_command_unrecognized_option([arg]) 277 278 279def test_libinput_debug_events_multiple_devices(libinput_debug_events): 280 libinput_debug_events.run_command_success(['--device', '/dev/input/event0', '/dev/input/event1']) 281 # same event path multiple times? meh, your problem 282 libinput_debug_events.run_command_success(['--device', '/dev/input/event0', '/dev/input/event0']) 283 libinput_debug_events.run_command_success(['/dev/input/event0', '/dev/input/event1']) 284 285 286def test_libinput_debug_events_too_many_devices(libinput_debug_events): 287 # Too many arguments just bails with the usage message 288 rc, stdout, stderr = libinput_debug_events.run_command(['/dev/input/event0'] * 61) 289 assert rc == 2, (stdout, stderr) 290 291 292@pytest.mark.parametrize('arg', ['--quiet']) 293def test_libinput_debug_gui_invalid_arg(libinput_debug_gui, arg): 294 libinput_debug_gui.run_command_unrecognized_option([arg]) 295 296 297def test_libinput_debug_gui_verbose(libinput_debug_gui): 298 libinput_debug_gui.run_command_success(['--verbose']) 299 300 301@pytest.mark.parametrize('arg', ['--help', '--show-keycodes', '--with-libinput']) 302def test_libinput_record_args(libinput_record, arg): 303 libinput_record.run_command_success([arg]) 304 305 306def test_libinput_record_multiple_arg(libinput_record): 307 # this arg is deprecated and a noop 308 libinput_record.run_command_success(['--multiple']) 309 310 311@pytest.fixture 312def recording(tmp_path): 313 return str((tmp_path / 'record.out').resolve()) 314 315 316def test_libinput_record_all(libinput_record, recording): 317 libinput_record.run_command_success(['--all', '-o', recording]) 318 libinput_record.run_command_success(['--all', recording]) 319 320 321def test_libinput_record_outfile(libinput_record, recording): 322 libinput_record.run_command_success(['-o', recording]) 323 libinput_record.run_command_success(['--output-file', recording]) 324 libinput_record.run_command_success(['--output-file={}'.format(recording)]) 325 326 327def test_libinput_record_single(libinput_record, recording): 328 libinput_record.run_command_success(['/dev/input/event0']) 329 libinput_record.run_command_success(['-o', recording, '/dev/input/event0']) 330 libinput_record.run_command_success(['/dev/input/event0', recording]) 331 libinput_record.run_command_success([recording, '/dev/input/event0']) 332 333 334def test_libinput_record_multiple(libinput_record, recording): 335 libinput_record.run_command_success(['-o', recording, '/dev/input/event0', '/dev/input/event1']) 336 libinput_record.run_command_success([recording, '/dev/input/event0', '/dev/input/event1']) 337 libinput_record.run_command_success(['/dev/input/event0', '/dev/input/event1', recording]) 338 339 340def test_libinput_record_autorestart(libinput_record, recording): 341 libinput_record.run_command_invalid(['--autorestart']) 342 libinput_record.run_command_invalid(['--autorestart=2']) 343 libinput_record.run_command_success(['-o', recording, '--autorestart=2']) 344 345 346def main(): 347 args = ['-m', 'pytest'] 348 try: 349 import xdist # noqa 350 args += ['-n', 'auto'] 351 except ImportError: 352 logger.info('python-xdist missing, this test will be slow') 353 pass 354 355 args += ['@MESON_BUILD_ROOT@'] 356 357 os.environ['LIBINPUT_RUNNING_TEST_SUITE'] = '1' 358 359 return subprocess.run([sys.executable] + args).returncode 360 361 362if __name__ == '__main__': 363 raise SystemExit(main()) 364