diff --git a/socketio/asyncio_manager.py b/socketio/asyncio_manager.py index eb3b7cb..66283f9 100644 --- a/socketio/asyncio_manager.py +++ b/socketio/asyncio_manager.py @@ -33,7 +33,7 @@ class AsyncioManager(BaseManager): else: del self.callbacks[sid][namespace][id] if callback is not None: - if asyncio.iscoroutinefunction(callback): + if asyncio.iscoroutinefunction(callback) is True: try: await callback(*data) except asyncio.CancelledError: # pragma: no cover diff --git a/socketio/asyncio_namespace.py b/socketio/asyncio_namespace.py new file mode 100644 index 0000000..6befd37 --- /dev/null +++ b/socketio/asyncio_namespace.py @@ -0,0 +1,83 @@ +import asyncio + +from socketio import namespace + + +class AsyncNamespace(namespace.Namespace): + """Base class for asyncio class-based namespaces. + + A class-based namespace is a class that contains all the event handlers + for a Socket.IO namespace. The event handlers are methods of the class + with the prefix ``on_``, such as ``on_connect``, ``on_disconnect``, + ``on_message``, ``on_json``, and so on. These can be regular functions or + coroutines. + + :param namespace: The Socket.IO namespace to be used with all the event + handlers defined in this class. If this argument is + omitted, the default namespace is used. + """ + def is_asyncio_based(self): + return True + + async def trigger_event(self, event, *args): + """Dispatch an event to the proper handler method. + + In the most common usage, this method is not overloaded by subclasses, + as it performs the routing of events to methods. However, this + method can be overriden if special dispatching rules are needed, or if + having a single method that catches all events is desired. + + Note: this method is a coroutine. + """ + handler_name = 'on_' + event + if hasattr(self, handler_name): + handler = getattr(self, handler_name) + if asyncio.iscoroutinefunction(handler) is True: + try: + ret = await handler(*args) + except asyncio.CancelledError: # pragma: no cover + pass + else: + ret = handler(*args) + return ret + + async def emit(self, event, data=None, room=None, skip_sid=None, + namespace=None, callback=None): + """Emit a custom event to one or more connected clients. + + The only difference with the :func:`socketio.Server.emit` method is + that when the ``namespace`` argument is not given the namespace + associated with the class is used. + + Note: this method is a coroutine. + """ + return await self.server.emit(event, data=data, room=room, + skip_sid=skip_sid, + namespace=namespace or self.namespace, + callback=callback) + + async def send(self, data, room=None, skip_sid=None, namespace=None, + callback=None): + """Send a message to one or more connected clients. + + The only difference with the :func:`socketio.Server.send` method is + that when the ``namespace`` argument is not given the namespace + associated with the class is used. + + Note: this method is a coroutine. + """ + return await self.server.send(data, room=room, skip_sid=skip_sid, + namespace=namespace or self.namespace, + callback=callback) + + async def disconnect(self, sid, namespace=None): + """Disconnect a client. + + The only difference with the :func:`socketio.Server.disconnect` method + is that when the ``namespace`` argument is not given the namespace + associated with the class is used. + + Note: this method is a coroutine. + """ + return await self.server.disconnect( + sid, namespace=namespace or self.namespace) diff --git a/socketio/asyncio_server.py b/socketio/asyncio_server.py index 857a97a..146b2fd 100644 --- a/socketio/asyncio_server.py +++ b/socketio/asyncio_server.py @@ -305,7 +305,8 @@ class AsyncServer(server.Server): """Invoke an application event handler.""" # first see if we have an explicit handler for the event if namespace in self.handlers and event in self.handlers[namespace]: - if asyncio.iscoroutinefunction(self.handlers[namespace][event]): + if asyncio.iscoroutinefunction(self.handlers[namespace][event]) \ + is True: try: ret = await self.handlers[namespace][event](*args) except asyncio.CancelledError: # pragma: no cover diff --git a/socketio/namespace.py b/socketio/namespace.py index 6450ffe..14a29e1 100644 --- a/socketio/namespace.py +++ b/socketio/namespace.py @@ -17,6 +17,9 @@ class Namespace(object): def _set_server(self, server): self.server = server + def is_asyncio_based(self): + return False + def trigger_event(self, event, *args): """Dispatch an event to the proper handler method. diff --git a/socketio/server.py b/socketio/server.py index cd04a42..64501f2 100644 --- a/socketio/server.py +++ b/socketio/server.py @@ -113,6 +113,9 @@ class Server(object): self.async_mode = self.eio.async_mode + def is_asyncio_based(self): + return False + def on(self, event, handler=None, namespace=None): """Register an event handler. @@ -171,6 +174,8 @@ class Server(object): """ if not isinstance(namespace_handler, namespace.Namespace): raise ValueError('Not a namespace instance') + if self.is_asyncio_based() != namespace_handler.is_asyncio_based(): + raise ValueError('Not a valid namespace class for this server') namespace_handler._set_server(self) self.namespace_handlers[namespace_handler.namespace] = \ namespace_handler diff --git a/tests/test_asyncio_manager.py b/tests/test_asyncio_manager.py new file mode 100644 index 0000000..46127ff --- /dev/null +++ b/tests/test_asyncio_manager.py @@ -0,0 +1,280 @@ +import sys +import unittest + +import six +if six.PY3: + from unittest import mock +else: + import mock + +if sys.version_info >= (3, 5): + import asyncio + from asyncio import coroutine + from socketio import asyncio_manager +else: + # mock coroutine so that Python 2 doesn't complain + def coroutine(f): + return f + + +def AsyncMock(*args, **kwargs): + """Return a mock asynchronous function.""" + m = mock.MagicMock(*args, **kwargs) + + @coroutine + def mock_coro(*args, **kwargs): + return m(*args, **kwargs) + + mock_coro.mock = m + return mock_coro + + +def _run(coro): + """Run the given coroutine.""" + return asyncio.get_event_loop().run_until_complete(coro) + + +@unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') +class TestAsyncioManager(unittest.TestCase): + def setUp(self): + mock_server = mock.MagicMock() + mock_server._emit_internal = AsyncMock() + self.bm = asyncio_manager.AsyncioManager() + self.bm.set_server(mock_server) + self.bm.initialize() + + def test_connect(self): + self.bm.connect('123', '/foo') + self.assertIn(None, self.bm.rooms['/foo']) + self.assertIn('123', self.bm.rooms['/foo']) + self.assertIn('123', self.bm.rooms['/foo'][None]) + self.assertIn('123', self.bm.rooms['/foo']['123']) + self.assertEqual(self.bm.rooms['/foo'], {None: {'123': True}, + '123': {'123': True}}) + + def test_pre_disconnect(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + self.bm.pre_disconnect('123', '/foo') + self.assertEqual(self.bm.pending_disconnect, {'/foo': ['123']}) + self.assertFalse(self.bm.is_connected('123', '/foo')) + self.bm.pre_disconnect('456', '/foo') + self.assertEqual(self.bm.pending_disconnect, {'/foo': ['123', '456']}) + self.assertFalse(self.bm.is_connected('456', '/foo')) + self.bm.disconnect('123', '/foo') + self.assertEqual(self.bm.pending_disconnect, {'/foo': ['456']}) + self.bm.disconnect('456', '/foo') + self.assertEqual(self.bm.pending_disconnect, {}) + + def test_disconnect(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.enter_room('456', '/foo', 'baz') + self.bm.disconnect('123', '/foo') + self.assertEqual(self.bm.rooms['/foo'], {None: {'456': True}, + '456': {'456': True}, + 'baz': {'456': True}}) + + def test_disconnect_default_namespace(self): + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + self.bm.connect('456', '/') + self.bm.connect('456', '/foo') + self.assertTrue(self.bm.is_connected('123', '/')) + self.assertTrue(self.bm.is_connected('123', '/foo')) + self.bm.disconnect('123', '/') + self.assertFalse(self.bm.is_connected('123', '/')) + self.assertTrue(self.bm.is_connected('123', '/foo')) + self.bm.disconnect('123', '/foo') + self.assertFalse(self.bm.is_connected('123', '/foo')) + self.assertEqual(self.bm.rooms['/'], {None: {'456': True}, + '456': {'456': True}}) + self.assertEqual(self.bm.rooms['/foo'], {None: {'456': True}, + '456': {'456': True}}) + + def test_disconnect_twice(self): + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + self.bm.connect('456', '/') + self.bm.connect('456', '/foo') + self.bm.disconnect('123', '/') + self.bm.disconnect('123', '/foo') + self.bm.disconnect('123', '/') + self.bm.disconnect('123', '/foo') + self.assertEqual(self.bm.rooms['/'], {None: {'456': True}, + '456': {'456': True}}) + self.assertEqual(self.bm.rooms['/foo'], {None: {'456': True}, + '456': {'456': True}}) + + def test_disconnect_all(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.enter_room('456', '/foo', 'baz') + self.bm.disconnect('123', '/foo') + self.bm.disconnect('456', '/foo') + self.assertEqual(self.bm.rooms, {}) + + def test_disconnect_with_callbacks(self): + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + self.bm._generate_ack_id('123', '/', 'f') + self.bm._generate_ack_id('123', '/foo', 'g') + self.bm.disconnect('123', '/foo') + self.assertNotIn('/foo', self.bm.callbacks['123']) + self.bm.disconnect('123', '/') + self.assertNotIn('123', self.bm.callbacks) + + def test_trigger_sync_callback(self): + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + cb = mock.MagicMock() + id1 = self.bm._generate_ack_id('123', '/', cb) + id2 = self.bm._generate_ack_id('123', '/foo', cb) + _run(self.bm.trigger_callback('123', '/', id1, ['foo'])) + _run(self.bm.trigger_callback('123', '/foo', id2, ['bar', 'baz'])) + self.assertEqual(cb.call_count, 2) + cb.assert_any_call('foo') + cb.assert_any_call('bar', 'baz') + + def test_trigger_async_callback(self): + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + cb = AsyncMock() + id1 = self.bm._generate_ack_id('123', '/', cb) + id2 = self.bm._generate_ack_id('123', '/foo', cb) + _run(self.bm.trigger_callback('123', '/', id1, ['foo'])) + _run(self.bm.trigger_callback('123', '/foo', id2, ['bar', 'baz'])) + self.assertEqual(cb.mock.call_count, 2) + cb.mock.assert_any_call('foo') + cb.mock.assert_any_call('bar', 'baz') + + def test_invalid_callback(self): + self.bm.connect('123', '/') + cb = mock.MagicMock() + id = self.bm._generate_ack_id('123', '/', cb) + + # these should not raise an exception + _run(self.bm.trigger_callback('124', '/', id, ['foo'])) + _run(self.bm.trigger_callback('123', '/foo', id, ['foo'])) + _run(self.bm.trigger_callback('123', '/', id + 1, ['foo'])) + self.assertEqual(cb.mock.call_count, 0) + + def test_get_namespaces(self): + self.assertEqual(list(self.bm.get_namespaces()), []) + self.bm.connect('123', '/') + self.bm.connect('123', '/foo') + namespaces = list(self.bm.get_namespaces()) + self.assertEqual(len(namespaces), 2) + self.assertIn('/', namespaces) + self.assertIn('/foo', namespaces) + + def test_get_participants(self): + self.bm.connect('123', '/') + self.bm.connect('456', '/') + self.bm.connect('789', '/') + self.bm.disconnect('789', '/') + self.assertNotIn('789', self.bm.rooms['/'][None]) + participants = list(self.bm.get_participants('/', None)) + self.assertEqual(len(participants), 2) + self.assertNotIn('789', participants) + + def test_leave_invalid_room(self): + self.bm.connect('123', '/foo') + self.bm.leave_room('123', '/foo', 'baz') + self.bm.leave_room('123', '/bar', 'baz') + + def test_no_room(self): + rooms = self.bm.get_rooms('123', '/foo') + self.assertEqual([], rooms) + + def test_close_room(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + self.bm.connect('789', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.close_room('bar', '/foo') + self.assertNotIn('bar', self.bm.rooms['/foo']) + + def test_close_invalid_room(self): + self.bm.close_room('bar', '/foo') + + def test_rooms(self): + self.bm.connect('123', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + r = self.bm.get_rooms('123', '/foo') + self.assertEqual(len(r), 2) + self.assertIn('123', r) + self.assertIn('bar', r) + + def test_emit_to_sid(self): + self.bm.connect('123', '/foo') + self.bm.connect('456', '/foo') + _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', + room='123')) + self.bm.server._emit_internal.mock.assert_called_once_with( + '123', 'my event', {'foo': 'bar'}, '/foo', None) + + def test_emit_to_room(self): + self.bm.connect('123', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.connect('456', '/foo') + self.bm.enter_room('456', '/foo', 'bar') + self.bm.connect('789', '/foo') + _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', + room='bar')) + self.assertEqual(self.bm.server._emit_internal.mock.call_count, 2) + self.bm.server._emit_internal.mock.assert_any_call( + '123', 'my event', {'foo': 'bar'}, '/foo', None) + self.bm.server._emit_internal.mock.assert_any_call( + '456', 'my event', {'foo': 'bar'}, '/foo', None) + + def test_emit_to_all(self): + self.bm.connect('123', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.connect('456', '/foo') + self.bm.enter_room('456', '/foo', 'bar') + self.bm.connect('789', '/foo') + self.bm.connect('abc', '/bar') + _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) + self.assertEqual(self.bm.server._emit_internal.mock.call_count, 3) + self.bm.server._emit_internal.mock.assert_any_call( + '123', 'my event', {'foo': 'bar'}, '/foo', None) + self.bm.server._emit_internal.mock.assert_any_call( + '456', 'my event', {'foo': 'bar'}, '/foo', None) + self.bm.server._emit_internal.mock.assert_any_call( + '789', 'my event', {'foo': 'bar'}, '/foo', None) + + def test_emit_to_all_skip_one(self): + self.bm.connect('123', '/foo') + self.bm.enter_room('123', '/foo', 'bar') + self.bm.connect('456', '/foo') + self.bm.enter_room('456', '/foo', 'bar') + self.bm.connect('789', '/foo') + self.bm.connect('abc', '/bar') + _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', + skip_sid='456')) + self.assertEqual(self.bm.server._emit_internal.mock.call_count, 2) + self.bm.server._emit_internal.mock.assert_any_call( + '123', 'my event', {'foo': 'bar'}, '/foo', None) + self.bm.server._emit_internal.mock.assert_any_call( + '789', 'my event', {'foo': 'bar'}, '/foo', None) + + def test_emit_with_callback(self): + self.bm.connect('123', '/foo') + self.bm._generate_ack_id = mock.MagicMock() + self.bm._generate_ack_id.return_value = 11 + _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo', + callback='cb')) + self.bm._generate_ack_id.assert_called_once_with('123', '/foo', 'cb') + self.bm.server._emit_internal.mock.assert_called_once_with( + '123', 'my event', {'foo': 'bar'}, '/foo', 11) + + def test_emit_to_invalid_room(self): + _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/', + room='123')) + + def test_emit_to_invalid_namespace(self): + _run(self.bm.emit('my event', {'foo': 'bar'}, namespace='/foo')) diff --git a/tests/test_asyncio_namespace.py b/tests/test_asyncio_namespace.py new file mode 100644 index 0000000..d7e205d --- /dev/null +++ b/tests/test_asyncio_namespace.py @@ -0,0 +1,179 @@ +import sys +import unittest + +import six +if six.PY3: + from unittest import mock +else: + import mock + +if sys.version_info >= (3, 5): + import asyncio + from asyncio import coroutine + from socketio import asyncio_namespace +else: + # mock coroutine so that Python 2 doesn't complain + def coroutine(f): + return f + + +def AsyncMock(*args, **kwargs): + """Return a mock asynchronous function.""" + m = mock.MagicMock(*args, **kwargs) + + @coroutine + def mock_coro(*args, **kwargs): + return m(*args, **kwargs) + + mock_coro.mock = m + return mock_coro + + +def _run(coro): + """Run the given coroutine.""" + return asyncio.get_event_loop().run_until_complete(coro) + + +@unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') +class TestAsyncNamespace(unittest.TestCase): + def test_connect_event(self): + result = {} + + class MyNamespace(asyncio_namespace.AsyncNamespace): + @coroutine + def on_connect(self, sid, environ): + result['result'] = (sid, environ) + + ns = MyNamespace('/foo') + ns._set_server(mock.MagicMock()) + _run(ns.trigger_event('connect', 'sid', {'foo': 'bar'})) + self.assertEqual(result['result'], ('sid', {'foo': 'bar'})) + + def test_disconnect_event(self): + result = {} + + class MyNamespace(asyncio_namespace.AsyncNamespace): + @coroutine + def on_disconnect(self, sid): + result['result'] = sid + + ns = MyNamespace('/foo') + ns._set_server(mock.MagicMock()) + _run(ns.trigger_event('disconnect', 'sid')) + self.assertEqual(result['result'], 'sid') + + def test_sync_event(self): + result = {} + + class MyNamespace(asyncio_namespace.AsyncNamespace): + def on_custom_message(self, sid, data): + result['result'] = (sid, data) + + ns = MyNamespace('/foo') + ns._set_server(mock.MagicMock()) + _run(ns.trigger_event('custom_message', 'sid', {'data': 'data'})) + self.assertEqual(result['result'], ('sid', {'data': 'data'})) + + def test_async_event(self): + result = {} + + class MyNamespace(asyncio_namespace.AsyncNamespace): + @coroutine + def on_custom_message(self, sid, data): + result['result'] = (sid, data) + + ns = MyNamespace('/foo') + ns._set_server(mock.MagicMock()) + _run(ns.trigger_event('custom_message', 'sid', {'data': 'data'})) + self.assertEqual(result['result'], ('sid', {'data': 'data'})) + + def test_event_not_found(self): + result = {} + + class MyNamespace(asyncio_namespace.AsyncNamespace): + @coroutine + def on_custom_message(self, sid, data): + result['result'] = (sid, data) + + ns = MyNamespace('/foo') + ns._set_server(mock.MagicMock()) + _run(ns.trigger_event('another_custom_message', 'sid', + {'data': 'data'})) + self.assertEqual(result, {}) + + def test_emit(self): + ns = asyncio_namespace.AsyncNamespace('/foo') + mock_server = mock.MagicMock() + mock_server.emit = AsyncMock() + ns._set_server(mock_server) + _run(ns.emit('ev', data='data', room='room', skip_sid='skip', + callback='cb')) + ns.server.emit.mock.assert_called_with( + 'ev', data='data', room='room', skip_sid='skip', namespace='/foo', + callback='cb') + _run(ns.emit('ev', data='data', room='room', skip_sid='skip', + namespace='/bar', callback='cb')) + ns.server.emit.mock.assert_called_with( + 'ev', data='data', room='room', skip_sid='skip', namespace='/bar', + callback='cb') + + def test_send(self): + ns = asyncio_namespace.AsyncNamespace('/foo') + mock_server = mock.MagicMock() + mock_server.send = AsyncMock() + ns._set_server(mock_server) + _run(ns.send(data='data', room='room', skip_sid='skip', callback='cb')) + ns.server.send.mock.assert_called_with( + 'data', room='room', skip_sid='skip', namespace='/foo', + callback='cb') + _run(ns.send(data='data', room='room', skip_sid='skip', + namespace='/bar', callback='cb')) + ns.server.send.mock.assert_called_with( + 'data', room='room', skip_sid='skip', namespace='/bar', + callback='cb') + + def test_enter_room(self): + ns = asyncio_namespace.AsyncNamespace('/foo') + ns._set_server(mock.MagicMock()) + ns.enter_room('sid', 'room') + ns.server.enter_room.assert_called_with('sid', 'room', + namespace='/foo') + ns.enter_room('sid', 'room', namespace='/bar') + ns.server.enter_room.assert_called_with('sid', 'room', + namespace='/bar') + + def test_leave_room(self): + ns = asyncio_namespace.AsyncNamespace('/foo') + ns._set_server(mock.MagicMock()) + ns.leave_room('sid', 'room') + ns.server.leave_room.assert_called_with('sid', 'room', + namespace='/foo') + ns.leave_room('sid', 'room', namespace='/bar') + ns.server.leave_room.assert_called_with('sid', 'room', + namespace='/bar') + + def test_close_room(self): + ns = asyncio_namespace.AsyncNamespace('/foo') + ns._set_server(mock.MagicMock()) + ns.close_room('room') + ns.server.close_room.assert_called_with('room', namespace='/foo') + ns.close_room('room', namespace='/bar') + ns.server.close_room.assert_called_with('room', namespace='/bar') + + def test_rooms(self): + ns = asyncio_namespace.AsyncNamespace('/foo') + ns._set_server(mock.MagicMock()) + ns.rooms('sid') + ns.server.rooms.assert_called_with('sid', namespace='/foo') + ns.rooms('sid', namespace='/bar') + ns.server.rooms.assert_called_with('sid', namespace='/bar') + + def test_disconnect(self): + ns = asyncio_namespace.AsyncNamespace('/foo') + mock_server = mock.MagicMock() + mock_server.disconnect = AsyncMock() + ns._set_server(mock_server) + _run(ns.disconnect('sid')) + ns.server.disconnect.mock.assert_called_with('sid', namespace='/foo') + _run(ns.disconnect('sid', namespace='/bar')) + ns.server.disconnect.mock.assert_called_with('sid', namespace='/bar') diff --git a/tests/test_asyncio_server.py b/tests/test_asyncio_server.py new file mode 100644 index 0000000..77f7eb7 --- /dev/null +++ b/tests/test_asyncio_server.py @@ -0,0 +1,594 @@ +import json +import logging +import sys +import unittest + +import six +if six.PY3: + from unittest import mock +else: + import mock + +from socketio import packet +from socketio import namespace +if sys.version_info >= (3, 5): + import asyncio + from asyncio import coroutine + from socketio import asyncio_server + from socketio import asyncio_namespace +else: + # mock coroutine so that Python 2 doesn't complain + def coroutine(f): + return f + + +def AsyncMock(*args, **kwargs): + """Return a mock asynchronous function.""" + m = mock.MagicMock(*args, **kwargs) + + @coroutine + def mock_coro(*args, **kwargs): + return m(*args, **kwargs) + + mock_coro.mock = m + return mock_coro + + +def _run(coro): + """Run the given coroutine.""" + return asyncio.get_event_loop().run_until_complete(coro) + + +@unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') +@mock.patch('socketio.server.engineio.AsyncServer') +class TestAsyncServer(unittest.TestCase): + def tearDown(self): + # restore JSON encoder, in case a test changed it + packet.Packet.json = json + + def _get_mock_manager(self): + mgr = mock.MagicMock() + mgr.emit = AsyncMock() + mgr.trigger_callback = AsyncMock() + return mgr + + def test_create(self, eio): + eio.return_value.handle_request = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr, foo='bar') + _run(s.handle_request({})) + _run(s.handle_request({})) + eio.assert_called_once_with(**{'foo': 'bar', 'async_handlers': False}) + self.assertEqual(s.manager, mgr) + self.assertEqual(s.eio.on.call_count, 3) + self.assertEqual(s.binary, False) + self.assertEqual(s.async_handlers, False) + + def test_attach(self, eio): + s = asyncio_server.AsyncServer() + s.attach('app', 'path') + eio.return_value.attach.assert_called_once_with('app', 'path') + + def test_on_event(self, eio): + s = asyncio_server.AsyncServer() + + @s.on('connect') + def foo(): + pass + + def bar(): + pass + s.on('disconnect', bar) + s.on('disconnect', bar, namespace='/foo') + + self.assertEqual(s.handlers['/']['connect'], foo) + self.assertEqual(s.handlers['/']['disconnect'], bar) + self.assertEqual(s.handlers['/foo']['disconnect'], bar) + + def test_emit(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + _run(s.emit('my event', {'foo': 'bar'}, 'room', '123', + namespace='/foo', callback='cb')) + s.manager.emit.mock.assert_called_once_with( + 'my event', {'foo': 'bar'}, '/foo', 'room', '123', 'cb') + + def test_emit_default_namespace(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + _run(s.emit('my event', {'foo': 'bar'}, 'room', '123', callback='cb')) + s.manager.emit.mock.assert_called_once_with('my event', {'foo': 'bar'}, + '/', 'room', '123', 'cb') + + def test_send(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + _run(s.send('foo', 'room', '123', namespace='/foo', callback='cb')) + s.manager.emit.mock.assert_called_once_with('message', 'foo', '/foo', + 'room', '123', 'cb') + + def test_enter_room(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.enter_room('123', 'room', namespace='/foo') + s.manager.enter_room.assert_called_once_with('123', '/foo', 'room') + + def test_enter_room_default_namespace(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.enter_room('123', 'room') + s.manager.enter_room.assert_called_once_with('123', '/', 'room') + + def test_leave_room(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.leave_room('123', 'room', namespace='/foo') + s.manager.leave_room.assert_called_once_with('123', '/foo', 'room') + + def test_leave_room_default_namespace(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.leave_room('123', 'room') + s.manager.leave_room.assert_called_once_with('123', '/', 'room') + + def test_close_room(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.close_room('room', namespace='/foo') + s.manager.close_room.assert_called_once_with('room', '/foo') + + def test_close_room_default_namespace(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.close_room('room') + s.manager.close_room.assert_called_once_with('room', '/') + + def test_rooms(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.rooms('123', namespace='/foo') + s.manager.get_rooms.assert_called_once_with('123', '/foo') + + def test_rooms_default_namespace(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.rooms('123') + s.manager.get_rooms.assert_called_once_with('123', '/') + + def test_handle_request(self, eio): + eio.return_value.handle_request = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s.handle_request('environ')) + s.eio.handle_request.mock.assert_called_once_with('environ') + + def test_emit_internal(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._emit_internal('123', 'my event', 'my data', namespace='/foo')) + s.eio.send.mock.assert_called_once_with( + '123', '2/foo,["my event","my data"]', binary=False) + + def test_emit_internal_with_tuple(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._emit_internal('123', 'my event', ('foo', 'bar'), + namespace='/foo')) + s.eio.send.mock.assert_called_once_with( + '123', '2/foo,["my event","foo","bar"]', binary=False) + + def test_emit_internal_with_list(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._emit_internal('123', 'my event', ['foo', 'bar'], + namespace='/foo')) + s.eio.send.mock.assert_called_once_with( + '123', '2/foo,["my event",["foo","bar"]]', binary=False) + + def test_emit_internal_with_callback(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + id = s.manager._generate_ack_id('123', '/foo', 'cb') + _run(s._emit_internal('123', 'my event', 'my data', namespace='/foo', + id=id)) + s.eio.send.mock.assert_called_once_with( + '123', '2/foo,1["my event","my data"]', binary=False) + + def test_emit_internal_default_namespace(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._emit_internal('123', 'my event', 'my data')) + s.eio.send.mock.assert_called_once_with( + '123', '2["my event","my data"]', binary=False) + + def test_emit_internal_binary(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer(binary=True) + _run(s._emit_internal('123', u'my event', b'my binary data')) + self.assertEqual(s.eio.send.mock.call_count, 2) + + def test_transport(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + s.eio.transport = mock.MagicMock(return_value='polling') + _run(s._handle_eio_connect('foo', 'environ')) + self.assertEqual(s.transport('foo'), 'polling') + s.eio.transport.assert_called_once_with('foo') + + def test_handle_connect(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + handler = mock.MagicMock() + s.on('connect', handler) + _run(s._handle_eio_connect('123', 'environ')) + handler.assert_called_once_with('123', 'environ') + s.manager.connect.assert_called_once_with('123', '/') + s.eio.send.mock.assert_called_once_with('123', '0', binary=False) + self.assertEqual(mgr.initialize.call_count, 1) + _run(s._handle_eio_connect('456', 'environ')) + self.assertEqual(mgr.initialize.call_count, 1) + + def test_handle_connect_async(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + handler = AsyncMock() + s.on('connect', handler) + _run(s._handle_eio_connect('123', 'environ')) + handler.mock.assert_called_once_with('123', 'environ') + s.manager.connect.assert_called_once_with('123', '/') + s.eio.send.mock.assert_called_once_with('123', '0', binary=False) + self.assertEqual(mgr.initialize.call_count, 1) + _run(s._handle_eio_connect('456', 'environ')) + self.assertEqual(mgr.initialize.call_count, 1) + + def test_handle_connect_namespace(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + handler = mock.MagicMock() + s.on('connect', handler, namespace='/foo') + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0/foo')) + handler.assert_called_once_with('123', 'environ') + s.manager.connect.assert_any_call('123', '/') + s.manager.connect.assert_any_call('123', '/foo') + s.eio.send.mock.assert_any_call('123', '0/foo', binary=False) + + def test_handle_connect_rejected(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + handler = mock.MagicMock(return_value=False) + s.on('connect', handler) + _run(s._handle_eio_connect('123', 'environ')) + handler.assert_called_once_with('123', 'environ') + self.assertEqual(s.manager.connect.call_count, 1) + self.assertEqual(s.manager.disconnect.call_count, 1) + s.eio.send.mock.assert_called_once_with('123', '4', binary=False) + + def test_handle_connect_namespace_rejected(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + handler = mock.MagicMock(return_value=False) + s.on('connect', handler, namespace='/foo') + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0/foo')) + self.assertEqual(s.manager.connect.call_count, 2) + self.assertEqual(s.manager.disconnect.call_count, 1) + s.eio.send.mock.assert_any_call('123', '4/foo', binary=False) + + def test_handle_disconnect(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + handler = mock.MagicMock() + s.on('disconnect', handler) + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_disconnect('123')) + handler.assert_called_once_with('123') + s.manager.disconnect.assert_called_once_with('123', '/') + self.assertEqual(s.environ, {}) + + def test_handle_disconnect_namespace(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.manager.get_namespaces = mock.MagicMock(return_value=['/', '/foo']) + handler = mock.MagicMock() + s.on('disconnect', handler) + handler_namespace = mock.MagicMock() + s.on('disconnect', handler_namespace, namespace='/foo') + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0/foo')) + _run(s._handle_eio_disconnect('123')) + handler.assert_called_once_with('123') + handler_namespace.assert_called_once_with('123') + self.assertEqual(s.environ, {}) + + def test_handle_disconnect_only_namespace(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.manager.get_namespaces = mock.MagicMock(return_value=['/', '/foo']) + handler = mock.MagicMock() + s.on('disconnect', handler) + handler_namespace = mock.MagicMock() + s.on('disconnect', handler_namespace, namespace='/foo') + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0/foo')) + _run(s._handle_eio_message('123', '1/foo')) + self.assertEqual(handler.call_count, 0) + handler_namespace.assert_called_once_with('123') + self.assertEqual(s.environ, {'123': 'environ'}) + + def test_handle_disconnect_unknown_client(self, eio): + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + _run(s._handle_eio_disconnect('123')) + + def test_handle_event(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + handler = AsyncMock() + s.on('my message', handler) + _run(s._handle_eio_message('123', '2["my message","a","b","c"]')) + handler.mock.assert_called_once_with('123', 'a', 'b', 'c') + + def test_handle_event_with_namespace(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + handler = mock.MagicMock() + s.on('my message', handler, namespace='/foo') + _run(s._handle_eio_message('123', '2/foo,["my message","a","b","c"]')) + handler.assert_called_once_with('123', 'a', 'b', 'c') + + def test_handle_event_binary(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + handler = mock.MagicMock() + s.on('my message', handler) + _run(s._handle_eio_message('123', '52-["my message","a",' + '{"_placeholder":true,"num":1},' + '{"_placeholder":true,"num":0}]')) + _run(s._handle_eio_message('123', b'foo')) + _run(s._handle_eio_message('123', b'bar')) + handler.assert_called_once_with('123', 'a', b'bar', b'foo') + + def test_handle_event_binary_ack(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + s.manager.initialize(s) + _run(s._handle_eio_message('123', '61-321["my message","a",' + '{"_placeholder":true,"num":0}]')) + _run(s._handle_eio_message('123', b'foo')) + mgr.trigger_callback.mock.assert_called_once_with( + '123', '/', 321, ['my message', 'a', b'foo']) + + def test_handle_event_with_ack(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + handler = mock.MagicMock(return_value='foo') + s.on('my message', handler) + _run(s._handle_eio_message('123', '21000["my message","foo"]')) + handler.assert_called_once_with('123', 'foo') + s.eio.send.mock.assert_called_once_with('123', '31000["foo"]', + binary=False) + + def test_handle_event_with_ack_none(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + handler = mock.MagicMock(return_value=None) + s.on('my message', handler) + _run(s._handle_eio_message('123', '21000["my message","foo"]')) + handler.assert_called_once_with('123', 'foo') + s.eio.send.mock.assert_called_once_with('123', '31000[]', + binary=False) + + def test_handle_event_with_ack_tuple(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + handler = mock.MagicMock(return_value=(1, '2', True)) + s.on('my message', handler) + _run(s._handle_eio_message('123', '21000["my message","a","b","c"]')) + handler.assert_called_once_with('123', 'a', 'b', 'c') + s.eio.send.mock.assert_called_once_with('123', '31000[1,"2",true]', + binary=False) + + def test_handle_event_with_ack_list(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr) + handler = mock.MagicMock(return_value=[1, '2', True]) + s.on('my message', handler) + _run(s._handle_eio_message('123', '21000["my message","a","b","c"]')) + handler.assert_called_once_with('123', 'a', 'b', 'c') + s.eio.send.mock.assert_called_once_with('123', '31000[[1,"2",true]]', + binary=False) + + def test_handle_event_with_ack_binary(self, eio): + eio.return_value.send = AsyncMock() + mgr = self._get_mock_manager() + s = asyncio_server.AsyncServer(client_manager=mgr, binary=True) + handler = mock.MagicMock(return_value=b'foo') + s.on('my message', handler) + _run(s._handle_eio_message('123', '21000["my message","foo"]')) + handler.assert_any_call('123', 'foo') + + def test_handle_error_packet(self, eio): + s = asyncio_server.AsyncServer() + self.assertRaises(ValueError, _run, s._handle_eio_message('123', '4')) + + def test_handle_invalid_packet(self, eio): + s = asyncio_server.AsyncServer() + self.assertRaises(ValueError, _run, s._handle_eio_message('123', '9')) + + def test_send_with_ack(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._handle_eio_connect('123', 'environ')) + cb = mock.MagicMock() + id1 = s.manager._generate_ack_id('123', '/', cb) + id2 = s.manager._generate_ack_id('123', '/', cb) + _run(s._emit_internal('123', 'my event', ['foo'], id=id1)) + _run(s._emit_internal('123', 'my event', ['bar'], id=id2)) + _run(s._handle_eio_message('123', '31["foo",2]')) + cb.assert_called_once_with('foo', 2) + + def test_send_with_ack_namespace(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0/foo')) + cb = mock.MagicMock() + id = s.manager._generate_ack_id('123', '/foo', cb) + _run(s._emit_internal('123', 'my event', ['foo'], namespace='/foo', + id=id)) + _run(s._handle_eio_message('123', '3/foo,1["foo",2]')) + cb.assert_called_once_with('foo', 2) + + def test_disconnect(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._handle_eio_connect('123', 'environ')) + _run(s.disconnect('123')) + s.eio.send.mock.assert_any_call('123', '1', binary=False) + + def test_disconnect_namespace(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0/foo')) + _run(s.disconnect('123', namespace='/foo')) + s.eio.send.mock.assert_any_call('123', '1/foo', binary=False) + + def test_disconnect_twice(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._handle_eio_connect('123', 'environ')) + _run(s.disconnect('123')) + calls = s.eio.send.mock.call_count + _run(s.disconnect('123')) + self.assertEqual(calls, s.eio.send.mock.call_count) + + def test_disconnect_twice_namespace(self, eio): + eio.return_value.send = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0/foo')) + _run(s.disconnect('123', namespace='/foo')) + calls = s.eio.send.mock.call_count + _run(s.disconnect('123', namespace='/foo')) + self.assertEqual(calls, s.eio.send.mock.call_count) + + def test_namespace_handler(self, eio): + eio.return_value.send = AsyncMock() + result = {} + + class MyNamespace(asyncio_namespace.AsyncNamespace): + def on_connect(self, sid, environ): + result['result'] = (sid, environ) + + @coroutine + def on_disconnect(self, sid): + result['result'] = ('disconnect', sid) + + @coroutine + def on_foo(self, sid, data): + result['result'] = (sid, data) + + def on_bar(self, sid): + result['result'] = 'bar' + + @coroutine + def on_baz(self, sid, data1, data2): + result['result'] = (data1, data2) + + s = asyncio_server.AsyncServer() + s.register_namespace(MyNamespace('/foo')) + _run(s._handle_eio_connect('123', 'environ')) + _run(s._handle_eio_message('123', '0/foo')) + self.assertEqual(result['result'], ('123', 'environ')) + _run(s._handle_eio_message('123', '2/foo,["foo","a"]')) + self.assertEqual(result['result'], ('123', 'a')) + _run(s._handle_eio_message('123', '2/foo,["bar"]')) + self.assertEqual(result['result'], 'bar') + _run(s._handle_eio_message('123', '2/foo,["baz","a","b"]')) + self.assertEqual(result['result'], ('a', 'b')) + _run(s.disconnect('123', '/foo')) + self.assertEqual(result['result'], ('disconnect', '123')) + + def test_bad_namespace_handler(self, eio): + class Dummy(object): + pass + + class SyncNS(namespace.Namespace): + pass + + s = asyncio_server.AsyncServer() + self.assertRaises(ValueError, s.register_namespace, 123) + self.assertRaises(ValueError, s.register_namespace, Dummy) + self.assertRaises(ValueError, s.register_namespace, Dummy()) + self.assertRaises(ValueError, s.register_namespace, + namespace.Namespace) + self.assertRaises(ValueError, s.register_namespace, SyncNS()) + + def test_logger(self, eio): + s = asyncio_server.AsyncServer(logger=False) + self.assertEqual(s.logger.getEffectiveLevel(), logging.ERROR) + s.logger.setLevel(logging.NOTSET) + s = asyncio_server.AsyncServer(logger=True) + self.assertEqual(s.logger.getEffectiveLevel(), logging.INFO) + s.logger.setLevel(logging.WARNING) + s = asyncio_server.AsyncServer(logger=True) + self.assertEqual(s.logger.getEffectiveLevel(), logging.WARNING) + s.logger.setLevel(logging.NOTSET) + s = asyncio_server.AsyncServer(logger='foo') + self.assertEqual(s.logger, 'foo') + + def test_engineio_logger(self, eio): + asyncio_server.AsyncServer(engineio_logger='foo') + eio.assert_called_once_with(**{'logger': 'foo', + 'async_handlers': False}) + + def test_custom_json(self, eio): + # Warning: this test cannot run in parallel with other tests, as it + # changes the JSON encoding/decoding functions + + class CustomJSON(object): + @staticmethod + def dumps(*args, **kwargs): + return '*** encoded ***' + + @staticmethod + def loads(*args, **kwargs): + return '+++ decoded +++' + + asyncio_server.AsyncServer(json=CustomJSON) + eio.assert_called_once_with(**{'json': CustomJSON, + 'async_handlers': False}) + + pkt = packet.Packet(packet_type=packet.EVENT, + data={six.text_type('foo'): six.text_type('bar')}) + self.assertEqual(pkt.encode(), '2*** encoded ***') + pkt2 = packet.Packet(encoded_packet=pkt.encode()) + self.assertEqual(pkt2.data, '+++ decoded +++') + + # restore the default JSON module + packet.Packet.json = json + + def test_start_background_task(self, eio): + eio.return_value.start_background_task = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s.start_background_task('foo', 'bar', baz='baz')) + s.eio.start_background_task.mock.assert_called_once_with('foo', 'bar', + baz='baz') + + def test_sleep(self, eio): + eio.return_value.sleep = AsyncMock() + s = asyncio_server.AsyncServer() + _run(s.sleep(1.23)) + s.eio.sleep.mock.assert_called_once_with(1.23) diff --git a/tests/test_server.py b/tests/test_server.py index 62262ee..b646a27 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -180,6 +180,8 @@ class TestServer(unittest.TestCase): s.manager.connect.assert_called_once_with('123', '/') s.eio.send.assert_called_once_with('123', '0', binary=False) self.assertEqual(mgr.initialize.call_count, 1) + s._handle_eio_connect('456', 'environ') + self.assertEqual(mgr.initialize.call_count, 1) def test_handle_connect_namespace(self, eio): mgr = mock.MagicMock() @@ -439,12 +441,17 @@ class TestServer(unittest.TestCase): class Dummy(object): pass + class AsyncNS(namespace.Namespace): + def is_asyncio_based(self): + return True + s = server.Server() self.assertRaises(ValueError, s.register_namespace, 123) self.assertRaises(ValueError, s.register_namespace, Dummy) self.assertRaises(ValueError, s.register_namespace, Dummy()) self.assertRaises(ValueError, s.register_namespace, namespace.Namespace) + self.assertRaises(ValueError, s.register_namespace, AsyncNS()) def test_logger(self, eio): s = server.Server(logger=False)