返回教程正文

配套源码

test_socketcan.py

app/tests/test_socketcan.py
generic_can Brick:在 VENTUNO Q 上自定义 CAN 报文app/tests/test_socketcan.py
Python265 行
"""SocketCANBackend与python-can边界映射测试。"""

from types import SimpleNamespace
import unittest
from unittest.mock import patch

from generic_can import CANBackendError, CanFrame
from generic_can.backends import SocketCANBackend


class FakePythonCanBus:
    """记录SocketCANBackend对python-can Bus的调用。"""

    def __init__(self):
        """
        @description         : 创建空的发送记录和接收脚本
        @param self          : 当前FakePythonCanBus对象
        @return              : 无
        """
        self.sent = []
        self.receive_values = []
        self.receive_timeouts = []
        self.shutdown_count = 0

    def send(self, message):
        """
        @description         : 记录一条模拟python-can Message
        @param self          : 当前FakePythonCanBus对象
        @param message       : 待记录的模拟Message
        @return              : 无
        """
        self.sent.append(message)

    def recv(self, timeout):
        """
        @description         : 按测试脚本返回下一条模拟接收结果
        @param self          : 当前FakePythonCanBus对象
        @param timeout       : 后端传入的等待秒数
        @return              : 模拟Message或None
        """
        self.receive_timeouts.append(timeout)
        if not self.receive_values:
            return None
        return self.receive_values.pop(0)

    def shutdown(self):
        """
        @description         : 记录一次python-can Bus关闭动作
        @param self          : 当前FakePythonCanBus对象
        @return              : 无
        """
        self.shutdown_count += 1


class FakePythonCanModule:
    """提供SocketCANBackend实际使用的Bus和Message入口。"""

    def __init__(self):
        """
        @description         : 创建一个可注入后端的python-can兼容模块
        @param self          : 当前FakePythonCanModule对象
        @return              : 无
        """
        self.bus = FakePythonCanBus()
        self.bus_options = None

    def Bus(self, **options):
        """
        @description         : 保存Bus构造参数并返回模拟Bus
        @param self          : 当前FakePythonCanModule对象
        @param options       : SocketCANBackend传入的关键字参数
        @return              : 模拟python-can Bus
        """
        self.bus_options = options
        return self.bus

    def Message(self, **fields):
        """
        @description         : 把Message字段保存为简单属性对象
        @param self          : 当前FakePythonCanModule对象
        @param fields        : SocketCANBackend传入的报文字段
        @return              : 模拟python-can Message
        """
        return SimpleNamespace(**fields)


def incoming_message(**overrides):
    """
    @description         : 创建一条包含python-can常用字段的模拟接收消息
    @param overrides     : 需要覆盖的消息字段
    @return              : 模拟python-can Message
    """
    fields = {
        "arbitration_id": 0x123,
        "data": bytearray([1, 2]),
        "is_extended_id": False,
        "is_fd": False,
        "bitrate_switch": False,
        "timestamp": 12.5,
        "is_error_frame": False,
        "is_remote_frame": False,
    }
    fields.update(overrides)
    return SimpleNamespace(**fields)


class SocketCANBackendTests(unittest.TestCase):
    """验证唯一允许接触python-can的后端模块。"""

    def setUp(self):
        """
        @description         : 为每个测试创建新的模拟python-can模块和后端
        @param self          : 当前测试用例
        @return              : 无
        """
        self.can_module = FakePythonCanModule()
        self.backend = SocketCANBackend(
            device="can7",
            can_module=self.can_module,
        )

    def test_open_passes_selected_interface_and_fd_capability(self):
        """
        @description         : 验证用户可选择接口且后端启用FD帧处理能力
        @param self          : 当前测试用例
        @return              : 无
        """
        self.backend.open()
        self.assertEqual(
            self.can_module.bus_options,
            {"interface": "socketcan", "channel": "can7", "fd": True},
        )

    def test_send_maps_every_public_frame_field(self):
        """
        @description         : 验证CanFrame字段完整映射为python-can Message
        @param self          : 当前测试用例
        @return              : 无
        """
        self.backend.open()
        frame = CanFrame(
            0x123456,
            b"\x01\x02",
            is_extended=True,
            is_fd=True,
            bitrate_switch=True,
        )
        self.backend.send(frame)
        message = self.can_module.bus.sent[0]
        self.assertEqual(message.arbitration_id, 0x123456)
        self.assertEqual(message.data, b"\x01\x02")
        self.assertTrue(message.is_extended_id)
        self.assertTrue(message.is_fd)
        self.assertTrue(message.bitrate_switch)
        self.assertTrue(message.check)

    def test_receive_maps_data_frame_and_timestamp(self):
        """
        @description         : 验证python-can接收消息完整转换为CanFrame
        @param self          : 当前测试用例
        @return              : 无
        """
        self.backend.open()
        self.can_module.bus.receive_values.append(
            incoming_message(
                arbitration_id=0x456,
                is_fd=True,
                bitrate_switch=True,
            )
        )
        self.assertEqual(
            self.backend.receive(0.1),
            CanFrame(
                0x456,
                b"\x01\x02",
                is_fd=True,
                bitrate_switch=True,
                timestamp=12.5,
            ),
        )

    def test_error_and_remote_frames_are_ignored_and_counted(self):
        """
        @description         : 验证错误帧和远程帧不进入业务队列且可诊断
        @param self          : 当前测试用例
        @return              : 无
        """
        self.backend.open()
        self.can_module.bus.receive_values.extend(
            [
                incoming_message(is_error_frame=True),
                incoming_message(is_remote_frame=True),
                incoming_message(arbitration_id=0x555),
            ]
        )
        self.assertEqual(self.backend.receive(1.0).arbitration_id, 0x555)
        self.assertEqual(self.backend.ignored_error_frames, 1)
        self.assertEqual(self.backend.ignored_remote_frames, 1)

    def test_nonblocking_receive_skips_filtered_frames(self):
        """
        @description         : 验证零超时时也会跳过过滤帧并读取其后的数据帧
        @param self          : 当前测试用例
        @return              : 无
        """
        self.backend.open()
        self.can_module.bus.receive_values.extend(
            [
                incoming_message(is_error_frame=True),
                incoming_message(arbitration_id=0x600),
            ]
        )
        self.assertEqual(self.backend.receive(0).arbitration_id, 0x600)

    def test_receive_timeout_still_expires_while_filtered_frames_arrive(self):
        """
        @description         : 验证错误帧和远程帧不会无限延长正数接收超时
        @param self          : 当前测试用例
        @return              : 无
        """
        self.backend.open()
        self.can_module.bus.receive_values.extend(
            [
                incoming_message(is_error_frame=True),
                incoming_message(is_remote_frame=True),
                incoming_message(is_error_frame=True),
            ]
        )
        with patch(
            "generic_can.backends.socketcan.time.monotonic",
            side_effect=(10.0, 10.001, 10.002, 10.006),
        ):
            self.assertIsNone(self.backend.receive(0.005))

        self.assertEqual(len(self.can_module.bus.receive_timeouts), 2)
        self.assertGreater(self.can_module.bus.receive_timeouts[0], 0.0)
        self.assertGreater(self.can_module.bus.receive_timeouts[1], 0.0)
        self.assertEqual(self.backend.ignored_error_frames, 1)
        self.assertEqual(self.backend.ignored_remote_frames, 1)

    def test_close_is_idempotent(self):
        """
        @description         : 验证关闭两次只释放一次python-can Bus
        @param self          : 当前测试用例
        @return              : 无
        """
        self.backend.open()
        self.backend.close()
        self.backend.close()
        self.assertEqual(self.can_module.bus.shutdown_count, 1)

    def test_operations_before_open_raise_unified_error(self):
        """
        @description         : 验证未打开时发送和接收抛出统一后端异常
        @param self          : 当前测试用例
        @return              : 无
        """
        with self.assertRaises(CANBackendError):
            self.backend.send(CanFrame(1))
        with self.assertRaises(CANBackendError):
            self.backend.receive(0)


if __name__ == "__main__":
    unittest.main()