From 7287dcf2fef9a9c5d351e02cce372541e3f08e9e Mon Sep 17 00:00:00 2001 From: Danil Akhtarov Date: Sun, 15 Oct 2023 23:25:59 +0300 Subject: [PATCH] tests: add teamwork tests --- sportorg/gui/dialogs/course_edit.py | 2 +- sportorg/gui/dialogs/group_edit.py | 2 +- sportorg/gui/dialogs/not_start_dialog.py | 2 +- sportorg/gui/dialogs/organization_edit.py | 2 +- sportorg/gui/dialogs/person_edit.py | 2 +- sportorg/gui/dialogs/result_edit.py | 2 +- sportorg/gui/main_window.py | 2 +- sportorg/gui/menu/actions.py | 2 +- sportorg/modules/teamwork/__init__.py | 139 --------------------- sportorg/modules/teamwork/client.py | 22 ++-- sportorg/modules/teamwork/packet_header.py | 20 +-- sportorg/modules/teamwork/server.py | 21 ++-- sportorg/modules/teamwork/teamwork.py | 138 ++++++++++++++++++++ tests/test_teamwork.py | 62 +++++++++ 14 files changed, 246 insertions(+), 172 deletions(-) create mode 100644 sportorg/modules/teamwork/teamwork.py create mode 100644 tests/test_teamwork.py diff --git a/sportorg/gui/dialogs/course_edit.py b/sportorg/gui/dialogs/course_edit.py index 1dcd564b..620681a9 100644 --- a/sportorg/gui/dialogs/course_edit.py +++ b/sportorg/gui/dialogs/course_edit.py @@ -9,7 +9,7 @@ from sportorg.models.result.result_checker import ResultChecker from sportorg.models.result.score_calculation import ScoreCalculation from sportorg.models.result.split_calculation import RaceSplits -from sportorg.modules.teamwork import Teamwork +from sportorg.modules.teamwork.teamwork import Teamwork class CourseEditDialog(BaseDialog): diff --git a/sportorg/gui/dialogs/group_edit.py b/sportorg/gui/dialogs/group_edit.py index d5c785a3..e155c129 100644 --- a/sportorg/gui/dialogs/group_edit.py +++ b/sportorg/gui/dialogs/group_edit.py @@ -16,7 +16,7 @@ from sportorg.models.memory import Limit, RaceType, find, race from sportorg.models.result.result_calculation import ResultCalculation from sportorg.modules.live.live import live_client -from sportorg.modules.teamwork import Teamwork +from sportorg.modules.teamwork.teamwork import Teamwork class GroupEditDialog(BaseDialog): diff --git a/sportorg/gui/dialogs/not_start_dialog.py b/sportorg/gui/dialogs/not_start_dialog.py index 594da4e5..e0a6a9f7 100644 --- a/sportorg/gui/dialogs/not_start_dialog.py +++ b/sportorg/gui/dialogs/not_start_dialog.py @@ -11,7 +11,7 @@ from sportorg.models.memory import ResultManual, ResultStatus, find, race from sportorg.models.result.result_calculation import ResultCalculation from sportorg.modules.live.live import live_client -from sportorg.modules.teamwork import Teamwork +from sportorg.modules.teamwork.teamwork import Teamwork class NotStartDialog(QDialog): diff --git a/sportorg/gui/dialogs/organization_edit.py b/sportorg/gui/dialogs/organization_edit.py index 7ce2ee27..e816be64 100644 --- a/sportorg/gui/dialogs/organization_edit.py +++ b/sportorg/gui/dialogs/organization_edit.py @@ -4,7 +4,7 @@ from sportorg.models.constant import get_countries, get_regions from sportorg.models.memory import find, race from sportorg.modules.live.live import live_client -from sportorg.modules.teamwork import Teamwork +from sportorg.modules.teamwork.teamwork import Teamwork class OrganizationEditDialog(BaseDialog): diff --git a/sportorg/gui/dialogs/person_edit.py b/sportorg/gui/dialogs/person_edit.py index 34dfa731..38b9a0ef 100644 --- a/sportorg/gui/dialogs/person_edit.py +++ b/sportorg/gui/dialogs/person_edit.py @@ -18,7 +18,7 @@ from sportorg.models.result.result_calculation import ResultCalculation from sportorg.modules.configs.configs import Config from sportorg.modules.live.live import live_client -from sportorg.modules.teamwork import Teamwork +from sportorg.modules.teamwork.teamwork import Teamwork class PersonEditDialog(BaseDialog): diff --git a/sportorg/gui/dialogs/result_edit.py b/sportorg/gui/dialogs/result_edit.py index 7190ab1a..23fb4f71 100644 --- a/sportorg/gui/dialogs/result_edit.py +++ b/sportorg/gui/dialogs/result_edit.py @@ -29,7 +29,7 @@ from sportorg.models.result.result_checker import ResultChecker, ResultCheckerException from sportorg.models.result.split_calculation import GroupSplits from sportorg.modules.live.live import live_client -from sportorg.modules.teamwork import Teamwork +from sportorg.modules.teamwork.teamwork import Teamwork from sportorg.utils.time import hhmmss_to_time diff --git a/sportorg/gui/main_window.py b/sportorg/gui/main_window.py index da2f5097..7d5a196d 100644 --- a/sportorg/gui/main_window.py +++ b/sportorg/gui/main_window.py @@ -56,8 +56,8 @@ from sportorg.modules.sportident.sireader import SIReaderClient from sportorg.modules.sportiduino.sportiduino import SportiduinoClient from sportorg.modules.srpid.srpid import SrpidClient -from sportorg.modules.teamwork import Teamwork from sportorg.modules.teamwork.packet_header import ObjectTypes +from sportorg.modules.teamwork.teamwork import Teamwork from sportorg.modules.telegram.telegram import telegram_client diff --git a/sportorg/gui/menu/actions.py b/sportorg/gui/menu/actions.py index c3a62f99..024fa560 100644 --- a/sportorg/gui/menu/actions.py +++ b/sportorg/gui/menu/actions.py @@ -64,7 +64,7 @@ from sportorg.modules.sportident.sireader import SIReaderClient from sportorg.modules.sportiduino.sportiduino import SportiduinoClient from sportorg.modules.srpid.srpid import SrpidClient -from sportorg.modules.teamwork import Teamwork +from sportorg.modules.teamwork.teamwork import Teamwork from sportorg.modules.telegram.telegram import telegram_client from sportorg.modules.updater import checker from sportorg.modules.winorient import winorient diff --git a/sportorg/modules/teamwork/__init__.py b/sportorg/modules/teamwork/__init__.py index e53e2061..e69de29b 100644 --- a/sportorg/modules/teamwork/__init__.py +++ b/sportorg/modules/teamwork/__init__.py @@ -1,139 +0,0 @@ -import logging -from queue import Empty, Queue -from threading import Event, main_thread - -from PySide2.QtCore import QThread, Signal - -from sportorg.common.broker import Broker -from sportorg.common.singleton import singleton - -from .client import ClientThread -from .packet_header import Operations -from .server import Command, ServerThread - - -class ResultThread(QThread): - data_sender = Signal(object) - - def __init__(self, queue, stop_event, logger=None): - super().__init__() - # self.setName(self.__class__.__name__) - self._queue = queue - self._stop_event = stop_event - self._logger = logger - - def run(self): - self._logger.debug('Teamwork result start') - while True: - try: - cmd = self._queue.get(timeout=5) - self.data_sender.emit(cmd) - - except Empty: - if not main_thread().is_alive() or self._stop_event.is_set(): - break - except Exception as e: - self._logger.debug(str(e)) - self._logger.debug('Teamwork result shutdown') - - -@singleton -class Teamwork: - def __init__(self): - self._in_queue = Queue() - self._out_queue = Queue() - self._stop_event = Event() - self.factory = {'client': ClientThread, 'server': ServerThread} - self._thread = None - self._result_thread = None - self._call_back = None - self._logger = logging.root - - self.host = '' - self.port = 50010 - self.token = '' - self.connection_type = 'client' - - def set_call(self, value): - if self._call_back is None: - self._call_back = value - return self - - def set_options(self, host, port, token, connection_type): - self.host = host - self.port = port - self.token = token - self.connection_type = connection_type - - def _start_thread(self): - if self.connection_type not in self.factory.keys(): - return - if self._thread is None: - self._thread = self.factory[self.connection_type]( - (self.host, self.port), - self._in_queue, - self._out_queue, - self._stop_event, - self._logger, - ) - self._thread.start() - elif not self._thread.is_alive(): - self._thread = None - self._start_thread() - - def _start_result_thread(self): - if self._result_thread is None: - self._result_thread = ResultThread( - self._out_queue, self._stop_event, self._logger - ) - if self._call_back is not None: - self._result_thread.data_sender.connect(self._call_back) - self._result_thread.start() - # elif not self._result_thread.is_alive(): - elif self._result_thread.isFinished(): - self._result_thread = None - self._start_result_thread() - - def is_alive(self): - return ( - self._thread is not None - and self._thread.is_alive() - and self._result_thread is not None - and not self._result_thread.isFinished() - ) - - def stop(self): - self._stop_event.set() - - def start(self): - self._stop_event.clear() - self._in_queue.queue.clear() - self._out_queue.queue.clear() - - self._start_thread() - self._start_result_thread() - - def toggle(self): - if self.is_alive(): - self.stop() - self._logger.info('{} stopping'.format(self.connection_type.upper())) - else: - self.start() - self._logger.info('{} starting'.format(self.connection_type.upper())) - - def send(self, data, op=Operations.Update.name): - """data is Dict or List[Dict]""" - if self.is_alive(): - Broker().produce('teamwork_sending', data) - if isinstance(data, list): - for item in data: - self._in_queue.put(Command(item, op)) - return - - self._in_queue.put(Command(data, op)) - - def delete(self, data): - """data is Dict or List[Dict]""" - Broker().produce('teamwork_deleting', data) - if self.is_alive(): - pass diff --git a/sportorg/modules/teamwork/client.py b/sportorg/modules/teamwork/client.py index 658eb66e..32001538 100644 --- a/sportorg/modules/teamwork/client.py +++ b/sportorg/modules/teamwork/client.py @@ -1,14 +1,15 @@ -import json import queue import socket -from threading import Thread, main_thread +from threading import Event, Thread, main_thread + +import orjson from .packet_header import Header, Operations from .server import Command class ClientSenderThread(Thread): - def __init__(self, conn, in_queue, stop_event, logger=None): + def __init__(self, conn, in_queue, stop_event, logger): super().__init__(daemon=True) self.setName(self.__class__.__name__) self.conn = conn @@ -37,7 +38,7 @@ def run(self): class ClientReceiverThread(Thread): - def __init__(self, conn, out_queue, stop_event, logger=None): + def __init__(self, conn, out_queue, stop_event, logger): super().__init__() self.setName(self.__class__.__name__) self.conn = conn @@ -58,13 +59,11 @@ def run(self): if not data: break full_data += data - # self._logger.debug('Client got data: {}'.format(full_data)) while True: # getting Header if is_new_pack: if len(full_data) >= hdr.header_size: hdr.unpack_header(full_data[: hdr.header_size]) - # self._logger.debug('Client Packet Header: {}, ver: {}, size: {}'.format(full_data[:hdr.header_size], hdr.version, hdr.size)) full_data = full_data[hdr.header_size :] is_new_pack = False else: @@ -73,7 +72,7 @@ def run(self): else: if len(full_data) >= hdr.size: command = Command( - json.loads(full_data[: hdr.size].decode()), + orjson.loads(full_data[: hdr.size].decode()), Operations(hdr.opType).name, ) self._out_queue.put(command) # for local @@ -98,7 +97,7 @@ def run(self): class ClientThread(Thread): - def __init__(self, addr, in_queue, out_queue, stop_event, logger=None): + def __init__(self, addr, in_queue, out_queue, stop_event, logger): super().__init__() self.setName(self.__class__.__name__) self.addr = addr @@ -106,8 +105,12 @@ def __init__(self, addr, in_queue, out_queue, stop_event, logger=None): self._out_queue = out_queue self._stop_event = stop_event self._logger = logger + self._client_started = Event() - def run(self): + def join_client(self) -> None: + self._client_started.wait() + + def run(self) -> None: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: try: s.connect(self.addr) @@ -120,6 +123,7 @@ def run(self): s, self._out_queue, self._stop_event, self._logger ) receiver.start() + self._client_started.set() sender.join() receiver.join() diff --git a/sportorg/modules/teamwork/packet_header.py b/sportorg/modules/teamwork/packet_header.py index 6ee4c14c..268bac0b 100644 --- a/sportorg/modules/teamwork/packet_header.py +++ b/sportorg/modules/teamwork/packet_header.py @@ -40,22 +40,25 @@ def __repr__(self): """ - obj_type = ObjectTypes['Race'] - obj_type.value - - op = Operations['Create'] - - >>> Operations(0).name +```python +obj_type = ObjectTypes['Race'] +obj_type.value + +op = Operations['Create'] + +Operations(0).name 'Create' +``` """ """ +``` ?: boolean H: Unsigned short L: unsigned long i: int f: float -Q: Unsigned long long int +Q: Unsigned long long int s: bytes string Header Tag: 2 Bytes @@ -66,6 +69,7 @@ def __repr__(self): size: 8 Bytes (Unsigned Long long) Total Header Size = 56 Bytes +``` """ @@ -120,7 +124,7 @@ def prepare_header(self, obj_data, op_type): try: self.objType = ObjectTypes[obj_type].value - except: + except Exception: self.objType = 255 # Unknown self.uuid = obj_uuid diff --git a/sportorg/modules/teamwork/server.py b/sportorg/modules/teamwork/server.py index 689c098c..8826772f 100644 --- a/sportorg/modules/teamwork/server.py +++ b/sportorg/modules/teamwork/server.py @@ -14,13 +14,15 @@ def __init__(self, data=None, op=Operations.Update.name, addr=None): self.addr_exclude = [] self.next_cmd_obj_type = ObjectTypes.Unknown.value + def __repr__(self) -> str: + return str(self.data) + def exclude(self, addr): self.addr_exclude.append(addr) return self def get_packet(self): pack_data = json.dumps(self.data).encode() - # logging.debug('Command->get_header: Header: {}, Pack_data: {}'.format( self.header.pack_header(len(pack_data)), pack_data)) return self.header.pack_header(len(pack_data)) + pack_data @@ -38,9 +40,8 @@ def is_alive(self): class ServerReceiverThread(Thread): - def __init__(self, conn, in_queue, out_queue, stop_event, logger=None): + def __init__(self, conn, in_queue, out_queue, stop_event, logger): super().__init__(daemon=True) - # self.setName(self.__class__.__name__) self.connect = conn self._in_queue = in_queue self._out_queue = out_queue @@ -101,7 +102,7 @@ def run(self): class ServerSenderThread(Thread): - def __init__(self, in_queue, connections_queue, stop_event, logger=None): + def __init__(self, in_queue, connections_queue, stop_event, logger): super().__init__(daemon=True) self.setName(self.__class__.__name__) self._connections_queue = connections_queue @@ -115,7 +116,6 @@ def run(self): while True: try: command = self._in_queue.get(timeout=5) - # self._logger.debug('Server sender: Got new command {}'.format(command)) for connect in self._connections: try: if ( @@ -141,7 +141,7 @@ def run(self): class ServerThread(Thread): - def __init__(self, addr, in_queue, out_queue, stop_event, logger=None): + def __init__(self, addr, in_queue, out_queue, stop_event, logger): super().__init__(daemon=True) self.setName(self.__class__.__name__) self.addr = addr @@ -149,8 +149,12 @@ def __init__(self, addr, in_queue, out_queue, stop_event, logger=None): self._out_queue = out_queue self._stop_event = stop_event self._logger = logger + self._server_started = Event() - def run(self): + def join_server(self) -> None: + self._server_started.wait() + + def run(self) -> None: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -164,11 +168,12 @@ def run(self): self._logger.info('Server start') - conns_queue = Queue() + conns_queue = Queue() # type: ignore sender = ServerSenderThread( self._in_queue, conns_queue, self._stop_event, self._logger ) sender.start() + self._server_started.set() connections = [] diff --git a/sportorg/modules/teamwork/teamwork.py b/sportorg/modules/teamwork/teamwork.py new file mode 100644 index 00000000..fef0d01a --- /dev/null +++ b/sportorg/modules/teamwork/teamwork.py @@ -0,0 +1,138 @@ +import logging +from queue import Empty, Queue +from threading import Event, main_thread + +from PySide2.QtCore import QThread, Signal + +from sportorg.common.broker import Broker +from sportorg.common.singleton import singleton + +from .client import ClientThread +from .packet_header import Operations +from .server import Command, ServerThread + + +class ResultThread(QThread): + data_sender = Signal(object) + + def __init__(self, queue, stop_event, logger=None): + super().__init__() + self._queue = queue + self._stop_event = stop_event + self._logger = logger + + def run(self): + self._logger.debug('Teamwork result start') + while True: + try: + cmd = self._queue.get(timeout=5) + self.data_sender.emit(cmd) + + except Empty: + if not main_thread().is_alive() or self._stop_event.is_set(): + break + except Exception as e: + self._logger.debug(str(e)) + self._logger.debug('Teamwork result shutdown') + + +@singleton +class Teamwork: + def __init__(self): + self._in_queue = Queue() + self._out_queue = Queue() + self._stop_event = Event() + self.factory = {'client': ClientThread, 'server': ServerThread} + self._thread = None + self._result_thread = None + self._call_back = None + self._logger = logging.root + + self.host = '' + self.port = 50010 + self.token = '' + self.connection_type = 'client' + + def set_call(self, value): + if self._call_back is None: + self._call_back = value + return self + + def set_options(self, host, port, token, connection_type): + self.host = host + self.port = port + self.token = token + self.connection_type = connection_type + + def _start_thread(self): + if self.connection_type not in self.factory.keys(): + return + if self._thread is None: + self._thread = self.factory[self.connection_type]( + (self.host, self.port), + self._in_queue, + self._out_queue, + self._stop_event, + self._logger, + ) + self._thread.start() + elif not self._thread.is_alive(): + self._thread = None + self._start_thread() + + def _start_result_thread(self): + if self._result_thread is None: + self._result_thread = ResultThread( + self._out_queue, self._stop_event, self._logger + ) + if self._call_back is not None: + self._result_thread.data_sender.connect(self._call_back) + self._result_thread.start() + # elif not self._result_thread.is_alive(): + elif self._result_thread.isFinished(): + self._result_thread = None + self._start_result_thread() + + def is_alive(self): + return ( + self._thread is not None + and self._thread.is_alive() + and self._result_thread is not None + and not self._result_thread.isFinished() + ) + + def stop(self): + self._stop_event.set() + + def start(self): + self._stop_event.clear() + self._in_queue.queue.clear() + self._out_queue.queue.clear() + + self._start_thread() + self._start_result_thread() + + def toggle(self): + if self.is_alive(): + self.stop() + self._logger.info('{} stopping'.format(self.connection_type.upper())) + else: + self.start() + self._logger.info('{} starting'.format(self.connection_type.upper())) + + def send(self, data, op=Operations.Update.name): + """data is Dict or List[Dict]""" + if self.is_alive(): + Broker().produce('teamwork_sending', data) + if isinstance(data, list): + for item in data: + self._in_queue.put(Command(item, op)) + return + + self._in_queue.put(Command(data, op)) + + def delete(self, data): + """data is Dict or List[Dict]""" + Broker().produce('teamwork_deleting', data) + if self.is_alive(): + pass diff --git a/tests/test_teamwork.py b/tests/test_teamwork.py new file mode 100644 index 00000000..bfb4ed03 --- /dev/null +++ b/tests/test_teamwork.py @@ -0,0 +1,62 @@ +import logging +import time +from queue import Queue +from threading import Event + +from sportorg.modules.teamwork.client import ClientThread +from sportorg.modules.teamwork.server import Command, ServerThread + + +def test_teamwork(): + in_queue = Queue() + out_queue = Queue() + event = Event() + server = ServerThread(('0.0.0.0', 50010), in_queue, out_queue, event, logging.root) + server.start() + server.join_server() + client_in_queue = Queue() + client_out_queue = Queue() + client = ClientThread( + ('localhost', 50010), client_in_queue, client_out_queue, event, logging.root + ) + client.start() + client.join_client() + time.sleep(5) + + in_queue.put( + Command( + { + 'object': 'Person', + 'id': 'c24eef6c-a33b-4581-a6d1-78294711aef1', + 'name': 'Danil', + }, + 'Create', + ) + ) + result = client_out_queue.get(timeout=5) + assert result.data == { + 'object': 'Person', + 'id': 'c24eef6c-a33b-4581-a6d1-78294711aef1', + 'name': 'Danil', + } + + client_in_queue.put( + Command( + { + 'object': 'Person', + 'id': 'c24eef6c-a33b-4581-a6d1-78294711aef1', + 'name': 'Danil', + }, + 'Create', + ) + ) + result = out_queue.get(timeout=5) + assert result.data == { + 'object': 'Person', + 'id': 'c24eef6c-a33b-4581-a6d1-78294711aef1', + 'name': 'Danil', + } + + event.set() + server.join() + client.join()