diff --git a/.codecov.yml b/.codecov.yml index ea67fd544..16168f521 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -8,7 +8,10 @@ coverage: round: down range: 50...100 status: - # pull-requests only + project: + default: + # coverage may fall by <1.0% and still be considered "passing" + threshold: 1.0% patch: default: # coverage may fall by <1.0% and still be considered "passing" diff --git a/can/CAN.py b/can/CAN.py index c8791bcc4..127963c95 100644 --- a/can/CAN.py +++ b/can/CAN.py @@ -22,10 +22,10 @@ log = logging.getLogger('can') -# See #267 +# See #267. # Version 2.0 - 2.1: Log a Debug message # Version 2.2: Log a Warning # Version 2.3: Log an Error # Version 2.4: Remove the module -log.warning('Loading python-can via the old "CAN" API is deprecated since v2.0 an will get removed in v2.4. ' +log.error('Loading python-can via the old "CAN" API is deprecated since v2.0 an will get removed in v2.4. ' 'Please use `import can` instead.') diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py index 2bfa3714f..87f19edce 100644 --- a/can/broadcastmanager.py +++ b/can/broadcastmanager.py @@ -154,5 +154,5 @@ def send_periodic(bus, message, period, *args, **kwargs): :return: A started task instance """ log.warning("The function `can.send_periodic` is deprecated and will " + - "be removed in version 2.3. Please use `can.Bus.send_periodic` instead.") + "be removed in version 3.0. Please use `can.Bus.send_periodic` instead.") return bus.send_periodic(message, period, *args, **kwargs) diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py index 6f2b35546..51812b147 100644 --- a/can/interfaces/ixxat/canlib.py +++ b/can/interfaces/ixxat/canlib.py @@ -485,7 +485,7 @@ def send(self, msg, timeout=None): message = structures.CANMSG() message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 - message.uMsgInfo.Bits.ext = 1 if msg.id_type else 0 + message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 message.dwMsgId = msg.arbitration_id if msg.dlc: @@ -545,7 +545,7 @@ def __init__(self, scheduler, msg, period, duration, resolution): self._msg.wCycleTime = int(round(period * resolution)) self._msg.dwMsgId = msg.arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA - self._msg.uMsgInfo.Bits.ext = 1 if msg.id_type else 0 + self._msg.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 self._msg.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 self._msg.uMsgInfo.Bits.dlc = msg.dlc for i, b in enumerate(msg.data): diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py index 3b80c83ef..2d8305239 100644 --- a/can/interfaces/kvaser/canlib.py +++ b/can/interfaces/kvaser/canlib.py @@ -516,8 +516,6 @@ def _recv_internal(self, timeout=None): error_state_indicator=error_state_indicator, channel=self.channel, timestamp=msg_timestamp + self._timestamp_offset) - rx_msg.flags = flags - rx_msg.raw_timestamp = msg_timestamp #log.debug('Got message: %s' % rx_msg) return rx_msg, self._is_filtered else: @@ -526,7 +524,7 @@ def _recv_internal(self, timeout=None): def send(self, msg, timeout=None): #log.debug("Writing a message: {}".format(msg)) - flags = canstat.canMSG_EXT if msg.id_type else canstat.canMSG_STD + flags = canstat.canMSG_EXT if msg.is_extended_id else canstat.canMSG_STD if msg.is_remote_frame: flags |= canstat.canMSG_RTR if msg.is_error_frame: diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py index 54341848f..38cca7504 100644 --- a/can/interfaces/nican.py +++ b/can/interfaces/nican.py @@ -263,7 +263,7 @@ def send(self, msg, timeout=None): It does not wait for message to be ACKed currently. """ arb_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: arb_id |= NC_FL_CAN_ARBID_XTD raw_msg = TxMessageStruct(arb_id, bool(msg.is_remote_frame), diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py index d3a389a32..0c1881d29 100644 --- a/can/interfaces/pcan/pcan.py +++ b/can/interfaces/pcan/pcan.py @@ -226,7 +226,7 @@ def _recv_internal(self, timeout): return rx_msg, False def send(self, msg, timeout=None): - if msg.id_type: + if msg.is_extended_id: msgType = PCAN_MESSAGE_EXTENDED else: msgType = PCAN_MESSAGE_STANDARD diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py index c7c2fbbda..fee9e14ab 100644 --- a/can/interfaces/usb2can/usb2canInterface.py +++ b/can/interfaces/usb2can/usb2canInterface.py @@ -51,7 +51,7 @@ def message_convert_tx(msg): if msg.is_remote_frame: messagetx.flags |= IS_REMOTE_FRAME - if msg.id_type: + if msg.is_extended_id: messagetx.flags |= IS_ID_TYPE return messagetx diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py index 42f1ed616..9752501b1 100644 --- a/can/interfaces/vector/canlib.py +++ b/can/interfaces/vector/canlib.py @@ -319,7 +319,7 @@ def _recv_internal(self, timeout): def send(self, msg, timeout=None): msg_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: msg_id |= vxlapi.XL_CAN_EXT_MSG_ID flags = 0 diff --git a/can/io/blf.py b/can/io/blf.py index 3a043b1b2..df8f611b0 100644 --- a/can/io/blf.py +++ b/can/io/blf.py @@ -293,7 +293,7 @@ def on_message_received(self, msg): channel += 1 arb_id = msg.arbitration_id - if msg.id_type: + if msg.is_extended_id: arb_id |= CAN_MSG_EXT flags = REMOTE_FLAG if msg.is_remote_frame else 0 data = bytes(msg.data) diff --git a/can/io/csv.py b/can/io/csv.py index e77312113..92b6fb921 100644 --- a/can/io/csv.py +++ b/can/io/csv.py @@ -61,7 +61,7 @@ def on_message_received(self, msg): row = ','.join([ repr(msg.timestamp), # cannot use str() here because that is rounding hex(msg.arbitration_id), - '1' if msg.id_type else '0', + '1' if msg.is_extended_id else '0', '1' if msg.is_remote_frame else '0', '1' if msg.is_error_frame else '0', str(msg.dlc), diff --git a/can/io/sqlite.py b/can/io/sqlite.py index e412da346..3da3cefe5 100644 --- a/can/io/sqlite.py +++ b/can/io/sqlite.py @@ -195,7 +195,7 @@ def _db_writer_thread(self): messages.append(( msg.timestamp, msg.arbitration_id, - msg.id_type, + msg.is_extended_id, msg.is_remote_frame, msg.is_error_frame, msg.dlc, diff --git a/can/message.py b/can/message.py index 6c1f6e39d..57b61e34f 100644 --- a/can/message.py +++ b/can/message.py @@ -2,44 +2,107 @@ """ This module contains the implementation of :class:`can.Message`. + +.. note:: + Could use `@dataclass `__ + starting with Python 3.7. """ -import logging -logger = logging.getLogger(__name__) +from __future__ import absolute_import, division + +import warnings class Message(object): """ The :class:`~can.Message` object is used to represent CAN messages for - both sending and receiving. + sending, receiving and other purposes like converting between different + logging formats. Messages can use extended identifiers, be remote or error frames, contain - data and can be associated to a channel. - - When testing for equality of the messages, the timestamp and the channel - is not used for comparing. + data and may be associated to a channel. - .. note:: + Messages are always compared by identity and never by value, because that + may introduce unexpected behaviour. See also :meth:`~can.Message.equals`. - This class does not strictly check the input. Thus, the caller must - prevent the creation of invalid messages. Possible problems include - the `dlc` field not matching the length of `data` or creating a message - with both `is_remote_frame` and `is_error_frame` set to True. + :func:`~copy.copy`/:func:`~copy.deepcopy` is supported as well. + Messages do not support "dynamic" attributes, meaning any others that the + documented ones. """ - def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, - is_error_frame=False, arbitration_id=0, dlc=None, data=None, + __slots__ = ( + "timestamp", + "arbitration_id", + "is_extended_id", + "is_remote_frame", + "is_error_frame", + "channel", + "dlc", + "data", + "is_fd", + "bitrate_switch", + "error_state_indicator", + "__weakref__", # support weak references to messages + "_dict" # see __getattr__ + ) + + def __getattr__(self, key): + # TODO keep this for a version, in order to not break old code + # this entire method (as well as the _dict attribute in __slots__ and the __setattr__ method) + # can be removed in 3.0 + # this method is only called if the attribute was not found elsewhere, like in __slots__ + try: + warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) + return self._dict[key] + except KeyError: + raise AttributeError("'message' object has no attribute '{}'".format(key)) + + def __setattr__(self, key, value): + # see __getattr__ + try: + super(Message, self).__setattr__(key, value) + except AttributeError: + warnings.warn("Custom attributes of messages are deprecated and will be removed in the next major version", DeprecationWarning) + self._dict[key] = value + + @property + def id_type(self): + # TODO remove in 3.0 + warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) + return self.is_extended_id + + @id_type.setter + def id_type(self, value): + # TODO remove in 3.0 + warnings.warn("Message.id_type is deprecated, use is_extended_id", DeprecationWarning) + self.is_extended_id = value + + def __init__(self, timestamp=0.0, arbitration_id=0, extended_id=True, + is_remote_frame=False, is_error_frame=False, channel=None, + dlc=None, data=None, is_fd=False, bitrate_switch=False, error_state_indicator=False, - channel=None): + check=False): + """ + To create a message object, simply provide any of the below attributes + together with additional parameters as keyword arguments to the constructor. + + :param bool check: By default, the constructor of this class does not strictly check the input. + Thus, the caller must prevent the creation of invalid messages or + set this parameter to `True`, to raise an Error on invalid inputs. + Possible problems include the `dlc` field not matching the length of `data` + or creating a message with both `is_remote_frame` and `is_error_frame` set to `True`. + + :raises ValueError: iff `check` is set to `True` and one or more arguments were invalid + """ + self._dict = dict() # see __getattr__ self.timestamp = timestamp - self.id_type = extended_id + self.arbitration_id = arbitration_id self.is_extended_id = extended_id self.is_remote_frame = is_remote_frame self.is_error_frame = is_error_frame - self.arbitration_id = arbitration_id self.channel = channel self.is_fd = is_fd @@ -62,25 +125,24 @@ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True, else: self.dlc = dlc - if is_fd and self.dlc > 64: - logger.warning("data link count was %d but it should be less than or equal to 64", self.dlc) - if not is_fd and self.dlc > 8: - logger.warning("data link count was %d but it should be less than or equal to 8", self.dlc) + if check: + self._check() def __str__(self): field_strings = ["Timestamp: {0:>15.6f}".format(self.timestamp)] - if self.id_type: - # Extended arbitrationID + if self.is_extended_id: arbitration_id_string = "ID: {0:08x}".format(self.arbitration_id) else: arbitration_id_string = "ID: {0:04x}".format(self.arbitration_id) field_strings.append(arbitration_id_string.rjust(12, " ")) flag_string = " ".join([ - "X" if self.id_type else "S", + "X" if self.is_extended_id else "S", "E" if self.is_error_frame else " ", "R" if self.is_remote_frame else " ", "F" if self.is_fd else " ", + "BS" if self.bitrate_switch else " ", + "EI" if self.error_state_indicator else " " ]) field_strings.append(flag_string) @@ -101,68 +163,155 @@ def __str__(self): except UnicodeError: pass + if self.channel is not None: + field_strings.append("Channel: {}".format(self.channel)) + return " ".join(field_strings).strip() def __len__(self): return len(self.data) def __bool__(self): + # For Python 3 return True def __nonzero__(self): + # For Python 2 return self.__bool__() def __repr__(self): - data = ["{:#02x}".format(byte) for byte in self.data] args = ["timestamp={}".format(self.timestamp), - "is_remote_frame={}".format(self.is_remote_frame), - "extended_id={}".format(self.id_type), - "is_error_frame={}".format(self.is_error_frame), "arbitration_id={:#x}".format(self.arbitration_id), - "dlc={}".format(self.dlc), - "data=[{}]".format(", ".join(data))] + "extended_id={}".format(self.is_extended_id)] + + if self.is_remote_frame: + args.append("is_remote_frame={}".format(self.is_remote_frame)) + + if self.is_error_frame: + args.append("is_error_frame={}".format(self.is_error_frame)) + if self.channel is not None: - args.append("channel={!r}".format(self.channel)) + args.append("channel={!r}".format(self.channel)) + + data = ["{:#02x}".format(byte) for byte in self.data] + args += ["dlc={}".format(self.dlc), + "data=[{}]".format(", ".join(data))] + if self.is_fd: args.append("is_fd=True") args.append("bitrate_switch={}".format(self.bitrate_switch)) args.append("error_state_indicator={}".format(self.error_state_indicator)) + return "can.Message({})".format(", ".join(args)) - def __eq__(self, other): - if isinstance(other, self.__class__): - return ( + def __format__(self, format_spec): + if not format_spec: + return self.__str__() + else: + raise ValueError("non empty format_specs are not supported") + + def __bytes__(self): + return bytes(self.data) + + def __copy__(self): + new = Message( + timestamp=self.timestamp, + arbitration_id=self.arbitration_id, + extended_id=self.is_extended_id, + is_remote_frame=self.is_remote_frame, + is_error_frame=self.is_error_frame, + channel=self.channel, + dlc=self.dlc, + data=self.data, + is_fd=self.is_fd, + bitrate_switch=self.bitrate_switch, + error_state_indicator=self.error_state_indicator + ) + new._dict.update(self._dict) + return new + + def __deepcopy__(self, memo): + new = Message( + timestamp=self.timestamp, + arbitration_id=self.arbitration_id, + extended_id=self.is_extended_id, + is_remote_frame=self.is_remote_frame, + is_error_frame=self.is_error_frame, + channel=deepcopy(self.channel, memo), + dlc=self.dlc, + data=deepcopy(self.data, memo), + is_fd=self.is_fd, + bitrate_switch=self.bitrate_switch, + error_state_indicator=self.error_state_indicator + ) + new._dict.update(self._dict) + return new + + def _check(self): + """Checks if the message parameters are valid. + Assumes that the types are already correct. + + :raises AssertionError: iff one or more attributes are invalid + """ + + assert 0.0 <= self.timestamp, "the timestamp may not be negative" + + assert not (self.is_remote_frame and self.is_error_frame), \ + "a message cannot be a remote and an error frame at the sane time" + + assert 0 <= self.arbitration_id, "arbitration IDs may not be negative" + + if self.is_extended_id: + assert self.arbitration_id < 0x20000000, "Extended arbitration IDs must be less than 2^29" + else: + assert self.arbitration_id < 0x800, "Normal arbitration IDs must be less than 2^11" + + assert 0 <= self.dlc, "DLC may not be negative" + if self.is_fd: + assert self.dlc <= 64, "DLC was {} but it should be <= 64 for CAN FD frames".format(self.dlc) + else: + assert self.dlc <= 8, "DLC was {} but it should be <= 8 for normal CAN frames".format(self.dlc) + + if not self.is_remote_frame: + assert self.dlc == len(self.data), "the length of the DLC and the length of the data must match up" + + if not self.is_fd: + assert not self.bitrate_switch, "bitrate switch is only allowed for CAN FD frames" + assert not self.error_state_indicator, "error stat indicator is only allowed for CAN FD frames" + + def equals(self, other, timestamp_delta=1.0e-6): + """ + Compares a given message with this one. + + :param can.Message other: the message to compare with + + :type timestamp_delta: float or int or None + :param timestamp_delta: the maximum difference at which two timestamps are + still considered equal or None to not compare timestamps + + :rtype: bool + :return: True iff the given message equals this one + """ + # see https://github.com/hardbyte/python-can/pull/413 for a discussion + # on why a delta of 1.0e-6 was chosen + return ( + # check for identity first + self is other or + # then check for equality by value + ( + ( + timestamp_delta is None or + abs(self.timestamp - other.timestamp) <= timestamp_delta + ) and self.arbitration_id == other.arbitration_id and - #self.timestamp == other.timestamp and # allow the timestamp to differ - self.id_type == other.id_type and + self.is_extended_id == other.is_extended_id and self.dlc == other.dlc and self.data == other.data and self.is_remote_frame == other.is_remote_frame and self.is_error_frame == other.is_error_frame and + self.channel == other.channel and self.is_fd == other.is_fd and - self.bitrate_switch == other.bitrate_switch + self.bitrate_switch == other.bitrate_switch and + self.error_state_indicator == other.error_state_indicator ) - else: - return NotImplemented - - def __ne__(self, other): - if isinstance(other, self.__class__): - return not self.__eq__(other) - else: - return NotImplemented - - def __hash__(self): - return hash(( - self.arbitration_id, - # self.timestamp # excluded, like in self.__eq__(self, other) - self.id_type, - self.dlc, - self.data, - self.is_fd, - self.bitrate_switch, - self.is_remote_frame, - self.is_error_frame - )) - - def __format__(self, format_spec): - return self.__str__() + ) diff --git a/doc/bcm.rst b/doc/bcm.rst index b91d0755c..6f57192fa 100644 --- a/doc/bcm.rst +++ b/doc/bcm.rst @@ -49,7 +49,7 @@ Functional API .. warning:: The functional API in :func:`can.broadcastmanager.send_periodic` is now deprecated - and will be removed in version 2.3. + and will be removed in version 3.0. Use the object oriented API via :meth:`can.BusABC.send_periodic` instead. .. autofunction:: can.broadcastmanager.send_periodic diff --git a/doc/development.rst b/doc/development.rst index 182233e0a..118c2a3cc 100644 --- a/doc/development.rst +++ b/doc/development.rst @@ -120,14 +120,15 @@ Creating a new Release - Release from the ``master`` branch. - Update the library version in ``__init__.py`` using `semantic versioning `__. +- Check if any deprecations are pending. - Run all tests and examples against available hardware. - Update `CONTRIBUTORS.txt` with any new contributors. - For larger changes update ``doc/history.rst``. - Sanity check that documentation has stayed inline with code. -- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test`` -- Create and upload the distribution: ``python setup.py sdist bdist_wheel`` -- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl`` -- Upload with twine ``twine upload dist/python-can-X.Y.Z*`` -- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z`` +- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test``. +- Create and upload the distribution: ``python setup.py sdist bdist_wheel``. +- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``. +- Upload with twine ``twine upload dist/python-can-X.Y.Z*``. +- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z``. - Create a new tag in the repository. - Check the release on PyPi, Read the Docs and GitHub. diff --git a/doc/listeners.rst b/doc/listeners.rst index f90c5d027..3434f2b3e 100644 --- a/doc/listeners.rst +++ b/doc/listeners.rst @@ -19,6 +19,17 @@ the CAN bus. .. autoclass:: can.Listener :members: +There are some listeners that already ship together with `python-can` +and are listed below. +Some of them allow messages to be written to files, and the corresponding file +readers are also documented here. + +.. warning :: + + Please note that writing and the reading a message might not always yield a + completely unchanged message again, since some properties are not (yet) + supported by some file formats. + BufferedReader -------------- diff --git a/doc/message.rst b/doc/message.rst index cd350d9f1..e7e9fb353 100644 --- a/doc/message.rst +++ b/doc/message.rst @@ -23,6 +23,15 @@ Message 2.0B) in length, and ``python-can`` exposes this difference with the :attr:`~can.Message.is_extended_id` attribute. + .. attribute:: timestamp + + :type: float + + The timestamp field in a CAN message is a floating point number representing when + the message was received since the epoch in seconds. Where possible this will be + timestamped in hardware. + + .. attribute:: arbitration_id :type: int @@ -30,7 +39,7 @@ Message The frame identifier used for arbitration on the bus. The arbitration ID can take an int between 0 and the - maximum value allowed depending on the is_extended_id flag + maximum value allowed depending on the ``is_extended_id`` flag (either 2\ :sup:`11` - 1 for 11-bit IDs, or 2\ :sup:`29` - 1 for 29-bit identifiers). @@ -63,7 +72,7 @@ Message :type: int - The :abbr:`DLC (Data Link Count)` parameter of a CAN message is an integer + The :abbr:`DLC (Data Length Code)` parameter of a CAN message is an integer between 0 and 8 representing the frame payload length. In the case of a CAN FD message, this indicates the data length in @@ -82,6 +91,12 @@ Message represents the amount of data contained in the message, in remote frames it represents the amount of data being requested. + .. attribute:: channel + + :type: str or int or None + + This might store the channel from which the message came. + .. attribute:: is_extended_id @@ -96,6 +111,7 @@ Message Previously this was exposed as `id_type`. + Please use `is_extended_id` from now on. .. attribute:: is_error_frame @@ -141,15 +157,6 @@ Message If this is a CAN FD message, this indicates an error active state. - .. attribute:: timestamp - - :type: float - - The timestamp field in a CAN message is a floating point number representing when - the message was received since the epoch in seconds. Where possible this will be - timestamped in hardware. - - .. method:: __str__ A string representation of a CAN message: diff --git a/setup.py b/setup.py index a07dba0d2..cb6d4facc 100644 --- a/setup.py +++ b/setup.py @@ -99,7 +99,7 @@ # see https://www.python.org/dev/peps/pep-0345/#version-specifiers python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3", install_requires=[ - 'wrapt~= 1.10', 'typing', 'windows-curses;platform_system=="Windows"', + 'wrapt~=1.10', 'typing', 'windows-curses;platform_system=="Windows"', ], extras_require=extras_require, diff --git a/test/back2back_test.py b/test/back2back_test.py index 5d0034330..25629bc0e 100644 --- a/test/back2back_test.py +++ b/test/back2back_test.py @@ -53,7 +53,7 @@ def _check_received_message(self, recv_msg, sent_msg): self.assertIsNotNone(recv_msg, "No message was received on %s" % self.INTERFACE_2) self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id) - self.assertEqual(recv_msg.id_type, sent_msg.id_type) + self.assertEqual(recv_msg.is_extended_id, sent_msg.is_extended_id) self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) self.assertEqual(recv_msg.is_error_frame, sent_msg.is_error_frame) self.assertEqual(recv_msg.is_fd, sent_msg.is_fd) diff --git a/test/data/example_data.py b/test/data/example_data.py index e1a446384..d4b1b877c 100644 --- a/test/data/example_data.py +++ b/test/data/example_data.py @@ -58,6 +58,22 @@ def sort_messages(messages): # empty data data=[0xFF, 0xFE, 0xFD], ), + Message( + # with channel as integer + channel=0, + ), + Message( + # with channel as integer + channel=42, + ), + Message( + # with channel as string + channel="vcan0", + ), + Message( + # with channel as string + channel="awesome_channel", + ), Message( arbitration_id=0xABCDEF, extended_id=True, timestamp=TEST_TIME, diff --git a/test/logformats_test.py b/test/logformats_test.py index 2a315352d..039b3f768 100644 --- a/test/logformats_test.py +++ b/test/logformats_test.py @@ -10,6 +10,7 @@ different writer/reader pairs - e.g., some don't handle error frames and comments. +TODO: correctly set preserves_channel and adds_default_channel TODO: implement CAN FD support testing """ @@ -33,15 +34,15 @@ from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \ TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \ sort_messages +from .message_helper import ComparingMessagesTestCase logging.basicConfig(level=logging.DEBUG) -class ReaderWriterTest(unittest.TestCase): +class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. - """ __test__ = False @@ -49,7 +50,7 @@ class ReaderWriterTest(unittest.TestCase): __metaclass__ = ABCMeta def __init__(self, *args, **kwargs): - super(ReaderWriterTest, self).__init__(*args, **kwargs) + unittest.TestCase.__init__(self, *args, **kwargs) self._setup_instance() @abstractmethod @@ -59,22 +60,28 @@ def _setup_instance(self): def _setup_instance_helper(self, writer_constructor, reader_constructor, binary_file=False, - check_remote_frames=True, check_error_frames=True, check_comments=False, - test_append=False, round_timestamps=False): + check_remote_frames=True, check_error_frames=True, check_fd=True, + check_comments=False, test_append=False, + allowed_timestamp_delta=0.0, + preserves_channel=True, adds_default_channel=None): """ :param Callable writer_constructor: the constructor of the writer class :param Callable reader_constructor: the constructor of the reader class + :param bool binary_file: if True, opens files in binary and not in text mode :param bool check_remote_frames: if True, also tests remote frames :param bool check_error_frames: if True, also tests error frames + :param bool check_fd: if True, also tests CAN FD frames :param bool check_comments: if True, also inserts comments at some locations and checks if they are contained anywhere literally in the resulting file. The locations as selected randomly but deterministically, which makes the test reproducible. :param bool test_append: tests the writer in append mode as well - :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round` - before comparing the read messages/events + :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals` + :param bool preserves_channel: if True, checks that the channel attribute is preserved + :param any adds_default_channel: sets this as the channel when not other channel was given + ignored, if *preserves_channel* is True """ # get all test messages self.original_messages = TEST_MESSAGES_BASE @@ -82,6 +89,8 @@ def _setup_instance_helper(self, self.original_messages += TEST_MESSAGES_REMOTE_FRAMES if check_error_frames: self.original_messages += TEST_MESSAGES_ERROR_FRAMES + if check_fd: + self.original_messages += [] # TODO: add TEST_MESSAGES_CAN_FD # sort them so that for example ASCWriter does not "fix" any messages with timestamp 0.0 self.original_messages = sort_messages(self.original_messages) @@ -101,7 +110,11 @@ def _setup_instance_helper(self, self.reader_constructor = reader_constructor self.binary_file = binary_file self.test_append_enabled = test_append - self.round_timestamps = round_timestamps + + ComparingMessagesTestCase.__init__(self, + allowed_timestamp_delta=allowed_timestamp_delta, + preserves_channel=preserves_channel) + #adds_default_channel=adds_default_channel # TODO inlcude in tests def setUp(self): with tempfile.NamedTemporaryFile('w+', delete=False) as test_file: @@ -136,7 +149,7 @@ def test_path_like_explicit_stop(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_path_like_context_manager(self): @@ -163,7 +176,7 @@ def test_path_like_context_manager(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_file_like_explicit_stop(self): @@ -193,7 +206,7 @@ def test_file_like_explicit_stop(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_file_like_context_manager(self): @@ -222,7 +235,7 @@ def test_file_like_context_manager(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_append_mode(self): @@ -259,7 +272,7 @@ def test_append_mode(self): with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) def _write_all(self, writer): """Writes messages and insert comments here and there.""" @@ -278,25 +291,14 @@ def _ensure_fsync(self, io_handler): io_handler.file.flush() os.fsync(io_handler.file.fileno()) - def assertMessagesEqual(self, read_messages): + def assertMessagesEqual(self, messages_1, messages_2): """ Checks the order and content of the individual messages. """ - for index, (original, read) in enumerate(zip(self.original_messages, read_messages)): - try: - # check everything except the timestamp - self.assertEqual(original, read, "messages are not equal at index #{}".format(index)) - # check the timestamp - if self.round_timestamps: - original.timestamp = round(original.timestamp) - read.timestamp = round(read.timestamp) - self.assertAlmostEqual(read.timestamp, original.timestamp, places=6, - msg="message timestamps are not almost_equal at index #{} ({!r} !~= {!r})" - .format(index, original.timestamp, read.timestamp)) - except: - print("Comparing: original message: {!r}".format(original)) - print(" read message: {!r}".format(read)) - raise + self.assertEqual(len(messages_1), len(messages_2)) + + for message_1, message_2 in zip(messages_1, messages_2): + self.assertMessageEqual(message_1, message_2) def assertIncludesComments(self, filename): """ @@ -321,7 +323,9 @@ class TestAscFileFormat(ReaderWriterTest): def _setup_instance(self): super(TestAscFileFormat, self)._setup_instance_helper( can.ASCWriter, can.ASCReader, - check_comments=True, round_timestamps=True + check_fd=False, + check_comments=True, + preserves_channel=False, adds_default_channel=0 ) @@ -334,26 +338,31 @@ def _setup_instance(self): super(TestBlfFileFormat, self)._setup_instance_helper( can.BLFWriter, can.BLFReader, binary_file=True, - check_comments=False + check_fd=False, + check_comments=False, + allowed_timestamp_delta=1.0e-6, + preserves_channel=False, adds_default_channel=0 ) def test_read_known_file(self): logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf") with can.BLFReader(logfile) as reader: messages = list(reader) - self.assertEqual(len(messages), 2) - self.assertEqual(messages[0], - can.Message( - extended_id=False, - arbitration_id=0x64, - data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8])) - self.assertEqual(messages[0].channel, 0) - self.assertEqual(messages[1], - can.Message( - is_error_frame=True, - extended_id=True, - arbitration_id=0x1FFFFFFF)) - self.assertEqual(messages[1].channel, 0) + + expected = [ + can.Message( + timestamp=1.0, + extended_id=False, + arbitration_id=0x64, + data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]), + can.Message( + timestamp=73.0, + extended_id=True, + arbitration_id=0x1FFFFFFF, + is_error_frame=True,) + ] + + self.assertMessagesEqual(messages, expected) class TestCanutilsFileFormat(ReaderWriterTest): @@ -364,7 +373,9 @@ class TestCanutilsFileFormat(ReaderWriterTest): def _setup_instance(self): super(TestCanutilsFileFormat, self)._setup_instance_helper( can.CanutilsLogWriter, can.CanutilsLogReader, - test_append=True, check_comments=False + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel='vcan0' ) @@ -376,7 +387,9 @@ class TestCsvFileFormat(ReaderWriterTest): def _setup_instance(self): super(TestCsvFileFormat, self)._setup_instance_helper( can.CSVWriter, can.CSVReader, - test_append=True, check_comments=False + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel=None ) @@ -388,7 +401,9 @@ class TestSqliteDatabaseFormat(ReaderWriterTest): def _setup_instance(self): super(TestSqliteDatabaseFormat, self)._setup_instance_helper( can.SqliteWriter, can.SqliteReader, - test_append=True, check_comments=False + check_fd=False, + test_append=True, check_comments=False, + preserves_channel=False, adds_default_channel=None ) @unittest.skip("not implemented") @@ -417,12 +432,13 @@ def test_read_all(self): self.assertEqual(len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages") - self.assertMessagesEqual(read_messages) + self.assertMessagesEqual(self.original_messages, read_messages) class TestPrinter(unittest.TestCase): """Tests that can.Printer does not crash""" + # TODO add CAN FD messages messages = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES def test_not_crashes_with_stdout(self): diff --git a/test/message_helper.py b/test/message_helper.py new file mode 100644 index 000000000..497e5498f --- /dev/null +++ b/test/message_helper.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# coding: utf-8 + +from __future__ import absolute_import, print_function + +from copy import copy + + +class ComparingMessagesTestCase(object): + """Must be extended by a class also extending a unittest.TestCase. + """ + + def __init__(self, allowed_timestamp_delta=0.0, preserves_channel=True): + """ + :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals` + :param bool preserves_channel: if True, checks that the channel attribute is preserved + """ + self.allowed_timestamp_delta = allowed_timestamp_delta + self.preserves_channel = preserves_channel + + def assertMessageEqual(self, message_1, message_2): + """ + Checks that two messages are equal, according to the given rules. + """ + + if message_1.equals(message_2, timestamp_delta=self.allowed_timestamp_delta): + return + elif self.preserves_channel: + print("Comparing: message 1: {!r}".format(message_1)) + print(" message 2: {!r}".format(message_2)) + self.fail("messages are unequal with allowed timestamp delta {}".format(self.allowed_timestamp_delta)) + else: + message_2 = copy(message_2) # make sure this method is pure + message_2.channel = message_1.channel + if message_1.equals(message_2, timestamp_delta=self.allowed_timestamp_delta): + return + else: + print("Comparing: message 1: {!r}".format(message_1)) + print(" message 2: {!r}".format(message_2)) + self.fail("messages are unequal with allowed timestamp delta {} even when ignoring channels" \ + .format(self.allowed_timestamp_delta)) diff --git a/test/network_test.py b/test/network_test.py index 830adceca..cf5acca76 100644 --- a/test/network_test.py +++ b/test/network_test.py @@ -101,7 +101,7 @@ def testProducerConsumer(self): self.assertIsNotNone(msg, "Didn't receive a message") #logging.debug("Received message {} with data: {}".format(i, msg.data)) - self.assertEqual(msg.id_type, self.extended_flags[i]) + self.assertEqual(msg.is_extended_id, self.extended_flags[i]) if not msg.is_remote_frame: self.assertEqual(msg.data, self.data[i]) self.assertEqual(msg.arbitration_id, self.ids[i]) diff --git a/test/serial_test.py b/test/serial_test.py index da67cfaa2..5b26ae42a 100644 --- a/test/serial_test.py +++ b/test/serial_test.py @@ -7,14 +7,18 @@ Copyright: 2017 Boris Wenzlaff """ +from __future__ import division + import unittest from mock import patch import can from can.interfaces.serial.serial_can import SerialBus +from .message_helper import ComparingMessagesTestCase + -class SerialDummy: +class SerialDummy(object): """ Dummy to mock the serial communication """ @@ -36,9 +40,13 @@ def reset(self): self.msg = None -class SimpleSerialTestBase(object): +class SimpleSerialTestBase(ComparingMessagesTestCase): + MAX_TIMESTAMP = 0xFFFFFFFF / 1000 + def __init__(self): + ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=None, preserves_channel=True) + def test_rx_tx_min_max_data(self): """ Tests the transfer from 0x00 to 0xFF for a 1 byte payload @@ -47,7 +55,7 @@ def test_rx_tx_min_max_data(self): msg = can.Message(data=[b]) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_min_max_dlc(self): """ @@ -59,7 +67,7 @@ def test_rx_tx_min_max_dlc(self): msg = can.Message(data=payload) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_data_none(self): """ @@ -68,7 +76,7 @@ def test_rx_tx_data_none(self): msg = can.Message(data=None) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_min_id(self): """ @@ -77,7 +85,7 @@ def test_rx_tx_min_id(self): msg = can.Message(arbitration_id=0) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_max_id(self): """ @@ -86,7 +94,7 @@ def test_rx_tx_max_id(self): msg = can.Message(arbitration_id=536870911) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) def test_rx_tx_max_timestamp(self): """ @@ -96,7 +104,7 @@ def test_rx_tx_max_timestamp(self): msg = can.Message(timestamp=self.MAX_TIMESTAMP) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) self.assertEqual(msg.timestamp, msg_receive.timestamp) def test_rx_tx_max_timestamp_error(self): @@ -113,7 +121,7 @@ def test_rx_tx_min_timestamp(self): msg = can.Message(timestamp=0) self.bus.send(msg) msg_receive = self.bus.recv() - self.assertEqual(msg, msg_receive) + self.assertMessageEqual(msg, msg_receive) self.assertEqual(msg.timestamp, msg_receive.timestamp) def test_rx_tx_min_timestamp_error(self): @@ -126,6 +134,10 @@ def test_rx_tx_min_timestamp_error(self): class SimpleSerialTest(unittest.TestCase, SimpleSerialTestBase): + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + SimpleSerialTestBase.__init__(self) + def setUp(self): self.patcher = patch('serial.Serial') self.mock_serial = self.patcher.start() @@ -141,6 +153,10 @@ def tearDown(self): class SimpleSerialLoopTest(unittest.TestCase, SimpleSerialTestBase): + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + SimpleSerialTestBase.__init__(self) + def setUp(self): self.bus = SerialBus('loop://') diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py index 482bf8ace..70a017937 100644 --- a/test/simplecyclic_test.py +++ b/test/simplecyclic_test.py @@ -13,9 +13,14 @@ import can from .config import * +from .message_helper import ComparingMessagesTestCase -class SimpleCyclicSendTaskTest(unittest.TestCase): +class SimpleCyclicSendTaskTest(unittest.TestCase, ComparingMessagesTestCase): + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + ComparingMessagesTestCase.__init__(self, allowed_timestamp_delta=None, preserves_channel=True) @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server") def test_cycle_time(self): @@ -31,7 +36,7 @@ def test_cycle_time(self): self.assertTrue(80 <= size <= 120, '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size)) last_msg = bus2.recv() - self.assertEqual(last_msg, msg) + self.assertMessageEqual(last_msg, msg) bus1.shutdown() bus2.shutdown() diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py index 417a3eb3e..153f91e3a 100644 --- a/test/test_detect_available_configs.py +++ b/test/test_detect_available_configs.py @@ -15,7 +15,7 @@ from can import detect_available_configs -from .config import IS_LINUX +from .config import IS_LINUX, IS_CI class TestDetectAvailableConfigs(unittest.TestCase): @@ -45,7 +45,7 @@ def test_content_socketcan(self): for config in configs: self.assertEqual(config['interface'], 'socketcan') - @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux") + @unittest.skipUnless(IS_LINUX and IS_CI, "socketcan is only available on Linux") def test_socketcan_on_ci_server(self): configs = detect_available_configs(interfaces='socketcan') self.assertGreaterEqual(len(configs), 1) diff --git a/test/test_kvaser.py b/test/test_kvaser.py index 229381934..173b80d48 100644 --- a/test/test_kvaser.py +++ b/test/test_kvaser.py @@ -136,7 +136,7 @@ def test_recv_extended(self): msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0xc0ffef) self.assertEqual(msg.dlc, 8) - self.assertEqual(msg.id_type, True) + self.assertEqual(msg.is_extended_id, True) self.assertSequenceEqual(msg.data, self.msg_in_cue.data) self.assertTrue(now - 1 < msg.timestamp < now + 1) @@ -149,7 +149,7 @@ def test_recv_standard(self): msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0x123) self.assertEqual(msg.dlc, 2) - self.assertEqual(msg.id_type, False) + self.assertEqual(msg.is_extended_id, False) self.assertSequenceEqual(msg.data, [100, 101]) def test_available_configs(self): @@ -178,7 +178,7 @@ def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout): dlc._obj.value = self.msg_in_cue.dlc data._obj.raw = self.msg_in_cue.data flags_temp = 0 - if self.msg_in_cue.id_type: + if self.msg_in_cue.is_extended_id: flags_temp |= constants.canMSG_EXT else: flags_temp |= constants.canMSG_STD