bindings: python: use unittest to implement a proper test-suite
authorBartosz Golaszewski <bgolaszewski@baylibre.com>
Wed, 7 Aug 2019 12:28:30 +0000 (14:28 +0200)
committerBartosz Golaszewski <bgolaszewski@baylibre.com>
Fri, 9 Aug 2019 12:52:01 +0000 (14:52 +0200)
Currently the only tests we have for python bindings are in a simple
script that makes a lot of assumptions about the environment and
doesn't even work anymore with recent kernels.

Remove it and replace it with a proper test-suite implemented using
libgpiod and the standard python unittest package.

Signed-off-by: Bartosz Golaszewski <bgolaszewski@baylibre.com>
TODO
bindings/python/Makefile.am
bindings/python/examples/Makefile.am
bindings/python/examples/gpiod_tests.py [deleted file]
bindings/python/tests/Makefile.am [new file with mode: 0644]
bindings/python/tests/gpiod_py_test.py [new file with mode: 0755]
bindings/python/tests/gpiomockupmodule.c [new file with mode: 0644]
configure.ac

diff --git a/TODO b/TODO
index e9862d0b41a8bb1315d20840ad93f3aef4a1e580..654fff690c8607401db7e42221daf69bdf88af08 100644 (file)
--- a/TODO
+++ b/TODO
@@ -25,15 +25,6 @@ and is partially functional.
 
 ----------
 
-* use the python unit testing library for python bindings and reuse
-  libgpiod-test
-
-The best candidate for the testing framework is the standard unittest module
-available in cpython. We'd need however to wrap libgpiod-test in a C module
-for python just like we did for the bindings of the core C library.
-
-----------
-
 * implement a simple daemon for controlling GPIOs in C together with a client
   program
 
index 2c00c6e667a57514abdd5c56a0add83c63f45dd5..5b33857cd51bee0578218726f43cb3d615b0d9ec 100644 (file)
@@ -16,3 +16,9 @@ gpiod_la_CFLAGS = -I$(top_srcdir)/include/
 gpiod_la_CFLAGS += -Wall -Wextra -g $(PYTHON_CPPFLAGS)
 gpiod_la_LDFLAGS = -module -avoid-version
 gpiod_la_LIBADD = $(top_builddir)/lib/libgpiod.la $(PYTHON_LIBS)
+
+if WITH_TESTS
+
+SUBDIRS += tests
+
+endif
index 27457183abdbf32cacde0041f874f4bf792eb6dc..0703e81f9ca3b09672ab80c2cea531dcb12ef539 100644 (file)
@@ -6,8 +6,7 @@
 # Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
 #
 
-EXTRA_DIST =   gpiod_tests.py \
-               gpiodetect.py \
+EXTRA_DIST =   gpiodetect.py \
                gpiofind.py \
                gpioget.py \
                gpioinfo.py \
diff --git a/bindings/python/examples/gpiod_tests.py b/bindings/python/examples/gpiod_tests.py
deleted file mode 100755 (executable)
index 910613a..0000000
+++ /dev/null
@@ -1,437 +0,0 @@
-#!/usr/bin/env python3
-# SPDX-License-Identifier: LGPL-2.1-or-later
-
-#
-# This file is part of libgpiod.
-#
-# Copyright (C) 2017-2018 Bartosz Golaszewski <bartekgola@gmail.com>
-#
-
-'''Misc tests of libgpiod python bindings.
-
-These tests assume that at least one dummy gpiochip is present in the
-system and that it's detected as gpiochip0.
-'''
-
-import gpiod
-import select
-
-test_cases = []
-
-def add_test(name, func):
-    global test_cases
-
-    test_cases.append((name, func))
-
-def fire_line_event(chip, offset, rising):
-    path = '/sys/kernel/debug/gpio-mockup-event/{}/{}'.format(chip, offset)
-    with open(path, 'w') as fd:
-        fd.write('{}'.format(1 if rising else 0))
-
-def print_event(event):
-    print('type: {}'.format('rising' if event.type == gpiod.LineEvent.RISING_EDGE else 'falling'))
-    print('timestamp: {}.{}'.format(event.sec, event.nsec))
-    print('source line offset: {}'.format(event.source.offset()))
-
-def chip_open_default_lookup():
-    by_name = gpiod.Chip('gpiochip0')
-    by_path = gpiod.Chip('/dev/gpiochip0')
-    by_label = gpiod.Chip('gpio-mockup-A')
-    by_number = gpiod.Chip('0')
-    print('All good')
-    by_name.close()
-    by_path.close()
-    by_label.close()
-    by_number.close()
-
-add_test('Open a GPIO chip using different lookup modes', chip_open_default_lookup)
-
-def chip_open_different_modes():
-    chip = gpiod.Chip('/dev/gpiochip0', gpiod.Chip.OPEN_BY_PATH)
-    chip.close()
-    chip = gpiod.Chip('gpiochip0', gpiod.Chip.OPEN_BY_NAME)
-    chip.close()
-    chip = gpiod.Chip('gpio-mockup-A', gpiod.Chip.OPEN_BY_LABEL)
-    chip.close()
-    chip = gpiod.Chip('0', gpiod.Chip.OPEN_BY_NUMBER)
-    chip.close()
-    print('All good')
-
-add_test('Open a GPIO chip using different modes', chip_open_different_modes)
-
-def chip_open_nonexistent():
-    try:
-        chip = gpiod.Chip('/nonexistent_gpiochip')
-    except OSError as ex:
-        print('Exception raised as expected: {}'.format(ex))
-        return
-
-    assert False, 'OSError expected'
-
-add_test('Try to open a nonexistent GPIO chip', chip_open_nonexistent)
-
-def chip_open_no_args():
-    try:
-        chip = gpiod.Chip()
-    except TypeError:
-        print('Error as expected')
-        return
-
-    assert False, 'TypeError expected'
-
-add_test('Open a GPIO chip without arguments', chip_open_no_args)
-
-def chip_use_after_close():
-    chip = gpiod.Chip('gpiochip0')
-    line = chip.get_line(2)
-    chip.close()
-
-    try:
-        chip.name()
-    except ValueError as ex:
-        print('Error as expected: {}'.format(ex))
-
-    try:
-        line = chip.get_line(3)
-    except ValueError as ex:
-        print('Error as expected: {}'.format(ex))
-        return
-
-    assert False, 'ValueError expected'
-
-add_test('Use a GPIO chip after closing it', chip_use_after_close)
-
-def chip_with_statement():
-    with gpiod.Chip('gpiochip0') as chip:
-        print('Chip name in controlled execution: {}'.format(chip.name()))
-        line = chip.get_line(3)
-        print('Got line from chip in controlled execution: {}'.format(line.name()))
-
-add_test('Use a GPIO chip in controlled execution', chip_with_statement)
-
-def chip_info():
-    chip = gpiod.Chip('gpiochip0')
-    print('name: {}'.format(chip.name()))
-    print('label: {}'.format(chip.label()))
-    print('lines: {}'.format(chip.num_lines()))
-    chip.close()
-
-add_test('Print chip info', chip_info)
-
-def print_chip():
-    chip = gpiod.Chip('/dev/gpiochip0')
-    print(chip)
-    chip.close()
-
-add_test('Print chip object', print_chip)
-
-def create_chip_without_arguments():
-    try:
-        chip = gpiod.Chip()
-    except TypeError as ex:
-        print('Exception raised as expected: {}'.format(ex))
-        return
-
-    assert False, 'TypeError expected'
-
-add_test('Create chip object without arguments', create_chip_without_arguments)
-
-def create_line_object():
-    try:
-        line = gpiod.Line()
-    except NotImplementedError:
-        print('Error as expected')
-        return
-
-    assert False, 'NotImplementedError expected'
-
-add_test('Create a line object - should fail', create_line_object)
-
-def print_line():
-    chip = gpiod.Chip('gpio-mockup-A')
-    line = chip.get_line(3)
-    print(line)
-    chip.close()
-
-add_test('Print line object', print_line)
-
-def find_line():
-    line = gpiod.find_line('gpio-mockup-A-4')
-    print('found line - offset: {}'.format(line.offset()))
-    line.owner().close()
-
-add_test('Find line globally', find_line)
-
-def create_empty_line_bulk():
-    try:
-        lines = gpiod.LineBulk()
-    except TypeError:
-        print('Error as expected')
-        return
-
-    assert False, 'TypeError expected'
-
-add_test('Create a line bulk object - should fail', create_empty_line_bulk)
-
-def get_lines():
-    chip = gpiod.Chip('gpio-mockup-A')
-
-    print('getting four lines from chip')
-    lines = chip.get_lines([2, 4, 5, 7])
-
-    print('Retrieved lines:')
-    for line in lines:
-        print(line)
-
-    chip.close()
-
-add_test('Get lines from chip', get_lines)
-
-def get_all_lines():
-    chip = gpiod.Chip('gpio-mockup-A')
-
-    print('Retrieving all lines from chip')
-    lines = chip.get_all_lines()
-
-    print('Retrieved lines:')
-    for line in lines:
-        print(line)
-
-    chip.close()
-
-add_test('Get all lines from chip', get_all_lines)
-
-def find_lines():
-    chip = gpiod.Chip('gpiochip0')
-
-    print('looking up lines by names')
-    lines = chip.find_lines(['gpio-mockup-A-3', 'gpio-mockup-A-4', 'gpio-mockup-A-7'])
-
-    print('Retrieved lines:')
-    for line in lines:
-        print(line)
-
-    chip.close()
-
-add_test('Find multiple lines by name', find_lines)
-
-def find_lines_one_bad():
-    chip = gpiod.Chip('gpiochip0')
-
-    print('looking up lines by names')
-    try:
-        lines = chip.find_lines(['gpio-mockup-A-3', 'nonexistent', 'gpio-mockup-A-7'])
-    except TypeError as ex:
-        print('Error as expected')
-        return
-
-    assert False, 'TypeError expected'
-
-add_test('Find multiple lines but one line name is non-existent', find_lines_one_bad)
-
-def create_line_bulk_from_lines():
-    chip = gpiod.Chip('gpio-mockup-A')
-    line1 = chip.get_line(2)
-    line2 = chip.get_line(4)
-    line3 = chip.get_line(6)
-    lines = gpiod.LineBulk([line1, line2, line3])
-    print('Created LineBulk:')
-    print(lines)
-    chip.close()
-
-add_test('Create a LineBulk from a list of lines', create_line_bulk_from_lines)
-
-def line_bulk_to_list():
-    chip = gpiod.Chip('gpio-mockup-A')
-    lines = chip.get_lines((1, 2, 3))
-    print(lines.to_list())
-    chip.close()
-
-add_test('Convert a LineBulk to a list', line_bulk_to_list)
-
-def line_flags():
-    chip = gpiod.Chip('gpiochip0')
-    line = chip.get_line(3)
-
-    print('line is used: {}'.format(line.is_used()))
-    print('line is requested: {}'.format(line.is_requested()))
-
-    print('requesting line')
-    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_OUT,
-                 flags=(gpiod.LINE_REQ_FLAG_OPEN_DRAIN | gpiod.LINE_REQ_FLAG_ACTIVE_LOW))
-
-    print('line is used: {}'.format(line.is_used()))
-    print('line is open drain: {}'.format(line.is_open_drain()))
-    print('line is open source: {}'.format(line.is_open_source()))
-    print('line is requested: {}'.format(line.is_requested()))
-    print('line is active-low: {}'.format(
-            "True" if line.active_state() == gpiod.Line.ACTIVE_LOW else "False"))
-
-    chip.close()
-
-add_test('Check various line flags', line_flags)
-
-def get_value_single_line():
-    chip = gpiod.Chip('gpio-mockup-A')
-    line = chip.get_line(2)
-    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_IN)
-    print('line value: {}'.format(line.get_value()))
-    chip.close()
-
-add_test('Get value - single line', get_value_single_line)
-
-def set_value_single_line():
-    chip = gpiod.Chip('gpiochip0')
-    line = chip.get_line(3)
-    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_IN)
-
-    print('line value before: {}'.format(line.get_value()))
-    line.release()
-    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_OUT)
-    line.set_value(1)
-    line.release()
-    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_DIR_IN)
-    print('line value after: {}'.format(line.get_value()))
-
-    chip.close()
-
-add_test('Set value - single line', set_value_single_line)
-
-def request_line_with_default_values():
-    chip = gpiod.Chip('gpiochip0')
-    line = chip.get_line(3)
-
-    print('requesting a single line with a default value')
-    line.request(consumer='gpiod_test.py', type=gpiod.LINE_REQ_DIR_OUT, default_vals=[ 1 ])
-
-    print('line value after request: {}'.format(line.get_value()))
-
-    chip.close()
-
-add_test('Request line with default value', request_line_with_default_values)
-
-def request_multiple_lines_with_default_values():
-    chip = gpiod.Chip('gpiochip0')
-    lines = chip.get_lines(( 1, 2, 3, 4, 5 ))
-
-    print('requesting lines with default values')
-    lines.request(consumer='gpiod_test.py', type=gpiod.LINE_REQ_DIR_OUT, default_vals=( 1, 0, 1, 0, 1 ))
-
-    print('line values after request: {}'.format(lines.get_values()))
-
-    chip.close()
-
-add_test('Request multiple lines with default values', request_multiple_lines_with_default_values)
-
-def request_line_incorrect_number_of_def_vals():
-    with gpiod.Chip('gpiochip0') as chip:
-        lines = chip.get_lines(( 1, 2, 3, 4, 5 ))
-
-        print('requesting lines with incorrect number of default values')
-        try:
-            lines.request(consumer='gpiod_test.py',
-                          type=gpiod.LINE_REQ_DIR_OUT,
-                          default_vals=( 1, 0, 1, 0 ))
-        except TypeError:
-            print('TypeError raised as expected')
-            return
-
-        assert False, 'TypeError expected'
-
-add_test('Request with incorrect number of default values', request_line_incorrect_number_of_def_vals)
-
-def line_event_single_line():
-    chip = gpiod.Chip('gpiochip0')
-    line = chip.get_line(1)
-
-    print('requesting line for events')
-    line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-
-    print('generating a line event')
-    fire_line_event('gpiochip0', 1, True)
-    assert line.event_wait(sec=1), 'Expected a line event to occur'
-
-    print('event received')
-    event = line.event_read()
-    print_event(event)
-
-    chip.close()
-
-add_test('Monitor a single line for events', line_event_single_line)
-
-def line_event_multiple_lines():
-    chip = gpiod.Chip('gpiochip0')
-    lines = chip.get_lines((1, 2, 3, 4, 5))
-
-    print('requesting lines for events')
-    lines.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-
-    print('generating two line events')
-    fire_line_event('gpiochip0', 1, True)
-    fire_line_event('gpiochip0', 2, True)
-
-    events = lines.event_wait(sec=1)
-    assert events is not None and len(events) == 2, 'Expected to receive two line events'
-
-    print('events received:')
-    for line in events:
-        event = line.event_read()
-        print_event(event)
-
-    chip.close()
-
-add_test('Monitor multiple lines for events', line_event_multiple_lines)
-
-def line_event_poll_fd():
-    chip = gpiod.Chip('gpiochip0')
-    lines = chip.get_lines((1, 2, 3, 4, 5, 6))
-    print('requesting lines for events')
-    lines.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-
-    print('generating three line events')
-    fire_line_event('gpiochip0', 2, True)
-    fire_line_event('gpiochip0', 3, False)
-    fire_line_event('gpiochip0', 5, True)
-
-    print('retrieving the file descriptors')
-    inputs = []
-    fd_mapping = {}
-    for line in lines:
-        inputs.append(line.event_get_fd())
-        fd_mapping[line.event_get_fd()] = line
-
-    readable, writable, exceptional = select.select(inputs, [], inputs, 1.0)
-    assert len(readable) == 3, 'Expected to receive three line events'
-
-    print('events received:')
-    for fd in readable:
-        line = fd_mapping[fd]
-        event = line.event_read()
-        print_event(event)
-
-    chip.close()
-
-add_test('Monitor multiple lines using their file descriptors', line_event_poll_fd)
-
-def line_event_repr():
-    with gpiod.Chip('gpiochip0') as chip:
-        line = chip.get_line(1)
-
-        print('requesting line for events')
-        line.request(consumer="gpiod_test.py", type=gpiod.LINE_REQ_EV_BOTH_EDGES)
-
-        print('generating a line event')
-        fire_line_event('gpiochip0', 1, True)
-        assert line.event_wait(sec=1), 'Expected a line event to occur'
-
-        print('event received: {}'.format(line.event_read()))
-
-add_test('Line event string repr', line_event_repr)
-
-print('API version is {}'.format(gpiod.version_string()))
-
-for name, func in test_cases:
-    print('==============================================')
-    print('{}:'.format(name))
-    print()
-    func()
diff --git a/bindings/python/tests/Makefile.am b/bindings/python/tests/Makefile.am
new file mode 100644 (file)
index 0000000..937c673
--- /dev/null
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2019 Bartosz Golaszewski <bgolaszewski@baylibre.com>
+#
+
+bin_SCRIPTS = gpiod_py_test.py
+
+pyexec_LTLIBRARIES = gpiomockup.la
+
+gpiomockup_la_SOURCES = gpiomockupmodule.c
+gpiomockup_la_CFLAGS = -I$(top_srcdir)/tests/mockup/
+gpiomockup_la_CFLAGS += -Wall -Wextra -g $(PYTHON_CPPFLAGS)
+gpiomockup_la_LDFLAGS = -module -avoid-version
+gpiomockup_la_LIBADD = $(top_builddir)/tests/mockup/libgpiomockup.la
+gpiomockup_la_LIBADD += $(PYTHON_LIBS)
diff --git a/bindings/python/tests/gpiod_py_test.py b/bindings/python/tests/gpiod_py_test.py
new file mode 100755 (executable)
index 0000000..ede1ae9
--- /dev/null
@@ -0,0 +1,650 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+#
+# This file is part of libgpiod.
+#
+# Copyright (C) 2019 Bartosz Golaszewski <bgolaszewski@baylibre.com>
+#
+
+import errno
+import gpiod
+import gpiomockup
+import os
+import select
+import threading
+import unittest
+
+from packaging import version
+
+mockup = None
+default_consumer = 'gpiod-py-test'
+
+class MockupTestCase(unittest.TestCase):
+
+    chip_sizes = None
+    flags = 0
+
+    def setUp(self):
+        mockup.probe(self.chip_sizes, flags=self.flags)
+
+    def tearDown(self):
+        mockup.remove()
+
+class EventThread(threading.Thread):
+
+    def __init__(self, chip_idx, line_offset, freq):
+        threading.Thread.__init__(self)
+        self.chip_idx = chip_idx
+        self.line_offset = line_offset
+        self.freq = freq
+        self.lock = threading.Lock()
+        self.cond = threading.Condition(self.lock)
+        self.should_stop = False
+
+    def run(self):
+        i = 0;
+        while True:
+            with self.lock:
+                if self.should_stop:
+                    break;
+
+                if not self.cond.wait(float(self.freq) / 1000):
+                    mockup.chip_set_pull(self.chip_idx,
+                                         self.line_offset, i % 2)
+                    i += 1
+
+    def stop(self):
+        with self.lock:
+            self.should_stop = True
+            self.cond.notify_all()
+
+    def __enter__(self):
+        self.start()
+        return self
+
+    def __exit__(self, exc_type, exc_value, traceback):
+        self.stop()
+        self.join()
+
+def check_kernel(major, minor, release):
+    current = os.uname().release
+    required = '{}.{}.{}'.format(major, minor, release)
+    if version.parse(current) < version.parse(required):
+        raise NotImplementedError(
+                'linux kernel version must be at least {} - got {}'.format(required, current))
+
+#
+# Chip test cases
+#
+
+class ChipOpen(MockupTestCase):
+
+    chip_sizes = ( 8, 8, 8 )
+
+    def test_open_chip_by_name(self):
+        with gpiod.Chip(mockup.chip_name(1), gpiod.Chip.OPEN_BY_NAME) as chip:
+            self.assertEqual(chip.name(), mockup.chip_name(1))
+
+    def test_open_chip_by_path(self):
+        with gpiod.Chip(mockup.chip_path(1), gpiod.Chip.OPEN_BY_PATH) as chip:
+            self.assertEqual(chip.name(), mockup.chip_name(1))
+
+    def test_open_chip_by_num(self):
+        with gpiod.Chip('{}'.format(mockup.chip_num(1)),
+                        gpiod.Chip.OPEN_BY_NUMBER) as chip:
+            self.assertEqual(chip.name(), mockup.chip_name(1))
+
+    def test_open_chip_by_label(self):
+        with gpiod.Chip('gpio-mockup-B', gpiod.Chip.OPEN_BY_LABEL) as chip:
+            self.assertEqual(chip.name(), mockup.chip_name(1))
+
+    def test_lookup_chip_by_name(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            self.assertEqual(chip.name(), mockup.chip_name(1))
+
+    def test_lookup_chip_by_path(self):
+        with gpiod.Chip(mockup.chip_path(1)) as chip:
+            self.assertEqual(chip.name(), mockup.chip_name(1))
+
+    def test_lookup_chip_by_num(self):
+        with gpiod.Chip('{}'.format(mockup.chip_num(1))) as chip:
+            self.assertEqual(chip.name(), mockup.chip_name(1))
+
+    def test_lookup_chip_by_label(self):
+        with gpiod.Chip('gpio-mockup-B') as chip:
+            self.assertEqual(chip.name(), mockup.chip_name(1))
+
+    def test_nonexistent_chip(self):
+        with self.assertRaises(FileNotFoundError):
+            chip = gpiod.Chip('nonexistent-chip')
+
+    def test_open_chip_no_arguments(self):
+        with self.assertRaises(TypeError):
+            chip = gpiod.Chip()
+
+class ChipClose(MockupTestCase):
+
+    chip_sizes = ( 8, )
+
+    def test_use_chip_after_close(self):
+        chip = gpiod.Chip(mockup.chip_name(0))
+        self.assertEqual(chip.name(), mockup.chip_name(0))
+        chip.close()
+        with self.assertRaises(ValueError):
+            chip.name()
+
+class ChipInfo(MockupTestCase):
+
+    chip_sizes = ( 16, )
+
+    def test_chip_get_info(self):
+        chip = gpiod.Chip(mockup.chip_name(0))
+        self.assertEqual(chip.name(), mockup.chip_name(0))
+        self.assertEqual(chip.label(), 'gpio-mockup-A')
+        self.assertEqual(chip.num_lines(), 16)
+
+class ChipGetLines(MockupTestCase):
+
+    chip_sizes = ( 8, 8, 4 )
+    flags = gpiomockup.Mockup.FLAG_NAMED_LINES
+
+    def test_get_single_line_by_offset(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            line = chip.get_line(4)
+            self.assertEqual(line.name(), 'gpio-mockup-B-4')
+
+    def test_find_single_line_by_name(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            line = chip.find_line('gpio-mockup-B-4')
+            self.assertEqual(line.offset(), 4)
+
+    def test_get_single_line_invalid_offset(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            with self.assertRaises(OSError) as err_ctx:
+                line = chip.get_line(11)
+
+            self.assertEqual(err_ctx.exception.errno, errno.EINVAL)
+
+    def test_find_single_line_nonexistent(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            line = chip.find_line('nonexistent-line')
+            self.assertEqual(line, None)
+
+    def test_get_multiple_lines_by_offsets_in_tuple(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            lines = chip.get_lines(( 1, 3, 6, 7 )).to_list()
+            self.assertEqual(len(lines), 4)
+            self.assertEqual(lines[0].name(), 'gpio-mockup-B-1')
+            self.assertEqual(lines[1].name(), 'gpio-mockup-B-3')
+            self.assertEqual(lines[2].name(), 'gpio-mockup-B-6')
+            self.assertEqual(lines[3].name(), 'gpio-mockup-B-7')
+
+    def test_get_multiple_lines_by_offsets_in_list(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            lines = chip.get_lines([ 1, 3, 6, 7 ]).to_list()
+            self.assertEqual(len(lines), 4)
+            self.assertEqual(lines[0].name(), 'gpio-mockup-B-1')
+            self.assertEqual(lines[1].name(), 'gpio-mockup-B-3')
+            self.assertEqual(lines[2].name(), 'gpio-mockup-B-6')
+            self.assertEqual(lines[3].name(), 'gpio-mockup-B-7')
+
+    def test_find_multiple_lines_by_names_in_tuple(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            lines = chip.find_lines(( 'gpio-mockup-B-0',
+                                      'gpio-mockup-B-3',
+                                      'gpio-mockup-B-4',
+                                      'gpio-mockup-B-6' )).to_list()
+            self.assertEqual(len(lines), 4)
+            self.assertEqual(lines[0].offset(), 0)
+            self.assertEqual(lines[1].offset(), 3)
+            self.assertEqual(lines[2].offset(), 4)
+            self.assertEqual(lines[3].offset(), 6)
+
+    def test_find_multiple_lines_by_names_in_list(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            lines = chip.find_lines([ 'gpio-mockup-B-0',
+                                      'gpio-mockup-B-3',
+                                      'gpio-mockup-B-4',
+                                      'gpio-mockup-B-6' ]).to_list()
+            self.assertEqual(len(lines), 4)
+            self.assertEqual(lines[0].offset(), 0)
+            self.assertEqual(lines[1].offset(), 3)
+            self.assertEqual(lines[2].offset(), 4)
+            self.assertEqual(lines[3].offset(), 6)
+
+    def test_get_multiple_lines_invalid_offset(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            with self.assertRaises(OSError) as err_ctx:
+                line = chip.get_lines(( 1, 3, 11, 7 ))
+
+            self.assertEqual(err_ctx.exception.errno, errno.EINVAL)
+
+    def test_find_multiple_lines_nonexistent(self):
+        with gpiod.Chip(mockup.chip_name(1)) as chip:
+            with self.assertRaises(TypeError):
+                lines = chip.find_lines(( 'gpio-mockup-B-0',
+                                          'nonexistent-line',
+                                          'gpio-mockup-B-4',
+                                          'gpio-mockup-B-6' )).to_list()
+
+    def test_get_all_lines(self):
+        with gpiod.Chip(mockup.chip_name(2)) as chip:
+            lines = chip.get_all_lines().to_list()
+            self.assertEqual(len(lines), 4)
+            self.assertEqual(lines[0].name(), 'gpio-mockup-C-0')
+            self.assertEqual(lines[1].name(), 'gpio-mockup-C-1')
+            self.assertEqual(lines[2].name(), 'gpio-mockup-C-2')
+            self.assertEqual(lines[3].name(), 'gpio-mockup-C-3')
+
+#
+# Line test cases
+#
+
+class LineGlobalFindLine(MockupTestCase):
+
+    chip_sizes = ( 4, 8, 16 )
+    flags = gpiomockup.Mockup.FLAG_NAMED_LINES
+
+    def test_global_find_line_function(self):
+        line = gpiod.find_line('gpio-mockup-B-4')
+        self.assertNotEqual(line, None)
+        try:
+            self.assertEqual(line.owner().label(), 'gpio-mockup-B')
+            self.assertEqual(line.offset(), 4)
+        finally:
+            line.owner().close()
+
+    def test_global_find_line_function_nonexistent(self):
+        line = gpiod.find_line('nonexistent-line')
+        self.assertEqual(line, None)
+
+class LineInfo(MockupTestCase):
+
+    chip_sizes = ( 8, )
+    flags = gpiomockup.Mockup.FLAG_NAMED_LINES
+
+    def test_unexported_line(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(4)
+            self.assertEqual(line.offset(), 4)
+            self.assertEqual(line.name(), 'gpio-mockup-A-4')
+            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_INPUT)
+            self.assertEqual(line.active_state(), gpiod.Line.ACTIVE_HIGH)
+            self.assertEqual(line.consumer(), None)
+            self.assertFalse(line.is_used())
+            self.assertFalse(line.is_requested())
+
+    def test_exported_line(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(4)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_OUT,
+                         flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
+            self.assertEqual(line.offset(), 4)
+            self.assertEqual(line.name(), 'gpio-mockup-A-4')
+            self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
+            self.assertEqual(line.active_state(), gpiod.Line.ACTIVE_LOW)
+            self.assertEqual(line.consumer(), default_consumer)
+            self.assertTrue(line.is_used())
+            self.assertTrue(line.is_requested())
+
+    def test_exported_line_with_flags(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(4)
+            flags = (gpiod.LINE_REQ_FLAG_ACTIVE_LOW |
+                     gpiod.LINE_REQ_FLAG_OPEN_DRAIN)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_OUT,
+                         flags=flags)
+            self.assertEqual(line.offset(), 4)
+            self.assertEqual(line.name(), 'gpio-mockup-A-4')
+            # FIXME Uncomment the line below once this issue is fixed in the kernel.
+            #self.assertEqual(line.direction(), gpiod.Line.DIRECTION_OUTPUT)
+            self.assertEqual(line.active_state(), gpiod.Line.ACTIVE_LOW)
+            self.assertEqual(line.consumer(), default_consumer)
+            self.assertTrue(line.is_used())
+            self.assertTrue(line.is_requested())
+            self.assertTrue(line.is_open_drain())
+            self.assertFalse(line.is_open_source())
+
+class LineValues(MockupTestCase):
+
+    chip_sizes = ( 8, )
+
+    def test_get_value_single_line(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_IN)
+            self.assertEqual(line.get_value(), 0)
+            mockup.chip_set_pull(0, 3, 1)
+            self.assertEqual(line.get_value(), 1)
+
+    def test_set_value_single_line(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_OUT)
+            line.set_value(1)
+            self.assertEqual(mockup.chip_get_value(0, 3), 1)
+            line.set_value(0)
+            self.assertEqual(mockup.chip_get_value(0, 3), 0)
+
+    def test_set_value_with_default_value_argument(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_OUT,
+                         default_val=1)
+            self.assertEqual(mockup.chip_get_value(0, 3), 1)
+
+    def test_get_value_multiple_lines(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            lines = chip.get_lines(( 0, 3, 4, 6 ))
+            lines.request(consumer=default_consumer,
+                          type=gpiod.LINE_REQ_DIR_IN)
+            self.assertEqual(lines.get_values(), [ 0, 0, 0, 0 ])
+            mockup.chip_set_pull(0, 0, 1)
+            mockup.chip_set_pull(0, 4, 1)
+            mockup.chip_set_pull(0, 6, 1)
+            self.assertEqual(lines.get_values(), [ 1, 0, 1, 1 ])
+
+    def test_set_value_multiple_lines(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            lines = chip.get_lines(( 0, 3, 4, 6 ))
+            lines.request(consumer=default_consumer,
+                          type=gpiod.LINE_REQ_DIR_OUT)
+            lines.set_values(( 1, 0, 1, 1 ))
+            self.assertEqual(mockup.chip_get_value(0, 0), 1)
+            self.assertEqual(mockup.chip_get_value(0, 3), 0)
+            self.assertEqual(mockup.chip_get_value(0, 4), 1)
+            self.assertEqual(mockup.chip_get_value(0, 6), 1)
+            lines.set_values(( 0, 0, 1, 0 ))
+            self.assertEqual(mockup.chip_get_value(0, 0), 0)
+            self.assertEqual(mockup.chip_get_value(0, 3), 0)
+            self.assertEqual(mockup.chip_get_value(0, 4), 1)
+            self.assertEqual(mockup.chip_get_value(0, 6), 0)
+
+    def test_set_multiple_values_with_default_vals_argument(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            lines = chip.get_lines(( 0, 3, 4, 6 ))
+            lines.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_OUT,
+                         default_vals=( 1, 0, 1, 1 ))
+            self.assertEqual(mockup.chip_get_value(0, 0), 1)
+            self.assertEqual(mockup.chip_get_value(0, 3), 0)
+            self.assertEqual(mockup.chip_get_value(0, 4), 1)
+            self.assertEqual(mockup.chip_get_value(0, 6), 1)
+
+    def test_get_value_active_low(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_IN,
+                         flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
+            self.assertEqual(line.get_value(), 1)
+            mockup.chip_set_pull(0, 3, 1)
+            self.assertEqual(line.get_value(), 0)
+
+    def test_set_value_active_low(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_OUT,
+                         flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
+            line.set_value(1)
+            self.assertEqual(mockup.chip_get_value(0, 3), 0)
+            line.set_value(0)
+            self.assertEqual(mockup.chip_get_value(0, 3), 1)
+
+class LineRequestBehavior(MockupTestCase):
+
+    chip_sizes = ( 8, )
+
+    def test_line_export_release(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_IN)
+            self.assertTrue(line.is_requested())
+            self.assertEqual(line.get_value(), 0)
+            line.release()
+            self.assertFalse(line.is_requested())
+
+    def test_line_request_twice_two_calls(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_IN)
+            with self.assertRaises(OSError):
+                line.request(consumer=default_consumer,
+                             type=gpiod.LINE_REQ_DIR_IN)
+
+    def test_line_request_twice_in_bulk(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            lines = chip.get_lines(( 2, 3, 6, 6 ))
+            with self.assertRaises(OSError):
+                lines.request(consumer=default_consumer,
+                              type=gpiod.LINE_REQ_DIR_IN)
+
+    def test_use_value_unrequested(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            with self.assertRaises(OSError):
+                line.get_value()
+
+#
+# Iterator test cases
+#
+
+class ChipIterator(MockupTestCase):
+
+    chip_sizes = ( 4, 8, 16 )
+
+    def test_iterate_over_chips(self):
+        gotA = False
+        gotB = False
+        gotC = False
+
+        for chip in gpiod.ChipIter():
+            if chip.label() == 'gpio-mockup-A':
+                gotA = True
+            elif chip.label() == 'gpio-mockup-B':
+                gotB = True
+            elif chip.label() == 'gpio-mockup-C':
+                gotC = True
+
+        self.assertTrue(gotA)
+        self.assertTrue(gotB)
+        self.assertTrue(gotC)
+
+class LineIterator(MockupTestCase):
+
+    chip_sizes = ( 4, )
+
+    def test_iterate_over_lines(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            count = 0
+
+            for line in gpiod.LineIter(chip):
+                self.assertEqual(line.offset(), count)
+                count += 1
+
+            self.assertEqual(count, chip.num_lines())
+
+class LineBulkIter(MockupTestCase):
+
+    chip_sizes = ( 4, )
+
+    def test_line_bulk_iterator(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            lines = chip.get_all_lines()
+            count = 0
+
+            for line in lines:
+                self.assertEqual(line.offset(), count)
+                count += 1
+
+            self.assertEqual(count, chip.num_lines())
+
+#
+# Event test cases
+#
+
+class EventSingleLine(MockupTestCase):
+
+    chip_sizes = ( 8, )
+
+    def test_single_line_rising_edge_event(self):
+        with EventThread(0, 4, 200):
+            with gpiod.Chip(mockup.chip_name(0)) as chip:
+                line = chip.get_line(4)
+                line.request(consumer=default_consumer,
+                             type=gpiod.LINE_REQ_EV_RISING_EDGE)
+                self.assertTrue(line.event_wait(sec=1))
+                event = line.event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
+                self.assertEqual(event.source.offset(), 4)
+
+    def test_single_line_falling_edge_event(self):
+        with EventThread(0, 4, 200):
+            with gpiod.Chip(mockup.chip_name(0)) as chip:
+                line = chip.get_line(4)
+                line.request(consumer=default_consumer,
+                             type=gpiod.LINE_REQ_EV_FALLING_EDGE)
+                self.assertTrue(line.event_wait(sec=1))
+                event = line.event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.FALLING_EDGE)
+                self.assertEqual(event.source.offset(), 4)
+
+    def test_single_line_both_edges_events(self):
+        with EventThread(0, 4, 200):
+            with gpiod.Chip(mockup.chip_name(0)) as chip:
+                line = chip.get_line(4)
+                line.request(consumer=default_consumer,
+                             type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+                self.assertTrue(line.event_wait(sec=1))
+                event = line.event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
+                self.assertEqual(event.source.offset(), 4)
+                self.assertTrue(line.event_wait(sec=1))
+                event = line.event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.FALLING_EDGE)
+                self.assertEqual(event.source.offset(), 4)
+
+    def test_single_line_both_edges_events_active_low(self):
+        with EventThread(0, 4, 200):
+            with gpiod.Chip(mockup.chip_name(0)) as chip:
+                line = chip.get_line(4)
+                line.request(consumer=default_consumer,
+                             type=gpiod.LINE_REQ_EV_BOTH_EDGES,
+                             flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
+                self.assertTrue(line.event_wait(sec=1))
+                event = line.event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.FALLING_EDGE)
+                self.assertEqual(event.source.offset(), 4)
+                self.assertTrue(line.event_wait(sec=1))
+                event = line.event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
+                self.assertEqual(event.source.offset(), 4)
+
+class EventBulk(MockupTestCase):
+
+    chip_sizes = ( 8, )
+
+    def test_watch_multiple_lines_for_events(self):
+        with EventThread(0, 2, 200):
+            with gpiod.Chip(mockup.chip_name(0)) as chip:
+                lines = chip.get_lines(( 0, 1, 2, 3, 4 ))
+                lines.request(consumer=default_consumer,
+                              type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+                event_lines = lines.event_wait(sec=1)
+                self.assertEqual(len(event_lines), 1)
+                line = event_lines[0]
+                event = line.event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
+                self.assertEqual(event.source.offset(), 2)
+                event_lines = lines.event_wait(sec=1)
+                self.assertEqual(len(event_lines), 1)
+                line = event_lines[0]
+                event = line.event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.FALLING_EDGE)
+                self.assertEqual(event.source.offset(), 2)
+
+class EventValues(MockupTestCase):
+
+    chip_sizes = ( 8, )
+
+    def test_request_for_events_get_value(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+            self.assertEqual(line.get_value(), 0)
+            mockup.chip_set_pull(0, 3, 1)
+            self.assertEqual(line.get_value(), 1)
+
+    def test_request_for_events_get_value_active_low(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_EV_BOTH_EDGES,
+                         flags=gpiod.LINE_REQ_FLAG_ACTIVE_LOW)
+            self.assertEqual(line.get_value(), 1)
+            mockup.chip_set_pull(0, 3, 1)
+            self.assertEqual(line.get_value(), 0)
+
+class EventFileDescriptor(MockupTestCase):
+
+    chip_sizes = ( 8, )
+
+    def test_event_get_fd(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+            fd = line.event_get_fd();
+            self.assertGreaterEqual(fd, 0)
+
+    def test_event_get_fd_not_requested(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            with self.assertRaises(OSError):
+                fd = line.event_get_fd();
+
+    def test_event_get_fd_requested_for_values(self):
+        with gpiod.Chip(mockup.chip_name(0)) as chip:
+            line = chip.get_line(3)
+            line.request(consumer=default_consumer,
+                         type=gpiod.LINE_REQ_DIR_IN)
+            with self.assertRaises(OSError):
+                fd = line.event_get_fd();
+
+    def test_event_fd_polling(self):
+        with EventThread(0, 2, 200):
+            with gpiod.Chip(mockup.chip_name(0)) as chip:
+                lines = chip.get_lines(( 0, 1, 2, 3, 4, 5, 6 ))
+                lines.request(consumer=default_consumer,
+                              type=gpiod.LINE_REQ_EV_BOTH_EDGES)
+
+                inputs = []
+                for line in lines:
+                    inputs.append(line.event_get_fd())
+
+                readable, writable, exceptional = select.select(inputs, [],
+                                                                inputs, 1.0)
+
+                self.assertEqual(len(readable), 1)
+                event = lines.to_list()[2].event_read()
+                self.assertEqual(event.type, gpiod.LineEvent.RISING_EDGE)
+                self.assertEqual(event.source.offset(), 2)
+
+#
+# Main
+#
+
+if __name__ == '__main__':
+    check_kernel(5, 2, 7)
+    mockup = gpiomockup.Mockup()
+    unittest.main()
diff --git a/bindings/python/tests/gpiomockupmodule.c b/bindings/python/tests/gpiomockupmodule.c
new file mode 100644 (file)
index 0000000..d54ecf9
--- /dev/null
@@ -0,0 +1,313 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * This file is part of libgpiod.
+ *
+ * Copyright (C) 2019 Bartosz Golaszewski <bgolaszewski@baylibre.com>
+ */
+
+#include <Python.h>
+#include <gpio-mockup.h>
+
+typedef struct {
+       PyObject_HEAD
+       struct gpio_mockup *mockup;
+} gpiomockup_MockupObject;
+
+enum {
+       gpiomockup_FLAG_NAMED_LINES = 1,
+};
+
+static int gpiomockup_Mockup_init(gpiomockup_MockupObject *self,
+                                 PyObject *Py_UNUSED(ignored0),
+                                 PyObject *Py_UNUSED(ignored1))
+{
+       Py_BEGIN_ALLOW_THREADS;
+       self->mockup = gpio_mockup_new();
+       Py_END_ALLOW_THREADS;
+       if (!self->mockup) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return -1;
+       }
+
+       return 0;
+}
+
+static void gpiomockup_Mockup_dealloc(gpiomockup_MockupObject *self)
+{
+       if (self->mockup) {
+               Py_BEGIN_ALLOW_THREADS;
+               gpio_mockup_unref(self->mockup);
+               Py_END_ALLOW_THREADS;
+       }
+
+       PyObject_Del(self);
+}
+
+static PyObject *gpiomockup_Mockup_probe(gpiomockup_MockupObject *self,
+                                        PyObject *args, PyObject *kwds)
+{
+       static char *kwlist[] = { "chip_sizes",
+                                 "flags",
+                                 NULL };
+
+       PyObject *chip_sizes_obj, *iter, *next;
+       unsigned int *chip_sizes;
+       int ret, flags = 0, i;
+       Py_ssize_t num_chips;
+
+       ret = PyArg_ParseTupleAndKeywords(args, kwds, "O|i", kwlist,
+                                         &chip_sizes_obj, &flags);
+       if (!ret)
+               return NULL;
+
+       num_chips = PyObject_Size(chip_sizes_obj);
+       if (num_chips < 0) {
+               return NULL;
+       } else if (num_chips == 0) {
+               PyErr_SetString(PyExc_TypeError,
+                               "Number of chips must be greater thatn 0");
+               return NULL;
+       }
+
+       chip_sizes = PyMem_RawCalloc(num_chips, sizeof(unsigned int));
+       if (!chip_sizes)
+               return NULL;
+
+       iter = PyObject_GetIter(chip_sizes_obj);
+       if (!iter) {
+               PyMem_RawFree(chip_sizes);
+               return NULL;
+       }
+
+       for (i = 0;; i++) {
+               next = PyIter_Next(iter);
+               if (!next) {
+                       Py_DECREF(iter);
+                       break;
+               }
+
+               chip_sizes[i] = PyLong_AsUnsignedLong(next);
+               Py_DECREF(next);
+               if (PyErr_Occurred()) {
+                       Py_DECREF(iter);
+                       PyMem_RawFree(chip_sizes);
+                       return NULL;
+               }
+       }
+
+       if (flags & gpiomockup_FLAG_NAMED_LINES)
+               flags |= GPIO_MOCKUP_FLAG_NAMED_LINES;
+
+       Py_BEGIN_ALLOW_THREADS;
+       ret = gpio_mockup_probe(self->mockup, num_chips, chip_sizes, flags);
+       Py_END_ALLOW_THREADS;
+       PyMem_RawFree(chip_sizes);
+       if (ret)
+               return NULL;
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *gpiomockup_Mockup_remove(gpiomockup_MockupObject *self,
+                                         PyObject *Py_UNUSED(ignored))
+{
+       int ret;
+
+       Py_BEGIN_ALLOW_THREADS;
+       ret = gpio_mockup_remove(self->mockup);
+       Py_END_ALLOW_THREADS;
+       if (ret) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return NULL;
+       }
+
+       Py_RETURN_NONE;
+}
+
+static PyObject *gpiomockup_Mockup_chip_name(gpiomockup_MockupObject *self,
+                                            PyObject *args)
+{
+       unsigned int idx;
+       const char *name;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "I", &idx);
+       if (!ret)
+               return NULL;
+
+       name = gpio_mockup_chip_name(self->mockup, idx);
+       if (!name) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return NULL;
+       }
+
+       return PyUnicode_FromString(name);
+}
+
+static PyObject *gpiomockup_Mockup_chip_path(gpiomockup_MockupObject *self,
+                                            PyObject *args)
+{
+       unsigned int idx;
+       const char *path;
+       int ret;
+
+       ret = PyArg_ParseTuple(args, "I", &idx);
+       if (!ret)
+               return NULL;
+
+       path = gpio_mockup_chip_path(self->mockup, idx);
+       if (!path) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return NULL;
+       }
+
+       return PyUnicode_FromString(path);
+}
+
+static PyObject *gpiomockup_Mockup_chip_num(gpiomockup_MockupObject *self,
+                                            PyObject *args)
+{
+       unsigned int idx;
+       int ret, num;
+
+       ret = PyArg_ParseTuple(args, "I", &idx);
+       if (!ret)
+               return NULL;
+
+       num = gpio_mockup_chip_num(self->mockup, idx);
+       if (num < 0) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return NULL;
+       }
+
+       return PyLong_FromLong(num);
+}
+
+static PyObject *gpiomockup_Mockup_chip_get_value(gpiomockup_MockupObject *self,
+                                                 PyObject *args)
+{
+       unsigned int chip_idx, line_offset;
+       int ret, val;
+
+       ret = PyArg_ParseTuple(args, "II", &chip_idx, &line_offset);
+       if (!ret)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       val = gpio_mockup_get_value(self->mockup, chip_idx, line_offset);
+       Py_END_ALLOW_THREADS;
+       if (val < 0) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return NULL;
+       }
+
+       return PyLong_FromUnsignedLong(val);
+}
+
+static PyObject *gpiomockup_Mockup_chip_set_pull(gpiomockup_MockupObject *self,
+                                                PyObject *args)
+{
+       unsigned int chip_idx, line_offset;
+       int ret, pull;
+
+       ret = PyArg_ParseTuple(args, "IIi", &chip_idx, &line_offset, &pull);
+       if (!ret)
+               return NULL;
+
+       Py_BEGIN_ALLOW_THREADS;
+       ret = gpio_mockup_set_pull(self->mockup, chip_idx, line_offset, pull);
+       Py_END_ALLOW_THREADS;
+       if (ret) {
+               PyErr_SetFromErrno(PyExc_OSError);
+               return NULL;
+       }
+
+       Py_RETURN_NONE;
+}
+
+static PyMethodDef gpiomockup_Mockup_methods[] = {
+       {
+               .ml_name = "probe",
+               .ml_meth = (PyCFunction)(void (*)(void))gpiomockup_Mockup_probe,
+               .ml_flags = METH_VARARGS | METH_KEYWORDS,
+       },
+       {
+               .ml_name = "remove",
+               .ml_meth = (PyCFunction)gpiomockup_Mockup_remove,
+               .ml_flags = METH_NOARGS,
+       },
+       {
+               .ml_name = "chip_name",
+               .ml_meth = (PyCFunction)gpiomockup_Mockup_chip_name,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "chip_path",
+               .ml_meth = (PyCFunction)gpiomockup_Mockup_chip_path,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "chip_num",
+               .ml_meth = (PyCFunction)gpiomockup_Mockup_chip_num,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "chip_get_value",
+               .ml_meth = (PyCFunction)gpiomockup_Mockup_chip_get_value,
+               .ml_flags = METH_VARARGS,
+       },
+       {
+               .ml_name = "chip_set_pull",
+               .ml_meth = (PyCFunction)gpiomockup_Mockup_chip_set_pull,
+               .ml_flags = METH_VARARGS,
+       },
+       { }
+};
+
+static PyTypeObject gpiomockup_MockupType = {
+       PyVarObject_HEAD_INIT(NULL, 0)
+       .tp_name = "gpiomockup.Mockup",
+       .tp_basicsize = sizeof(gpiomockup_MockupObject),
+       .tp_flags = Py_TPFLAGS_DEFAULT,
+       .tp_new = PyType_GenericNew,
+       .tp_init = (initproc)gpiomockup_Mockup_init,
+       .tp_dealloc = (destructor)gpiomockup_Mockup_dealloc,
+       .tp_methods = gpiomockup_Mockup_methods,
+};
+
+static PyModuleDef gpiomockup_Module = {
+       PyModuleDef_HEAD_INIT,
+       .m_name = "gpiomockup",
+       .m_size = -1,
+};
+
+PyMODINIT_FUNC PyInit_gpiomockup(void)
+{
+       PyObject *module, *val;
+       int ret;
+
+       module = PyModule_Create(&gpiomockup_Module);
+       if (!module)
+               return NULL;
+
+       ret = PyType_Ready(&gpiomockup_MockupType);
+       if (ret)
+               return NULL;
+       Py_INCREF(&gpiomockup_MockupType);
+
+       ret = PyModule_AddObject(module, "Mockup",
+                                (PyObject *)&gpiomockup_MockupType);
+       if (ret)
+               return NULL;
+
+       val = PyLong_FromLong(gpiomockup_FLAG_NAMED_LINES);
+       if (!val)
+               return NULL;
+
+       ret = PyDict_SetItemString(gpiomockup_MockupType.tp_dict,
+                                  "FLAG_NAMED_LINES", val);
+       if (ret)
+               return NULL;
+
+       return module;
+}
index bf364e79419b67b5dd75617717da59190f5bcb7b..1978eea6d65c3bc6a825cf4038bc6f7562afa030 100644 (file)
@@ -220,6 +220,7 @@ AC_CONFIG_FILES([libgpiod.pc
                 bindings/cxx/tests/Makefile
                 bindings/python/Makefile
                 bindings/python/examples/Makefile
+                bindings/python/tests/Makefile
                 man/Makefile])
 
 AC_OUTPUT