Skip to content

Commit 4503031

Browse files
author
David Walker
committed
Add Support for timeout on the tornado IOLoop
1 parent 2d3a2e1 commit 4503031

8 files changed

Lines changed: 89 additions & 35 deletions

File tree

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ python:
44
- "3.4"
55
- "3.5"
66
install:
7-
pip install -e .[tornado]
7+
- if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then travis_retry pip install mock; fi
8+
- pip install -e .[tornado]
89
# command to run tests
910
script: py.test

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,6 @@
2929
packages=find_packages(exclude=['tests', '*.tests']),
3030
install_requires=['pyserial'],
3131
extras_require={
32-
'tornado': ['tornado']
32+
'tornado': ['tornado~=4.5']
3333
}
3434
)

xbee/tests/Fake.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(self, port='/dev/null', baudrate=19200, timeout=1,
2929
self.xonxoff = xonxoff
3030
self.rtscts = rtscts
3131
self._is_open = True
32+
self.fd = 0
3233

3334
self._data_written = ""
3435
self._read_data = ""

xbee/tornado/base.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"""
1212
from xbee.frame import APIFrame
1313
from xbee.backend.base import XBeeBase as _XBeeBase
14+
from xbee.backend.base import TimeoutException as _TimeoutException
1415
from tornado import ioloop, gen
1516
from tornado.locks import Event
1617
from tornado.concurrent import Future
@@ -45,27 +46,28 @@ class XBeeBase(_XBeeBase):
4546
argument is also used.
4647
"""
4748
def __init__(self, *args, **kwargs):
49+
if 'io_loop' in kwargs:
50+
self._ioloop = kwargs.pop('io_loop')
51+
else:
52+
self._ioloop = ioloop.IOLoop.current()
53+
4854
super(XBeeBase, self).__init__(*args, **kwargs)
4955

5056
self._running = Event()
5157
self._running.set()
5258

53-
if 'io_loop' in kwargs:
54-
self._ioloop = kwargs['io_loop']
55-
else:
56-
self._ioloop = ioloop.IOLoop.current()
57-
5859
self._frame_future = None
5960
self._frame_queue = deque()
6061

6162
if self._callback:
6263
# Make Non-Blocking
6364
self.serial.timeout = 0
64-
self._ioloop.add_handler(self.serial.fd,
65-
self._process_input,
66-
ioloop.IOLoop.READ)
6765
self.process_frames()
6866

67+
self._ioloop.add_handler(self.serial.fd,
68+
self._process_input,
69+
ioloop.IOLoop.READ)
70+
6971
def halt(self):
7072
"""
7173
halt: None -> None
@@ -99,15 +101,25 @@ def process_frames(self):
99101
self._error_callback(e)
100102

101103
@gen.coroutine
102-
def wait_read_frame(self):
103-
frame = yield self._get_frame()
104+
def wait_read_frame(self, timeout=None):
105+
frame = yield self._get_frame(timeout=timeout)
104106
raise gen.Return(self._split_response(frame.data))
105107

106-
def _get_frame(self):
108+
def _get_frame(self, timeout=None):
107109
future = Future()
108110
if self._frame_queue:
109111
future.set_result(self._frame_queue.popleft())
110112
else:
113+
if timeout is not None:
114+
def on_timeout():
115+
future.set_exception(_TimeoutException())
116+
117+
handle = self._ioloop.add_timeout(
118+
self._ioloop.time() + timeout, on_timeout
119+
)
120+
future.add_done_callback(lambda _:
121+
self._ioloop.remove_timeout(handle))
122+
111123
self._frame_future = future
112124

113125
return future

xbee/tornado/tests/test_base.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,17 @@
99
"""
1010

1111
import unittest
12+
try:
13+
from unittest.mock import Mock
14+
except ImportError:
15+
from mock import Mock
16+
1217
from xbee.tornado import has_tornado
1318

1419
if not has_tornado:
1520
raise unittest.SkipTest("Requires Tornado")
1621

22+
from tornado import ioloop # noqa
1723
from tornado.testing import AsyncTestCase, gen_test # noqa
1824
from tornado.test.util import unittest # noqa
1925
from xbee.tornado.base import XBeeBase # noqa
@@ -26,14 +32,19 @@ class TestReadFromDevice(AsyncTestCase):
2632
API frame
2733
"""
2834

35+
def setUp(self):
36+
super(TestReadFromDevice, self).setUp()
37+
self._patch_io = ioloop.IOLoop.current()
38+
self._patch_io.add_handler = Mock()
39+
2940
@gen_test
3041
def test_read(self):
3142
"""
3243
_wait_for_frame should properly read a frame of data
3344
"""
3445
device = Serial()
3546
device.set_read_data(b'\x7E\x00\x01\x00\xFF')
36-
xbee = XBeeBase(device)
47+
xbee = XBeeBase(device, io_loop=self._patch_io)
3748

3849
xbee._process_input(None, None)
3950
frame = yield xbee._get_frame()
@@ -46,7 +57,7 @@ def test_read_invalid_followed_by_valid(self):
4657
"""
4758
device = Serial()
4859
device.set_read_data(b'\x7E\x00\x01\x00\xFA' + b'\x7E\x00\x01\x05\xFA')
49-
xbee = XBeeBase(device)
60+
xbee = XBeeBase(device, io_loop=self._patch_io)
5061

5162
xbee._process_input(None, None)
5263
# First process ends with no good frame, process next
@@ -64,7 +75,7 @@ def test_read_escaped(self):
6475
device.set_read_data(
6576
b'\x7E\x00\x04\x7D\x5E\x7D\x5D\x7D\x31\x7D\x33\xE0')
6677

67-
xbee = XBeeBase(device, escaped=True)
78+
xbee = XBeeBase(device, escaped=True, io_loop=self._patch_io)
6879

6980
xbee._process_input(None, None)
7081
frame = yield xbee._get_frame()

xbee/tornado/tests/test_digimesh.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@
77
Tests the XBee DigiMesh implementation class for API compliance
88
"""
99
import unittest
10+
try:
11+
from unittest.mock import Mock
12+
except ImportError:
13+
from mock import Mock
14+
1015
from xbee.tornado import has_tornado
1116

1217
if not has_tornado:
1318
raise unittest.SkipTest("Requires Tornado")
1419

20+
from tornado import ioloop # noqa
21+
from xbee.tests.Fake import Serial # noqa
1522
from xbee.tornado.digimesh import DigiMesh # noqa
1623

1724

@@ -22,8 +29,11 @@ class TestDigiMesh(unittest.TestCase):
2229
"""
2330

2431
def setUp(self):
25-
self.digimesh = DigiMesh(None)
2632
super(TestDigiMesh, self).setUp()
33+
patch_io = ioloop.IOLoop.current()
34+
patch_io.add_handler = Mock()
35+
serial_port = Serial()
36+
self.digimesh = DigiMesh(serial_port, io_loop=patch_io)
2737

2838
def test_split_tx_status(self):
2939
data = b'\x8b\x01\xff\xff\x01\x01\x01'

xbee/tornado/tests/test_ieee.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
Tests the XBee (IEEE 802.15.4) implementation class for XBee API compliance
99
"""
1010
import unittest
11+
try:
12+
from unittest.mock import Mock
13+
except ImportError:
14+
from mock import Mock
1115
from xbee.tornado import has_tornado
1216

1317
if not has_tornado:
@@ -17,6 +21,7 @@
1721
from xbee.tornado.ieee import XBee # noqa
1822
from xbee.frame import APIFrame # noqa
1923
from xbee.python2to3 import intToByte, stringToBytes # noqa
24+
from tornado import ioloop # noqa
2025
from tornado.testing import AsyncTestCase, gen_test # noqa
2126
from tornado.test.util import unittest # noqa
2227
import sys # noqa
@@ -33,7 +38,10 @@ def setUp(self):
3338
Initialize XBee object
3439
"""
3540
super(InitXBee, self).setUp()
36-
self.xbee = XBee(None)
41+
self._patch_io = ioloop.IOLoop.current()
42+
self._patch_io.add_handler = Mock()
43+
serial_port = Serial()
44+
self.xbee = XBee(serial_port, io_loop=self._patch_io)
3745

3846

3947
class TestBuildCommand(InitXBee):
@@ -462,7 +470,7 @@ def test_send_at_command(self):
462470
"""
463471

464472
serial_port = Serial()
465-
xbee = XBee(serial_port)
473+
xbee = XBee(serial_port, io_loop=self._patch_io)
466474

467475
# Send an AT command
468476
xbee.send('at', frame_id=stringToBytes('A'),
@@ -480,7 +488,7 @@ def test_send_at_command_with_param(self):
480488
"""
481489

482490
serial_port = Serial()
483-
xbee = XBee(serial_port)
491+
xbee = XBee(serial_port, io_loop=self._patch_io)
484492

485493
# Send an AT command
486494
xbee.send(
@@ -508,7 +516,7 @@ def setUp(self):
508516
"""
509517
super(TestSendShorthand, self).setUp()
510518
self.ser = Serial()
511-
self.xbee = XBee(self.ser)
519+
self.xbee = XBee(self.ser, io_loop=self._patch_io)
512520

513521
def test_send_at_command(self):
514522
"""
@@ -552,7 +560,7 @@ def test_shorthand_disabled(self):
552560
When shorthand is disabled, any attempt at calling a
553561
non-existant attribute should raise AttributeError
554562
"""
555-
self.xbee = XBee(self.ser, shorthand=False)
563+
self.xbee = XBee(self.ser, shorthand=False, io_loop=self._patch_io)
556564

557565
try:
558566
self.xbee.at
@@ -575,7 +583,7 @@ def test_read_at(self):
575583
"""
576584
device = Serial()
577585
device.set_read_data(b'\x7E\x00\x05\x88DMY\x01\x8c')
578-
xbee = XBee(device)
586+
xbee = XBee(device, io_loop=self._patch_io)
579587

580588
xbee._process_input(None, None)
581589
info = yield xbee.wait_read_frame()
@@ -592,7 +600,7 @@ def test_read_at_params(self):
592600
"""
593601
device = Serial()
594602
device.set_read_data(b'\x7E\x00\x08\x88DMY\x01\x00\x00\x00\x8c')
595-
xbee = XBee(device)
603+
xbee = XBee(device, io_loop=self._patch_io)
596604

597605
xbee._process_input(None, None)
598606
info = yield xbee.wait_read_frame()
@@ -620,7 +628,7 @@ def test_is_response_parsed_as_io(self):
620628

621629
device = Serial()
622630
device.set_read_data(APIFrame(data=b'\x88DIS\x00' + data).output())
623-
xbee = XBee(device)
631+
xbee = XBee(device, io_loop=self._patch_io)
624632

625633
xbee._process_input(None, None)
626634
info = yield xbee.wait_read_frame()
@@ -655,7 +663,7 @@ def test_is_remote_response_parsed_as_io(self):
655663
data=b'\x97D\x00\x13\xa2\x00@oG\xe4v\x1aIS\x00' + data).output()
656664
)
657665

658-
xbee = XBee(device)
666+
xbee = XBee(device, io_loop=self._patch_io)
659667

660668
xbee._process_input(None, None)
661669
info = yield xbee.wait_read_frame()
@@ -693,7 +701,7 @@ def test_read_io_data(self):
693701

694702
device = Serial()
695703
device.set_read_data(b'\x7E\x00\x0C' + rx_io_resp + data + b'\xfd')
696-
xbee = XBee(device)
704+
xbee = XBee(device, io_loop=self._patch_io)
697705

698706
xbee._process_input(None, None)
699707
info = yield xbee.wait_read_frame()
@@ -737,7 +745,7 @@ def read(self, length=1):
737745
return super(BadReadDevice, self).read()
738746

739747
badDevice = BadReadDevice(1, b'\x7E\x00\x05\x88DMY\x01\x8c')
740-
xbee = XBee(badDevice)
748+
xbee = XBee(badDevice, io_loop=self._patch_io)
741749

742750
try:
743751
xbee._process_input(None, None)
@@ -755,7 +763,7 @@ def test_read_at_params_in_escaped_mode(self):
755763
"""
756764
device = Serial()
757765
device.set_read_data(b'~\x00\t\x88DMY\x01}^}]}1}3m')
758-
xbee = XBee(device, escaped=True)
766+
xbee = XBee(device, escaped=True, io_loop=self._patch_io)
759767

760768
xbee._process_input(None, None)
761769
info = yield xbee.wait_read_frame()
@@ -773,7 +781,7 @@ def test_empty_frame_ignored(self):
773781
"""
774782
device = Serial()
775783
device.set_read_data(b'\x7E\x00\x00\xFF\x7E\x00\x05\x88DMY\x01\x8c')
776-
xbee = XBee(device)
784+
xbee = XBee(device, io_loop=self._patch_io)
777785

778786
xbee._process_input(None, None)
779787
xbee._process_input(None, None)
@@ -791,7 +799,7 @@ def test_read_rx_with_close_brace(self):
791799
"""
792800
device = Serial()
793801
device.set_read_data(APIFrame(b'\x81\x01\x02\x55\x00{test=1}').output())
794-
xbee = XBee(device)
802+
xbee = XBee(device, io_loop=self._patch_io)
795803

796804
xbee._process_input(None, None)
797805
info = yield xbee.wait_read_frame()
@@ -810,7 +818,7 @@ def test_read_rx_with_close_brace_escaped(self):
810818
device = Serial()
811819
device.set_read_data(APIFrame(b'\x81\x01\x02\x55\x00{test=1}',
812820
escaped=True).output())
813-
xbee = XBee(device, escaped=True)
821+
xbee = XBee(device, escaped=True, io_loop=self._patch_io)
814822

815823
xbee._process_input(None, None)
816824
info = yield xbee.wait_read_frame()

xbee/tornado/tests/test_zigbee.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77
Tests the XBee ZB (ZigBee) implementation class for API compliance
88
"""
99
import unittest
10+
try:
11+
from unittest.mock import Mock
12+
except ImportError:
13+
from mock import Mock
1014
from xbee.tornado import has_tornado
1115

1216
if not has_tornado:
1317
raise unittest.SkipTest("Requires Tornado")
1418

19+
from tornado import ioloop # noqa
1520
from xbee.tornado.zigbee import ZigBee # noqa
1621
from xbee.tests.Fake import Serial # noqa
1722

@@ -22,15 +27,18 @@ class TestZigBee(unittest.TestCase):
2227
"""
2328

2429
def setUp(self):
25-
self.zigbee = ZigBee(None)
2630
super(TestZigBee, self).setUp()
31+
self._patch_io = ioloop.IOLoop.current()
32+
self._patch_io.add_handler = Mock()
33+
serial_port = Serial()
34+
self.zigbee = ZigBee(serial_port, io_loop=self._patch_io)
2735

2836
def test_send(self):
2937
"""
3038
Test send() with AT command.
3139
"""
3240
device = Serial()
33-
xbee = ZigBee(device)
41+
xbee = ZigBee(device, io_loop=self._patch_io)
3442
xbee.send('at', command='MY')
3543
result = device.get_data_written()
3644
expected = b'~\x00\x04\x08\x01MYP'
@@ -236,8 +244,11 @@ class TestParseZigBeeIOData(unittest.TestCase):
236244
"""
237245

238246
def setUp(self):
239-
self.zigbee = ZigBee(None)
240247
super(TestParseZigBeeIOData, self).setUp()
248+
patch_io = ioloop.IOLoop.current()
249+
patch_io.add_handler = Mock()
250+
serial_port = Serial()
251+
self.zigbee = ZigBee(serial_port, io_loop=patch_io)
241252

242253
def test_parse_dio_adc(self):
243254
data = b'\x01\x08\x00\x0e\x08\x00\x00\x00\x02P\x02\x06'

0 commit comments

Comments
 (0)