irctest/irctest/client_mock.py

167 lines
6.2 KiB
Python
Raw Normal View History

2021-02-22 18:04:23 +00:00
import socket
2015-12-25 14:45:06 +00:00
import ssl
import sys
2015-12-21 19:13:16 +00:00
import time
from typing import Any, Callable, List, Optional, Union
2021-02-22 18:04:23 +00:00
from .exceptions import ConnectionClosed, NoMessageException
2015-12-21 19:13:16 +00:00
from .irc_utils import message_parser
2021-02-22 18:02:13 +00:00
2015-12-21 19:13:16 +00:00
class ClientMock:
def __init__(self, name: Any, show_io: bool):
2015-12-21 19:13:16 +00:00
self.name = name
self.show_io = show_io
self.inbuffer: List[message_parser.Message] = []
2015-12-25 14:45:06 +00:00
self.ssl = False
2021-02-22 18:02:13 +00:00
def connect(self, hostname: str, port: int) -> None:
2015-12-21 19:13:16 +00:00
self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# probably useful for test_buffering, as it relies on chunking
# the packets to be useful
self.conn.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
2021-02-22 18:02:13 +00:00
self.conn.settimeout(1) # TODO: configurable
2015-12-21 19:13:16 +00:00
self.conn.connect((hostname, port))
if self.show_io:
2021-02-22 18:02:13 +00:00
print("{:.3f} {}: connects to server.".format(time.time(), self.name))
def disconnect(self) -> None:
2015-12-21 19:13:16 +00:00
if self.show_io:
2021-02-22 18:02:13 +00:00
print("{:.3f} {}: disconnects from server.".format(time.time(), self.name))
2015-12-21 19:13:16 +00:00
self.conn.close()
2021-02-22 18:02:13 +00:00
def starttls(self) -> None:
2021-02-22 18:02:13 +00:00
assert not self.ssl, "SSL already active."
2015-12-25 14:45:06 +00:00
self.conn = ssl.wrap_socket(self.conn)
self.ssl = True
2021-02-22 18:02:13 +00:00
def getMessages(
self, synchronize: bool = True, assert_get_one: bool = False, raw: bool = False
) -> List[message_parser.Message]:
"""actually returns List[str] in the rare case where raw=True."""
__tracebackhide__ = True # Hide from pytest tracebacks on test failure.
token: Optional[str]
2015-12-21 19:13:16 +00:00
if synchronize:
2021-02-22 18:02:13 +00:00
token = "synchronize{}".format(time.monotonic())
self.sendLine("PING {}".format(token))
2021-02-22 18:46:34 +00:00
else:
token = None
2015-12-21 19:13:16 +00:00
got_pong = False
2021-02-22 18:02:13 +00:00
data = b""
(self.inbuffer, messages) = ([], self.inbuffer)
2015-12-21 19:13:16 +00:00
conn = self.conn
try:
while not got_pong:
try:
new_data = conn.recv(4096)
except socket.timeout:
2021-02-22 18:02:13 +00:00
if not assert_get_one and not synchronize and data == b"":
# Received nothing
return []
2021-02-22 20:25:48 +00:00
if self.show_io:
print("{:.3f} {}: waiting…".format(time.time(), self.name))
continue
2015-12-25 21:47:11 +00:00
except ConnectionResetError:
2015-12-27 11:45:03 +00:00
raise ConnectionClosed()
else:
if not new_data:
# Connection closed
raise ConnectionClosed()
data += new_data
2021-02-22 18:02:13 +00:00
if not new_data.endswith(b"\r\n"):
continue
if not synchronize:
got_pong = True
2021-02-22 18:02:13 +00:00
for line in data.decode().split("\r\n"):
if line:
if self.show_io:
2021-02-22 18:02:13 +00:00
print(
"{time:.3f}{ssl} S -> {client}: {line}".format(
time=time.time(),
ssl=" (ssl)" if self.ssl else "",
client=self.name,
line=line,
)
)
2020-11-26 05:25:52 +00:00
message = message_parser.parse_message(line)
2021-02-22 18:02:13 +00:00
if message.command == "PONG" and token in message.params:
got_pong = True
elif (
synchronize
and message.command == "451"
and message.params[1] == "PING"
):
raise ValueError(
"Got '451 * PONG'. Did you forget synchronize=False?"
)
else:
2020-11-26 05:25:52 +00:00
if raw:
messages.append(line) # type: ignore
2020-11-26 05:25:52 +00:00
else:
messages.append(message)
2021-02-22 18:02:13 +00:00
data = b""
except ConnectionClosed:
if messages:
return messages
else:
raise
else:
return messages
2021-02-22 18:02:13 +00:00
def getMessage(
self,
filter_pred: Optional[Callable[[message_parser.Message], bool]] = None,
synchronize: bool = True,
raw: bool = False,
) -> message_parser.Message:
"""Returns str in the rare case where raw=True"""
__tracebackhide__ = True # Hide from pytest tracebacks on test failure.
2015-12-21 19:13:16 +00:00
while True:
if not self.inbuffer:
self.inbuffer = self.getMessages(
2021-02-22 18:02:13 +00:00
synchronize=synchronize, assert_get_one=True, raw=raw
)
2015-12-21 19:13:16 +00:00
if not self.inbuffer:
raise NoMessageException()
2021-02-22 18:02:13 +00:00
message = self.inbuffer.pop(0) # TODO: use dequeue
2015-12-21 19:13:16 +00:00
if not filter_pred or filter_pred(message):
return message
2021-02-22 18:02:13 +00:00
def sendLine(self, line: Union[str, bytes]) -> None:
2020-06-22 19:48:56 +00:00
if isinstance(line, str):
encoded_line = line.encode()
elif isinstance(line, bytes):
encoded_line = line
else:
raise ValueError(line)
2021-02-22 18:02:13 +00:00
if not encoded_line.endswith(b"\r\n"):
encoded_line += b"\r\n"
2015-12-27 11:45:03 +00:00
try:
ret = self.conn.sendall(encoded_line) # type: ignore
2015-12-27 11:45:03 +00:00
except BrokenPipeError:
raise ConnectionClosed()
2021-02-22 18:02:13 +00:00
if (
sys.version_info <= (3, 6) and self.ssl
): # https://bugs.python.org/issue25951
2015-12-25 14:45:06 +00:00
assert ret == len(encoded_line), (ret, repr(encoded_line))
else:
assert ret is None, ret
2015-12-21 19:13:16 +00:00
if self.show_io:
if isinstance(line, str):
escaped_line = line
2021-02-22 18:02:13 +00:00
escaped = ""
else:
escaped_line = repr(line)
2021-02-22 18:02:13 +00:00
escaped = " (escaped)"
print(
"{time:.3f}{escaped}{ssl} {client} -> S: {line}".format(
time=time.time(),
escaped=escaped,
ssl=" (ssl)" if self.ssl else "",
client=self.name,
line=escaped_line.strip("\r\n"),
)
)