diff --git a/tests/README.md b/tests/README.md index 20fdf9f..d04915a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -15,57 +15,23 @@ script, capture it and store the capture to `custom.pcapng`. ----------------------- A new 'capture' test is created by means of `capture.py` script: -1. Create (if needed) a directory for the driver under `tests` - directory: +1. Make sure that libfprint is built with support for the device driver + that you want to capture a test case for. - `mkdir DRIVER` +2. From the build directory, run tests/create-driver-test.py as root. Note + that if you're capturing data for a driver which already has a test case + but the hardware is slightly different, you might want to pass a variant + name as a command-line options, for example: +```sh +$ sudo tests/create-driver-test.py driver [variant] +``` - Note that the name must be the exact name of the libfprint driver, - or the exact name of the driver followed by a `-` and a unique identifier - of your choosing. +3. If the capture is not successful, run the tool again to start another capture. -2. Prepare your execution environment. +4. Add driver test name to `drivers_tests` in the `meson.build`, as instructed, + and change the ownership of the just-created test directory in the source. - In the next step a working and up to date libfprint is needed. This can be - achieved by installing it into your system. Alternatively, you can set - the following environment variables to run a local build: - - `export LD_PRELOAD=/libfprint/libfprint-2.so` - - `export GI_TYPELIB_PATH=/libfprint` - - Also, sometimes the driver must be adapted to the emulated environment - (mainly if it uses random numbers, see `synaptics.c` for an example). - Set the following environment variable to enable this adaptation: - - `export FP_DEVICE_EMULATION=1` - - Run the next steps in the same terminal. - -3. Find the real USB fingerprint device with `lsusb`, e.g.: - - `Bus 001 Device 005: ID 138a:0090 Validity Sensors, Inc. VFS7500 Touch Fingerprint Sensor` - - The following USB device is used in the example above: - `/dev/bus/usb/001/005`. - - For the following commands, it is assumed that the user that's - running the commands has full access to the device node, whether - by running the commands as `root`, or changing the permissions for - that device node. - -4. Record information about this device: - - `umockdev-record /dev/bus/usb/001/005 > DRIVER/device` - -5. Record interaction of `capture.py` (or other test) with the device. To do - so, start wireshark and record `usbmonX` (where X is the bus number). Then - run the test script: - - `python3 ./capture.py DRIVER/capture.png` - - Save the wireshark recording as `capture.pcapng`. The command will create - `capture.png`. - -6. Add driver's name to `drivers_tests` in the `meson.build`. -7. Check whether everything works as expected. +5. Check whether `meson test` passes with this new test. **Note.** To avoid submitting a real fingerprint, the side of finger, arm, or anything else producing an image with the device can be used. diff --git a/tests/create-driver-test.py.in b/tests/create-driver-test.py.in new file mode 100755 index 0000000..70b48f5 --- /dev/null +++ b/tests/create-driver-test.py.in @@ -0,0 +1,162 @@ +#!/usr/bin/python3 + +BUILDDIR='@BUILDDIR@' +SRCDIR='@SRCDIR@' + +import os +import sys +import signal +library_path = BUILDDIR + '/libfprint/' + +# Relaunch ourselves with a changed environment so +# that we're loading the development version of libfprint +if 'LD_LIBRARY_PATH' not in os.environ or not library_path in os.environ['LD_LIBRARY_PATH']: + os.environ['LD_LIBRARY_PATH'] = library_path + os.environ['GI_TYPELIB_PATH'] = f'{BUILDDIR}/libfprint/' + os.environ['FP_DEVICE_EMULATION'] = '1' + try: + os.execv(sys.argv[0], sys.argv) + except Exception as e: + print('Could not run script with new library path') + sys.exit(1) + +import gi +gi.require_version('FPrint', '2.0') +from gi.repository import FPrint + +gi.require_version('GUsb', '1.0') +from gi.repository import GUsb + +import re +import shutil +import subprocess +import tempfile +import time + +def print_usage(): + print(f'Usage: {sys.argv[0]} driver [test-variant-name]') + print('A test variant name is optional, and must be all lower case letters, or dashes, with no spaces') + print(f'The captured data will be stored in {SRCDIR}/tests/[driver name]-[test variant name]') + +if len(sys.argv) > 3: + print_usage() + sys.exit(1) + +driver_name = sys.argv[1] +os.environ['FP_DRIVERS_WHITELIST'] = driver_name + +test_variant = None +if len(sys.argv) == 3: + valid_re = re.compile('[a-z-]*') + test_variant = sys.argv[2] + if (not valid_re.match(test_variant) or + test_variant.startswith('-') or + test_variant.endswith('-')): + print(f'Invalid variant name {test_variant}\n') + print_usage() + sys.exit(1) + +# Check that running as root + +if os.geteuid() != 0: + print(f'{sys.argv[0]} is expected to be run as root') + sys.exit(1) + +# Check that tshark is available + +tshark = shutil.which('tshark') +if not tshark: + print("The 'tshark' WireShark command-line tool must be installed to capture USB traffic") + sys.exit(1) + +# Find the fingerprint reader +ctx = FPrint.Context() +ctx.enumerate() +devices = ctx.get_devices() +if len(devices) == 0: + print('Could not find a supported fingerprint reader') + sys.exit(1) +elif len(devices) > 1: + print('Capture requires a single supported fingerprint reader to be plugged in') + sys.exit(1) + +test_name = driver_name +if test_variant: + test_name = driver_name + '-' + test_variant +usb_device = devices[0].get_property('fpi-usb-device') +bus_num = usb_device.get_bus() +device_num = usb_device.get_address() + +print(f'### Detected USB device /dev/bus/usb/{bus_num:03d}/{device_num:03d}') + +# Make directory + +test_dir = SRCDIR + '/tests/' + test_name +os.makedirs(test_dir, mode=0o775, exist_ok=True) + +# Capture device info + +args = ['umockdev-record', f'/dev/bus/usb/{bus_num:03d}/{device_num:03d}'] +device_out = open(test_dir + '/device', 'w') +process = subprocess.Popen(args, stdout=device_out) +process.wait() + +# Run capture +# https://osqa-ask.wireshark.org/questions/53919/how-can-i-precisely-specify-a-usb-device-to-capture-with-tshark/ + +print(f'### Starting USB capture on usbmon{bus_num}') +capture_pid = os.fork() +assert(capture_pid >= 0) + +unfiltered_cap_path = os.path.join(tempfile.gettempdir(), 'capture-unfiltered.pcapng') +if capture_pid == 0: + os.setpgrp() + args = ['tshark', '-q', '-i', f'usbmon{bus_num}', '-w', unfiltered_cap_path] + os.execv(tshark, args) + +# Wait 1 sec to settle (we can assume setpgrp happened) +time.sleep(1) + +print('### Capturing fingerprint, please swipe or press your finger on the reader') +with subprocess.Popen(['python3', SRCDIR + '/tests/capture.py', test_dir + '/capture.png']) as capture_process: + capture_process.wait() + if capture_process.returncode != 0: + print('Failed to capture fingerprint') + os.killpg(capture_pid, signal.SIGKILL) + sys.exit(1) + +def t_waitpid(pid, timeout): + timeout = time.time() + timeout + r = os.waitpid(pid, os.WNOHANG) + while timeout > time.time() and r[0] == 0: + time.sleep(0.1) + r = os.waitpid(pid, os.WNOHANG) + + return r + +os.kill(capture_pid, signal.SIGTERM) +try: + r = t_waitpid(capture_pid, 2) + # Kill if nothing died + if r[0] == 0: + os.kill(capture_pid, signal.SIGKILL) +except ChildProcessError: + pass + +try: + while True: + r = t_waitpid(-capture_pid, timeout=2) + # Kill the process group, if nothing died (and there are children) + if r[0] == 0: + os.killpg(capture_pid, signal.SIGKILL) +except ChildProcessError: + pass + +# Filter the capture +print(f'\n### Saving USB capture as test case {test_name}') +args = ['tshark', '-r', unfiltered_cap_path, '-Y', f'usb.bus_id == {bus_num} and usb.device_address == {device_num}', + '-w', test_dir + '/capture.pcapng'] +with subprocess.Popen(args, stderr=subprocess.DEVNULL) as filter_process: + filter_process.wait() + +print(f"\nDone! Don't forget to add {test_name} to tests/meson.build") diff --git a/tests/meson.build b/tests/meson.build index 7a64a55..cbd06eb 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -41,6 +41,15 @@ drivers_tests = [ 'egis0570', ] +if get_option('introspection') + conf = configuration_data() + conf.set('SRCDIR', meson.project_source_root()) + conf.set('BUILDDIR', meson.project_build_root()) + configure_file(configuration: conf, + input: 'create-driver-test.py.in', + output: 'create-driver-test.py') +endif + if get_option('introspection') envs.prepend('GI_TYPELIB_PATH', join_paths(meson.build_root(), 'libfprint')) virtual_devices_tests = [