diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb4c9a03b..be222751c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,16 +13,13 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] experimental: [false] - python-version: ["3.6", "3.7", "3.8", "3.9", "pypy-3.7"] - include: - # Skipping Py 3.10 on Windows until windows-curses has a cp310 wheel, - # see https://github.com/zephyrproject-rtos/windows-curses/issues/26 - - os: ubuntu-latest - experimental: false - python-version: "3.10" - - os: macos-latest - experimental: false - python-version: "3.10" + python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.7", "pypy-3.8"] + # Do not test on Python 3.11 pre-releases since wrapt causes problems: https://github.com/GrahamDumpleton/wrapt/issues/196 + # include: + # Only test on a single configuration while there are just pre-releases + # - os: ubuntu-latest + # experimental: true + # python-version: "3.11.0-alpha.3" fail-fast: false steps: - uses: actions/checkout@v2 @@ -38,7 +35,7 @@ jobs: run: | tox -e gh - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 with: fail_ci_if_error: true @@ -46,7 +43,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Set up Python 3.10 + - name: Set up Python uses: actions/setup-python@v2 with: python-version: "3.10" diff --git a/.github/workflows/format-code.yml b/.github/workflows/format-code.yml index 81e8fdf03..b86789662 100644 --- a/.github/workflows/format-code.yml +++ b/.github/workflows/format-code.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -22,10 +22,7 @@ jobs: run: | black --verbose . - name: Commit Formated Code - uses: EndBug/add-and-commit@v5 - env: - # This is necessary in order to push a commit to the repo - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: EndBug/add-and-commit@v7 with: message: "Format code with black" # Ref https://git-scm.com/docs/git-add#_examples diff --git a/README.rst b/README.rst index ac5537f7e..84c270e0f 100644 --- a/README.rst +++ b/README.rst @@ -51,15 +51,14 @@ Python developers; providing common abstractions to different hardware devices, and a suite of utilities for sending and receiving messages on a can bus. -The library currently supports Python 3.6+ as well as PyPy 3 and runs -on Mac, Linux and Windows. +The library currently supports CPython as well as PyPy and runs on Mac, Linux and Windows. ============================== =========== Library Version Python ------------------------------ ----------- 2.x 2.6+, 3.4+ 3.x 2.7+, 3.5+ - 4.x *(currently on develop)* 3.6+ + 4.x *(currently on develop)* 3.7+ ============================== =========== diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py index a903435ac..ffd5b0241 100644 --- a/can/interfaces/virtual.py +++ b/can/interfaces/virtual.py @@ -59,6 +59,7 @@ def __init__( channel: Any = None, receive_own_messages: bool = False, rx_queue_size: int = 0, + preserve_timestamps: bool = False, **kwargs: Any, ) -> None: super().__init__( @@ -69,6 +70,7 @@ def __init__( self.channel_id = channel self.channel_info = f"Virtual bus channel {self.channel_id}" self.receive_own_messages = receive_own_messages + self.preserve_timestamps = preserve_timestamps self._open = True with channels_lock: @@ -103,7 +105,7 @@ def _recv_internal( def send(self, msg: Message, timeout: Optional[float] = None) -> None: self._check_if_open() - timestamp = time.time() + timestamp = msg.timestamp if self.preserve_timestamps else time.time() # Add message to all listening on this channel all_sent = True for bus_queue in self.channel: diff --git a/doc/interfaces/virtual.rst b/doc/interfaces/virtual.rst index b3fa7b38e..9258c9bbd 100644 --- a/doc/interfaces/virtual.rst +++ b/doc/interfaces/virtual.rst @@ -85,7 +85,7 @@ Example ------- .. code-block:: python - + import can bus1 = can.interface.Bus('test', bustype='virtual') @@ -100,6 +100,33 @@ Example assert msg1.data == msg2.data assert msg1.timestamp != msg2.timestamp +.. code-block:: python + + import can + + bus1 = can.interface.Bus('test', bustype='virtual', preserve_timestamps=True) + bus2 = can.interface.Bus('test', bustype='virtual') + + msg1 = can.Message(timestamp=1639740470.051948, arbitration_id=0xabcde, data=[1,2,3]) + + # Messages sent on bus1 will have their timestamps preserved when received + # on bus2 + bus1.send(msg1) + msg2 = bus2.recv() + + assert msg1.arbitration_id == msg2.arbitration_id + assert msg1.data == msg2.data + assert msg1.timestamp == msg2.timestamp + + # Messages sent on bus2 will not have their timestamps preserved when + # received on bus1 + bus2.send(msg1) + msg3 = bus1.recv() + + assert msg1.arbitration_id == msg3.arbitration_id + assert msg1.data == msg3.data + assert msg1.timestamp != msg3.timestamp + Bus Class Documentation ----------------------- diff --git a/requirements-lint.txt b/requirements-lint.txt index 9aefb7415..55d985d54 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,4 +1,4 @@ -pylint==2.11.1 -black==21.10b0 -mypy==0.910 +pylint==2.12.2 +black==21.12b0 +mypy==0.931 mypy-extensions==0.4.3 diff --git a/setup.py b/setup.py index fe0557d30..171b77eef 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,6 @@ classifiers=[ # a list of all available ones: https://pypi.org/classifiers/ "Programming Language :: Python", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -73,7 +72,7 @@ version=version, packages=find_packages(exclude=["test*", "doc", "scripts", "examples"]), scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))), - author="Python CAN contributors", + author="python-can contributors", license="LGPL v3", package_data={ "": ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.txt"], @@ -82,7 +81,7 @@ }, # Installation # see https://www.python.org/dev/peps/pep-0345/#version-specifiers - python_requires=">=3.6", + python_requires=">=3.7", install_requires=[ "setuptools", "wrapt~=1.10", diff --git a/test/data/logfile_errorframes.asc b/test/data/logfile_errorframes.asc new file mode 100644 index 000000000..bcb5584a7 --- /dev/null +++ b/test/data/logfile_errorframes.asc @@ -0,0 +1,21 @@ +date Sam Sep 30 15:06:13.191 2017 +base hex timestamps absolute +internal events logged +// version 9.0.0 +Begin Triggerblock Sam Sep 30 15:06:13.191 2017 + 0.000000 Start of measurement + 0.015991 CAN 1 Status:chip status error passive - TxErr: 132 RxErr: 0 + 0.015991 CAN 2 Status:chip status error active + 2.501000 1 ErrorFrame + 2.501010 1 ErrorFrame ECC: 10100010 + 2.501020 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300 + 2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x + 2.584921 4 300 Tx r 8 Length = 1704000 BitCount = 145 ID = 88888888x + 3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x + 3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x + 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784 + 20.105214 2 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273925 BitCount = 141 ID = 418119424x + 20.155119 2 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 272152 BitCount = 140 ID = 418119424x + 20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x + 20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x +End TriggerBlock diff --git a/test/test_interface_virtual.py b/test/test_interface_virtual.py new file mode 100644 index 000000000..009722779 --- /dev/null +++ b/test/test_interface_virtual.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# coding: utf-8 + +""" +This module tests :meth:`can.interface.virtual`. +""" + +import unittest + +from can import Bus, Message + +EXAMPLE_MSG1 = Message(timestamp=1639739471.5565314, arbitration_id=0x481, data=b"\x01") + + +class TestMessageFiltering(unittest.TestCase): + def setUp(self): + self.node1 = Bus("test", bustype="virtual", preserve_timestamps=True) + self.node2 = Bus("test", bustype="virtual") + + def tearDown(self): + self.node1.shutdown() + self.node2.shutdown() + + def test_sendmsg(self): + self.node2.send(EXAMPLE_MSG1) + r = self.node1.recv(0.1) + assert r.timestamp != EXAMPLE_MSG1.timestamp + assert r.arbitration_id == EXAMPLE_MSG1.arbitration_id + assert r.data == EXAMPLE_MSG1.data + + def test_sendmsg_preserve_timestamp(self): + self.node1.send(EXAMPLE_MSG1) + r = self.node2.recv(0.1) + assert r.timestamp == EXAMPLE_MSG1.timestamp + assert r.arbitration_id == EXAMPLE_MSG1.arbitration_id + assert r.data == EXAMPLE_MSG1.data + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_player.py b/test/test_player.py new file mode 100755 index 000000000..2f3307420 --- /dev/null +++ b/test/test_player.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +""" +This module tests the functions inside of player.py +""" + +import unittest +from unittest import mock +from unittest.mock import Mock +import os +import sys +import io +import can +import can.player + + +class TestPlayerScriptModule(unittest.TestCase): + def setUp(self) -> None: + # Patch VirtualBus object + patcher_virtual_bus = mock.patch("can.interfaces.virtual.VirtualBus", spec=True) + self.MockVirtualBus = patcher_virtual_bus.start() + self.addCleanup(patcher_virtual_bus.stop) + self.mock_virtual_bus = self.MockVirtualBus.return_value + self.mock_virtual_bus.__enter__ = Mock(return_value=self.mock_virtual_bus) + + # Patch time sleep object + patcher_sleep = mock.patch("can.io.player.sleep", spec=True) + self.MockSleep = patcher_sleep.start() + self.addCleanup(patcher_sleep.stop) + + self.baseargs = [sys.argv[0], "-i", "virtual"] + self.logfile = os.path.join( + os.path.dirname(__file__), "data", "test_CanMessage.asc" + ) + + def assertSuccessfulCleanup(self): + self.MockVirtualBus.assert_called_once() + self.mock_virtual_bus.__exit__.assert_called_once() + + def test_play_virtual(self): + sys.argv = self.baseargs + [self.logfile] + can.player.main() + msg1 = can.Message( + timestamp=2.501, + arbitration_id=0xC8, + is_extended_id=False, + is_fd=False, + is_rx=False, + channel=1, + dlc=8, + data=[0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2], + ) + msg2 = can.Message( + timestamp=17.876708, + arbitration_id=0x6F9, + is_extended_id=False, + is_fd=False, + is_rx=True, + channel=0, + dlc=8, + data=[0x5, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], + ) + self.assertEqual(self.MockSleep.call_count, 2) + if sys.version_info >= (3, 8): + # The args argument was introduced with python 3.8 + self.assertTrue( + msg1.equals(self.mock_virtual_bus.send.mock_calls[0].args[0]) + ) + self.assertTrue( + msg2.equals(self.mock_virtual_bus.send.mock_calls[1].args[0]) + ) + self.assertSuccessfulCleanup() + + def test_play_virtual_verbose(self): + sys.argv = self.baseargs + ["-v", self.logfile] + with unittest.mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout: + can.player.main() + self.assertIn("09 08 07 06 05 04 03 02", mock_stdout.getvalue()) + self.assertIn("05 0c 00 00 00 00 00 00", mock_stdout.getvalue()) + self.assertEqual(self.mock_virtual_bus.send.call_count, 2) + self.assertEqual(self.MockSleep.call_count, 2) + self.assertSuccessfulCleanup() + + def test_play_virtual_exit(self): + self.MockSleep.side_effect = [None, KeyboardInterrupt] + + sys.argv = self.baseargs + [self.logfile] + can.player.main() + self.assertEqual(self.mock_virtual_bus.send.call_count, 1) + self.assertEqual(self.MockSleep.call_count, 2) + self.assertSuccessfulCleanup() + + def test_play_skip_error_frame(self): + logfile = os.path.join( + os.path.dirname(__file__), "data", "logfile_errorframes.asc" + ) + sys.argv = self.baseargs + ["-v", logfile] + can.player.main() + self.assertEqual(self.mock_virtual_bus.send.call_count, 9) + self.assertEqual(self.MockSleep.call_count, 12) + self.assertSuccessfulCleanup() + + def test_play_error_frame(self): + logfile = os.path.join( + os.path.dirname(__file__), "data", "logfile_errorframes.asc" + ) + sys.argv = self.baseargs + ["-v", "--error-frames", logfile] + can.player.main() + self.assertEqual(self.mock_virtual_bus.send.call_count, 12) + self.assertEqual(self.MockSleep.call_count, 12) + self.assertSuccessfulCleanup() + + +if __name__ == "__main__": + unittest.main() diff --git a/tox.ini b/tox.ini index 9964fb1e6..6b407dfeb 100644 --- a/tox.ini +++ b/tox.ini @@ -3,11 +3,11 @@ [testenv] deps = pytest==6.2.*,>=6.2.5 - pytest-timeout==2.0.1 + pytest-timeout==2.0.2 pytest-cov==3.0.0 - coverage==6.0.2 + coverage==6.2 codecov==2.1.12 - hypothesis~=6.24.0 + hypothesis~=6.35.0 pyserial~=3.5 parameterized~=0.8