1
# -*- coding: utf-8 -*-
3
# Copyright 2011-2012 Canonical Ltd.
4
# Copyright 2015-2016 Chicharreros (https://launchpad.net/~chicharreros)
6
# This program is free software: you can redistribute it and/or modify it
7
# under the terms of the GNU General Public License version 3, as published
8
# by the Free Software Foundation.
10
# This program is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranties of
12
# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
13
# PURPOSE. See the GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License along
16
# with this program. If not, see <http://www.gnu.org/licenses/>.
18
# In addition, as a special exception, the copyright holders give
19
# permission to link the code of portions of this program with the
20
# OpenSSL library under certain conditions as described in each
21
# individual source file, and distribute linked combinations
23
# You must obey the GNU General Public License in all respects
24
# for all of the code used other than OpenSSL. If you modify
25
# file(s) with this exception, you may extend this exception to your
26
# version of the file(s), but you are not obligated to do so. If you
27
# do not wish to do so, delete this exception statement from your
28
# version. If you delete this exception statement from all source
29
# files in the program, then also delete it here.
31
"""Perspective Broker IPC test cases."""
35
from collections import namedtuple
37
from twisted.internet import defer
38
from twisted.spread.pb import (
42
from ubuntuone.devtools.handlers import MementoHandler
43
from ubuntuone.devtools.testcases import skipIfOS
44
from ubuntuone.devtools.testcases.txsocketserver import (
49
from ubuntuone.tests import TestCase
50
from ubuntuone.utils import ipc
53
TEST_SERVICE = 'foo-service'
54
TEST_CMDLINE = 'foo.bin'
55
TEST_SERVER_DESCRIPTION = 'tcp:40404:interface=127.0.0.1'
56
TEST_CLIENT_DESCRIPTION = 'tcp:host=127.0.0.1:port=40404'
59
class RandomException(Exception):
60
"""A random exception."""
63
class FakeActivationClient(object):
64
"""A fake ActivationClient."""
66
def __init__(self, config):
67
"""Initialize this fake instance."""
70
def get_active_client_description(self):
71
"""Return the description where the pb server is running."""
72
return defer.succeed(self.config.description.client)
75
class FakeActivationInstance(object):
76
"""A fake ActivationInstance."""
78
def __init__(self, config):
79
"""Initialize the fake instance."""
82
def get_server_description(self):
83
"""Return the description where the pb server is running."""
84
return defer.succeed(self.config.description.server)
87
class FakeDescriptionFactory(object):
88
"""A fake description factory."""
90
def __init__(self, server_description, client_description):
91
"""Create a new instace."""
92
self.server = server_description
93
self.client = client_description
96
class FakeReactor(object):
100
"""Initialize this faker."""
101
self.connection_class = namedtuple("Connection",
102
"host port factory backlog")
103
self.connections = []
105
def connectTCP(self, host, port, factory, timeout=None, bindAddress=None):
106
"""Store the connected factory."""
107
connection = self.connection_class(host, port, factory, None)
108
self.connections.append(connection)
110
def listenTCP(self, port, factory, interface=None, backlog=None):
111
"""Store the listenning factory."""
112
connection = self.connection_class(interface, port, factory, backlog)
113
self.connections.append(connection)
116
class FakeTCP4ClientEndpoint(object):
117
"""A fake tcp4 client."""
119
def __init__(self, protocol):
120
"""Create a new instance."""
121
self.protocol = protocol
123
def connect(self, *args, **kwargs):
124
"""Return the client."""
125
return defer.succeed(self.protocol)
128
class FakeRemoteClient(object):
129
"""A fake RemoteClient."""
131
missing_signal = "missing"
132
failing_signal = "failing"
134
random_exception = RandomException()
136
def __init__(self, dead_remote=False):
138
self.dead_remote = dead_remote
140
def callRemote(self, signal_name, *a, **kw):
141
"""Fake a call to a given remote method."""
143
raise DeadReferenceError("Calling Stale Broker")
145
if signal_name == self.missing_signal:
146
return defer.fail(NoSuchMethod())
147
if signal_name == self.failing_signal:
148
return defer.fail(self.random_exception)
150
self.called = (a, kw)
151
return defer.succeed(None)
154
class DummyRemoteService(ipc.RemoteService):
155
"""Represent a dummy IPC object."""
157
remote_calls = ['foo', 'bar']
162
self.Success(self.next_result)
164
def bar(self, error):
166
self.NotSuccess(error)
169
def Success(self, param):
170
"""Fire a signal to notify a success."""
173
def NotSuccess(self, error):
174
"""Fire a signal to notify a not-success."""
178
"""Get no args passed."""
181
def JustArgs(self, *args):
185
def JustKwargs(self, **kwargs):
186
"""Just get kwargs."""
189
def BothArgsAndKwargs(self, *args, **kwargs):
190
"""Both args and kwargs."""
193
class DummyService(ipc.BaseService):
194
"""Represent a dummy root service."""
196
services = {'dummy': DummyRemoteService}
197
name = 'Dummy Service'
198
description = TEST_CLIENT_DESCRIPTION
202
class DummyRemoteClient(ipc.RemoteClient):
203
"""Represent a dummy remote client."""
205
call_remote_functions = DummyRemoteService.remote_calls
206
signal_handlers = ['Success', 'NotSuccess']
209
class DummyClient(ipc.BaseClient):
210
"""Represent a dummy base client."""
212
clients = {'dummy': DummyRemoteClient}
213
service_name = DummyService.name
214
service_port = TEST_SERVER_DESCRIPTION
215
service_cmdline = DummyService.cmdline
218
class DummyDescription(object):
219
"""Return the descriptions accordingly."""
221
def __init__(self, client, server):
222
"""Create a new instance."""
227
class BaseIPCTestCase(TCPPbServerTestCase, TestCase):
228
"""Set the ipc to a random port for this instance."""
232
client_class = None # the BaseClient instance
233
service_class = None # the BaseService instance
235
remote_client_name = None # the name of the remote client in the client
236
remote_service_name = None # the name of the remote service in the service
241
@defer.inlineCallbacks
243
yield super(BaseIPCTestCase, self).setUp()
248
if self.service_class is not None:
250
self.service = self.service_class()
251
self.client = self.client_class()
253
# patch server connection and client connection to ensure that
254
# we have clean connections
256
@defer.inlineCallbacks
257
def server_listen(server_factory, service_name, activation_cmd,
258
description, reactor=None):
259
"""Connect to the local running service."""
260
yield self.listen_server(self.service)
261
defer.returnValue(self.listener)
263
self.patch(ipc, 'server_listen', server_listen)
265
@defer.inlineCallbacks
266
def client_connect(client_factory, service_name,
267
activation_cmdline, description, reactor=None):
268
"""Connect the local running client."""
269
yield self.connect_client()
270
self.client.factory = self.client_factory
271
defer.returnValue(self.connector)
273
self.patch(ipc, 'client_connect', client_connect)
275
yield self.service.start()
276
yield self.client.connect()
279
def remote_service(self):
280
"""Get the service named 'service_name'."""
281
return getattr(self.service, self.remote_service_name)
284
def remote_client(self):
285
"""Get the client named 'remote_client_name'."""
286
return getattr(self.client, self.remote_client_name)
288
@defer.inlineCallbacks
289
def assert_method_called(self, service, method, result, *args, **kwargs):
290
"""Check that calling 'method(*args, **kwargs)' should query 'service'.
292
The returned result from calling 'method(*args, **kwargs)' should be
293
equal to the given parameter 'result'. If 'result' is a deferred, its
294
result attribute will be used as expected result (ergo the deferred
295
should be already called).
298
client = self.remote_client
300
# hack to handle service methods returning a deferred with result
301
if isinstance(result, defer.Deferred):
302
real_result = result.result
306
self.patch(service, method, lambda *a, **kw: result)
307
actual = yield client.call_method(method, *args, **kwargs)
308
self.assertEqual(real_result, actual)
309
self.assertEqual(service.called, {method: [(args, kwargs)]})
311
def assert_remote_method(self, method_name, *args, **kwargs):
312
"""Assert that 'method_name' is a remote method.
314
The parameters args and kwargs will be passed as such to the method
315
itself, to exercise it.
318
self.assertIn(method_name, self.remote_service.remote_calls)
319
method = getattr(self.remote_service, method_name)
320
method(*args, **kwargs)
322
def assert_remote_signal(self, signal_name, *args, **kwargs):
323
"""Assert that 'signal' is a remote signal.
325
The parameters args and kwargs will be passed as such to the signal
326
itself, to exercise it.
329
self.patch(self.remote_service, 'emit_signal', self._set_called)
330
signal = getattr(self.remote_service, signal_name)
331
signal(*args, **kwargs)
333
expected = (signal_name,) + args
334
self.assertEqual(self._called, (expected, kwargs))
336
def test_remote_methods(self):
337
"""Check every method defined in self.method_mapping.
339
Assert that every method is a remote method and that it has the
343
for method, args, kwargs in self.method_mapping:
344
self.assert_remote_method(method, *args, **kwargs)
346
def test_remote_signals(self):
347
"""Check every signal defined in self.signal_mapping.
349
Assert that every signal is a remote signal and that it has the
353
for signal_name, args, kwargs in self.signal_mapping:
354
self.assert_remote_signal(signal_name, *args, **kwargs)
357
class TCPListenConnectTestCase(BaseIPCTestCase):
358
"""Test suite for the server_listen and client_connect methods."""
360
@defer.inlineCallbacks
362
yield super(TCPListenConnectTestCase, self).setUp()
363
self.fake_reactor = FakeReactor()
365
@defer.inlineCallbacks
366
def test_server_listen(self):
367
"""Test the server_listen function."""
368
self.patch(ipc, "ActivationInstance", FakeActivationInstance)
370
description_factory = FakeDescriptionFactory(TEST_SERVER_DESCRIPTION,
371
TEST_CLIENT_DESCRIPTION)
373
fake_factory = object()
374
yield ipc.server_listen(fake_factory, TEST_SERVICE,
375
TEST_CMDLINE, description_factory,
376
reactor=self.fake_reactor)
378
self.assertEqual(len(self.fake_reactor.connections), 1)
379
conn = self.fake_reactor.connections[0]
380
self.assertEqual(conn.factory, fake_factory)
381
self.assertEqual(conn.host, ipc.LOCALHOST)
383
@defer.inlineCallbacks
384
def test_client_connect(self):
385
"""Test the client_connect function."""
387
self.patch(ipc, "ActivationClient", FakeActivationClient)
389
protocol = 'protocol'
390
client = FakeTCP4ClientEndpoint(protocol)
392
def client_from_string(reactor, description):
393
"""Create a client from the given string."""
394
called.append(('clientFromString', reactor, description))
397
self.patch(ipc.endpoints, 'clientFromString', client_from_string)
399
description_factory = FakeDescriptionFactory(TEST_SERVER_DESCRIPTION,
400
TEST_CLIENT_DESCRIPTION)
402
fake_factory = object()
403
returned_protocol = yield ipc.client_connect(fake_factory,
407
reactor=self.fake_reactor)
409
self.assertIn(('clientFromString', self.fake_reactor,
410
description_factory.client), called)
411
self.assertEqual(protocol, returned_protocol)
414
@skipIfOS('win32', 'Unix domain sockets not supported on windows.')
415
class DomainListenConnectTestCase(TCPListenConnectTestCase):
416
"""Test suite for the server_listen and client_connect methods."""
418
def get_server(self):
419
"""Return the server to be used to run the tests."""
420
return TidyUnixServer()
423
class TCPDummyClientTestCase(BaseIPCTestCase):
424
"""Test the status client class."""
426
client_class = DummyClient
427
service_class = DummyService
429
remote_client_name = remote_service_name = 'dummy'
433
('bar', (object(),), {}),
436
('Success', ('test',), {}),
437
('NotSuccess', ('yadda',), {}),
439
('JustArgs', (object(), 'foo'), {}),
440
('JustKwargs', (), {'foo': 'bar'}),
441
('BothArgsAndKwargs', ('zaraza', 8), {'foo': -42}),
444
@defer.inlineCallbacks
445
def test_deprecated_siganl_is_also_sent(self):
446
"""Old-style, deprecated signals handler are also called."""
447
d1 = defer.Deferred()
448
d2 = defer.Deferred()
450
self.remote_service.next_result = 'yadda'
452
# old, deprecated way
453
self.remote_client.connect_to_signal('Success', d1.callback)
454
self.remote_client.on_success_cb = d2.callback
456
self.remote_client.foo()
458
result = yield defer.gatherResults([d1, d2])
460
self.assertEqual(result, ['yadda', 'yadda'])
462
@defer.inlineCallbacks
463
def test_register_to_signals(self):
464
"""Test the register_to_signals method."""
465
yield self.remote_client.register_to_signals()
466
self.addCleanup(self.remote_client.unregister_to_signals)
468
for signal in self.remote_client.signal_handlers:
469
self.assertIn(signal, self.service.dummy.clients_per_signal)
471
@defer.inlineCallbacks
472
def test_unregister_to_signals(self):
473
"""Test the register_to_signals method."""
474
yield self.remote_client.register_to_signals()
475
yield self.remote_client.unregister_to_signals()
477
for signal in self.remote_client.signal_handlers:
478
actual = len(self.remote_service.clients_per_signal[signal])
479
self.assertEqual(0, actual)
482
@skipIfOS('win32', 'Unix domain sockets not supported on windows.')
483
class DomainDummyClientTestCase(TCPDummyClientTestCase):
484
"""Test the status client class."""
486
def get_server(self):
487
"""Return the server to be used to run the tests."""
488
return TidyUnixServer()
491
class RemoteMetaTestCase(TestCase):
492
"""Tests for the RemoteMeta metaclass."""
494
def test_remote_calls_renamed(self):
495
"""The remote_calls are renamed."""
496
test_token = object()
498
class TestClass(ipc.meta_base(ipc.RemoteMeta)):
499
"""A class for testing."""
501
remote_calls = ['test_method']
503
def test_method(self):
508
self.assertEquals(tc.test_method(), test_token)
509
self.assertEquals(tc.remote_test_method(), test_token)
512
class SignalBroadcasterTestCase(TestCase):
513
"""Test the signal broadcaster code."""
515
@defer.inlineCallbacks
517
yield super(SignalBroadcasterTestCase, self).setUp()
518
self.client = FakeRemoteClient()
519
self.sb = ipc.SignalBroadcaster()
521
self.memento = MementoHandler()
522
ipc.logger.addHandler(self.memento)
523
ipc.logger.setLevel(logging.DEBUG)
524
self.addCleanup(ipc.logger.removeHandler, self.memento)
526
def test_remote_register_to_signals(self):
527
"""Assert that the client was added."""
528
signals = ["demo_signal1", "demo_signal2"]
529
self.sb.remote_register_to_signals(self.client, signals)
530
for signal in signals:
531
clients = self.sb.clients_per_signal[signal]
532
self.assertTrue(self.client in clients)
534
def test_emit_signal(self):
535
"""Assert that the client method was called."""
539
signal_name = 'on_test'
541
self.client.callRemote(signal_name, first, second, word=word)
543
signals = [signal_name]
544
self.sb.remote_register_to_signals(self.client, signals)
545
self.sb.emit_signal(signal_name, first, second, foo=word)
547
self.assertEqual(self.client.called, ((first, second), dict(foo=word)))
549
def test_emit_signal_dead_reference(self):
550
"""Test dead reference while emitting a signal."""
551
sample_signal = "sample_signal"
552
fake_remote_client = FakeRemoteClient(dead_remote=True)
554
self.sb.remote_register_to_signals(fake_remote_client, [sample_signal])
555
self.assertIn(fake_remote_client,
556
self.sb.clients_per_signal[sample_signal])
558
self.sb.emit_signal(sample_signal)
559
self.assertNotIn(fake_remote_client,
560
self.sb.clients_per_signal[sample_signal])
562
def test_emit_signal_some_dead_some_not(self):
563
"""Test a clean reference after a dead one."""
564
sample_signal = "sample_signal"
565
fake_dead_remote = FakeRemoteClient(dead_remote=True)
566
fake_alive_remote = FakeRemoteClient()
568
self.sb.remote_register_to_signals(fake_dead_remote, [sample_signal])
569
self.sb.remote_register_to_signals(fake_alive_remote, [sample_signal])
570
self.sb.emit_signal(sample_signal)
572
self.assertTrue(fake_alive_remote.called, "The alive must be called.")
574
def test_emit_signal_ignore_missing_handlers(self):
575
"""A missing signal handler should just log a debug line."""
576
fake_remote_client = FakeRemoteClient()
578
signal = fake_remote_client.missing_signal
580
self.sb.remote_register_to_signals(fake_remote_client, signals)
581
sb_clients = self.sb.clients_per_signal[signal]
582
self.assertIn(fake_remote_client, sb_clients)
583
self.sb.emit_signal(signal)
585
expected = ipc.SignalBroadcaster.MSG_NO_SIGNAL_HANDLER % (
589
self.assertTrue(self.memento.check_debug(*expected))
591
def test_emit_signal_log_other_errors(self):
592
"""Other errors should be logged as warnings."""
593
fake_remote_client = FakeRemoteClient()
595
signal = fake_remote_client.failing_signal
597
self.sb.remote_register_to_signals(fake_remote_client, signals)
598
sb_clients = self.sb.clients_per_signal[signal]
599
self.assertIn(fake_remote_client, sb_clients)
600
self.sb.emit_signal(signal)
602
expected = ipc.SignalBroadcaster.MSG_COULD_NOT_EMIT_SIGNAL % (
605
fake_remote_client.random_exception,
607
self.assertTrue(self.memento.check_warning(*expected))
610
class FakeRootObject(object):
611
"""A fake root object."""
613
def __init__(self, called, remote_obj):
614
"""Create a new instance."""
616
self.remote_obj = remote_obj
618
def callRemote(self, method_name):
619
"""A fake call remove method."""
620
self.called.append(method_name)
621
return defer.succeed(self.remote_obj)
624
class FakeWorkingRemoteClient(object):
625
"""A fake remote client."""
627
def __init__(self, called):
628
"""Create a new instance."""
632
def register_to_signals(self):
633
"""Register to signals."""
634
self.called.append('register_to_signals')
635
return defer.succeed(True)
638
class ReconnectTestCase(TestCase):
639
"""Test the reconnection when service is dead."""
641
@defer.inlineCallbacks
643
"""Set the different tests."""
644
yield super(ReconnectTestCase, self).setUp()
646
self.remote_obj = 'remote'
647
self.root_obj = FakeRootObject(self.called, self.remote_obj)
649
def fake_get_root_object():
650
"""Fake getting the root object."""
651
self.called.append('getRootObject')
652
return defer.succeed(self.root_obj)
654
def fake_client_connect(factory, service_name, cmd, description):
655
"""Fake the client connect."""
656
self.called.append('client_connect')
657
self.patch(factory, 'getRootObject', fake_get_root_object)
658
return defer.succeed(True)
660
self.patch(ipc, 'client_connect', fake_client_connect)
662
@defer.inlineCallbacks
663
def test_reconnect_method(self):
664
"""Test the execcution of the reconnect method."""
665
clients = dict(first=FakeWorkingRemoteClient(self.called),
666
second=FakeWorkingRemoteClient(self.called))
668
base_client = ipc.BaseClient()
669
base_client.clients = clients
670
for name, client in clients.items():
671
setattr(base_client, name, client)
673
yield base_client.reconnect()
674
# assert that we did call the correct methods
675
self.assertIn('client_connect', self.called)
676
self.assertIn('getRootObject', self.called)
679
self.assertIn('get_%s' % name, self.called)
681
self.assertEqual(len(clients),
682
self.called.count('register_to_signals'))