Skip to content

Commit 548a200

Browse files
committed
BusABC.recv: keep calling _recv_internal until it returns None
Even if recv() is called with timeout=0, the caller's intention is probably for recv() to check all of the messages that have already arrived at the interface until one of them matches the filters. This is already the way recv() behaves for interface drivers that take advantage of hardware or OS-level filtering, but those that use BusABC's default software-based filtering might return None even if a matching message has already arrived.
1 parent 38c4dc4 commit 548a200

File tree

2 files changed

+60
-14
lines changed

2 files changed

+60
-14
lines changed

can/bus.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -120,24 +120,18 @@ def recv(self, timeout: Optional[float] = None) -> Optional[Message]:
120120
# try to get a message
121121
msg, already_filtered = self._recv_internal(timeout=time_left)
122122

123+
# propagate timeouts from _recv_internal()
124+
if not msg:
125+
return None
126+
123127
# return it, if it matches
124-
if msg and (already_filtered or self._matches_filters(msg)):
128+
if already_filtered or self._matches_filters(msg):
125129
LOG.log(self.RECV_LOGGING_LEVEL, "Received: %s", msg)
126130
return msg
127131

128-
# if not, and timeout is None, try indefinitely
129-
elif timeout is None:
130-
continue
131-
132-
# try next one only if there still is time, and with
133-
# reduced timeout
134-
else:
135-
time_left = timeout - (time() - start)
136-
137-
if time_left > 0:
138-
continue
139-
140-
return None
132+
# try again with reduced timeout
133+
if timeout is not None:
134+
time_left = max(0, timeout - (time() - start))
141135

142136
def _recv_internal(
143137
self, timeout: Optional[float]

test/test_message_filtering.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import unittest
8+
from unittest.mock import MagicMock, patch
89

910
from can import Bus, Message
1011

@@ -46,6 +47,57 @@ def test_match_example_message(self):
4647
self.assertFalse(self.bus._matches_filters(EXAMPLE_MSG))
4748
self.assertTrue(self.bus._matches_filters(HIGHEST_MSG))
4849

50+
def test_empty_queue_up_to_match(self):
51+
bus2 = Bus(interface="virtual", channel="testy")
52+
self.bus.set_filters(MATCH_EXAMPLE)
53+
bus2.send(HIGHEST_MSG)
54+
bus2.send(EXAMPLE_MSG)
55+
actual = self.bus.recv(timeout=0)
56+
bus2.shutdown()
57+
self.assertTrue(
58+
EXAMPLE_MSG.equals(
59+
actual, timestamp_delta=None, check_direction=False, check_channel=False
60+
)
61+
)
62+
63+
64+
@patch("can.bus.time")
65+
class TestMessageFilterRetryTiming(unittest.TestCase):
66+
def setUp(self):
67+
self.bus = Bus(interface="virtual", channel="testy")
68+
self.bus._recv_internal = MagicMock(name="_recv_internal")
69+
70+
def tearDown(self):
71+
self.bus.shutdown()
72+
73+
def test_propagate_recv_internal_timeout(self, time: MagicMock) -> None:
74+
self.bus._recv_internal.side_effect = [
75+
(None, False),
76+
]
77+
time.side_effect = [0]
78+
self.bus.set_filters(MATCH_EXAMPLE)
79+
self.assertIsNone(self.bus.recv(timeout=3))
80+
81+
def test_retry_with_adjusted_timeout(self, time: MagicMock) -> None:
82+
self.bus._recv_internal.side_effect = [
83+
(HIGHEST_MSG, False),
84+
(EXAMPLE_MSG, False),
85+
]
86+
time.side_effect = [0, 1]
87+
self.bus.set_filters(MATCH_EXAMPLE)
88+
self.bus.recv(timeout=3)
89+
self.bus._recv_internal.assert_called_with(timeout=2)
90+
91+
def test_keep_timeout_non_negative(self, time: MagicMock) -> None:
92+
self.bus._recv_internal.side_effect = [
93+
(HIGHEST_MSG, False),
94+
(EXAMPLE_MSG, False),
95+
]
96+
time.side_effect = [0, 1]
97+
self.bus.set_filters(MATCH_EXAMPLE)
98+
self.bus.recv(timeout=0.5)
99+
self.bus._recv_internal.assert_called_with(timeout=0)
100+
49101

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

0 commit comments

Comments
 (0)