From 6e25a278707d6d0eeb1e97f98b1f0b9c102ff7cb Mon Sep 17 00:00:00 2001
From: Benjamin Berg <bberg@redhat.com>
Date: Wed, 3 Jul 2019 23:39:08 +0200
Subject: [PATCH] tests: Add basic unit test based on virtual_image device

---
 meson.build            |   5 +
 tests/meson.build      |  17 +++
 tests/virtual-image.py | 280 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 302 insertions(+)
 create mode 100644 tests/meson.build
 create mode 100755 tests/virtual-image.py

diff --git a/meson.build b/meson.build
index de73d6d..ec9221d 100644
--- a/meson.build
+++ b/meson.build
@@ -140,6 +140,11 @@ if get_option('gtk-examples')
     subdir('demo')
 endif
 
+# The tests require introspeciton support to run
+if get_option('introspection')
+    subdir('tests')
+endif
+
 pkgconfig = import('pkgconfig')
 pkgconfig.generate(
   name: 'libfprint',
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 0000000..26b8e76
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,17 @@
+envs = environment()
+envs.set('G_DEBUG', 'fatal-warnings')
+envs.set('G_MESSAGES_DEBUG', 'all')
+envs.set('MESON_SOURCE_ROOT', meson.build_root())
+envs.prepend('GI_TYPELIB_PATH', join_paths(meson.build_root(), 'libfprint'))
+envs.prepend('LD_LIBRARY_PATH', join_paths(meson.build_root(), 'libfprint'))
+envs.set('NO_AT_BRIDGE', '1')
+
+if 'virtual_image' in drivers
+    test(
+        'virtual-image',
+        find_program('virtual-image.py'),
+        args: '--verbose',
+        env: envs,
+    )
+endif
+
diff --git a/tests/virtual-image.py b/tests/virtual-image.py
new file mode 100755
index 0000000..a9fe8f5
--- /dev/null
+++ b/tests/virtual-image.py
@@ -0,0 +1,280 @@
+#!/usr/bin/env python3
+
+
+import gi
+gi.require_version('FPrint', '2.0')
+from gi.repository import FPrint, GLib, Gio
+
+import os
+import sys
+import unittest
+import socket
+import struct
+import shutil
+import glob
+import cairo
+import tempfile
+
+class Connection:
+
+    def __init__(self, addr):
+        self.addr = addr
+
+    def __enter__(self):
+        self.con = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self.con.connect(self.addr)
+        return self.con
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.con.close()
+        del self.con
+
+def load_image(img):
+    png = cairo.ImageSurface.create_from_png(img)
+
+    # Cairo wants 4 byte aligned rows, so just add a few pixel if necessary
+    w = png.get_width()
+    h = png.get_height()
+    w = (w + 3) // 4 * 4
+    h = (h + 3) // 4 * 4
+    img = cairo.ImageSurface(cairo.Format.A8, w, h)
+    cr = cairo.Context(img)
+
+    cr.set_source_rgba(1, 1, 1, 1)
+    cr.paint()
+
+    cr.set_source_rgba(0, 0, 0, 0)
+    cr.set_operator(cairo.OPERATOR_SOURCE)
+
+    cr.set_source_surface(png)
+    cr.paint()
+
+    return img
+
+if hasattr(os.environ, 'MESON_SOURCE_ROOT'):
+    root = os.environ['MESON_SOURCE_ROOT']
+else:
+    root = os.path.join(os.path.dirname(__file__), '..')
+
+imgdir = os.path.join(root, 'examples', 'prints')
+
+ctx = GLib.main_context_default()
+
+class VirtualImage(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.tmpdir = tempfile.mkdtemp(prefix='libfprint-')
+
+        cls.sockaddr = os.path.join(cls.tmpdir, 'virtual-image.socket')
+        os.environ['FP_VIRTUAL_IMAGE'] = cls.sockaddr
+
+        cls.ctx = FPrint.Context()
+
+        cls.dev = None
+        for dev in cls.ctx.get_devices():
+            # We might have a USB device in the test system that needs skipping
+            if dev.get_driver() == 'virtual_image':
+                cls.dev = dev
+                break
+
+        assert cls.dev is not None, "You need to compile with virtual_image for testing"
+
+        cls.prints = {}
+        for f in glob.glob(os.path.join(imgdir, '*.png')):
+            n = os.path.basename(f)[:-4]
+            cls.prints[n] = load_image(f)
+
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.tmpdir)
+
+    def setUp(self):
+        self.dev.open_sync()
+
+    def tearDown(self):
+        self.dev.close_sync()
+
+    def report_finger(self, state):
+        with Connection(self.sockaddr) as con:
+            con.write(struct.pack('ii', -1, 1 if state else 0))
+
+    def send_image(self, image):
+        img = self.prints[image]
+        with Connection(self.sockaddr) as con:
+            mem = img.get_data()
+            mem = mem.tobytes()
+            assert len(mem) == img.get_width() * img.get_height()
+
+            encoded_img = struct.pack('ii', img.get_width(), img.get_height())
+            encoded_img += mem
+
+            con.sendall(encoded_img)
+
+    def test_capture_prevents_close(self):
+        cancel = Gio.Cancellable()
+        def cancelled_cb(dev, res, obj):
+            print("Capture operation finished")
+            with self.assertRaises(GLib.GError) as cm:
+                dev.capture_finish(res)
+            assert cm.exception.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED)
+            print("Capture cancelled as expected")
+            obj._cancelled = True
+
+        self._cancelled = False
+        self.dev.capture(True, cancel, cancelled_cb, self)
+
+        with self.assertRaises(GLib.GError) as cm:
+            self.dev.close_sync()
+        assert cm.exception.matches(FPrint.device_error_quark(), FPrint.DeviceError.BUSY)
+
+        cancel.cancel()
+        while not self._cancelled:
+            ctx.iteration(True)
+
+    def enroll_print(self, image):
+        self._step = 0
+        self._enrolled = None
+
+        def progress_cb(dev, step, fp, user_data):
+            print('Print was processed, continuing')
+            self._step = step
+
+        def done_cb(dev, res):
+            print("Enroll done")
+            fp = dev.enroll_finish(res)
+            self._enrolled = fp
+
+        template = FPrint.Print.new(self.dev)
+        template.props.finger = FPrint.Finger.LEFT_THUMB
+        template.props.username = "testuser"
+        template.props.description = "test print"
+        datetime = GLib.DateTime.new_now_local()
+        date = GLib.Date()
+        date.set_dmy(*datetime.get_ymd()[::-1])
+        template.props.enroll_date = date
+        self.dev.enroll(template, None, progress_cb, tuple(), done_cb)
+
+        # Note: Assumes 5 enroll steps for this device!
+        self.send_image(image)
+        while self._step < 1:
+            ctx.iteration(True)
+
+        self.send_image(image)
+        while self._step < 2:
+            ctx.iteration(True)
+
+        self.send_image(image)
+        while self._step < 3:
+            ctx.iteration(True)
+
+        self.send_image(image)
+        while self._step < 4:
+            ctx.iteration(True)
+
+        self.send_image(image)
+        while self._enrolled is None:
+            ctx.iteration(True)
+
+        return self._enrolled
+
+    def test_enroll_verify(self):
+        done = False
+
+        def verify_cb(dev, res):
+            match, fp = dev.verify_finish(res)
+            self._verify_match = match
+            self._verify_fp = fp
+
+        fp_whorl = self.enroll_print('whorl')
+
+        self._verify_match = None
+        self._verify_fp = None
+        self.dev.verify(fp_whorl, None, verify_cb)
+        self.send_image('whorl')
+        while self._verify_match is None:
+            ctx.iteration(True)
+        assert(self._verify_match)
+
+        self._verify_match = None
+        self._verify_fp = None
+        self.dev.verify(fp_whorl, None, verify_cb)
+        self.send_image('tented_arch')
+        while self._verify_match is None:
+            ctx.iteration(True)
+        assert(not self._verify_match)
+
+    def test_identify(self):
+        done = False
+
+        def verify_cb(dev, res):
+            r, fp = dev.verify_finish(res)
+            self._verify_match = r
+            self._verify_fp = fp
+
+        fp_whorl = self.enroll_print('whorl')
+        fp_tented_arch = self.enroll_print('tented_arch')
+
+        def identify_cb(dev, res):
+            print('Identify finished')
+            self._identify_match, self._identify_fp = self.dev.identify_finish(res)
+
+        self._identify_fp = None
+        self.dev.identify([fp_whorl, fp_tented_arch], None, identify_cb)
+        self.send_image('tented_arch')
+        while self._identify_fp is None:
+            ctx.iteration(True)
+        assert(self._identify_match is fp_tented_arch)
+
+        self._identify_fp = None
+        self.dev.identify([fp_whorl, fp_tented_arch], None, identify_cb)
+        self.send_image('whorl')
+        while self._identify_fp is None:
+            ctx.iteration(True)
+        assert(self._identify_match is fp_whorl)
+
+    def test_verify_serialized(self):
+        done = False
+
+        def verify_cb(dev, res):
+            r, fp = dev.verify_finish(res)
+            self._verify_match = r
+            self._verify_fp = fp
+
+        fp_whorl = self.enroll_print('whorl')
+
+        fp_data = fp_whorl.serialize()
+        fp_whorl_new = FPrint.Print.deserialize(fp_data)
+
+        # The serialized/deserialized prints need to be equal
+        assert fp_whorl.equal(fp_whorl_new)
+
+        datetime = GLib.DateTime.new_now_local()
+        date = GLib.Date()
+        date.set_dmy(*datetime.get_ymd()[::-1])
+
+        assert fp_whorl_new.props.username == "testuser"
+        assert fp_whorl_new.props.description == "test print"
+        assert fp_whorl_new.props.finger == FPrint.Finger.LEFT_THUMB
+        assert date.compare(fp_whorl_new.props.enroll_date) == 0
+
+        self._verify_match = None
+        self._verify_fp = None
+        self.dev.verify(fp_whorl_new, None, verify_cb)
+        self.send_image('whorl')
+        while self._verify_match is None:
+            ctx.iteration(True)
+        assert(self._verify_match)
+
+        self._verify_match = None
+        self._verify_fp = None
+        self.dev.verify(fp_whorl_new, None, verify_cb)
+        self.send_image('tented_arch')
+        while self._verify_match is None:
+            ctx.iteration(True)
+        assert(not self._verify_match)
+
+
+# avoid writing to stderr
+unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2))
+