6 Commits

Author SHA1 Message Date
157b7bd118 test(dbus): Add support for testing driver reset notification
Introduce `FakeResetDriverWatchdog` to simulate driver reset triggers.
Update existing tests and add a new test, `test_notify_reset_driver`, to
verify reset notifications using the event signaling mechanism.
2025-04-19 15:56:59 +02:00
26c3ac0afb refactor(dbus): D-Bus interface management with cleanup support.
Introduced a `DbusInterface` base class with a `cleanup` method to
standardize resource cleanup for D-Bus interfaces. Modified
`InterfacePiControl` to inherit from it and implemented `cleanup` logic
for proper watchdog resource handling. Adjusted `BusProvider` to manage
multiple interfaces and ensure cleanup on shutdown.
2025-04-19 15:55:49 +02:00
f8bc1532e3 refactor(dbus): Fix typo and remove unused thread instance
Corrected a typo in the `timeout` parameter name in `method_await_reset`
and removed an unused thread instance `th_sleep` from `dbus_helper.py`.
These changes improve code clarity and eliminate redundant components.
2025-04-19 15:05:47 +02:00
7207845b13 test(dbus): Add unit tests for PiControl D-Bus interface
Introduces initial tests for the PiControl interface in
`dbus_middleware1`. These tests cover basic functionality like checking
activity status and resetting the driver while verifying IOCTL calls.
2025-04-19 14:40:05 +02:00
3f808c55f8 test(dbus): Add unit test framework for dbus_middleware1 module
Introduces test infrastructure and mockup classes for
`dbus_middleware1`. Includes `BusProvider` test setup, fake device
implementations, and initialization logic to support session bus
testing. Enhances testability and isolation of the D-Bus middleware
components.
2025-04-19 14:40:05 +02:00
4ccc328d0b refactor(dbus): piControl driver reset with PiControlIoctl class
Replaces inline ioctl logic with the new `PiControlIoctl` class for
cleaner and reusable code. This improves readability, encapsulates
device operations, and simplifies error handling for resetting the
piControl driver.
2025-04-19 13:57:56 +02:00
12 changed files with 276 additions and 23 deletions

View File

@@ -44,10 +44,11 @@ def method_reset():
log.info("ResetDriver called via D-Bus")
def method_await_reset(timout: int = 0):
def method_await_reset(timeout: int = 0):
detected_signal = await_signal(
"NotifyDriverReset",
timout, extend_interface("picontrol"),
timeout,
extend_interface("picontrol"),
bus_type=BusType.SESSION if pi.pargs.use_session_bus else BusType.SYSTEM,
)
if detected_signal:

View File

@@ -96,7 +96,6 @@ def await_signal(
detected_signal = False
timeout = int(timeout)
loop = GLib.MainLoop()
th_sleep = Thread()
def th_timeout():
sleep(timeout)

View File

@@ -34,10 +34,19 @@ class BusProvider(Thread):
def run(self):
log.debug("enter BusProvider.run")
# The 2nd, 3rd, ... arguments can be objects or tuples of a path and an object
# Example(),
# ("Subdir1", Example()),
# ("Subdir2", Example()),
# ("Subdir2/Whatever", Example())
lst_interfaces = [
InterfacePiControl(self.picontrol_device, self.config_rsc),
]
try:
self._bus.publish(
REVPI_DBUS_NAME,
InterfacePiControl(self.picontrol_device, self.config_rsc),
*lst_interfaces,
)
except Exception as e:
log.error(f"can not publish dbus {REVPI_DBUS_NAME}: {e}")
@@ -47,6 +56,12 @@ class BusProvider(Thread):
except Exception as e:
log.error(f"can not run dbus mainloop: {e}")
# Clean up all interfaces
for interface in lst_interfaces:
if type(interface) is tuple:
_, interface = interface
interface.cleanup()
log.debug("leave BusProvider.run")
def stop(self):

View File

@@ -11,6 +11,18 @@ REVPI_DBUS_NAME = "com.revolutionpi.middleware1"
REVPI_DBUS_BASE_PATH = "/com/revolutionpi/middleware1"
class DbusInterface:
def cleanup(self):
"""
Represents a method responsible for performing cleanup operations. This method is executed to properly
release resources, close connections, or perform other necessary finalization tasks.
This method does not take any arguments or return a value.
"""
pass
def extend_interface(*args) -> str:
"""
Extends an interface name by appending additional segments to a pre-defined base name.

View File

@@ -2,18 +2,17 @@
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
"""D-Bus interfaces for piControl."""
import os
from fcntl import ioctl
from logging import getLogger
from pydbus.generic import signal
from .process_image_helper import ResetDriverWatchdog
from .process_image_helper import PiControlIoctl, ResetDriverWatchdog
from ..dbus_helper import DbusInterface
log = getLogger(__name__)
class InterfacePiControl:
class InterfacePiControl(DbusInterface):
"""
<node>
<interface name='com.revolutionpi.middleware1.picontrol'>
@@ -34,6 +33,9 @@ class InterfacePiControl:
self.wd_reset_driver = ResetDriverWatchdog(self.picontrol_device)
self.wd_reset_driver.register_call(self.notify_reset_driver)
def cleanup(self):
self.wd_reset_driver.stop()
def notify_reset_driver(self):
self.NotifyDriverReset()
@@ -41,21 +43,9 @@ class InterfacePiControl:
log.debug("enter InterfacePiControl.ResetDriver")
try:
fd = os.open(self.picontrol_device, os.O_WRONLY)
except Exception as e:
log.warning(f"could not open ${self.picontrol_device} to reset driver")
raise e
execption = None
try:
# KB_RESET _IO('K', 12 ) // reset the piControl driver including the config file
ioctl(fd, 19212)
picontrol_ioctl = PiControlIoctl(self.picontrol_device)
picontrol_ioctl.ioctl(PiControlIoctl.IOCTL_RESET_DRIVER)
log.info("reset piControl driver")
except Exception as e:
log.warning(f"could not reset piControl driver: ${e}")
execption = e
finally:
os.close(fd)
if execption:
raise execption
raise e

View File

@@ -8,6 +8,7 @@ The ResetDriverWatchdog class is a copy of revpipyload project module "watchdogs
https://github.com/naruxde/revpipyload/blob/b51c2b617a57cc7d96fd67e1da9f090a0624eacb/src/revpipyload/watchdogs.py
"""
import os
from ctypes import c_int
from fcntl import ioctl
from logging import getLogger
from threading import Thread
@@ -100,3 +101,24 @@ class ResetDriverWatchdog(Thread):
rc = self._triggered
self._triggered = False
return rc
class PiControlIoctl:
IOCTL_RESET_DRIVER = 19212
def __init__(self, picontrol_device: str):
self.picontrol_device = picontrol_device
def ioctl(self, request, arg=0):
if type(arg) is c_int:
arg = arg.value
_fd = os.open(self.picontrol_device, os.O_WRONLY)
return_value = ioctl(_fd, 19212, arg)
os.close(_fd)
return return_value
@property
def name(self):
return self.picontrol_device

3
tests/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later

View File

@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
from os import environ
# D-BUS needs a DISPLAY variable to work with the session bus
environ["DISPLAY"] = ":0"

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
from time import sleep
from tests.dbus_middleware1.fake_devices import PiControlDeviceMockup
class TestBusProvider(PiControlDeviceMockup):
def setUp(self):
super().setUp()
# Do not import things on top of the module. Some classes or functions need to be mocked up first.
from revpi_middleware.dbus_middleware1 import BusProvider
# Prepare the bus provider and start it
self.bp = BusProvider(
self.picontrol.name,
use_system_bus=False,
)
self.bp.start()
# Wait 5 seconds until the bus provider has started the main loop
counter = 50
while not self.bp.running and counter > 0:
counter -= 1
sleep(0.1)
def tearDown(self):
self.bp.stop()
self.bp.join(10.0)
if self.bp.is_alive():
raise RuntimeError("Bus provider thread is still running")
super().tearDown()

View File

@@ -0,0 +1,120 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
from ctypes import c_int
from queue import Empty, Queue
from tempfile import NamedTemporaryFile
from threading import Event, Thread
from unittest import TestCase
IOCTL_QUEUE = Queue()
RESET_DRIVER_EVENT = Event()
class FakePiControlDevice:
IOCTL_RESET_DRIVER = 19212
def __init__(self, picontrol_device: str):
self._fh = NamedTemporaryFile("wb+", 0, prefix="fake_device_")
self.name = self._fh.name
def __del__(self):
self._fh.close()
def reset_process_image(self):
self._fh.write(b"\x00" * 4096)
self._fh.seek(0)
def ioctl(self, request, arg=0) -> int:
if type(arg) is c_int:
arg = arg.value
if request == self.IOCTL_RESET_DRIVER:
pass
else:
raise NotImplementedError(f"Unknown IOCTL request: {request}")
IOCTL_QUEUE.put_nowait((request, arg))
return arg
def close(self):
self._fh.close()
def read(self, size):
return self._fh.read(size)
def seek(self, offset, whence):
self._fh.seek(offset, whence)
def write(self, buffer):
return self._fh.write(buffer)
class FakeResetDriverWatchdog(Thread):
def __init__(self, picontrol_device: str):
super().__init__()
self.daemon = True
self._calls = []
self._exit = False
self.not_implemented = True
self._triggered = False
self.start()
def run(self):
while not self._exit:
if RESET_DRIVER_EVENT.wait(0.1):
RESET_DRIVER_EVENT.clear()
self._triggered = True
for func in self._calls:
func()
def register_call(self, function):
"""Register a function, if watchdog triggers."""
if not callable(function):
raise ValueError("Function is not callable.")
if function not in self._calls:
self._calls.append(function)
def stop(self):
"""Stop watchdog for reset_driver."""
self._exit = True
def unregister_call(self, function=None):
"""Remove a function from the watchdog trigger."""
if function is None:
self._calls.clear()
elif function in self._calls:
self._calls.remove(function)
@property
def triggered(self):
"""Will return True one time after watchdog was triggered."""
rc = self._triggered
self._triggered = False
return rc
class PiControlDeviceMockup(TestCase):
def setUp(self):
super().setUp()
# Empty the queue
while True:
try:
IOCTL_QUEUE.get_nowait()
except Empty:
break
# Replace classes with mockup classes
import revpi_middleware.dbus_middleware1.process_image.interface_picontrol as test_helpers
test_helpers.PiControlIoctl = FakePiControlDevice
test_helpers.ResetDriverWatchdog = FakeResetDriverWatchdog
# Create a fake picontrol0 device
self.picontrol = FakePiControlDevice(picontrol_device="/dev/fake_device_0")
def tearDown(self):
self.picontrol.close()
super().tearDown()

View File

@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# SPDX-FileCopyrightText: 2025 KUNBUS GmbH
# SPDX-License-Identifier: GPL-2.0-or-later
from threading import Thread
from time import sleep
from revpi_middleware.cli_commands.dbus_helper import BusType, await_signal, simple_call
from revpi_middleware.dbus_middleware1 import extend_interface
from tests.dbus_middleware1.bus_provider import TestBusProvider
from tests.dbus_middleware1.fake_devices import IOCTL_QUEUE, RESET_DRIVER_EVENT
class TestObjectPicontrol(TestBusProvider):
def test_is_active(self):
self.assertTrue(self.bp.running)
def test_reset_driver(self):
simple_call(
"ResetDriver",
interface=extend_interface("picontrol"),
bus_type=BusType.SESSION,
)
ioctl_call = IOCTL_QUEUE.get(timeout=2.0)
self.assertEqual((19212, 0), ioctl_call)
def test_notify_reset_driver(self):
timeout = 5
def target_call_reset_driver():
sleep(1.0)
RESET_DRIVER_EVENT.set()
th_wait_for_reset = Thread(target=target_call_reset_driver, daemon=True)
th_wait_for_reset.start()
result = await_signal(
"NotifyDriverReset",
timeout,
extend_interface("picontrol"),
bus_type=BusType.SESSION,
)
self.assertTrue(result)
th_wait_for_reset.join(timeout=timeout)