Skip to content

Commit cde45d5

Browse files
authored
Merge pull request #28 from scivisum/bug/RD-37592_dont_let_flushmessages_block
Bug/rd 37592 dont let flushmessages block
2 parents 8522067 + f12141f commit cde45d5

File tree

7 files changed

+211
-110
lines changed

7 files changed

+211
-110
lines changed

browserdebuggertools/chrome/interface.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def set_timeout(self, value):
8484
""" Switches the timeout to the given value.
8585
"""
8686
_timeout = self._socket_handler.timeout
87-
self._socket_handler.timeout = value
87+
self._socket_handler.timer.timeout = value
8888
try:
8989
yield
9090
finally:

browserdebuggertools/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ class DevToolsTimeoutException(DevToolsException):
1414
pass
1515

1616

17+
class TimerException(DevToolsException):
18+
pass
19+
20+
1721
class TabNotFoundError(NotFoundError):
1822
pass
1923

browserdebuggertools/sockethandler.py

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import json
23
import logging
34
import socket
@@ -14,7 +15,7 @@
1415
from browserdebuggertools.exceptions import (
1516
DevToolsException, ResultNotFoundError, TabNotFoundError, MaxRetriesException,
1617
DevToolsTimeoutException, DomainNotEnabledError,
17-
MethodNotFoundError, UnknownError, ResourceNotFoundError
18+
MethodNotFoundError, UnknownError, ResourceNotFoundError, TimerException
1819
)
1920

2021

@@ -33,6 +34,42 @@ def retry_if_exception(socket_handler_instance, *args, **kwargs):
3334
return retry_if_exception
3435

3536

37+
class _Timer(object):
38+
39+
def __init__(self, timeout):
40+
"""
41+
:param timeout: <int> seconds elapsed until considered timed out.
42+
"""
43+
self.timeout = timeout
44+
self._start = None
45+
46+
def start(self):
47+
"""
48+
Capture the start time, cannot be called again without first calling clear().
49+
"""
50+
if self._start:
51+
raise TimerException("You cannot start a timer that's already started")
52+
self._start = time.time()
53+
54+
def clear(self):
55+
"""
56+
Make the timer object ready to be used again.
57+
"""
58+
self._start = None
59+
60+
@property
61+
def timed_out(self):
62+
"""
63+
:return: <bool> True if the time from start to now is greater than the timeout threshold
64+
"""
65+
if not self._start:
66+
raise TimerException("Timer must have started for there to have been a timeout")
67+
68+
if (time.time() - self._start) > self.timeout:
69+
return True
70+
return False
71+
72+
3673
class SocketHandler(object):
3774

3875
MAX_CONNECTION_RETRIES = 3
@@ -42,6 +79,7 @@ class SocketHandler(object):
4279
def __init__(self, port, timeout, domains=None):
4380

4481
self.timeout = timeout
82+
self.timer = _Timer(self.timeout)
4583

4684
if not domains:
4785
domains = {}
@@ -163,9 +201,11 @@ def _flush_messages(self):
163201
"""
164202
try:
165203
message = self._recv()
166-
while message:
204+
while not self.timer.timed_out and message:
167205
self._append(message)
168206
message = self._recv()
207+
if self.timer.timed_out:
208+
raise DevToolsTimeoutException("Timed out flushing messages")
169209
except socket.error:
170210
return
171211

@@ -226,7 +266,9 @@ def get_events(self, domain, clear=False):
226266
'The domain "%s" is not enabled, try enabling it via the interface.' % domain
227267
)
228268

229-
self._flush_messages()
269+
with self.timed_method():
270+
self._flush_messages()
271+
230272
events = self._events[domain]
231273
if clear:
232274
self._events[domain] = []
@@ -243,18 +285,28 @@ def reset(self):
243285
self._results = {}
244286
self._next_result_id = 0
245287

288+
@contextlib.contextmanager
289+
def timed_method(self):
290+
291+
self.timer.start()
292+
try:
293+
yield
294+
295+
finally:
296+
self.timer.clear()
297+
246298
def _wait_for_result(self):
247299
""" Waits for a result to complete within the timeout duration then returns it.
248300
Raises a DevToolsTimeoutException if it cannot find the result.
249301
250302
:return: The result.
251303
"""
252-
start = time.time()
253-
while not self.timeout or (time.time() - start) < self.timeout:
254-
try:
255-
return self._find_next_result()
256-
except ResultNotFoundError:
257-
time.sleep(0.01)
304+
with self.timed_method():
305+
while not self.timer.timed_out:
306+
try:
307+
return self._find_next_result()
308+
except ResultNotFoundError:
309+
time.sleep(0.01)
258310
raise DevToolsTimeoutException(
259311
"Reached timeout limit of {}, waiting for a response message".format(self.timeout)
260312
)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
setup(
1515
name="browserdebuggertools",
16-
version="5.1.0",
16+
version="5.2.0",
1717
packages=PACKAGES,
1818
install_requires=requires,
1919
license="GNU General Public License v3",

tests/e2etests/chrome/test_interface.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,12 @@ def test_take_screenshot_incomplete_main_exchange(self):
121121

122122
assert isinstance(self, ChromeInterfaceTest)
123123

124-
self.devtools_client.navigate(
125-
url="http://localhost:%s?main_exchange_response_time=10" % self.testSite.port
126-
)
124+
with self.devtools_client.set_timeout(30):
125+
126+
self.devtools_client.navigate(
127+
url="http://localhost:%s?main_exchange_response_time=10" % self.testSite.port
128+
)
129+
127130
self.devtools_client.take_screenshot(self.file_path)
128131
self.assertTrue(os.path.exists(self.file_path))
129132
self.assertTrue(os.path.getsize(self.file_path) >= 5000)
@@ -184,9 +187,10 @@ def test_take_screenshot_incomplete_main_exchange(self):
184187

185188
assert isinstance(self, ChromeInterfaceTest)
186189

187-
self.devtools_client.navigate(
188-
url="http://localhost:%s?main_exchange_response_time=10" % self.testSite.port
189-
)
190+
with self.devtools_client.set_timeout(30):
191+
self.devtools_client.navigate(
192+
url="http://localhost:%s?main_exchange_response_time=10" % self.testSite.port
193+
)
190194
self.devtools_client.navigate(url="http://localhost:%s" % self.testSite.port)
191195
self._assert_dom_complete()
192196
self.assertEqual("complete", self.devtools_client.get_document_readystate())
Lines changed: 58 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,98 @@
1+
import json
12
import socket
23
from unittest import TestCase
34

45
from mock import MagicMock
56

7+
from browserdebuggertools.exceptions import DevToolsTimeoutException
68
from browserdebuggertools.sockethandler import SocketHandler
79

810
MODULE_PATH = "browserdebuggertools.sockethandler."
911

1012

11-
class MockSocketHandler(SocketHandler):
13+
class Test_SocketHandler_get_events(TestCase):
1214

13-
def __init__(self):
14-
self._websocket = MagicMock()
15+
class _DummyWebsocket(object):
1516

16-
self._next_result_id = 0
17-
self._domains = []
18-
self._results = {}
19-
self._events = {}
20-
self._internal_events = {}
17+
def __init__(self):
18+
self._result_id = -1
2119

20+
def recv(self):
21+
self._result_id += 1
22+
return json.dumps({"method": "Network.Something", "params": {}})
2223

23-
class SocketHandlerTest(TestCase):
24+
class MockSocketHandler(SocketHandler):
2425

25-
def setUp(self):
26-
self.socket_handler = MockSocketHandler()
27-
28-
29-
class Test_SocketHandler_can_get_messages(SocketHandlerTest):
26+
def __init__(self):
27+
SocketHandler.__init__(self, 1234, 30)
3028

31-
def test_get_results(self):
32-
mock_result = {"key": "value"}
33-
mock_message = '{"id": 1, "result": {"key": "value"}}'
34-
self.socket_handler._websocket.recv.side_effect = [mock_message, None]
29+
def _get_websocket_url(self, port):
30+
return "ws://localhost:1234/devtools/page/test"
3531

36-
self.socket_handler._flush_messages()
32+
def _setup_websocket(self):
33+
return MagicMock()
3734

38-
self.assertEqual(mock_result, self.socket_handler._results[1])
39-
40-
def test_get_errors(self):
41-
mock_error = {"error": {"key": "value"}}
42-
mock_message = '{"id": 1, "error": {"key": "value"}}'
43-
self.socket_handler._websocket.recv.side_effect = [mock_message, None]
35+
def setUp(self):
36+
self.socket_handler = self.MockSocketHandler()
4437

45-
self.socket_handler._flush_messages()
38+
def test_timeout_when_getting_events(self):
4639

47-
self.assertEqual(mock_error, self.socket_handler._results[1])
40+
self.socket_handler._websocket = self._DummyWebsocket()
41+
self.socket_handler.timer.timeout = 1
42+
self.socket_handler._domains = {"Network": []}
4843

49-
def test_get_events(self):
50-
mock_event = {"method": "MockDomain.mockEvent", "params": {"key": "value"}}
51-
mock_message = '{"method": "MockDomain.mockEvent", "params": {"key": "value"}}'
52-
self.socket_handler._events["MockDomain"] = []
53-
self.socket_handler._websocket.recv.side_effect = [mock_message, None]
44+
with self.assertRaises(DevToolsTimeoutException):
45+
self.socket_handler.get_events("Network")
5446

55-
self.socket_handler._flush_messages()
5647

57-
self.assertIn(mock_event, self.socket_handler._events["MockDomain"])
48+
class Test_SocketHandler_wait_for_result(TestCase):
5849

59-
def test_get_mixed(self):
60-
mock_result = {"key": "value"}
61-
mock_error = {"error": {"key": "value"}}
62-
mock_event = {"method": "MockDomain.mockEvent", "params": {"key": "value"}}
63-
mock_result_message = '{"id": 1, "result": {"key": "value"}}'
64-
mock_error_message = '{"id": 2, "error": {"key": "value"}}'
65-
mock_event_message = '{"method": "MockDomain.mockEvent", "params": {"key": "value"}}'
50+
class _DummyWebsocket(object):
6651

67-
self.socket_handler._events["MockDomain"] = []
68-
self.socket_handler._websocket.recv.side_effect = [
69-
mock_result_message, mock_error_message, mock_event_message, None
70-
]
52+
def __init__(self):
53+
self._result_id = -1
7154

72-
self.socket_handler._flush_messages()
55+
def recv(self):
56+
self._result_id += 1
57+
return json.dumps({"method": "Network.Something", "params": {}})
7358

74-
self.assertEqual(mock_result, self.socket_handler._results[1])
75-
self.assertEqual(mock_error, self.socket_handler._results[2])
76-
self.assertIn(mock_event, self.socket_handler._events["MockDomain"])
59+
class MockSocketHandler(SocketHandler):
7760

78-
def test_get_messages_then_except(self):
79-
mock_result = {"key": "value"}
80-
mock_message = '{"id": 1, "result": {"key": "value"}}'
81-
self.socket_handler._websocket.recv.side_effect = [mock_message, socket.error]
61+
def __init__(self):
62+
SocketHandler.__init__(self, 1234, 30)
8263

83-
self.socket_handler._flush_messages()
64+
def _get_websocket_url(self, port):
65+
return "ws://localhost:1234/devtools/page/test"
8466

85-
self.assertEqual(mock_result, self.socket_handler._results[1])
67+
def _setup_websocket(self):
68+
return MagicMock()
8669

70+
def setUp(self):
71+
self.socket_handler = self.MockSocketHandler()
8772

88-
class Test_SocketHandler_get_events(SocketHandlerTest):
73+
def test_no_messages_with_result_timeout(self):
8974

90-
def test_get_events_returns_copy(self):
91-
mock_message = '{"method": "MockDomain.mockEvent", "params": {"key": "value"}}'
92-
self.socket_handler._domains = ["MockDomain"]
93-
self.socket_handler._events["MockDomain"] = []
94-
self.socket_handler._websocket.recv.side_effect = [mock_message, None, None]
75+
self.socket_handler._websocket = MagicMock(recv=MagicMock(side_effect=socket.error()))
76+
self.socket_handler.timer.timeout = 1
77+
self.socket_handler._next_result_id = 2
9578

96-
events = self.socket_handler.get_events("MockDomain")
79+
with self.assertRaises(DevToolsTimeoutException):
80+
self.socket_handler._wait_for_result()
9781

98-
events[0] = ""
82+
def test_message_spamming_with_result_timeout(self):
9983

100-
self.assertEqual(
101-
{"method": "MockDomain.mockEvent", "params": {"key": "value"}},
102-
self.socket_handler.get_events("MockDomain")[0]
103-
)
84+
self.socket_handler._websocket = self._DummyWebsocket()
85+
self.socket_handler.timer.timeout = 1
86+
self.socket_handler._next_result_id = -1
10487

105-
def test_get_events_clear(self):
106-
mock_message = '{"method": "MockDomain.mockEvent", "params": {"key": "value"}}'
107-
self.socket_handler._domains = ["MockDomain"]
108-
self.socket_handler._events["MockDomain"] = []
109-
self.socket_handler._websocket.recv.side_effect = [mock_message, None, None]
88+
with self.assertRaises(DevToolsTimeoutException):
89+
self.socket_handler._wait_for_result()
11090

111-
events = self.socket_handler.get_events("MockDomain")
91+
def test_message_spamming_with_result_found(self):
11292

113-
events[0] = ""
93+
self.socket_handler._websocket = self._DummyWebsocket()
94+
self.socket_handler.timer.timeout = 1
95+
self.socket_handler._next_result_id = 2
11496

115-
self.assertEqual(
116-
{"method": "MockDomain.mockEvent", "params": {"key": "value"}},
117-
self.socket_handler.get_events("MockDomain")[0]
118-
)
97+
with self.assertRaises(DevToolsTimeoutException):
98+
self.socket_handler._wait_for_result()

0 commit comments

Comments
 (0)