From 96e9fe658355d6f859508bfff28508c979403409 Mon Sep 17 00:00:00 2001 From: SamDanielThangarajan <12202554+SamDanielThangarajan@users.noreply.github.com> Date: Sat, 5 Oct 2024 14:12:02 +0200 Subject: [PATCH] set asyncio_default_fixture_loop_scope=function for test cases --- pytest.ini | 3 +- src/nasdaq_protocols/soup/session.py | 4 +- tests/test_common_asyncsession.py | 2 +- tests/test_fix_session.py | 12 +- tests/test_itch_tools.py | 6 +- tests/test_ouch_session.py | 2 +- tests/test_soup_session.py | 209 ++++++++++++++++++++------- 7 files changed, 171 insertions(+), 67 deletions(-) diff --git a/pytest.ini b/pytest.ini index eb7ce88..91864a5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,8 +1,9 @@ [pytest] pythonpath = src asyncio_mode=auto +asyncio_default_fixture_loop_scope=function log_cli=true -log_level=INFO +log_level=DEBUG log_format = %(name)-20s: %(message)s log_date_format = %I:%M:%S addopts = --cov=src --cov-fail-under=97 \ No newline at end of file diff --git a/src/nasdaq_protocols/soup/session.py b/src/nasdaq_protocols/soup/session.py index 1a42cdc..7b4de94 100644 --- a/src/nasdaq_protocols/soup/session.py +++ b/src/nasdaq_protocols/soup/session.py @@ -233,7 +233,7 @@ async def on_unsequenced(self, msg: UnSequencedData) -> None: :param msg: UnSequencedData message. """ - def send_seq_msg(self, data: bytes) -> None: + def send_seq_msg(self, data: bytes | SequencedData) -> None: """ Send sequenced data to the client. @@ -251,7 +251,7 @@ def end_session(self): self.initiate_close() async def on_debug(self, msg: Debug) -> None: - self.log.info('%s> ++ client debug : %s', msg) + self.log.info('%s> ++ client debug : %s', self.session_id, msg) async def send_heartbeat(self): """ diff --git a/tests/test_common_asyncsession.py b/tests/test_common_asyncsession.py index 07cb84d..356cd49 100644 --- a/tests/test_common_asyncsession.py +++ b/tests/test_common_asyncsession.py @@ -55,7 +55,7 @@ async def client_session(mock_server_session) -> SampleTestClientSession: event_loop = asyncio.get_running_loop() port, server_session = mock_server_session session_ = SampleTestClientSession(session_id=common.SessionId()) - await event_loop.create_connection(lambda: session_, '', port) + _, session_ = await event_loop.create_connection(lambda: session_, '127.0.0.1', port=port) # test server-client communication works server_session.when(lambda x: x == b'echo').do(lambda session, _: session.send('echoed')) diff --git a/tests/test_fix_session.py b/tests/test_fix_session.py index 9ce3166..63243de 100644 --- a/tests/test_fix_session.py +++ b/tests/test_fix_session.py @@ -50,7 +50,7 @@ async def test__fix_session__login_successful(mock_server_session, session_facto ) fix_session = await fix.connect_async( - ('', port), ENTER_LOGIN_MSG, session_factory + ('127.0.0.1', port), ENTER_LOGIN_MSG, session_factory ) await fix_session.close() @@ -71,7 +71,7 @@ async def test__fix_session__login_failed(mock_server_session, session_factory): with pytest.raises(ConnectionRefusedError): await fix.connect_async( - ('', port), ENTER_LOGIN_MSG, session_factory + ('127.0.0.1', port), ENTER_LOGIN_MSG, session_factory ) @@ -87,7 +87,7 @@ async def test__fix_session__login_failed__server_closes_connection(mock_server_ with pytest.raises(ConnectionRefusedError): await fix.connect_async( - ('', port), ENTER_LOGIN_MSG, session_factory + ('127.0.0.1', port), ENTER_LOGIN_MSG, session_factory ) @@ -103,7 +103,7 @@ async def test__fix_session__no_server_heartbeats__session_closed(mock_server_se ) session = await fix.connect_async( - ('', port), ENTER_LOGIN_MSG, + ('127.0.0.1', port), ENTER_LOGIN_MSG, lambda : session_factory( client_heartbeat_interval=100, server_heartbeat_interval=server_heartbeat_interval @@ -129,7 +129,7 @@ async def test__fix_session__client_heartbeats__session_is_active(mock_server_se ) session = await fix.connect_async( - ('', port), ENTER_LOGIN_MSG, + ('127.0.0.1', port), ENTER_LOGIN_MSG, lambda : session_factory( client_heartbeat_interval=heart_beat_interval, server_heartbeat_interval=100 @@ -170,7 +170,7 @@ async def test__fix_session__active_message_flow__no_heartbeats_sent(mock_server ) session = await fix.connect_async( - ('', port), ENTER_LOGIN_MSG, + ('127.0.0.1', port), ENTER_LOGIN_MSG, lambda : session_factory( client_heartbeat_interval=heart_beat_interval, server_heartbeat_interval=100 diff --git a/tests/test_itch_tools.py b/tests/test_itch_tools.py index 345aca8..1ed8b38 100644 --- a/tests/test_itch_tools.py +++ b/tests/test_itch_tools.py @@ -84,7 +84,7 @@ async def test__itch_tools__tail_itch(mock_server_session, load_itch_tools): # start tailing tailer = asyncio.create_task(tail_itch( - ('', port), 'test-u', 'test-p', '', 1, + ('127.0.0.1', port), 'test-u', 'test-p', '', 1, definitions.connect_async, 10, 10 )) assert not tailer.done() @@ -114,7 +114,7 @@ async def test__itch_tools__tail_itch__login_failed(mock_server_session, load_it # start tailing tailer = asyncio.create_task(tail_itch( - ('', port), 'test-u', 'test-p', '', 1, + ('127.0.0.1', port), 'test-u', 'test-p', '', 1, definitions.connect_async, 10, 10 )) # give some time for tail @@ -149,7 +149,7 @@ async def test__itch_tools__tail_itch__ctrl_c(mock_server_session, load_itch_too # start tailing tailer = asyncio.create_task(tail_itch( - ('', port), 'test-u', 'test-p', '', 1, + ('127.0.0.1', port), 'test-u', 'test-p', '', 1, definitions.connect_async, 10, 10 )) # give some time for tail diff --git a/tests/test_ouch_session.py b/tests/test_ouch_session.py index b1e67bc..5faf2fb 100644 --- a/tests/test_ouch_session.py +++ b/tests/test_ouch_session.py @@ -23,7 +23,7 @@ async def connect_to_mock_ouch_server(mock_server_session, session_factory=None) LOG.debug('connecting to server...') client_session = await ouch.connect_async( - ('', port), 'test-u', 'test-p', '', + ('127.0.0.1', port), 'test-u', 'test-p', '', session_factory=session_factory ) assert client_session is not None diff --git a/tests/test_soup_session.py b/tests/test_soup_session.py index 0216008..38c2ba7 100644 --- a/tests/test_soup_session.py +++ b/tests/test_soup_session.py @@ -1,54 +1,56 @@ import asyncio +import logging +import attrs import pytest -from nasdaq_protocols import common from nasdaq_protocols import soup -from nasdaq_protocols.soup import LoginRequest, LoginAccepted, LoginRejected +from nasdaq_protocols.soup import LoginRequest, LoginAccepted, LoginRejected, UnSequencedData +from tests.mocks import matches, send +LOG = logging.getLogger(__name__) -class SoupServerTestSession(soup.SoupServerSession, session_type='server'): - async def on_login(self, msg: LoginRequest) -> LoginAccepted | LoginRejected: - if msg.user == 'test-u' and msg.password == 'test-p': - return LoginAccepted('session', int(msg.sequence)) - else: - return LoginRejected(soup.LoginRejectReason.NOT_AUTHORIZED) - - async def on_debug(self, msg: soup.Debug) -> None: - self.send_msg(soup.SequencedData(f'{msg.msg}-ack'.encode('ascii'))) - async def on_unsequenced(self, msg: soup.UnSequencedData): - reply = msg.data.decode('ascii') + '-ack' - self.send_msg(soup.SequencedData(reply.encode('ascii'))) +@attrs.define(auto_attribs=True) +class SampleTestSourServerSession(soup.SoupServerSession): + output_sent: list[soup.SoupMessage] = attrs.field(factory=list) - def generate_load(self, number_of_messages): - for i in range(number_of_messages): - self.send_msg(soup.SequencedData(f'msg-{i}'.encode('ascii'))) - self.send_msg(soup.UnSequencedData('end'.encode('ascii'))) + async def on_login(self, msg: LoginRequest) -> LoginAccepted | LoginRejected: + return LoginAccepted('session', 1) + async def on_unsequenced(self, msg: UnSequencedData) -> None: + if msg.data == b'case1': + self.send_seq_msg(msg.data) + else: + self.send_seq_msg( + soup.SequencedData(b'case2') + ) -@pytest.fixture(scope='function') -async def soup_server_session(unused_tcp_port): - session = SoupServerTestSession() - server, serving_task = await common.start_server(('127.0.0.1', unused_tcp_port), lambda: session) - yield unused_tcp_port, session + def send_msg(self, msg): + self.output_sent.append(msg) - retry = 0 - while not session.is_closed() and retry < 5: - await asyncio.sleep(0.001) - assert session.is_closed() +def configure_login_accept(server_session): + server_session.when( + matches(soup.LoginRequest('test-u', 'test-p', 'session', '1')), 'login-request-match', + ).do( + send(LoginAccepted('session', 1)), 'login-accepted' + ) + return server_session - await common.stop_task(serving_task) +async def test__soup_session__invalid_credentials__login_rejected(mock_server_session): + port, server_session = mock_server_session -@pytest.mark.asyncio -async def test__soup_session__invalid_credentials__login_rejected(soup_server_session): - port, server_session = soup_server_session + server_session.when( + matches(LoginRequest('nouser', 'nopwd', 'session', '1')), 'login-request-match', + ).do( + send(LoginRejected(soup.LoginRejectReason.NOT_AUTHORIZED)), 'login-rejected' + ) with pytest.raises(ConnectionRefusedError): client_session = await soup.connect_async( - ('', port), + ('127.0.0.1', port), 'nouser', 'nopwd', 'session' @@ -56,12 +58,12 @@ async def test__soup_session__invalid_credentials__login_rejected(soup_server_se assert client_session is None -@pytest.mark.asyncio -async def test__soup_session__valid_credentials__login_accepted(soup_server_session): - port, server_session = soup_server_session +async def test__soup_session__valid_credentials__login_accepted(mock_server_session): + port, server_session = mock_server_session + server_session = configure_login_accept(server_session) client_session = await soup.connect_async( - ('', port), + ('127.0.0.1', port), 'test-u', 'test-p', 'session' @@ -71,18 +73,26 @@ async def test__soup_session__valid_credentials__login_accepted(soup_server_sess client_session.logout() -@pytest.mark.asyncio -async def test__soup_session__able_to_communicate(soup_server_session): - port, server_session = soup_server_session +async def test__soup_session__able_to_communicate(mock_server_session): + port, server_session = mock_server_session + server_session = configure_login_accept(server_session) client_session = await soup.connect_async( - ('', port), + ('127.0.0.1', port), 'test-u', 'test-p', 'session' ) assert client_session is not None + for i in range(1, 10): + server_session.when( + matches(soup.UnSequencedData(f'hello-{i}'.encode())), + f'sequenced-data-{i}' + ).do( + send(soup.SequencedData(f'hello-{i}-ack'.encode())) + ) + for i in range(1, 10): test_data = f'hello-{i}'.encode() client_session.send_msg(soup.UnSequencedData(test_data)) @@ -93,19 +103,25 @@ async def test__soup_session__able_to_communicate(soup_server_session): client_session.logout() -@pytest.mark.asyncio -async def test__soup_session__sending_debug_from_client(soup_server_session): - port, server_session = soup_server_session +async def test__soup_session__sending_debug_from_client(mock_server_session): + port, server_session = mock_server_session + server_session = configure_login_accept(server_session) client_session = await soup.connect_async( - ('', port), + ('127.0.0.1', port), 'test-u', 'test-p', 'session' ) assert client_session is not None - test_data = 'sending debug msg' + + server_session.when( + matches(soup.Debug(test_data)), 'debug-msg' + ).do( + send(soup.SequencedData(f'{test_data}-ack'.encode())) + ) + client_session.send_debug(test_data) reply = await client_session.receive_msg() assert isinstance(reply, soup.SequencedData) @@ -114,21 +130,28 @@ async def test__soup_session__sending_debug_from_client(soup_server_session): client_session.logout() -@pytest.mark.asyncio -async def test__soup_session__with_dispatcher__dispatcher_invoked(soup_server_session): - port, server_session = soup_server_session - +async def test__soup_session__with_dispatcher__dispatcher_invoked(mock_server_session): + port, server_session = mock_server_session closed = asyncio.Event() + server_session = configure_login_accept(server_session) + received_messages = asyncio.Queue() + burst_chunk = 10 async def on_msg(msg): - if msg.data == b'end': - server_session.end_session() + await received_messages.put(msg) async def on_close(): closed.set() + # method to generate load + def generate_load(number_of_messages): + def action(session, _data): + for i in range(number_of_messages): + session.send(soup.SequencedData(f'msg-{i}'.encode('ascii'))) + return action + client_session = await soup.connect_async( - ('', port), + ('127.0.0.1', port), 'test-u', 'test-p', 'session', @@ -137,9 +160,29 @@ async def on_close(): ) assert client_session is not None - server_session.generate_load(100) + # arm mocks + server_session.when( + matches(soup.Debug('start-burst-traffic')), 'start-burst-traffic' + ).do( + generate_load(burst_chunk) + ) + server_session.when( + matches(soup.Debug('end')), 'end' + ).do( + send(soup.EndOfSession()) + ) + for i in range(1, 5): + # start burst traffic + client_session.send_debug('start-burst-traffic') + for _ in range(burst_chunk): + msg = await received_messages.get() + assert isinstance(msg, soup.SequencedData) + + # end burst traffic + client_session.send_debug('end') await asyncio.wait_for(closed.wait(), 1) + assert client_session.is_closed() def test__soup_session__session_with_no_session_type(): @@ -147,3 +190,63 @@ class BaseSessionType(soup.SoupSession): pass assert BaseSessionType.SessionType is 'base' + + +async def test__soup_session__login_message_server_session(): + server_session = SampleTestSourServerSession() + assert server_session is not None + + await server_session.on_msg_coro( + soup.LoginRequest('test-u', 'test-p', 'session', '1') + ) + assert len(server_session.output_sent) == 1 + assert isinstance(server_session.output_sent[0], LoginAccepted) + + +async def test__soup_session__unsequenced_msg__server_session(): + server_session = SampleTestSourServerSession() + + await server_session.on_msg_coro( + soup.UnSequencedData(b'case1') + ) + assert len(server_session.output_sent) == 1 + assert isinstance(server_session.output_sent[0], soup.SequencedData) + + await server_session.on_msg_coro( + soup.UnSequencedData(b'case2') + ) + assert len(server_session.output_sent) == 2 + assert isinstance(server_session.output_sent[1], soup.SequencedData) + + +async def test__soup_session__send_debug(): + server_session = SampleTestSourServerSession() + await server_session.on_msg_coro( + soup.Debug('hello') + ) + assert len(server_session.output_sent) == 0 + + +async def test__soup_session__send_heartbeat(): + server_session = SampleTestSourServerSession() + await server_session.send_heartbeat() + assert len(server_session.output_sent) == 1 + assert isinstance(server_session.output_sent[0], soup.ServerHeartbeat) + + +async def test__soup_session__end_session(): + server_session = SampleTestSourServerSession() + server_session.end_session() + assert len(server_session.output_sent) == 1 + assert isinstance(server_session.output_sent[0], soup.EndOfSession) + + +async def test__soup_session__send_logout(): + server_session = SampleTestSourServerSession() + assert not server_session.is_closed() + + await server_session.on_msg_coro( + soup.LogoutRequest() + ) + assert len(server_session.output_sent) == 0 + assert server_session.is_closed() \ No newline at end of file