Skip to content

Commit

Permalink
make time related methods more testable
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed May 9, 2018
1 parent 07e9db4 commit fe33e3f
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 84 deletions.
2 changes: 1 addition & 1 deletion lomond/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from __future__ import unicode_literals

__version__ = "0.2.1"
__version__ = "0.2.2a0"
2 changes: 1 addition & 1 deletion lomond/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def validate(self):
"""Check the frame and raise any errors."""
if self.is_control and len(self.payload) > 125:
raise errors.ProtocolError(
"control frames must <= 125 bytes in length"
"control frames must be <= 125 bytes in length"
)
if self.rsv1 or self.rsv2 or self.rsv3:
raise errors.ProtocolError(
Expand Down
83 changes: 30 additions & 53 deletions lomond/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
class _SocketFail(Exception):
"""Used internally to respond to socket fails."""


class _ForceDisconnect(Exception):
"""Used internally when the close timeout is tripped."""

Expand Down Expand Up @@ -226,51 +227,47 @@ def _send_request(self):
"""Send the request over the wire."""
self.write(self.websocket.build_request())

def _check_poll(self, poll):
def _check_poll(self, poll, session_time):
"""Check if it is time for a poll."""
_time = self.session_time
_time = session_time
if self._poll_start is None or _time - self._poll_start >= poll:
self._poll_start = _time
return True
else:
return False

def _check_auto_ping(self, ping_rate):
def _check_auto_ping(self, ping_rate, session_time):
"""Check if a ping is required."""
if not ping_rate:
return
current_time = self.session_time
if current_time > self._next_ping:
if ping_rate and session_time > self._next_ping:
# Calculate next ping time that is in the future.
self._next_ping = (
math.ceil(current_time / ping_rate) * ping_rate
math.ceil(session_time / ping_rate) * ping_rate
)
try:
self.websocket.send_ping()
except errors.WebSocketError:
pass # If the websocket has gone away

def _check_ping_timeout(self, ping_timeout):
def _check_ping_timeout(self, ping_timeout, session_time):
"""Check if the server is not responding to pings."""
if ping_timeout:
time_since_last_pong = self.session_time - self._last_pong
time_since_last_pong = session_time - self._last_pong
if time_since_last_pong > ping_timeout:
log.debug('ping_timeout time exceeded')
return True
return False

def _check_close_timeout(self, close_timeout):
def _check_close_timeout(self, close_timeout, session_time):
"""Check if the close timeout was tripped."""
if not close_timeout:
return
sent_close_time = self.websocket.sent_close_time
if sent_close_time is None:
return
if self.session_time >= sent_close_time + close_timeout:
raise _ForceDisconnect(
"server didn't respond to close packet "
"within {}s".format(close_timeout)
)
if close_timeout:
sent_close_time = self.websocket.sent_close_time
if sent_close_time is None:
return
if session_time >= sent_close_time + close_timeout:
raise _ForceDisconnect(
"server didn't respond to close packet "
"within {}s".format(close_timeout)
)

def _recv(self, count):
"""Receive and return pending data from the socket."""
Expand All @@ -294,15 +291,15 @@ def _recv(self, count):
def _regular(self, poll, ping_rate, ping_timeout, close_timeout):
"""Run regularly to do polling / pings."""
# Check for regularly running actions.
if self._check_poll(poll):
if self._check_poll(poll, self.session_time):
yield events.Poll()
self._check_auto_ping(ping_rate)
if self._check_ping_timeout(ping_timeout):
self._check_auto_ping(ping_rate, self.session_time)
if self._check_ping_timeout(ping_timeout, self.session_time):
yield events.Unresponsive()
raise _ForceDisconnect(
'exceeded {:.0f}s ping timeout'.format(ping_timeout)
)
self._check_close_timeout(close_timeout)
self._check_close_timeout(close_timeout, self.session_time)

def _send_pong(self, event):
"""Send a pong message in response to ping event."""
Expand Down Expand Up @@ -385,20 +382,16 @@ def _regular():
readable = selector.wait_readable(poll)
for event in _regular():
yield event
if not readable:
continue
data = self._recv(64 * 1024)
if data:
for event in self.websocket.feed(data):
self._on_event(event, auto_pong)
yield event
for event in _regular():
if readable:
data = self._recv(64 * 1024)
if data:
for event in self.websocket.feed(data):
self._on_event(event, auto_pong)
yield event
else:
if websocket.is_active:
for event in _regular():
yield event
else:
self._socket_fail('connection lost')
break

except _ForceDisconnect as error:
self._close_socket()
yield events.Disconnected('disconnected; {}'.format(error))
Expand All @@ -419,19 +412,3 @@ def _regular():
yield events.Disconnected(graceful=True)
finally:
selector.close()


if __name__ == "__main__": # pragma: no cover

# Test with wstest -m echoserver -w ws://127.0.0.1:9001 -d
# Get wstest app from http://autobahn.ws/testsuite/

from .websocket import WebSocket

#ws = WebSocket('wss://echo.websocket.org')
ws = WebSocket('ws://127.0.0.1:9001/')
for event in ws.connect(poll=5):
print(event)
if isinstance(event, events.Poll):
ws.send_text('Hello, World')
ws.send_binary(b'hello world in binary')
8 changes: 2 additions & 6 deletions lomond/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
import json
import logging
import os
import time

import six
from six import text_type
from six.moves.urllib.parse import urlparse

from . import constants
Expand Down Expand Up @@ -208,7 +206,7 @@ def reset(self):

__iter__ = connect

def close(self, code=None, reason=None):
def close(self, code=Status.NORMAL, reason=b'goodbye'):
"""Close the websocket.
:param int code: A closing code, which should probably be one of
Expand Down Expand Up @@ -465,9 +463,7 @@ def send_text(self, text):

def _send_close(self, code, reason):
"""Send a close frame."""
_code = code if code is not None else Status.NORMAL
_reason = reason if reason is not None else b'goodbye'
frame_bytes = Frame.build_close_payload(_code, _reason)
frame_bytes = Frame.build_close_payload(code, reason)
try:
self.session.send(Opcode.CLOSE, frame_bytes)
except (errors.WebSocketUnavailable, errors.TransportFail):
Expand Down
1 change: 1 addition & 0 deletions tests/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def test_masking_key():
expected = b'\x81\x8c\xaa\xf7\x7f\x00\xe2\x92\x13l\xc5\xdb_W\xc5\x85\x13d'
assert frame_bytes == expected


def test_repr_of_frame(frame_factory):
assert repr(frame_factory()) == '<frame TEXT (0 bytes) fin=1>'
assert repr(
Expand Down
36 changes: 13 additions & 23 deletions tests/test_session.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import calendar
import select
import socket
import time
from datetime import datetime
import sys

import pytest
from freezegun import freeze_time
Expand Down Expand Up @@ -239,6 +234,7 @@ def return_fake_socket():
"ConnectFail('request failed; socket fail; error during sendall')"
)


def test_that_on_ping_responds_with_pong(session, mocker):
# we don't actually care that much for the whole stack underneath,
# we only want to check whether a certain method was called..
Expand Down Expand Up @@ -269,36 +265,27 @@ def close_which_raises_error():
)


@freeze_time("1994-05-01 18:40:00")
def test_check_poll(session):
session._on_ready()
assert session._check_poll(5 * 60)
assert not session._check_poll(60 * 60)
assert session._check_poll(60, 61)
assert not session._check_poll(60, 59)


@freeze_time("1994-05-01 18:40:00")
def test_check_auto_ping(session, mocker):
session._on_ready()

mocker.patch.object(session.websocket, 'send_ping')

assert session.websocket.send_ping.call_count == 0
session._check_auto_ping(10, 12)
assert session.websocket.send_ping.call_count == 1
session._check_auto_ping(10, 15)
assert session.websocket.send_ping.call_count == 1

with freeze_time('1994-05-01 18:41:00'):
session._check_auto_ping(15 * 60)

assert session.websocket.send_ping.call_count == 1
session._check_auto_ping(36 * 60)
assert session.websocket.send_ping.call_count == 1


@freeze_time("1994-05-01 18:40:00")
def test_check_ping_timeout(session, mocker):
session._on_ready()

assert not session._check_ping_timeout(10)
with freeze_time('1994-05-01 18:41:00'):
assert session._check_ping_timeout(10)
assert not session._check_ping_timeout(10, 5)
assert session._check_ping_timeout(10, 11)


@mocketize
Expand Down Expand Up @@ -415,6 +402,7 @@ def test_recv_no_sock(session):
session._sock = None
assert session._recv(1) == b''


def test_recv_with_secure_websocket(session):
def fake_recv(self):
return b'\x01'
Expand Down Expand Up @@ -469,5 +457,7 @@ def test_send_pong(session):
def test_check_close_timeout(session):
session._on_ready()
session.websocket = FakeWebSocket()
session.websocket.sent_close_time = 10
session._check_close_timeout(10, 19)
with pytest.raises(_ForceDisconnect):
session._check_close_timeout(10)
session._check_close_timeout(10, 21)
2 changes: 2 additions & 0 deletions tests/test_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,15 @@ def test_send_close_needs_open_socket(websocket):

def test_calling_close_yields_close_event(websocket_with_fake_session):
ws = websocket_with_fake_session
assert ws.is_active is True
ws.close()
close_message = Close(1000, b'bye')
close_events = list(ws._on_close(close_message))
assert len(close_events) == 1
assert isinstance(close_events[0], Closed)
assert ws.is_closed is True
assert ws.is_closing is False
assert ws.is_active is False


def test_calling_on_close_when_websocket_is_closed_results_in_noop(
Expand Down

0 comments on commit fe33e3f

Please sign in to comment.