diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..16281fe --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1 @@ +from .utils import * diff --git a/examples/cameraDiscoveryExample.py b/examples/cameraDiscoveryExample.py new file mode 100644 index 0000000..8931d57 --- /dev/null +++ b/examples/cameraDiscoveryExample.py @@ -0,0 +1,39 @@ +from context_logger import get_logger, setup_logging + +from examples import setup_shutdown +from hello import Hello, Group, ServiceQuery, DiscoveryEvent + +setup_logging('hello') + +log = get_logger('CameraDiscovery') + + +def main() -> None: + shutdown_event = setup_shutdown() + + # Define the group to discover camera services + group = Group(name='effectiverange/sniper', url='udp://239.0.1.1:5555') + + # Define the query to discover camera services + query = ServiceQuery(name='.+', role='camera') + + # Use a discoverer to find camera services + with Hello.builder().discoverer().default() as discoverer: + # Define an event handler to process discovery events + def process_event(event: DiscoveryEvent) -> None: + log.info('Service discovery event', type=event.type.name, service=event.service) + + # Register the event handler to process discovery events + discoverer.register(process_event) + + # Start the discoverer with the specified group + discoverer.start(group) + + # Send the service query + discoverer.discover(query) + + shutdown_event.wait() + + +if __name__ == '__main__': + main() diff --git a/examples/cameraServiceExample.py b/examples/cameraServiceExample.py new file mode 100644 index 0000000..f43ba09 --- /dev/null +++ b/examples/cameraServiceExample.py @@ -0,0 +1,38 @@ +from context_logger import get_logger, setup_logging + +from examples import setup_shutdown +from hello import ServiceInfo, Hello, Group + +setup_logging('hello') + +log = get_logger('CameraService') + + +def main() -> None: + shutdown_event = setup_shutdown() + + # Define the group to advertise the camera service + group = Group(name='effectiverange/sniper', url='udp://239.0.1.1:5555') + + # Define the service information for the camera + info = ServiceInfo(name='er-sniper-camera-1', role='camera', urls={ + 'device-api': 'grpc://er-sniper-camera-1/device', + 'video-stream': 'blob:http://er-sniper-camera-1/video' + }) + + # Use a scheduled advertizer to periodically announce the camera service + with Hello.builder().advertizer().scheduled() as advertizer: + # Start the advertizer with the specified group + advertizer.start(group) + + # Immediately advertise the service information + advertizer.advertise(info) + + # Schedule periodic advertisements every 10 seconds + advertizer.schedule(interval=10) + + shutdown_event.wait() + + +if __name__ == '__main__': + main() diff --git a/examples/utils.py b/examples/utils.py new file mode 100644 index 0000000..b2512cd --- /dev/null +++ b/examples/utils.py @@ -0,0 +1,15 @@ +from signal import signal, SIGINT, SIGTERM +from threading import Event +from typing import Any + + +def setup_shutdown() -> Event: + shutdown_event = Event() + + def handler(signum: int, frame: Any) -> None: + shutdown_event.set() + + signal(SIGINT, handler) + signal(SIGTERM, handler) + + return shutdown_event diff --git a/hello/__init__.py b/hello/__init__.py index c360ae2..b4ec81d 100644 --- a/hello/__init__.py +++ b/hello/__init__.py @@ -1,6 +1,7 @@ from .group import * from .sender import * from .receiver import * +from .scheduler import * from .service import * from .advertizer import * from .discoverer import * diff --git a/hello/advertizer.py b/hello/advertizer.py index 13e2bea..f2fe6b5 100644 --- a/hello/advertizer.py +++ b/hello/advertizer.py @@ -5,14 +5,14 @@ from common_utility import IReusableTimer from context_logger import get_logger -from hello import ServiceInfo, Group, Sender, GroupAccess, Receiver, ServiceMatcher, ServiceQuery +from hello import ServiceInfo, Group, Sender, Receiver, ServiceMatcher, ServiceQuery, DefaultScheduler log = get_logger('Advertizer') class Advertizer: - def start(self, address: str, group: Group, info: ServiceInfo | None = None) -> None: + def start(self, group: Group, info: ServiceInfo | None = None) -> None: raise NotImplementedError() def stop(self) -> None: @@ -35,8 +35,8 @@ def __enter__(self) -> Advertizer: def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.stop() - def start(self, address: str, group: Group, info: ServiceInfo | None = None) -> None: - self._sender.start(GroupAccess(address, group.hello())) + def start(self, group: Group, info: ServiceInfo | None = None) -> None: + self._sender.start(group.hello()) self._group = group self._info = info @@ -62,9 +62,9 @@ def __init__(self, sender: Sender, receiver: Receiver, max_response_delay: float self._receiver = receiver self._max_delay = max_response_delay - def start(self, address: str, group: Group, info: ServiceInfo | None = None) -> None: - super().start(address, group, info) - self._receiver.start(GroupAccess(address, group.query())) + def start(self, group: Group, info: ServiceInfo | None = None) -> None: + super().start(group, info) + self._receiver.start(group.query()) self._receiver.register(self._handle_message) def stop(self) -> None: @@ -89,42 +89,27 @@ def _handle_query(self, query: ServiceQuery, info: ServiceInfo) -> None: self.advertise(info) -class ScheduledAdvertizer(Advertizer): - - def schedule(self, info: ServiceInfo | None = None, interval: float = 10, one_shot: bool = False) -> None: - raise NotImplementedError() - - -class DefaultScheduledAdvertizer(ScheduledAdvertizer): +class ScheduledAdvertizer(DefaultScheduler[ServiceInfo], Advertizer): def __init__(self, advertizer: Advertizer, timer: IReusableTimer) -> None: + super().__init__(timer) self._advertizer = advertizer - self._timer = timer - def __enter__(self) -> ScheduledAdvertizer: + def __enter__(self) -> 'ScheduledAdvertizer': return self def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.stop() - def start(self, address: str, group: Group, info: ServiceInfo | None = None) -> None: - self._advertizer.start(address, group, info) + def start(self, group: Group, info: ServiceInfo | None = None) -> None: + self._advertizer.start(group, info) def stop(self) -> None: - self._timer.cancel() + super().stop() self._advertizer.stop() def advertise(self, info: ServiceInfo | None = None) -> None: self._advertizer.advertise(info) - def schedule(self, info: ServiceInfo | None = None, interval: float = 60, one_shot: bool = False) -> None: - if one_shot: - self._timer.start(interval, self.advertise, [info]) - log.info('One-shot service advertisement scheduled', service=info, interval=interval) - else: - self._timer.start(interval, self._advertise_and_restart, [info]) - log.info('Periodic service advertisement scheduled', service=info, interval=interval) - - def _advertise_and_restart(self, info: ServiceInfo | None = None) -> None: + def _execute(self, info: ServiceInfo | None = None) -> None: self.advertise(info) - self._timer.restart() diff --git a/hello/api.py b/hello/api.py index 57fc5c9..741fbb2 100644 --- a/hello/api.py +++ b/hello/api.py @@ -1,44 +1,85 @@ +from dataclasses import dataclass from typing import Any from common_utility import ReusableTimer from zmq import Context -from hello import Advertizer, Discoverer, RadioSender, DishReceiver, DefaultAdvertizer, DefaultDiscoverer, \ - ScheduledAdvertizer, RespondingAdvertizer, DefaultScheduledAdvertizer +from hello import RadioSender, DishReceiver, DefaultAdvertizer, DefaultDiscoverer, \ + RespondingAdvertizer, ScheduledAdvertizer, ScheduledDiscoverer -class Hello: +@dataclass +class HelloConfig: + context: Context[Any] = Context() + receiver_max_workers: int = 1 + receiver_poll_timeout: float = 0.1 + advertizer_responder: bool = True + advertizer_max_delay: float = 0.1 - def default_advertizer(self, respond: bool = True, delay: float = 0.1) -> Advertizer: - raise NotImplementedError() - def scheduled_advertizer(self, respond: bool = True, delay: float = 0.1) -> ScheduledAdvertizer: - raise NotImplementedError() +class Hello(object): - def discoverer(self) -> Discoverer: - raise NotImplementedError() + @classmethod + def default_advertizer(cls, config: HelloConfig) -> DefaultAdvertizer: + sender = RadioSender(config.context) + if config.advertizer_responder: + receiver = DishReceiver(config.context, config.receiver_max_workers, config.receiver_poll_timeout) + return RespondingAdvertizer(sender, receiver, config.advertizer_max_delay) + else: + return DefaultAdvertizer(sender) + @classmethod + def scheduled_advertizer(cls, config: HelloConfig) -> ScheduledAdvertizer: + advertizer = cls.default_advertizer(config) + return ScheduledAdvertizer(advertizer, ReusableTimer()) -class DefaultHello(Hello): + @classmethod + def default_discoverer(cls, config: HelloConfig) -> DefaultDiscoverer: + sender = RadioSender(config.context) + receiver = DishReceiver(config.context, config.receiver_max_workers, config.receiver_poll_timeout) + return DefaultDiscoverer(sender, receiver) - def __init__(self, context: Context[Any] | None = None, max_workers: int = 1, poll_timeout: float = 0.1) -> None: - self._context = context if context else Context() - self._max_workers = max_workers - self._poll_timeout = poll_timeout + @classmethod + def scheduled_discoverer(cls, config: HelloConfig) -> ScheduledDiscoverer: + discoverer = cls.default_discoverer(config) + return ScheduledDiscoverer(discoverer, ReusableTimer()) - def default_advertizer(self, respond: bool = True, delay: float = 0.1) -> Advertizer: - sender = RadioSender(self._context) - if respond: - receiver = DishReceiver(self._context, self._max_workers, self._poll_timeout) - return RespondingAdvertizer(sender, receiver, delay) - else: - return DefaultAdvertizer(sender) + @classmethod + def builder(cls, config: HelloConfig | None = None) -> 'HelloBuilder': + return HelloBuilder(config if config else HelloConfig()) - def scheduled_advertizer(self, respond: bool = True, delay: float = 0.1) -> ScheduledAdvertizer: - advertizer = self.default_advertizer(respond, delay) - return DefaultScheduledAdvertizer(advertizer, ReusableTimer()) - def discoverer(self) -> Discoverer: - sender = RadioSender(self._context) - receiver = DishReceiver(self._context, self._max_workers, self._poll_timeout) - return DefaultDiscoverer(sender, receiver) +class AdvertizerBuilder(object): + + def __init__(self, config: HelloConfig) -> None: + self._config = config + + def default(self) -> DefaultAdvertizer: + return Hello.default_advertizer(self._config) + + def scheduled(self) -> ScheduledAdvertizer: + return Hello.scheduled_advertizer(self._config) + + +class DiscovererBuilder(object): + + def __init__(self, config: HelloConfig) -> None: + self._config = config + + def default(self) -> DefaultDiscoverer: + return Hello.default_discoverer(self._config) + + def scheduled(self) -> ScheduledDiscoverer: + return Hello.scheduled_discoverer(self._config) + + +class HelloBuilder(object): + + def __init__(self, config: HelloConfig) -> None: + self._config = config + + def advertizer(self) -> AdvertizerBuilder: + return AdvertizerBuilder(self._config) + + def discoverer(self) -> DiscovererBuilder: + return DiscovererBuilder(self._config) diff --git a/hello/discoverer.py b/hello/discoverer.py index 192f644..449e89f 100644 --- a/hello/discoverer.py +++ b/hello/discoverer.py @@ -2,9 +2,10 @@ from enum import Enum from typing import Any, Protocol +from common_utility import IReusableTimer from context_logger import get_logger -from hello import Group, ServiceQuery, Sender, Receiver, GroupAccess, ServiceInfo, ServiceMatcher +from hello import Group, ServiceQuery, Sender, Receiver, ServiceInfo, ServiceMatcher, DefaultScheduler log = get_logger('Discoverer') @@ -26,7 +27,7 @@ def __call__(self, event: DiscoveryEvent) -> None: ... class Discoverer: - def start(self, address: str, group: Group, query: ServiceQuery | None = None) -> None: + def start(self, group: Group, query: ServiceQuery | None = None) -> None: raise NotImplementedError() def stop(self) -> None: @@ -64,13 +65,13 @@ def __enter__(self) -> Discoverer: def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.stop() - def start(self, address: str, group: Group, query: ServiceQuery | None = None) -> None: + def start(self, group: Group, query: ServiceQuery | None = None) -> None: self._group = group if query: self._matcher = ServiceMatcher(query) - self._sender.start(GroupAccess(address, group.query())) + self._sender.start(group.query()) self._receiver.register(self._handle_message) - self._receiver.start(GroupAccess(address, group.hello())) + self._receiver.start(group.hello()) def stop(self) -> None: self._group = None @@ -132,3 +133,41 @@ def _handle_event(self, event: DiscoveryEvent) -> None: callback(event) except Exception as error: log.warn('Error in event handler execution', event=event, error=error) + + +class ScheduledDiscoverer(DefaultScheduler[ServiceQuery], Discoverer): + + def __init__(self, discoverer: Discoverer, timer: IReusableTimer) -> None: + super().__init__(timer) + self._discoverer = discoverer + + def __enter__(self) -> 'ScheduledDiscoverer': + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.stop() + + def start(self, group: Group, query: ServiceQuery | None = None) -> None: + self._discoverer.start(group, query) + + def stop(self) -> None: + super().stop() + self._discoverer.stop() + + def discover(self, query: ServiceQuery | None = None) -> None: + self._discoverer.discover(query) + + def get_services(self) -> dict[str, ServiceInfo]: + return self._discoverer.get_services() + + def register(self, handler: OnDiscoveryEvent) -> None: + self._discoverer.register(handler) + + def deregister(self, handler: OnDiscoveryEvent) -> None: + self._discoverer.deregister(handler) + + def get_handlers(self) -> list[OnDiscoveryEvent]: + return self._discoverer.get_handlers() + + def _execute(self, query: ServiceQuery | None = None) -> None: + self.discover(query) diff --git a/hello/group.py b/hello/group.py index 95b0654..4f38913 100644 --- a/hello/group.py +++ b/hello/group.py @@ -7,33 +7,27 @@ class GroupPrefix(Enum): QUERY = 'query' -class IGroup: - - def hello(self) -> str: - raise NotImplementedError() - - def query(self) -> str: - raise NotImplementedError() - - -class Group(IGroup): - def __init__(self, name: str) -> None: - self.name = name +@dataclass +class Group: + name: str + url: str - def hello(self) -> str: - return self._prefix(GroupPrefix.HELLO) + def hello(self) -> 'PrefixedGroup': + return PrefixedGroup(self, GroupPrefix.HELLO) - def query(self) -> str: - return self._prefix(GroupPrefix.QUERY) + def query(self) -> 'PrefixedGroup': + return PrefixedGroup(self, GroupPrefix.QUERY) - def _prefix(self, group_type: GroupPrefix) -> str: - return f'{group_type.value}:{self.name}' - def __repr__(self) -> str: - return self.name +@dataclass +class PrefixedGroup: + group: Group + prefix: GroupPrefix + @property + def name(self) -> str: + return f'{self.prefix.value}:{self.group.name}' -@dataclass -class GroupAccess: - access_url: str - full_group: str + @property + def url(self) -> str: + return self.group.url diff --git a/hello/receiver.py b/hello/receiver.py index b9e0068..61ed167 100644 --- a/hello/receiver.py +++ b/hello/receiver.py @@ -4,7 +4,7 @@ from context_logger import get_logger from zmq import DISH, Poller, POLLIN, Context -from hello import GroupAccess +from hello import PrefixedGroup log = get_logger('Receiver') @@ -15,7 +15,7 @@ def __call__(self, message: dict[str, Any]) -> None: ... class Receiver: - def start(self, source: GroupAccess) -> None: + def start(self, group: PrefixedGroup) -> None: raise NotImplementedError() def stop(self) -> None: @@ -48,18 +48,18 @@ def __enter__(self) -> Receiver: def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.stop() - def start(self, source: GroupAccess) -> None: + def start(self, group: PrefixedGroup) -> None: try: if self._group: raise RuntimeError('Receiver already started') self._poller.register(self._dish, POLLIN) - self._dish.bind(source.access_url) - self._dish.join(source.full_group) - self._group = source.full_group + self._dish.bind(group.url) + self._dish.join(group.name) + self._group = group.name self._executor.submit(self._receive_loop) - log.debug('Receiver started', address=source.access_url, group=source.full_group) + log.debug('Receiver started', url=group.url, group=group.name) except Exception as error: - log.error('Failed to start receiver', address=source.access_url, group=source.full_group, error=error) + log.error('Failed to start receiver', url=group.url, group=group.name, error=error) raise error def stop(self) -> None: diff --git a/hello/scheduler.py b/hello/scheduler.py new file mode 100644 index 0000000..5cfd16c --- /dev/null +++ b/hello/scheduler.py @@ -0,0 +1,47 @@ +from typing import TypeVar, Generic, Any + +from common_utility import IReusableTimer +from context_logger import get_logger + +log = get_logger('Scheduler') + +T = TypeVar('T') + + +class Scheduler(Generic[T]): + + def stop(self) -> None: + raise NotImplementedError() + + def schedule(self, data: T | None = None, interval: float = 60, one_shot: bool = False) -> None: + raise NotImplementedError() + + +class DefaultScheduler(Scheduler[T]): + + def __init__(self, timer: IReusableTimer) -> None: + self._timer = timer + + def __enter__(self) -> Scheduler[T]: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.stop() + + def stop(self) -> None: + self._timer.cancel() + + def schedule(self, data: T | None = None, interval: float = 60, one_shot: bool = False) -> None: + if one_shot: + self._timer.start(interval, self._execute, [data]) + log.info('One-shot execution scheduled', data=data, interval=interval) + else: + self._timer.start(interval, self._execute_and_restart, [data]) + log.info('Periodic execution scheduled', data=data, interval=interval) + + def _execute(self, data: T | None = None) -> None: + raise NotImplementedError() + + def _execute_and_restart(self, data: T | None = None) -> None: + self._execute(data) + self._timer.restart() diff --git a/hello/sender.py b/hello/sender.py index 57574e5..1b4b03e 100644 --- a/hello/sender.py +++ b/hello/sender.py @@ -3,14 +3,14 @@ from context_logger import get_logger from zmq import Context, RADIO, Socket -from hello import GroupAccess +from hello import PrefixedGroup log = get_logger('Sender') class Sender: - def start(self, target: GroupAccess) -> None: + def start(self, group: PrefixedGroup) -> None: raise NotImplementedError() def stop(self) -> None: @@ -33,15 +33,15 @@ def __enter__(self) -> Sender: def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: self.stop() - def start(self, target: GroupAccess) -> None: + def start(self, group: PrefixedGroup) -> None: try: if self._group: raise RuntimeError('Sender already started') - self._radio.connect(target.access_url) - self._group = target.full_group - log.debug('Sender started', address=target.access_url, group=target.full_group) + self._radio.connect(group.url) + self._group = group.name + log.debug('Sender started', url=group.url, group=group.name) except Exception as error: - log.error('Failed to start sender', address=target.access_url, group=target.full_group, error=error) + log.error('Failed to start sender', url=group.url, group=group.name, error=error) raise error def stop(self) -> None: diff --git a/setup.cfg b/setup.cfg index ce39c45..124be19 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ packaging = fpm-deb [mypy] -packages = hello +packages = hello, examples strict = True [flake8] diff --git a/tests/advertizerIntegrationTest.py b/tests/advertizerIntegrationTest.py index 74554da..b19ea99 100644 --- a/tests/advertizerIntegrationTest.py +++ b/tests/advertizerIntegrationTest.py @@ -6,12 +6,10 @@ from test_utility import wait_for_assertion from zmq import Context -from hello import DefaultAdvertizer, ServiceInfo, Group, RadioSender, DishReceiver, GroupAccess, \ - RespondingAdvertizer, ServiceQuery, DefaultScheduledAdvertizer +from hello import DefaultAdvertizer, ServiceInfo, Group, RadioSender, DishReceiver, RespondingAdvertizer, \ + ServiceQuery, ScheduledAdvertizer -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') class AdvertizerIntegrationTest(TestCase): @@ -32,9 +30,9 @@ def test_sends_hello_when_advertises_service(self): messages = [] with DefaultAdvertizer(sender) as advertizer, DishReceiver(context) as test_receiver: - test_receiver.start(GroupAccess(ACCESS_URL, GROUP.hello())) + test_receiver.start(GROUP.hello()) test_receiver.register(lambda message: messages.append(message)) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When advertizer.advertise(self.SERVICE_INFO) @@ -54,11 +52,11 @@ def test_sends_hello_when_query_received(self): with (RespondingAdvertizer(sender, receiver, 0.01) as advertizer, RadioSender(context) as test_sender, DishReceiver(context) as test_receiver): - test_sender.start(GroupAccess(ACCESS_URL, GROUP.query())) - test_receiver.start(GroupAccess(ACCESS_URL, GROUP.hello())) + test_sender.start(GROUP.query()) + test_receiver.start(GROUP.hello()) test_receiver.register(lambda message: messages.append(message)) - advertizer.start(ACCESS_URL, GROUP, self.SERVICE_INFO) + advertizer.start(GROUP, self.SERVICE_INFO) # When test_sender.send(ServiceQuery('test-service', 'test-role')) @@ -78,11 +76,11 @@ def test_sends_hello_when_info_changed_and_query_received(self): with (RespondingAdvertizer(sender, receiver, 0.01) as advertizer, RadioSender(context) as test_sender, DishReceiver(context) as test_receiver): - test_sender.start(GroupAccess(ACCESS_URL, GROUP.query())) - test_receiver.start(GroupAccess(ACCESS_URL, GROUP.hello())) + test_sender.start(GROUP.query()) + test_receiver.start(GROUP.hello()) test_receiver.register(lambda message: messages.append(message)) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) advertizer.advertise(self.SERVICE_INFO) query = ServiceQuery('test-service', 'test-role') @@ -114,10 +112,10 @@ def test_sends_hello_when_schedules_advertisement_once(self): timer = ReusableTimer() messages = [] - with DefaultScheduledAdvertizer(_advertizer, timer) as advertizer, DishReceiver(context) as test_receiver: - test_receiver.start(GroupAccess(ACCESS_URL, GROUP.hello())) + with ScheduledAdvertizer(_advertizer, timer) as advertizer, DishReceiver(context) as test_receiver: + test_receiver.start(GROUP.hello()) test_receiver.register(lambda message: messages.append(message)) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When advertizer.schedule(self.SERVICE_INFO, interval=0.01, one_shot=True) @@ -135,10 +133,10 @@ def test_sends_hello_when_schedules_advertisement_periodically(self): timer = ReusableTimer() messages = [] - with DefaultScheduledAdvertizer(_advertizer, timer) as advertizer, DishReceiver(context) as test_receiver: - test_receiver.start(GroupAccess(ACCESS_URL, GROUP.hello())) + with ScheduledAdvertizer(_advertizer, timer) as advertizer, DishReceiver(context) as test_receiver: + test_receiver.start(GROUP.hello()) test_receiver.register(lambda message: messages.append(message)) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When advertizer.schedule(self.SERVICE_INFO, interval=0.01) diff --git a/tests/apiIntegrationTest.py b/tests/apiIntegrationTest.py index 5ea16f6..8e051a6 100644 --- a/tests/apiIntegrationTest.py +++ b/tests/apiIntegrationTest.py @@ -3,13 +3,10 @@ from context_logger import setup_logging from test_utility import wait_for_assertion -from zmq import Context -from hello import ServiceInfo, Group, DefaultHello, ServiceQuery +from hello import ServiceInfo, Group, ServiceQuery, Hello, HelloConfig -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) SERVICE_QUERY = ServiceQuery('test-service', 'test-role') @@ -25,12 +22,12 @@ def setUp(self): def test_discoverer_caches_advertised_service(self): # Given - context = Context() - hello = DefaultHello(context) + config = HelloConfig(advertizer_responder=False) - with hello.default_advertizer(respond=False) as advertizer, hello.discoverer() as discoverer: - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + with (Hello.builder(config).advertizer().default() as advertizer, + Hello.builder(config).discoverer().default() as discoverer): + advertizer.start(GROUP, SERVICE_INFO) + discoverer.start(GROUP, SERVICE_QUERY) # When advertizer.advertise() @@ -40,52 +37,86 @@ def test_discoverer_caches_advertised_service(self): # Then self.assertEqual({SERVICE_INFO.name: SERVICE_INFO}, discoverer.get_services()) - def test_discoverer_caches_advertised_service_when_scheduled_once(self): + def test_discoverer_caches_advertised_service_when_advertisement_scheduled_once(self): # Given - context = Context() - hello = DefaultHello(context) + config = HelloConfig(advertizer_responder=False) - with hello.scheduled_advertizer(respond=False) as advertizer, hello.discoverer() as discoverer: - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + with (Hello.builder(config).advertizer().scheduled() as advertizer, + Hello.builder(config).discoverer().default() as discoverer): + advertizer.start(GROUP, SERVICE_INFO) + discoverer.start(GROUP, SERVICE_QUERY) # When advertizer.schedule(interval=0.01, one_shot=True) - wait_for_assertion(0.1, lambda: self.assertEqual(1, len(discoverer.get_services()))) + wait_for_assertion(0.2, lambda: self.assertEqual(1, len(discoverer.get_services()))) # Then self.assertEqual({SERVICE_INFO.name: SERVICE_INFO}, discoverer.get_services()) - def test_discoverer_caches_advertised_service_when_scheduled_periodically(self): + def test_discoverer_caches_advertised_service_when_advertisement_scheduled_periodically(self): # Given - context = Context() - hello = DefaultHello(context) + config = HelloConfig() - with hello.scheduled_advertizer() as advertizer, hello.discoverer() as discoverer: - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + with (Hello.builder(config).advertizer().scheduled() as advertizer, + Hello.builder(config).discoverer().default() as discoverer): + advertizer.start(GROUP, SERVICE_INFO) + discoverer.start(GROUP, SERVICE_QUERY) # When advertizer.schedule(interval=0.01) - wait_for_assertion(0.1, lambda: self.assertEqual(1, len(discoverer.get_services()))) + wait_for_assertion(0.2, lambda: self.assertEqual(1, len(discoverer.get_services()))) # Then self.assertEqual({SERVICE_INFO.name: SERVICE_INFO}, discoverer.get_services()) def test_discoverer_caches_discovery_response_service(self): # Given - context = Context() - hello = DefaultHello(context) + config = HelloConfig() - with hello.default_advertizer() as advertizer, hello.discoverer() as discoverer: - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + with (Hello.builder(config).advertizer().default() as advertizer, + Hello.builder(config).discoverer().default() as discoverer): + advertizer.start(GROUP, SERVICE_INFO) + discoverer.start(GROUP, SERVICE_QUERY) # When discoverer.discover() + wait_for_assertion(0.1, lambda: self.assertEqual(1, len(discoverer.get_services()))) + + # Then + self.assertEqual({SERVICE_INFO.name: SERVICE_INFO}, discoverer.get_services()) + + def test_discoverer_caches_discovery_response_service_when_discovery_scheduled_once(self): + # Given + config = HelloConfig() + + with (Hello.builder(config).advertizer().default() as advertizer, + Hello.builder(config).discoverer().scheduled() as discoverer): + advertizer.start(GROUP, SERVICE_INFO) + discoverer.start(GROUP, SERVICE_QUERY) + + # When + discoverer.schedule(interval=0.01, one_shot=True) + + wait_for_assertion(0.2, lambda: self.assertEqual(1, len(discoverer.get_services()))) + + # Then + self.assertEqual({SERVICE_INFO.name: SERVICE_INFO}, discoverer.get_services()) + + def test_discoverer_caches_discovery_response_service_when_discovery_scheduled_periodically(self): + # Given + config = HelloConfig() + + with (Hello.builder(config).advertizer().default() as advertizer, + Hello.builder(config).discoverer().scheduled() as discoverer): + advertizer.start(GROUP, SERVICE_INFO) + discoverer.start(GROUP, SERVICE_QUERY) + + # When + discoverer.schedule(interval=0.01) + wait_for_assertion(0.2, lambda: self.assertEqual(1, len(discoverer.get_services()))) # Then diff --git a/tests/defaultAdvertizerTest.py b/tests/defaultAdvertizerTest.py index 474e3b3..bc26b0b 100644 --- a/tests/defaultAdvertizerTest.py +++ b/tests/defaultAdvertizerTest.py @@ -4,11 +4,9 @@ from context_logger import setup_logging -from hello import ServiceInfo, Group, Sender, DefaultAdvertizer, GroupAccess +from hello import ServiceInfo, Group, Sender, DefaultAdvertizer -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) @@ -26,7 +24,7 @@ def test_stops_sender_on_exit(self): sender = MagicMock(spec=Sender) with DefaultAdvertizer(sender) as advertizer: - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When @@ -37,7 +35,7 @@ def test_stops_sender_when_stopped(self): # Given sender = MagicMock(spec=Sender) advertizer = DefaultAdvertizer(sender) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When advertizer.stop() @@ -51,16 +49,16 @@ def test_starts_sender_when_started(self): advertizer = DefaultAdvertizer(sender) # When - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # Then - sender.start.assert_called_once_with(GroupAccess(ACCESS_URL, GROUP.hello())) + sender.start.assert_called_once_with(GROUP.hello()) def test_sends_info_when_passed_at_start(self): # Given sender = MagicMock(spec=Sender) advertizer = DefaultAdvertizer(sender) - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) + advertizer.start(GROUP, SERVICE_INFO) # When advertizer.advertise() @@ -72,7 +70,7 @@ def test_sends_info_when_passed_at_advertise(self): # Given sender = MagicMock(spec=Sender) advertizer = DefaultAdvertizer(sender) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When advertizer.advertise(SERVICE_INFO) @@ -84,7 +82,7 @@ def test_sends_last_info_when_passed_at_start_and_at_advertise(self): # Given sender = MagicMock(spec=Sender) advertizer = DefaultAdvertizer(sender) - advertizer.start(ACCESS_URL, GROUP, ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:9090'})) + advertizer.start(GROUP, ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:9090'})) # When advertizer.advertise(SERVICE_INFO) @@ -96,7 +94,7 @@ def test_does_not_send_info_when_no_info_provided(self): # Given sender = MagicMock(spec=Sender) advertizer = DefaultAdvertizer(sender) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When advertizer.advertise() diff --git a/tests/defaultDiscovererTest.py b/tests/defaultDiscovererTest.py index 70fd70b..6275923 100644 --- a/tests/defaultDiscovererTest.py +++ b/tests/defaultDiscovererTest.py @@ -4,12 +4,10 @@ from context_logger import setup_logging -from hello import ServiceInfo, Group, GroupAccess, \ - ServiceQuery, DefaultDiscoverer, Sender, Receiver, OnDiscoveryEvent, DiscoveryEventType, DiscoveryEvent +from hello import ServiceInfo, Group, ServiceQuery, DefaultDiscoverer, Sender, Receiver, OnDiscoveryEvent, \ + DiscoveryEventType, DiscoveryEvent -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_QUERY = ServiceQuery('test-.*', 'test-.*') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) @@ -29,7 +27,7 @@ def test_stops_sender_and_receiver_on_exit(self): receiver = MagicMock(spec=Receiver) with DefaultDiscoverer(sender, receiver) as discoverer: - discoverer.start(ACCESS_URL, GROUP) + discoverer.start(GROUP) # When @@ -42,7 +40,7 @@ def test_stops_sender_and_receiver_when_stopped(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP) + discoverer.start(GROUP) # When discoverer.stop() @@ -58,11 +56,11 @@ def test_starts_sender_and_receiver_when_started(self): discoverer = DefaultDiscoverer(sender, receiver) # When - discoverer.start(ACCESS_URL, GROUP) + discoverer.start(GROUP) # Then - sender.start.assert_called_once_with(GroupAccess(ACCESS_URL, GROUP.query())) - receiver.start.assert_called_once_with(GroupAccess(ACCESS_URL, GROUP.hello())) + sender.start.assert_called_once_with(GROUP.query()) + receiver.start.assert_called_once_with(GROUP.hello()) def test_registers_event_handler(self): # Given @@ -96,7 +94,7 @@ def test_caches_service_and_calls_handler_when_receives_matching_info(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + discoverer.start(GROUP, SERVICE_QUERY) handler = MagicMock(spec=OnDiscoveryEvent) discoverer.register(handler) @@ -112,7 +110,7 @@ def test_updates_service_and_calls_handler_when_receives_matching_info(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + discoverer.start(GROUP, SERVICE_QUERY) handler = MagicMock(spec=OnDiscoveryEvent) discoverer.register(handler) discoverer._handle_message(SERVICE_INFO.__dict__) @@ -131,7 +129,7 @@ def test_does_not_call_handler_when_service_info_not_changed(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + discoverer.start(GROUP, SERVICE_QUERY) handler = MagicMock(spec=OnDiscoveryEvent) discoverer.register(handler) discoverer._handle_message(SERVICE_INFO.__dict__) @@ -148,7 +146,7 @@ def test_handles_handler_error_gracefully(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + discoverer.start(GROUP, SERVICE_QUERY) handler = MagicMock(spec=OnDiscoveryEvent) handler.side_effect = Exception("Handler error") discoverer.register(handler) @@ -165,7 +163,7 @@ def test_handles_invalid_message_gracefully(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + discoverer.start(GROUP, SERVICE_QUERY) # When discoverer._handle_message({'invalid': 'message'}) @@ -178,7 +176,7 @@ def test_does_not_cache_service_when_info_not_matching_query(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + discoverer.start(GROUP, SERVICE_QUERY) non_matching_info = ServiceInfo('other-service', 'test-role', {'test': 'http://localhost:8080'}) @@ -193,7 +191,7 @@ def test_does_not_cache_service_when_no_query_set(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP) + discoverer.start(GROUP) # When discoverer._handle_message(SERVICE_INFO.__dict__) @@ -206,7 +204,7 @@ def test_sends_query_when_passed_at_start(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + discoverer.start(GROUP, SERVICE_QUERY) # When discoverer.discover() @@ -219,7 +217,7 @@ def test_sends_query_when_passed_at_discover(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP) + discoverer.start(GROUP) # When discoverer.discover(SERVICE_QUERY) @@ -232,7 +230,7 @@ def test_sends_last_query_when_passed_at_start_and_at_discover(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP, ServiceQuery('other-.*', 'test-.*')) + discoverer.start(GROUP, ServiceQuery('other-.*', 'test-.*')) # When discoverer.discover(SERVICE_QUERY) @@ -245,7 +243,7 @@ def test_does_not_send_query_when_no_query_provided(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) discoverer = DefaultDiscoverer(sender, receiver) - discoverer.start(ACCESS_URL, GROUP) + discoverer.start(GROUP) # When discoverer.discover() diff --git a/tests/defaultSchedulerTest.py b/tests/defaultSchedulerTest.py new file mode 100644 index 0000000..d1cbc23 --- /dev/null +++ b/tests/defaultSchedulerTest.py @@ -0,0 +1,90 @@ +import unittest +from typing import Any +from unittest import TestCase +from unittest.mock import MagicMock + +from common_utility import IReusableTimer +from context_logger import setup_logging + +from hello import DefaultScheduler + + +class DefaultSchedulerDiscovererTest(TestCase): + + @classmethod + def setUpClass(cls): + setup_logging('hello', 'DEBUG', warn_on_overwrite=False) + + def setUp(self): + print() + + def test_stops_timer_on_exit(self): + # Given + timer = MagicMock(spec=IReusableTimer) + + with TestScheduler(timer): + # When + pass + + # Then + timer.cancel.assert_called_once() + + def test_stops_timer_when_stopped(self): + # Given + timer = MagicMock(spec=IReusableTimer) + scheduler = TestScheduler(timer) + + # When + scheduler.stop() + + # Then + timer.cancel.assert_called_once() + + def test_schedules_execution_once(self): + # Given + timer = MagicMock(spec=IReusableTimer) + scheduler = TestScheduler(timer) + data = MagicMock() + + # When + scheduler.schedule(data, 60, True) + + # Then + timer.start.assert_called_once_with(60, scheduler._execute, [data]) + + def test_schedules_periodic_discover(self): + # Given + timer = MagicMock(spec=IReusableTimer) + scheduler = TestScheduler(timer) + data = MagicMock() + + # When + scheduler.schedule(data, 60, False) + + # Then + timer.start.assert_called_once_with(60, scheduler._execute_and_restart, [data]) + + def test_execute_and_restart_restarts_timer(self): + # Given + timer = MagicMock(spec=IReusableTimer) + scheduler = TestScheduler(timer) + data = MagicMock() + + # When + scheduler._execute_and_restart(data) + + # Then + timer.restart.assert_called_once() + + +class TestScheduler(DefaultScheduler[Any]): + + def __init__(self, timer: IReusableTimer) -> None: + super().__init__(timer) + + def _execute(self, data: Any | None = None) -> None: + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/discovererIntegrationTest.py b/tests/discovererIntegrationTest.py index 7d4cbbf..a067dd3 100644 --- a/tests/discovererIntegrationTest.py +++ b/tests/discovererIntegrationTest.py @@ -1,16 +1,14 @@ import unittest from unittest import TestCase +from common_utility import ReusableTimer from context_logger import setup_logging from test_utility import wait_for_assertion from zmq import Context -from hello import ServiceInfo, Group, RadioSender, DishReceiver, GroupAccess, \ - ServiceQuery, DefaultDiscoverer +from hello import ServiceInfo, Group, RadioSender, DishReceiver, ServiceQuery, DefaultDiscoverer, ScheduledDiscoverer -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_QUERY = ServiceQuery('test-service', 'test-role') @@ -32,8 +30,8 @@ def test_discovers_service_when_hello_received(self): receiver = DishReceiver(context) with DefaultDiscoverer(sender, receiver) as discoverer, RadioSender(context) as test_sender: - test_sender.start(GroupAccess(ACCESS_URL, GROUP.hello())) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + test_sender.start(GROUP.hello()) + discoverer.start(GROUP, SERVICE_QUERY) # When test_sender.send(self.SERVICE_INFO) @@ -50,8 +48,8 @@ def test_updates_service_when_info_changed(self): receiver = DishReceiver(context) with DefaultDiscoverer(sender, receiver) as discoverer, RadioSender(context) as test_sender: - test_sender.start(GroupAccess(ACCESS_URL, GROUP.hello())) - discoverer.start(ACCESS_URL, GROUP, SERVICE_QUERY) + test_sender.start(GROUP.hello()) + discoverer.start(GROUP, SERVICE_QUERY) test_sender.send(self.SERVICE_INFO) @@ -81,8 +79,8 @@ def test_sends_query(self): with DefaultDiscoverer(sender, receiver) as discoverer, DishReceiver(context) as test_receiver: test_receiver.register(lambda message: messages.append(message)) - test_receiver.start(GroupAccess(ACCESS_URL, GROUP.query())) - discoverer.start(ACCESS_URL, GROUP) + test_receiver.start(GROUP.query()) + discoverer.start(GROUP) # When discoverer.discover(SERVICE_QUERY) @@ -92,6 +90,48 @@ def test_sends_query(self): # Then self.assertEqual([SERVICE_QUERY.__dict__], messages) + def test_sends_query_when_schedules_discovery_once(self): + # Given + context = Context() + sender = RadioSender(context) + receiver = DishReceiver(context) + _discoverer = DefaultDiscoverer(sender, receiver) + timer = ReusableTimer() + messages = [] + + with ScheduledDiscoverer(_discoverer, timer) as discoverer, DishReceiver(context) as test_receiver: + test_receiver.start(GROUP.query()) + test_receiver.register(lambda message: messages.append(message)) + discoverer.start(GROUP) + + # When + discoverer.schedule(SERVICE_QUERY, interval=0.01, one_shot=True) + + wait_for_assertion(0.1, lambda: self.assertEqual(1, len(messages))) + + # Then + self.assertEqual([SERVICE_QUERY.__dict__], messages) + + def test_sends_query_when_schedules_discovery_periodically(self): + # Given + context = Context() + sender = RadioSender(context) + receiver = DishReceiver(context) + _discoverer = DefaultDiscoverer(sender, receiver) + timer = ReusableTimer() + messages = [] + + with ScheduledDiscoverer(_discoverer, timer) as discoverer, DishReceiver(context) as test_receiver: + test_receiver.start(GROUP.query()) + test_receiver.register(lambda message: messages.append(message)) + discoverer.start(GROUP) + + # When + discoverer.schedule(SERVICE_QUERY, interval=0.01) + + # Then + wait_for_assertion(0.1, lambda: self.assertEqual(5, len(messages))) + if __name__ == '__main__': unittest.main() diff --git a/tests/dishReceiverTest.py b/tests/dishReceiverTest.py index 5852f9c..2f3e233 100644 --- a/tests/dishReceiverTest.py +++ b/tests/dishReceiverTest.py @@ -6,11 +6,9 @@ from test_utility import wait_for_assertion from zmq import Context, ZMQError, Poller, POLLIN -from hello import ServiceInfo, Group, GroupAccess, DishReceiver, OnMessage +from hello import ServiceInfo, Group, DishReceiver, OnMessage -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) @@ -25,7 +23,7 @@ def setUp(self): def test_raises_error_when_restarted(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) with DishReceiver(context) as receiver: @@ -37,7 +35,7 @@ def test_raises_error_when_restarted(self): def test_raises_error_when_fails_to_bind_socket(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) context.socket.return_value.bind.side_effect = ZMQError(1, "Bind failed") receiver = DishReceiver(context) @@ -48,7 +46,7 @@ def test_raises_error_when_fails_to_bind_socket(self): def test_closes_socket_on_exit(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) with DishReceiver(context) as receiver: @@ -61,7 +59,7 @@ def test_closes_socket_on_exit(self): def test_closes_socket_when_stopped(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) receiver = DishReceiver(context) receiver.start(group_access) @@ -74,7 +72,7 @@ def test_closes_socket_when_stopped(self): def test_raises_error_when_fails_to_close_socket_on_stop(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) context.socket.return_value.close.side_effect = [ZMQError(1, "Close failed"), None] @@ -112,7 +110,7 @@ def test_deregisters_handler(self): def test_calls_registered_handler_on_message(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) context.socket.return_value.recv_json.return_value = SERVICE_INFO.__dict__ handler = MagicMock(spec=OnMessage) @@ -132,7 +130,7 @@ def test_calls_registered_handler_on_message(self): def test_handles_message_receive_error_gracefully(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) context.socket.return_value.recv_json.side_effect = ZMQError(1, "Receive failed") handler = MagicMock(spec=OnMessage) @@ -152,7 +150,7 @@ def test_handles_message_receive_error_gracefully(self): def test_handles_handler_execution_error_gracefully(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) context.socket.return_value.recv_json.return_value = SERVICE_INFO.__dict__ handler = MagicMock(spec=OnMessage) diff --git a/tests/radioSenderTest.py b/tests/radioSenderTest.py index d1a285b..bd36d80 100644 --- a/tests/radioSenderTest.py +++ b/tests/radioSenderTest.py @@ -5,12 +5,10 @@ from context_logger import setup_logging from zmq import Context, ZMQError -from hello import ServiceInfo, Group, GroupAccess +from hello import ServiceInfo, Group from hello.sender import RadioSender -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) @@ -25,7 +23,7 @@ def setUp(self): def test_raises_error_when_restarted(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) sender = RadioSender(context) sender.start(group_access) @@ -36,7 +34,7 @@ def test_raises_error_when_restarted(self): def test_raises_error_when_fails_to_connect_socket(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) context.socket.return_value.connect.side_effect = ZMQError(1, "Connect failed") sender = RadioSender(context) @@ -47,7 +45,7 @@ def test_raises_error_when_fails_to_connect_socket(self): def test_closes_socket_on_exit(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) with RadioSender(context) as sender: @@ -60,7 +58,7 @@ def test_closes_socket_on_exit(self): def test_closes_socket_when_stopped(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) sender = RadioSender(context) sender.start(group_access) @@ -73,7 +71,7 @@ def test_closes_socket_when_stopped(self): def test_raises_error_when_fails_to_close_socket_on_stop(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) context.socket.return_value.close.side_effect = ZMQError(1, "Close failed") sender = RadioSender(context) @@ -85,7 +83,7 @@ def test_raises_error_when_fails_to_close_socket_on_stop(self): def test_sends_message_when_convertible_to_dict(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) sender = RadioSender(context) sender.start(group_access) @@ -98,7 +96,7 @@ def test_sends_message_when_convertible_to_dict(self): def test_sends_message_when_type_is_dict(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) sender = RadioSender(context) sender.start(group_access) @@ -111,7 +109,7 @@ def test_sends_message_when_type_is_dict(self): def test_does_not_send_message_when_not_serializable(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) sender = RadioSender(context) sender.start(group_access) @@ -135,7 +133,7 @@ def test_does_not_send_message_when_not_started(self): def test_handles_send_message_error_gracefully(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = MagicMock(spec=Context) sender = RadioSender(context) sender.start(group_access) diff --git a/tests/receiverIntegrationTest.py b/tests/receiverIntegrationTest.py index b9ccd34..d70ca68 100644 --- a/tests/receiverIntegrationTest.py +++ b/tests/receiverIntegrationTest.py @@ -5,11 +5,9 @@ from test_utility import wait_for_assertion from zmq import Context -from hello import ServiceInfo, Group, DishReceiver, GroupAccess, RadioSender +from hello import ServiceInfo, Group, DishReceiver, RadioSender -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) @@ -24,7 +22,7 @@ def setUp(self): def test_raises_error_when_restarted(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = Context() with DishReceiver(context) as receiver: @@ -36,7 +34,7 @@ def test_raises_error_when_restarted(self): def test_receives_message(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = Context() messages = [] diff --git a/tests/respondingAdvertizerTest.py b/tests/respondingAdvertizerTest.py index d37581e..96e6334 100644 --- a/tests/respondingAdvertizerTest.py +++ b/tests/respondingAdvertizerTest.py @@ -4,11 +4,9 @@ from context_logger import setup_logging -from hello import ServiceInfo, Group, Sender, GroupAccess, Receiver, RespondingAdvertizer, ServiceQuery +from hello import ServiceInfo, Group, Sender, Receiver, RespondingAdvertizer, ServiceQuery -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) @@ -27,7 +25,7 @@ def test_stops_sender_and_receiver_on_exit(self): receiver = MagicMock(spec=Receiver) with RespondingAdvertizer(sender, receiver) as advertizer: - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When @@ -40,7 +38,7 @@ def test_stops_sender_and_receiver_when_stopped(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) advertizer = RespondingAdvertizer(sender, receiver) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When advertizer.stop() @@ -56,18 +54,18 @@ def test_starts_sender_and_receiver_when_started(self): advertizer = RespondingAdvertizer(sender, receiver) # When - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # Then - sender.start.assert_called_once_with(GroupAccess(ACCESS_URL, GROUP.hello())) - receiver.start.assert_called_once_with(GroupAccess(ACCESS_URL, GROUP.query())) + sender.start.assert_called_once_with(GROUP.hello()) + receiver.start.assert_called_once_with(GROUP.query()) def test_sends_service_info_when_receives_matching_query(self): # Given sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) advertizer = RespondingAdvertizer(sender, receiver) - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) + advertizer.start(GROUP, SERVICE_INFO) # When advertizer._handle_message(ServiceQuery('test-.*', 'test-.*').__dict__) @@ -80,7 +78,7 @@ def test_does_not_send_service_info_when_receives_non_matching_query(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) advertizer = RespondingAdvertizer(sender, receiver) - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) + advertizer.start(GROUP, SERVICE_INFO) # When advertizer._handle_message(ServiceQuery('other-.*', 'test-.*').__dict__) @@ -93,7 +91,7 @@ def test_does_not_send_service_info_when_no_service_info_set(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) advertizer = RespondingAdvertizer(sender, receiver) - advertizer.start(ACCESS_URL, GROUP) + advertizer.start(GROUP) # When advertizer._handle_message(ServiceQuery('test-.*', 'test-.*').__dict__) @@ -106,7 +104,7 @@ def test_handles_invalid_message_gracefully(self): sender = MagicMock(spec=Sender) receiver = MagicMock(spec=Receiver) advertizer = RespondingAdvertizer(sender, receiver) - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) + advertizer.start(GROUP, SERVICE_INFO) # When advertizer._handle_message({'invalid': 'message'}) diff --git a/tests/defaultScheduledAdvertizerTest.py b/tests/scheduledAdvertizerTest.py similarity index 62% rename from tests/defaultScheduledAdvertizerTest.py rename to tests/scheduledAdvertizerTest.py index 29ac55a..7e0b7e6 100644 --- a/tests/defaultScheduledAdvertizerTest.py +++ b/tests/scheduledAdvertizerTest.py @@ -5,15 +5,13 @@ from common_utility import IReusableTimer from context_logger import setup_logging -from hello import ServiceInfo, Group, DefaultScheduledAdvertizer, Advertizer +from hello import ServiceInfo, Group, ScheduledAdvertizer, Advertizer -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) -class DefaultScheduledAdvertizerTest(TestCase): +class ScheduledAdvertizerTest(TestCase): @classmethod def setUpClass(cls): @@ -27,8 +25,8 @@ def test_stops_timer_and_advertizer_on_exit(self): _advertizer = MagicMock(spec=Advertizer) timer = MagicMock(spec=IReusableTimer) - with DefaultScheduledAdvertizer(_advertizer, timer) as advertizer: - advertizer.start(ACCESS_URL, GROUP) + with ScheduledAdvertizer(_advertizer, timer) as advertizer: + advertizer.start(GROUP) # When @@ -40,8 +38,8 @@ def test_stops_timer_and_advertizer_when_stopped(self): # Given _advertizer = MagicMock(spec=Advertizer) timer = MagicMock(spec=IReusableTimer) - advertizer = DefaultScheduledAdvertizer(_advertizer, timer) - advertizer.start(ACCESS_URL, GROUP) + advertizer = ScheduledAdvertizer(_advertizer, timer) + advertizer.start(GROUP) # When advertizer.stop() @@ -54,19 +52,19 @@ def test_starts_advertizer_when_started(self): # Given _advertizer = MagicMock(spec=Advertizer) timer = MagicMock(spec=IReusableTimer) - advertizer = DefaultScheduledAdvertizer(_advertizer, timer) + advertizer = ScheduledAdvertizer(_advertizer, timer) # When - advertizer.start(ACCESS_URL, GROUP, SERVICE_INFO) + advertizer.start(GROUP, SERVICE_INFO) # Then - _advertizer.start.assert_called_once_with(ACCESS_URL, GROUP, SERVICE_INFO) + _advertizer.start.assert_called_once_with(GROUP, SERVICE_INFO) def test_sends_service_info(self): # Given _advertizer = MagicMock(spec=Advertizer) timer = MagicMock(spec=IReusableTimer) - advertizer = DefaultScheduledAdvertizer(_advertizer, timer) + advertizer = ScheduledAdvertizer(_advertizer, timer) # When advertizer.advertise(SERVICE_INFO) @@ -78,37 +76,37 @@ def test_schedules_advertise_once(self): # Given _advertizer = MagicMock(spec=Advertizer) timer = MagicMock(spec=IReusableTimer) - advertizer = DefaultScheduledAdvertizer(_advertizer, timer) - advertizer.start(ACCESS_URL, GROUP) + advertizer = ScheduledAdvertizer(_advertizer, timer) + advertizer.start(GROUP) # When advertizer.schedule(SERVICE_INFO, 60, True) # Then - timer.start.assert_called_once_with(60, advertizer.advertise, [SERVICE_INFO]) + timer.start.assert_called_once_with(60, advertizer._execute, [SERVICE_INFO]) def test_schedules_periodic_advertise(self): # Given _advertizer = MagicMock(spec=Advertizer) timer = MagicMock(spec=IReusableTimer) - advertizer = DefaultScheduledAdvertizer(_advertizer, timer) - advertizer.start(ACCESS_URL, GROUP) + advertizer = ScheduledAdvertizer(_advertizer, timer) + advertizer.start(GROUP) # When advertizer.schedule(SERVICE_INFO, 60, False) # Then - timer.start.assert_called_once_with(60, advertizer._advertise_and_restart, [SERVICE_INFO]) + timer.start.assert_called_once_with(60, advertizer._execute_and_restart, [SERVICE_INFO]) - def test_advertise_and_restart_calls_advertise_and_restarts_timer(self): + def test_execute_and_restart_calls_advertise_and_restarts_timer(self): # Given _advertizer = MagicMock(spec=Advertizer) timer = MagicMock(spec=IReusableTimer) - advertizer = DefaultScheduledAdvertizer(_advertizer, timer) - advertizer.start(ACCESS_URL, GROUP) + advertizer = ScheduledAdvertizer(_advertizer, timer) + advertizer.start(GROUP) # When - advertizer._advertise_and_restart(SERVICE_INFO) + advertizer._execute_and_restart(SERVICE_INFO) # Then _advertizer.advertise.assert_called_once_with(SERVICE_INFO) diff --git a/tests/scheduledDiscovererTest.py b/tests/scheduledDiscovererTest.py new file mode 100644 index 0000000..3b0522e --- /dev/null +++ b/tests/scheduledDiscovererTest.py @@ -0,0 +1,159 @@ +import unittest +from unittest import TestCase +from unittest.mock import MagicMock + +from common_utility import IReusableTimer +from context_logger import setup_logging + +from hello import ServiceQuery, Group, ScheduledDiscoverer, Discoverer, OnDiscoveryEvent + +GROUP = Group('test-group', 'udp://239.0.0.1:5555') +SERVICE_QUERY = ServiceQuery('test-service', 'test-role') + + +class ScheduledDiscovererTest(TestCase): + + @classmethod + def setUpClass(cls): + setup_logging('hello', 'DEBUG', warn_on_overwrite=False) + + def setUp(self): + print() + + def test_stops_timer_and_discoverer_on_exit(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + + with ScheduledDiscoverer(_discoverer, timer) as discoverer: + discoverer.start(GROUP) + + # When + + # Then + timer.cancel.assert_called_once() + _discoverer.stop.assert_called_once() + + def test_stops_timer_and_discoverer_when_stopped(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + discoverer.start(GROUP) + + # When + discoverer.stop() + + # Then + timer.cancel.assert_called_once() + _discoverer.stop.assert_called_once() + + def test_starts_discoverer_when_started(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + + # When + discoverer.start(GROUP, SERVICE_QUERY) + + # Then + _discoverer.start.assert_called_once_with(GROUP, SERVICE_QUERY) + + def test_registers_event_handler(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + handler = MagicMock(spec=OnDiscoveryEvent) + + # When + discoverer.register(handler) + + # Then + _discoverer.register.assert_called_once_with(handler) + + def test_deregisters_event_handler(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + handler = MagicMock(spec=OnDiscoveryEvent) + discoverer.register(handler) + + # When + discoverer.deregister(handler) + + # Then + _discoverer.deregister.assert_called_once_with(handler) + + def test_returns_event_handlers(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + discoverer.start(GROUP, SERVICE_QUERY) + handler = MagicMock(spec=OnDiscoveryEvent) + discoverer.register(handler) + + # When + result = discoverer.get_handlers() + + # Then + self.assertEqual(_discoverer.get_handlers(), result) + + def test_sends_service_query(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + + # When + discoverer.discover(SERVICE_QUERY) + + # Then + _discoverer.discover.assert_called_once_with(SERVICE_QUERY) + + def test_schedules_discover_once(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + discoverer.start(GROUP) + + # When + discoverer.schedule(SERVICE_QUERY, 60, True) + + # Then + timer.start.assert_called_once_with(60, discoverer._execute, [SERVICE_QUERY]) + + def test_schedules_periodic_discover(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + discoverer.start(GROUP) + + # When + discoverer.schedule(SERVICE_QUERY, 60, False) + + # Then + timer.start.assert_called_once_with(60, discoverer._execute_and_restart, [SERVICE_QUERY]) + + def test_execute_and_restart_calls_discover_and_restarts_timer(self): + # Given + _discoverer = MagicMock(spec=Discoverer) + timer = MagicMock(spec=IReusableTimer) + discoverer = ScheduledDiscoverer(_discoverer, timer) + discoverer.start(GROUP) + + # When + discoverer._execute_and_restart(SERVICE_QUERY) + + # Then + _discoverer.discover.assert_called_once_with(SERVICE_QUERY) + timer.restart.assert_called_once() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/senderIntegrationTest.py b/tests/senderIntegrationTest.py index c5b3c46..4a2a531 100644 --- a/tests/senderIntegrationTest.py +++ b/tests/senderIntegrationTest.py @@ -5,12 +5,10 @@ from test_utility import wait_for_assertion from zmq import Context -from hello import ServiceInfo, Group, GroupAccess, DishReceiver +from hello import ServiceInfo, Group, DishReceiver from hello.sender import RadioSender -ACCESS_URL = 'udp://239.0.0.1:5555' -GROUP_NAME = 'test-group' -GROUP = Group(GROUP_NAME) +GROUP = Group('test-group', 'udp://239.0.0.1:5555') SERVICE_INFO = ServiceInfo('test-service', 'test-role', {'test': 'http://localhost:8080'}) @@ -25,7 +23,7 @@ def setUp(self): def test_sends_message(self): # Given - group_access = GroupAccess(ACCESS_URL, GROUP.hello()) + group_access = GROUP.hello() context = Context() messages = []