tools: switch tool option parsing test to use pytest
authorPeter Hutterer <peter.hutterer@who-t.net>
Mon, 16 Mar 2020 03:13:41 +0000 (13:13 +1000)
committerPeter Hutterer <peter.hutterer@who-t.net>
Tue, 17 Mar 2020 00:08:48 +0000 (10:08 +1000)
pytest is more powerful than unittest, so let's switch to that instead. And in
the process fix a few tests that for some reason succeeded even though they
shouldn't have (e.g. the autorestart test).

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
.gitlab-ci.yml
.gitlab-ci/gitlab-ci.tmpl
meson.build
tools/test-tool-option-parsing.py [deleted file]
tools/test_tool_option_parsing.py [new file with mode: 0755]

index 39c7c0d..9aa8d97 100644 (file)
@@ -64,10 +64,10 @@ variables:
   # See the documentation here:                                                 #
   # https://wayland.freedesktop.org/libinput/doc/latest/building_libinput.html  #
   ###############################################################################
-  FEDORA_RPMS:        'git gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme libwacom-devel cairo-devel   gtk3-devel   glib2-devel    mtdev-devel diffutils'
-  FEDORA_QEMU_RPMS:   'git gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme libwacom-devel cairo-devel   gtk3-devel   glib2-devel    mtdev-devel diffutils valgrind'
-  UBUNTU_CUSTOM_DEBS: 'git gcc g++     pkg-config         meson check       libudev-dev   libevdev-dev   doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx-rtd-theme libwacom-dev   libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev'
-  ARCH_PKGS:          'git gcc         pkgconfig          meson check       libsystemd    libevdev       doxygen graphviz python-sphinx  python-recommonmark  python-sphinx_rtd_theme  libwacom                     gtk3                        mtdev      diffutils'
+  FEDORA_RPMS:        'git gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme python3-pytest-xdist libwacom-devel cairo-devel   gtk3-devel   glib2-devel    mtdev-devel diffutils'
+  FEDORA_QEMU_RPMS:   'git gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme python3-pytest-xdist libwacom-devel cairo-devel   gtk3-devel   glib2-devel    mtdev-devel diffutils valgrind'
+  UBUNTU_CUSTOM_DEBS: 'git gcc g++     pkg-config         meson check       libudev-dev   libevdev-dev   doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx-rtd-theme python3-pytest-xdist libwacom-dev   libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev'
+  ARCH_PKGS:          'git gcc         pkgconfig          meson check       libsystemd    libevdev       doxygen graphviz python-sphinx  python-recommonmark  python-sphinx_rtd_theme  python-pytest-xdist  libwacom                     gtk3                        mtdev      diffutils'
   FREEBSD_BUILD_PKGS: 'meson'
   FREEBSD_PKGS:       'libepoll-shim                                        libudev-devd  libevdev                                                                                     libwacom                     gtk3                        libmtdev   '
   ALPINE_PKGS:        'git gcc build-base pkgconfig       meson check-dev   eudev-dev     libevdev-dev                                                                                 libwacom-dev   cairo-dev     gtk+3.0-dev  mtdev-dev bash'
@@ -77,12 +77,12 @@ variables:
   # changing these will force rebuilding the associated image
   # Note: these tags have no meaning and are not tied to a particular
   # libinput version
-  FEDORA_TAG:  '2020-02-26.0'
-  UBUNTU_TAG:  '2020-02-26.0'
-  ARCH_TAG:    '2020-02-26.0'
+  FEDORA_TAG:  '2020-03-16.0'
+  UBUNTU_TAG:  '2020-03-16.0'
+  ARCH_TAG:    '2020-03-16.0'
   ALPINE_TAG:  '2020-02-26.0'
   FREEBSD_TAG: '2020-02-26.0'
-  QEMU_TAG:    'qemu-vm-2020-02-26.0'
+  QEMU_TAG:    'qemu-vm-2020-03-16.0'
 
   UBUNTU_EXEC: "bash .gitlab-ci/ubuntu_install.sh $UBUNTU_CUSTOM_DEBS"
 
index 4d81430..578949b 100644 (file)
@@ -54,10 +54,10 @@ variables:
   # See the documentation here:                                                 #
   # https://wayland.freedesktop.org/libinput/doc/latest/building_libinput.html  #
   ###############################################################################
-  FEDORA_RPMS:        'git gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme libwacom-devel cairo-devel   gtk3-devel   glib2-devel    mtdev-devel diffutils'
-  FEDORA_QEMU_RPMS:   'git gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme libwacom-devel cairo-devel   gtk3-devel   glib2-devel    mtdev-devel diffutils valgrind'
-  UBUNTU_CUSTOM_DEBS: 'git gcc g++     pkg-config         meson check       libudev-dev   libevdev-dev   doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx-rtd-theme libwacom-dev   libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev'
-  ARCH_PKGS:          'git gcc         pkgconfig          meson check       libsystemd    libevdev       doxygen graphviz python-sphinx  python-recommonmark  python-sphinx_rtd_theme  libwacom                     gtk3                        mtdev      diffutils'
+  FEDORA_RPMS:        'git gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme python3-pytest-xdist libwacom-devel cairo-devel   gtk3-devel   glib2-devel    mtdev-devel diffutils'
+  FEDORA_QEMU_RPMS:   'git gcc gcc-c++ pkgconf-pkg-config meson check-devel libudev-devel libevdev-devel doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx_rtd_theme python3-pytest-xdist libwacom-devel cairo-devel   gtk3-devel   glib2-devel    mtdev-devel diffutils valgrind'
+  UBUNTU_CUSTOM_DEBS: 'git gcc g++     pkg-config         meson check       libudev-dev   libevdev-dev   doxygen graphviz python3-sphinx python3-recommonmark python3-sphinx-rtd-theme python3-pytest-xdist libwacom-dev   libcairo2-dev libgtk-3-dev libglib2.0-dev libmtdev-dev'
+  ARCH_PKGS:          'git gcc         pkgconfig          meson check       libsystemd    libevdev       doxygen graphviz python-sphinx  python-recommonmark  python-sphinx_rtd_theme  python-pytest-xdist  libwacom                     gtk3                        mtdev      diffutils'
   FREEBSD_BUILD_PKGS: 'meson'
   FREEBSD_PKGS:       'libepoll-shim                                        libudev-devd  libevdev                                                                                     libwacom                     gtk3                        libmtdev   '
   ALPINE_PKGS:        'git gcc build-base pkgconfig       meson check-dev   eudev-dev     libevdev-dev                                                                                 libwacom-dev   cairo-dev     gtk+3.0-dev  mtdev-dev bash'
@@ -67,12 +67,12 @@ variables:
   # changing these will force rebuilding the associated image
   # Note: these tags have no meaning and are not tied to a particular
   # libinput version
-  FEDORA_TAG:  '2020-02-26.0'
-  UBUNTU_TAG:  '2020-02-26.0'
-  ARCH_TAG:    '2020-02-26.0'
+  FEDORA_TAG:  '2020-03-16.0'
+  UBUNTU_TAG:  '2020-03-16.0'
+  ARCH_TAG:    '2020-03-16.0'
   ALPINE_TAG:  '2020-02-26.0'
   FREEBSD_TAG: '2020-02-26.0'
-  QEMU_TAG:    'qemu-vm-2020-02-26.0'
+  QEMU_TAG:    'qemu-vm-2020-03-16.0'
 
   UBUNTU_EXEC: "bash .gitlab-ci/ubuntu_install.sh $UBUNTU_CUSTOM_DEBS"
 
index ffa2faa..7dedfb9 100644 (file)
@@ -701,13 +701,16 @@ executable('ptraccel-debug',
 # subtool lookup
 if get_option('buildtype') == 'debug' or get_option('buildtype') == 'debugoptimized'
        config_tool_option_test = configuration_data()
+       config_tool_option_test.set('DISABLE_WARNING', 'yes')
        config_tool_option_test.set('MESON_ENABLED_DEBUG_GUI', get_option('debug-gui'))
-       tool_option_test = configure_file(input: 'tools/test-tool-option-parsing.py',
-                                         output: '@BASENAME@',
+       config_tool_option_test.set('MESON_BUILD_ROOT', meson.current_build_dir())
+       config_tool_option_test.set('TOOL_PATH', libinput_tool.full_path())
+       tool_option_test = configure_file(input: 'tools/test_tool_option_parsing.py',
+                                         output: '@PLAINNAME@',
                                          configuration : config_tool_option_test)
        test('tool-option-parsing',
             tool_option_test,
-            args : ['--tool-path', libinput_tool.full_path()],
+            args : [tool_option_test, '-n', 'auto'],
             suite : ['all', 'root'],
             timeout : 240)
 endif
diff --git a/tools/test-tool-option-parsing.py b/tools/test-tool-option-parsing.py
deleted file mode 100755 (executable)
index 0776095..0000000
+++ /dev/null
@@ -1,308 +0,0 @@
-#!/usr/bin/env python3
-# vim: set expandtab shiftwidth=4:
-# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
-#
-# Copyright © 2018 Red Hat, Inc.
-#
-# Permission is hereby granted, free of charge, to any person obtaining a
-# copy of this software and associated documentation files (the "Software"),
-# to deal in the Software without restriction, including without limitation
-# the rights to use, copy, modify, merge, publish, distribute, sublicense,
-# and/or sell copies of the Software, and to permit persons to whom the
-# Software is furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice (including the next
-# paragraph) shall be included in all copies or substantial portions of the
-# Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
-# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-# DEALINGS IN THE SOFTWARE.
-
-import argparse
-import os
-import unittest
-import resource
-import sys
-import subprocess
-import tempfile
-from pathlib import Path
-
-
-def _disable_coredump():
-    resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
-
-
-def run_command(args):
-    with subprocess.Popen(args, preexec_fn=_disable_coredump,
-                          stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
-        try:
-            p.wait(0.7)
-        except subprocess.TimeoutExpired:
-            p.send_signal(3)  # SIGQUIT
-        stdout, stderr = p.communicate(timeout=5)
-        if p.returncode == -3:
-            p.returncode = 0
-        return p.returncode, stdout.decode('UTF-8'), stderr.decode('UTF-8')
-
-
-class TestLibinputTool(unittest.TestCase):
-    libinput_tool = 'libinput'
-    subtool = None
-
-    def run_command(self, args):
-        args = [self.libinput_tool] + args
-        if self.subtool is not None:
-            args.insert(1, self.subtool)
-
-        return run_command(args)
-
-    def run_command_success(self, args):
-        rc, stdout, stderr = self.run_command(args)
-        # if we're running as user, we might fail the command but we should
-        # never get rc 2 (invalid usage)
-        self.assertIn(rc, [0, 1], msg=(stdout, stderr))
-
-    def run_command_unrecognized_option(self, args):
-        rc, stdout, stderr = self.run_command(args)
-        self.assertEqual(rc, 2)
-        self.assertTrue(stdout.startswith('Usage') or stdout == '')
-        self.assertIn('unrecognized option', stderr)
-
-    def run_command_missing_arg(self, args):
-        rc, stdout, stderr = self.run_command(args)
-        self.assertEqual(rc, 2)
-        self.assertTrue(stdout.startswith('Usage') or stdout == '')
-        self.assertIn('requires an argument', stderr)
-
-    def run_command_unrecognized_tool(self, args):
-        rc, stdout, stderr = self.run_command(args)
-        self.assertEqual(rc, 2)
-        self.assertTrue(stdout.startswith('Usage') or stdout == '')
-        self.assertIn('is not a libinput command', stderr)
-
-
-class TestLibinputCommand(TestLibinputTool):
-    subtool = None
-
-    def test_help(self):
-        rc, stdout, stderr = self.run_command(['--help'])
-        self.assertEqual(rc, 0)
-        self.assertTrue(stdout.startswith('Usage:'))
-        self.assertEqual(stderr, '')
-
-    def test_version(self):
-        rc, stdout, stderr = self.run_command(['--version'])
-        self.assertEqual(rc, 0)
-        self.assertTrue(stdout.startswith('1'))
-        self.assertEqual(stderr, '')
-
-    def test_invalid_arguments(self):
-        self.run_command_unrecognized_option(['--banana'])
-        self.run_command_unrecognized_option(['--foo'])
-        self.run_command_unrecognized_option(['--quiet'])
-        self.run_command_unrecognized_option(['--verbose'])
-        self.run_command_unrecognized_option(['--quiet', 'foo'])
-
-    def test_invalid_tools(self):
-        self.run_command_unrecognized_tool(['foo'])
-        self.run_command_unrecognized_tool(['debug'])
-        self.run_command_unrecognized_tool(['foo', '--quiet'])
-
-
-class TestToolWithOptions(object):
-    options = {
-        'pattern': ['sendevents'],
-        # enable/disable options
-        'enable-disable': [
-            'tap',
-            'drag',
-            'drag-lock',
-            'middlebutton',
-            'natural-scrolling',
-            'left-handed',
-            'dwt'
-        ],
-        # options with distinct values
-        'enums': {
-            'set-click-method': ['none', 'clickfinger', 'buttonareas'],
-            'set-scroll-method': ['none', 'twofinger', 'edge', 'button'],
-            'set-profile': ['adaptive', 'flat'],
-            'set-tap-map': ['lrm', 'lmr'],
-        },
-        # options with a range
-        'ranges': {
-            'set-speed': (float, -1.0, +1.0),
-        }
-    }
-
-    def test_udev_seat(self):
-        self.run_command_missing_arg(['--udev'])
-        self.run_command_success(['--udev', 'seat0'])
-        self.run_command_success(['--udev', 'seat1'])
-
-    @unittest.skipIf(os.environ.get('UDEV_NOT_AVAILABLE'), "udev required")
-    def test_device(self):
-        self.run_command_missing_arg(['--device'])
-        self.run_command_success(['--device', '/dev/input/event0'])
-        self.run_command_success(['--device', '/dev/input/event1'])
-        self.run_command_success(['/dev/input/event0'])
-
-    def test_options_pattern(self):
-        for option in self.options['pattern']:
-            self.run_command_success(['--disable-{}'.format(option), '*'])
-            self.run_command_success(['--disable-{}'.format(option), 'abc*'])
-
-    def test_options_enable_disable(self):
-        for option in self.options['enable-disable']:
-            self.run_command_success(['--enable-{}'.format(option)])
-            self.run_command_success(['--disable-{}'.format(option)])
-
-    def test_options_enums(self):
-        for option, values in self.options['enums'].items():
-            for v in values:
-                self.run_command_success(['--{}'.format(option), v])
-                self.run_command_success(['--{}={}'.format(option, v)])
-
-    def test_options_ranges(self):
-        for option, values in self.options['ranges'].items():
-            range_type, minimum, maximum = values
-            self.assertEqual(range_type, float)
-            step = (maximum - minimum) / 10.0
-            value = minimum
-            while value < maximum:
-                self.run_command_success(['--{}'.format(option), str(value)])
-                self.run_command_success(['--{}={}'.format(option, value)])
-                value += step
-            self.run_command_success(['--{}'.format(option), str(maximum)])
-            self.run_command_success(['--{}={}'.format(option, maximum)])
-
-    def test_apply_to(self):
-        self.run_command_missing_arg(['--apply-to'])
-        self.run_command_success(['--apply-to', '*foo*'])
-        self.run_command_success(['--apply-to', 'foobar'])
-        self.run_command_success(['--apply-to', 'any'])
-
-
-class TestDebugEvents(TestToolWithOptions, TestLibinputTool):
-    subtool = 'debug-events'
-
-    def test_verbose_quiet(self):
-        rc, stdout, stderr = self.run_command(['--verbose'])
-        self.assertEqual(rc, 0)
-        rc, stdout, stderr = self.run_command(['--quiet'])
-        self.assertEqual(rc, 0)
-        rc, stdout, stderr = self.run_command(['--verbose', '--quiet'])
-        self.assertEqual(rc, 0)
-        rc, stdout, stderr = self.run_command(['--quiet', '--verbose'])
-        self.assertEqual(rc, 0)
-
-    def test_invalid_arguments(self):
-        self.run_command_unrecognized_option(['--banana'])
-        self.run_command_unrecognized_option(['--foo'])
-        self.run_command_unrecognized_option(['--version'])
-
-    def test_multiple_devices(self):
-        self.run_command_success(['--device', '/dev/input/event0', '/dev/input/event1'])
-        # same event path multiple times? meh, your problem
-        self.run_command_success(['--device', '/dev/input/event0', '/dev/input/event0'])
-        self.run_command_success(['/dev/input/event0', '/dev/input/event1'])
-
-    def test_too_many_devices(self):
-        # Too many arguments just bails with the usage message
-        rc, stdout, stderr = self.run_command(['/dev/input/event0'] * 61)
-        self.assertEqual(rc, 2, msg=(stdout, stderr))
-
-
-class TestDebugGUI(TestToolWithOptions, TestLibinputTool):
-    subtool = 'debug-gui'
-
-    @classmethod
-    def setUpClass(cls):
-        # This is set by meson
-        debug_gui_enabled = @MESON_ENABLED_DEBUG_GUI@ # noqa
-        if not debug_gui_enabled:
-            raise unittest.SkipTest()
-
-        if not os.getenv('DISPLAY') and not os.getenv('WAYLAND_DISPLAY'):
-            raise unittest.SkipTest()
-
-        # 77 means gtk_init() failed, which is probably because you can't
-        # connect to the display server.
-        rc, _, _ = run_command([TestLibinputTool.libinput_tool, cls.subtool, '--help'])
-        if rc == 77:
-            raise unittest.SkipTest()
-
-    def test_verbose_quiet(self):
-        rc, stdout, stderr = self.run_command(['--verbose'])
-        self.assertEqual(rc, 0)
-
-    def test_invalid_arguments(self):
-        self.run_command_unrecognized_option(['--quiet'])
-        self.run_command_unrecognized_option(['--banana'])
-        self.run_command_unrecognized_option(['--foo'])
-        self.run_command_unrecognized_option(['--version'])
-
-
-class TestRecord(TestLibinputTool):
-    subtool = 'record'
-
-    def setUp(self):
-        self.tmpdir = tempfile.TemporaryDirectory()
-        self.outfile = Path(self.tmpdir.name, 'record.out')
-
-    def tearDown(self):
-        self.tmpdir.cleanup()
-
-    def test_args(self):
-        self.run_command_success(['--help'])
-        self.run_command_success(['--show-keycodes'])
-        self.run_command_success(['--with-libinput'])
-
-    def test_multiple_deprecated(self):
-        # this arg is deprecated and a noop
-        self.run_command_success(['--multiple'])
-
-    def test_all(self):
-        self.run_command_success(['--all', '-o', self.outfile])
-        self.run_command_success(['--all', self.outfile])
-
-    def test_autorestart(self):
-        self.run_command_success(['--autorestart=2'])
-
-    def test_outfile(self):
-        self.run_command_success(['-o', self.outfile])
-        self.run_command_success(['--output-file', self.outfile])
-        self.run_command_success(['--output-file={}'.format(self.outfile)])
-
-    def test_device_single(self):
-        self.run_command_success(['/dev/input/event0'])
-        self.run_command_success(['/dev/input/event0', self.outfile])
-        self.run_command_success([self.outfile, '/dev/input/event0'])
-        self.run_command_success([self.outfile, '/dev/input/event0'])
-
-    def test_device_multiple(self):
-        self.run_command_success(['-o', self.outfile, '/dev/input/event0', '/dev/input/event1'])
-        self.run_command_success([self.outfile, '/dev/input/event0', '/dev/input/event1'])
-        self.run_command_success(['/dev/input/event0', '/dev/input/event1', self.outfile])
-
-
-if __name__ == '__main__':
-    parser = argparse.ArgumentParser(description='Verify a libinput tool\'s option parsing')
-    parser.add_argument('--tool-path', metavar='/path/to/builddir/libinput',
-                        type=str,
-                        help='Path to the libinput tool in the builddir')
-    parser.add_argument('--verbose', action='store_true')
-    args, remainder = parser.parse_known_args()
-    if args.tool_path is not None:
-        TestLibinputTool.libinput_tool = args.tool_path
-    verbosity = 1
-    if args.verbose:
-        verbosity = 3
-
-    argv = [sys.argv[0], *remainder]
-    unittest.main(verbosity=verbosity, argv=argv)
diff --git a/tools/test_tool_option_parsing.py b/tools/test_tool_option_parsing.py
new file mode 100755 (executable)
index 0000000..a48962b
--- /dev/null
@@ -0,0 +1,355 @@
+#!/usr/bin/env python3
+# vim: set expandtab shiftwidth=4:
+# -*- Mode: python; coding: utf-8; indent-tabs-mode: nil -*- */
+#
+# Copyright © 2018 Red Hat, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice (including the next
+# paragraph) shall be included in all copies or substantial portions of the
+# Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+import os
+import pytest
+import resource
+import sys
+import subprocess
+import logging
+
+logger = logging.getLogger('test')
+logger.setLevel(logging.DEBUG)
+
+if '@DISABLE_WARNING@' != 'yes':
+    print('This is the source file, run the one in the meson builddir instead')
+    sys.exit(1)
+
+
+def _disable_coredump():
+    resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
+
+
+def run_command(args):
+    logger.debug('run command: {}'.format(' '.join(args)))
+    with subprocess.Popen(args, preexec_fn=_disable_coredump,
+                          stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
+        try:
+            p.wait(0.7)
+        except subprocess.TimeoutExpired:
+            p.send_signal(3)  # SIGQUIT
+        stdout, stderr = p.communicate(timeout=5)
+        if p.returncode == -3:
+            p.returncode = 0
+        return p.returncode, stdout.decode('UTF-8'), stderr.decode('UTF-8')
+
+
+class LibinputTool(object):
+    libinput_tool = 'libinput'
+    subtool = None
+
+    def __init__(self, subtool=None):
+        self.libinput_tool = "@TOOL_PATH@"
+        self.subtool = subtool
+
+    def run_command(self, args):
+        args = [self.libinput_tool] + args
+        if self.subtool is not None:
+            args.insert(1, self.subtool)
+
+        return run_command(args)
+
+    def run_command_success(self, args):
+        rc, stdout, stderr = self.run_command(args)
+        # if we're running as user, we might fail the command but we should
+        # never get rc 2 (invalid usage)
+        assert rc in [0, 1], (stdout, stderr)
+        return stdout, stderr
+
+    def run_command_invalid(self, args):
+        rc, stdout, stderr = self.run_command(args)
+        assert rc == 2, (rc, stdout, stderr)
+        return rc, stdout, stderr
+
+    def run_command_unrecognized_option(self, args):
+        rc, stdout, stderr = self.run_command(args)
+        assert rc == 2, (rc, stdout, stderr)
+        assert stdout.startswith('Usage') or stdout == ''
+        assert 'unrecognized option' in stderr
+
+    def run_command_missing_arg(self, args):
+        rc, stdout, stderr = self.run_command(args)
+        assert rc == 2, (rc, stdout, stderr)
+        assert stdout.startswith('Usage') or stdout == ''
+        assert 'requires an argument' in stderr
+
+    def run_command_unrecognized_tool(self, args):
+        rc, stdout, stderr = self.run_command(args)
+        assert rc == 2, (rc, stdout, stderr)
+        assert stdout.startswith('Usage') or stdout == ''
+        assert 'is not a libinput command' in stderr
+
+
+class LibinputDebugGui(LibinputTool):
+    def __init__(self, subtool='debug-gui'):
+        assert subtool == 'debug-gui'
+        super().__init__(subtool)
+
+        debug_gui_enabled = '@MESON_ENABLED_DEBUG_GUI@' == 'True'
+        if not debug_gui_enabled:
+            pytest.skip()
+
+        if not os.getenv('DISPLAY') and not os.getenv('WAYLAND_DISPLAY'):
+            pytest.skip()
+
+        # 77 means gtk_init() failed, which is probably because you can't
+        # connect to the display server.
+        rc, _, _ = self.run_command(['--help'])
+        if rc == 77:
+            pytest.skip()
+
+
+def get_tool(subtool=None):
+    if subtool == 'debug-gui':
+        return LibinputDebugGui()
+    else:
+        return LibinputTool(subtool)
+
+
+@pytest.fixture
+def libinput():
+    return get_tool()
+
+
+@pytest.fixture(params=['debug-events', 'debug-gui'])
+def libinput_debug_tool(request):
+    yield get_tool(request.param)
+
+
+@pytest.fixture
+def libinput_debug_events():
+    return get_tool('debug-events')
+
+
+@pytest.fixture
+def libinput_debug_gui():
+    return get_tool('debug-gui')
+
+
+@pytest.fixture
+def libinput_record():
+    return get_tool('record')
+
+
+def test_help(libinput):
+    stdout, stderr = libinput.run_command_success(['--help'])
+    assert stdout.startswith('Usage:')
+    assert stderr == ''
+
+
+def test_version(libinput):
+    stdout, stderr = libinput.run_command_success(['--version'])
+    assert stdout.startswith('1')
+    assert stderr == ''
+
+
+@pytest.mark.parametrize('argument', ['--banana', '--foo', '--quiet', '--verbose'])
+def test_invalid_arguments(libinput, argument):
+    libinput.run_command_unrecognized_option([argument])
+
+
+@pytest.mark.parametrize('tool', [['foo'], ['debug'], ['foo', '--quiet']])
+def test_invalid_tool(libinput, tool):
+    libinput.run_command_unrecognized_tool(tool)
+
+
+def test_udev_seat(libinput_debug_tool):
+    libinput_debug_tool.run_command_missing_arg(['--udev'])
+    libinput_debug_tool.run_command_success(['--udev', 'seat0'])
+    libinput_debug_tool.run_command_success(['--udev', 'seat1'])
+
+
+@pytest.mark.skipif(os.environ.get('UDEV_NOT_AVAILABLE'), reason='udev required')
+def test_device_arg(libinput_debug_tool):
+    libinput_debug_tool.run_command_missing_arg(['--device'])
+    libinput_debug_tool.run_command_success(['--device', '/dev/input/event0'])
+    libinput_debug_tool.run_command_success(['--device', '/dev/input/event1'])
+    libinput_debug_tool.run_command_success(['/dev/input/event0'])
+
+
+options = {
+    'pattern': ['sendevents'],
+    # enable/disable options
+    'enable-disable': [
+        'tap',
+        'drag',
+        'drag-lock',
+        'middlebutton',
+        'natural-scrolling',
+        'left-handed',
+        'dwt'
+    ],
+    # options with distinct values
+    'enums': {
+        'set-click-method': ['none', 'clickfinger', 'buttonareas'],
+        'set-scroll-method': ['none', 'twofinger', 'edge', 'button'],
+        'set-profile': ['adaptive', 'flat'],
+        'set-tap-map': ['lrm', 'lmr'],
+    },
+    # options with a range
+    'ranges': {
+        'set-speed': (float, -1.0, +1.0),
+    }
+}
+
+
+# Options that allow for glob patterns
+@pytest.mark.parametrize('option', options['pattern'])
+def test_options_pattern(libinput_debug_tool, option):
+    libinput_debug_tool.run_command_success(['--disable-{}'.format(option), '*'])
+    libinput_debug_tool.run_command_success(['--disable-{}'.format(option), 'abc*'])
+
+
+@pytest.mark.parametrize('option', options['enable-disable'])
+def test_options_enable_disable(libinput_debug_tool, option):
+    libinput_debug_tool.run_command_success(['--enable-{}'.format(option)])
+    libinput_debug_tool.run_command_success(['--disable-{}'.format(option)])
+
+
+@pytest.mark.parametrize('option', options['enums'].items())
+def test_options_enums(libinput_debug_tool, option):
+    name, values = option
+    for v in values:
+        libinput_debug_tool.run_command_success(['--{}'.format(name), v])
+        libinput_debug_tool.run_command_success(['--{}={}'.format(name, v)])
+
+
+@pytest.mark.parametrize('option', options['ranges'].items())
+def test_options_ranges(libinput_debug_tool, option):
+    name, values = option
+    range_type, minimum, maximum = values
+    assert range_type == float
+    step = (maximum - minimum) / 10.0
+    value = minimum
+    while value < maximum:
+        libinput_debug_tool.run_command_success(['--{}'.format(name), str(value)])
+        libinput_debug_tool.run_command_success(['--{}={}'.format(name, value)])
+        value += step
+    libinput_debug_tool.run_command_success(['--{}'.format(name), str(maximum)])
+    libinput_debug_tool.run_command_success(['--{}={}'.format(name, maximum)])
+
+
+def test_apply_to(libinput_debug_tool):
+    libinput_debug_tool.run_command_missing_arg(['--apply-to'])
+    libinput_debug_tool.run_command_success(['--apply-to', '*foo*'])
+    libinput_debug_tool.run_command_success(['--apply-to', 'foobar'])
+    libinput_debug_tool.run_command_success(['--apply-to', 'any'])
+
+
+@pytest.mark.parametrize('args', [['--verbose'], ['--quiet'],
+                                  ['--verbose', '--quiet'],
+                                  ['--quiet', '--verbose']])
+def test_debug_events_verbose_quiet(libinput_debug_events, args):
+    libinput_debug_events.run_command_success(args)
+
+
+@pytest.mark.parametrize('arg', ['--banana', '--foo', '--version'])
+def test_invalid_args(libinput_debug_tool, arg):
+    libinput_debug_tool.run_command_unrecognized_option([arg])
+
+
+def test_libinput_debug_events_multiple_devices(libinput_debug_events):
+    libinput_debug_events.run_command_success(['--device', '/dev/input/event0', '/dev/input/event1'])
+    # same event path multiple times? meh, your problem
+    libinput_debug_events.run_command_success(['--device', '/dev/input/event0', '/dev/input/event0'])
+    libinput_debug_events.run_command_success(['/dev/input/event0', '/dev/input/event1'])
+
+
+def test_libinput_debug_events_too_many_devices(libinput_debug_events):
+    # Too many arguments just bails with the usage message
+    rc, stdout, stderr = libinput_debug_events.run_command(['/dev/input/event0'] * 61)
+    assert rc == 2, (stdout, stderr)
+
+
+@pytest.mark.parametrize('arg', ['--quiet'])
+def test_libinput_debug_gui_invalid_arg(libinput_debug_gui, arg):
+    libinput_debug_gui.run_command_unrecognized_option([arg])
+
+
+def test_libinput_debug_gui_verbose(libinput_debug_gui):
+    libinput_debug_gui.run_command_success(['--verbose'])
+
+
+@pytest.mark.parametrize('arg', ['--help', '--show-keycodes', '--with-libinput'])
+def test_libinput_record_args(libinput_record, arg):
+    libinput_record.run_command_success([arg])
+
+
+def test_libinput_record_multiple_arg(libinput_record):
+    # this arg is deprecated and a noop
+    libinput_record.run_command_success(['--multiple'])
+
+
+@pytest.fixture
+def recording(tmp_path):
+    return str((tmp_path / 'record.out').resolve())
+
+
+def test_libinput_record_all(libinput_record, recording):
+    libinput_record.run_command_success(['--all', '-o', recording])
+    libinput_record.run_command_success(['--all', recording])
+
+
+def test_libinput_record_outfile(libinput_record, recording):
+    libinput_record.run_command_success(['-o', recording])
+    libinput_record.run_command_success(['--output-file', recording])
+    libinput_record.run_command_success(['--output-file={}'.format(recording)])
+
+
+def test_libinput_record_single(libinput_record, recording):
+    libinput_record.run_command_success(['/dev/input/event0'])
+    libinput_record.run_command_success(['-o', recording, '/dev/input/event0'])
+    libinput_record.run_command_success(['/dev/input/event0', recording])
+    libinput_record.run_command_success([recording, '/dev/input/event0'])
+
+
+def test_libinput_record_multiple(libinput_record, recording):
+    libinput_record.run_command_success(['-o', recording, '/dev/input/event0', '/dev/input/event1'])
+    libinput_record.run_command_success([recording, '/dev/input/event0', '/dev/input/event1'])
+    libinput_record.run_command_success(['/dev/input/event0', '/dev/input/event1', recording])
+
+
+def test_libinput_record_autorestart(libinput_record, recording):
+    libinput_record.run_command_invalid(['--autorestart'])
+    libinput_record.run_command_invalid(['--autorestart=2'])
+    libinput_record.run_command_success(['-o', recording, '--autorestart=2'])
+
+
+def main():
+    args = ['-m', 'pytest']
+    try:
+        import xdist  # noqa
+        args += ['-n', 'auto']
+    except ImportError:
+        logger.info('python-xdist missing, this test will be slow')
+        pass
+
+    args += ['@MESON_BUILD_ROOT@']
+
+    return subprocess.run([sys.executable] + args).returncode
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())