Merge lp:~mandel/ubuntuone-dev-tools/tcp-testcases into lp:ubuntuone-dev-tools

Proposed by Manuel de la Peña on 2012-03-28
Status: Merged
Approved by: Alejandro J. Cura on 2012-04-13
Approved revision: 75
Merged at revision: 61
Proposed branch: lp:~mandel/ubuntuone-dev-tools/tcp-testcases
Merge into: lp:ubuntuone-dev-tools
Diff against target: 656 lines (+647/-0)
2 files modified
ubuntuone/devtools/testcases/tests/test_txtcpserver.py (+417/-0)
ubuntuone/devtools/testcases/txtcpserver.py (+230/-0)
To merge this branch: bzr merge lp:~mandel/ubuntuone-dev-tools/tcp-testcases
Reviewer Review Type Date Requested Status
Alejandro J. Cura (community) 2012-03-28 Approve on 2012-04-13
Manuel de la Peña (community) Abstain on 2012-04-13
dobey (community) 2012-03-28 Approve on 2012-04-13
Review via email: mp+99759@code.launchpad.net

Commit Message

- Added a two new test cases that allow to start a server and connect a client in a safe manner (LP: #963082)

Description of the Change

- Added a two new test cases that allow to start a service and connect a client in a safe manner (LP: #963082)

To post a comment you must log in.
Alejandro J. Cura (alecu) wrote :

444 +def server_protocol_factory(cls=protocol.Protocol):
.
.
.
465 +def client_protocol_factory(cls=protocol.Protocol):

Since the above functions are never used with optional arguments, they should take only one mandatory argument.
----
479 + ClientSaveProtocol.connected_clients.append(self)

What's the list of connected_clients used for?
Instead of a class variable of ClientSaveProtocol, it should be an instance variable of the twisted factory that creates all these instances, and that's usually available as "self.factory" inside the protocol instnace.
----
"is a complicated manner." -> "is a complicated matter."
"arre" -> "are"
'%shas not started' <- missing space

review: Needs Fixing
65. By Manuel de la Peña on 2012-04-09

Fixed code according to the reviews.

dobey (dobey) wrote :

38 +class Aditioner(pb.Referenceable):
39 + """A remote aditioner."""

Bad spelling. And "Additioner" isn't a word. Maybe "Adder" would be what you want to call it?

Also, I am not fond of having the module be "tx" as it's not a general twisted module, but for a very specific feature which is provided by twisted. Perhaps "twistedpb" would be a better name. Maybe I'm wrong, but this seems like a specific set of test cases for use with testing usage of the IPC mechanism we've built for use on Windows using TCP. The naming should better reflect that, I think.

7 +# Copyright 2011 Canonical Ltd.
419 +# Copyright 2011 Canonical Ltd.

The year is not 2011 any more. Stop living in the past!

Also, all copyright headers need to include the block of text for the OpenSSL exception. You can see it in the other source files now, as well as LICENSE in the root of the tree.

492 +class SaveServiceRunner(object):

Can you please explain the usage of the terms "Save" and "Service" in this code? It seems to me for "Service" you mean "twisted pb server" though it isn't entirely clear. And I have no idea what "Save" is supposed to mean in any of the contexts here.

review: Needs Fixing
66. By Manuel de la Peña on 2012-04-12

Made changes according to review.

67. By Manuel de la Peña on 2012-04-12

Fixed errors during the merge.

68. By Manuel de la Peña on 2012-04-12

Merged with trunk updated license.

69. By Manuel de la Peña on 2012-04-12

Use the correct dates.

dobey (dobey) wrote :

I still see plenty usage of "save" and "Save" in the code. Is this not supposed to be "safe" and "Safe" instead?

Alejandro J. Cura (alecu) wrote :

Great work on this branch, mandel!

I love that the SafeTCPServer internally handles the port and that there's no need for the user of this classes to worry about that.

I'm going to ask for a change, though I'm already hearing you cursing from afar :-)

So, looking at test_multiple_services it seems that only one SafeTCPServer is used for both listen_server calls. I think this adds a lot of seemingly unneeded complexity in SafeTCPServer, that needs the self.server_factories dictionary and associated dict entries for each connection, and then each client needs to specify which server it wants to connect to as the first parameter to connect_client(). This also limits to one the number of servers of a given class that can be started with each SafeTCPServer.

My requested change is to simplify the API so, instead of having a SafeTCPServer for multiple ServerFactories...

        self.tcp_server = SafeTCPServer()
        calculator_s = self.tcp_server.listen_server(pb.PBServerFactory, self.calculator)
        self.addCleanup(self.tcp_server.clean_up, calculator_s)
        calculator_c = self.tcp_server.connect_client(calculator_s, pb.PBClientFactory)

... we would have one SafeTCPServer *per* ServerFactory, like this:

        self.tcp_server = SafeTCPServer(pb.PBServerFactory, self.calculator)
        self.addCleanup(self.tcp_server.clean_up)
        calculator_c = self.tcp_server.connect_client(pb.PBClientFactory)

The above change means that a lot of code and complexity in SafeTCPServer can be chopped by removing the self.server_factories dictionary.

Even better it would be to be allow multiple client connections to the same SafeTCPServer, but I think that it can wait till a later branch.

Oh, and two smaller issues:

1) I think we should somehow mark the instance attributes added to the factories so they don't collide with similar attributes that may be added by the code being tested. For instance, instead of .disconnecting or .on_connection_lost, the instance attributes added could be named something like _testserver_disconnecting, ._testserver_on_connection_lost.

2) I agree with dobey regarding the "Safe" and "Save" terminology. Instead of "SafeTCPServer" we should find another name that conveys a bit better what this classes are about. My (admittedly also ugly) proposals: "CleanableTCPServer" or "CloseableTCPServer".

review: Needs Fixing
Manuel de la Peña (mandel) wrote :

> Great work on this branch, mandel!
>
> I love that the SafeTCPServer internally handles the port and that there's no
> need for the user of this classes to worry about that.
>
> I'm going to ask for a change, though I'm already hearing you cursing from
> afar :-)
>
> So, looking at test_multiple_services it seems that only one SafeTCPServer is
> used for both listen_server calls. I think this adds a lot of seemingly
> unneeded complexity in SafeTCPServer, that needs the self.server_factories
> dictionary and associated dict entries for each connection, and then each
> client needs to specify which server it wants to connect to as the first
> parameter to connect_client(). This also limits to one the number of servers
> of a given class that can be started with each SafeTCPServer.
>
> My requested change is to simplify the API so, instead of having a
> SafeTCPServer for multiple ServerFactories...
>
> self.tcp_server = SafeTCPServer()
> calculator_s = self.tcp_server.listen_server(pb.PBServerFactory,
> self.calculator)
> self.addCleanup(self.tcp_server.clean_up, calculator_s)
> calculator_c = self.tcp_server.connect_client(calculator_s,
> pb.PBClientFactory)
>
> ... we would have one SafeTCPServer *per* ServerFactory, like this:
>
> self.tcp_server = SafeTCPServer(pb.PBServerFactory, self.calculator)
> self.addCleanup(self.tcp_server.clean_up)
> calculator_c = self.tcp_server.connect_client(pb.PBClientFactory)
>
> The above change means that a lot of code and complexity in SafeTCPServer can
> be chopped by removing the self.server_factories dictionary.

Sure, no problem what so ever, will be ready by the time you are back :)

>
> Even better it would be to be allow multiple client connections to the same
> SafeTCPServer, but I think that it can wait till a later branch.
>
> Oh, and two smaller issues:
>
> 1) I think we should somehow mark the instance attributes added to the
> factories so they don't collide with similar attributes that may be added by
> the code being tested. For instance, instead of .disconnecting or
> .on_connection_lost, the instance attributes added could be named something
> like _testserver_disconnecting, ._testserver_on_connection_lost.
>

Ok

> 2) I agree with dobey regarding the "Safe" and "Save" terminology. Instead of
> "SafeTCPServer" we should find another name that conveys a bit better what
> this classes are about. My (admittedly also ugly) proposals:
> "CleanableTCPServer" or "CloseableTCPServer".

Does sound terrible, what a bout TidyTCPServer?

70. By Manuel de la Peña on 2012-04-13

Done the following per reviews:
1. Simplified the API so that the service runner only runs one service.
2. Renamed the deferreds so that the names are more explicit to avoid colisions.
3. Renamed the runner to use 'tidy' whichs explains better its use.

71. By Manuel de la Peña on 2012-04-13

clean_up does notlonger need to get the factory to clean.

72. By Manuel de la Peña on 2012-04-13

Use tidy not save/safe.

73. By Manuel de la Peña on 2012-04-13

Do not use save.

dobey (dobey) wrote :

+class ServerSaveProtocolTestCase(ProtocolTestCase):
+class ClientSaveProtocolTestCase(ProtocolTestCase):

These 2 are left. ;)

review: Needs Fixing
dobey (dobey) wrote :

+class MultipleSercicesTestCase(TestCase):

Also, just caught this spelling faux pas. But weren't we also going to use the term "Server" for all these as well?

74. By Manuel de la Peña on 2012-04-13

Do no use save.

75. By Manuel de la Peña on 2012-04-13

Remove the use of the word service, is misleading.

dobey (dobey) wrote :

Seems ok to me now.

review: Approve
review: Abstain
Alejandro J. Cura (alecu) wrote :

Love this branch.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'ubuntuone/devtools/testcases/tests/test_txtcpserver.py'
2--- ubuntuone/devtools/testcases/tests/test_txtcpserver.py 1970-01-01 00:00:00 +0000
3+++ ubuntuone/devtools/testcases/tests/test_txtcpserver.py 2012-04-13 17:08:21 +0000
4@@ -0,0 +1,417 @@
5+# -*- coding: utf-8 -*-
6+# Copyright 2012 Canonical Ltd.
7+#
8+# This program is free software: you can redistribute it and/or modify it
9+# under the terms of the GNU General Public License version 3, as published
10+# by the Free Software Foundation.
11+#
12+# This program is distributed in the hope that it will be useful, but
13+# WITHOUT ANY WARRANTY; without even the implied warranties of
14+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15+# PURPOSE. See the GNU General Public License for more details.
16+#
17+# You should have received a copy of the GNU General Public License along
18+# with this program. If not, see <http://www.gnu.org/licenses/>.
19+#
20+# In addition, as a special exception, the copyright holders give
21+# permission to link the code of portions of this program with the
22+# OpenSSL library under certain conditions as described in each
23+# individual source file, and distribute linked combinations
24+# including the two.
25+# You must obey the GNU General Public License in all respects
26+# for all of the code used other than OpenSSL. If you modify
27+# file(s) with this exception, you may extend this exception to your
28+# version of the file(s), but you are not obligated to do so. If you
29+# do not wish to do so, delete this exception statement from your
30+# version. If you delete this exception statement from all source
31+# files in the program, then also delete it here.
32+
33+"""Test the twisted test cases."""
34+
35+from twisted.internet import defer, protocol
36+from twisted.spread import pb
37+from twisted.trial.unittest import TestCase
38+
39+from ubuntuone.devtools.testcases.txtcpserver import (
40+ client_protocol_factory,
41+ server_protocol_factory,
42+ PbServerTestCase,
43+ TidyTCPServer,
44+ TCPServerTestCase,
45+)
46+
47+# no init
48+# pylint: disable=W0232
49+
50+
51+class Adder(pb.Referenceable):
52+ """A remote adder."""
53+
54+ def remote_add(self, first, second):
55+ """Remote adding numbers."""
56+ return first + second
57+
58+
59+class Calculator(pb.Root):
60+ """A calculator ran somewhere on the net."""
61+
62+ def __init__(self, adder):
63+ """Create a new instance."""
64+ # pb.Root has no __init__
65+ self.adder = adder
66+
67+ def remote_get_adder(self):
68+ """Get the remote added."""
69+ return self.adder
70+
71+ def remote_check_adder(self, other_adder):
72+ """Check if the are the same."""
73+ return self.adder == other_adder
74+
75+
76+class Echoer(pb.Root):
77+ """An echoer that repeats what we say."""
78+
79+ def remote_say(self, sentence):
80+ """Echo what we want to say."""
81+ return 'Echoer: %s' % sentence
82+# pylint: enable=W0232
83+
84+
85+class FakeFactory(object):
86+ """A fake server/client factory."""
87+
88+ def __init__(self):
89+ """Create a new instance."""
90+ self._disconnecting = False
91+ self.testserver_on_connection_lost = defer.Deferred()
92+ self.testserver_on_connection_made = defer.Deferred()
93+
94+
95+# class_factory not callable
96+# pylint: disable=E1102
97+class ProtocolTestCase(TestCase):
98+ """Test the protocol classes."""
99+
100+ class_factory = None
101+
102+ @defer.inlineCallbacks
103+ def setUp(self):
104+ """Set the different tests."""
105+ yield super(ProtocolTestCase, self).setUp()
106+ self.called = []
107+
108+ def connection_lost(*args):
109+ """Fake connection lost method."""
110+ self.called.append('connectionLost')
111+
112+ self.patch(pb.Broker, 'connectionLost', connection_lost)
113+
114+ def connection_made(*args):
115+ """Fake connection made."""
116+ self.called.append('connectionMade')
117+
118+ self.patch(pb.Broker, 'connectionMade', connection_made)
119+
120+ def test_correct_inheritance(self):
121+ """Test that the super class is correct."""
122+ if self.class_factory:
123+ protocol_cls = self.class_factory(pb.Broker)
124+ protocol_instance = protocol_cls()
125+ self.assertIsInstance(protocol_instance, pb.Broker)
126+
127+ def test_correct_none_inheritance(self):
128+ """Test the inheritance when the class is none."""
129+ if self.class_factory:
130+ protocol_cls = self.class_factory(None)
131+ protocol_instance = protocol_cls()
132+ self.assertIsInstance(protocol_instance, protocol.Protocol)
133+
134+ def _assert_disconnecting(self, disconnecting):
135+ """Assert the disconnection."""
136+ if self.class_factory:
137+ protocol_cls = self.class_factory(pb.Broker)
138+ prot = protocol_cls()
139+ prot.factory = FakeFactory()
140+ prot.factory._disconnecting = disconnecting
141+ prot.connectionLost()
142+ self.assertIn('connectionLost', self.called,
143+ 'Super connectionLost most be called')
144+ self.assertEqual(disconnecting,
145+ prot.factory.testserver_on_connection_lost.called)
146+
147+ def test_connection_lost_disconnecting(self):
148+ """Test the connectionLost method."""
149+ self._assert_disconnecting(True)
150+
151+ def test_connection_lost_not_disconnecting(self):
152+ """Test the connectionLost method."""
153+ self._assert_disconnecting(False)
154+
155+ def test_connection_lost_called(self):
156+ """Test the connectionLost method."""
157+ if self.class_factory:
158+ protocol_cls = self.class_factory(pb.Broker)
159+ prot = protocol_cls()
160+ prot.factory = FakeFactory()
161+ prot.factory._disconnecting = True
162+ # call the deferred, if the code does not work we will get an
163+ # exception
164+ prot.factory.testserver_on_connection_lost.callback(True)
165+ prot.connectionLost()
166+ self.assertIn('connectionLost', self.called,
167+ 'Super connectionLost must be called')
168+# pylint: enable=E1102
169+
170+
171+class TidyServerProtocolTestCase(ProtocolTestCase):
172+ """Test the generated tidy protocol."""
173+
174+ class_factory = lambda _, *a: server_protocol_factory(*a)
175+
176+
177+# pylint: disable=E1101
178+class TidyClientProtocolTestCase(ProtocolTestCase):
179+ """Test the generated tidy protocol."""
180+
181+ class_factory = lambda _, *a: client_protocol_factory(*a)
182+
183+ def test_connection_made(self):
184+ """Test the connectionMade method."""
185+ # setting the factory here is to work around a pylint bug
186+ pb.Broker.factory = FakeFactory()
187+ protocol_cls = self.class_factory(pb.Broker)
188+ protocol_instance = protocol_cls()
189+ protocol_instance.connectionMade()
190+ self.assertIn('connectionMade', self.called,
191+ 'Super connectionMade must be called')
192+ self.assertTrue(
193+ protocol_instance.factory.testserver_on_connection_made.called)
194+
195+ # factory outside init
196+ def test_connection_made_called(self):
197+ """Test the connectionMade method."""
198+ # setting the factory here is to work around a pylint bug
199+ pb.Broker.factory = FakeFactory()
200+ protocol_cls = self.class_factory(pb.Broker)
201+
202+ protocol_instance = protocol_cls()
203+
204+ # call the deferred, if the code does not work we will get an
205+ # exception
206+ protocol_instance.factory.testserver_on_connection_made.callback(True)
207+ protocol_instance.connectionMade()
208+ self.assertIn('connectionMade', self.called,
209+ 'Super connectionMade must be called')
210+# pylint: enable=E1101
211+
212+
213+class TestPlainTwistedTestCase(TCPServerTestCase):
214+ """Test using a server.
215+
216+ This test class is not testing the server and client perse but testing
217+ that we can use a PbServerFactory and PbClientFactory in a way that the
218+ connection will be closed correctly.
219+ """
220+
221+ @defer.inlineCallbacks
222+ def setUp(self):
223+ """Set the diff tests."""
224+ yield super(TestPlainTwistedTestCase, self).setUp()
225+ self.adder = Adder()
226+ self.calculator = Calculator(self.adder)
227+ self.listen_server(pb.PBServerFactory, self.calculator)
228+ self.connect_client(pb.PBClientFactory)
229+ yield self.client_connected
230+
231+ @defer.inlineCallbacks
232+ def test_addition(self):
233+ """Test adding numbers."""
234+ first_number = 1
235+ second_number = 2
236+ calculator = yield self.client_factory.getRootObject()
237+ adder = yield calculator.callRemote('get_adder')
238+ result = yield adder.callRemote('add', first_number, second_number)
239+ self.assertEqual(first_number + second_number, result)
240+
241+ @defer.inlineCallbacks
242+ def test_check_adder(self):
243+ """Test comparing the adder."""
244+ calculator = yield self.client_factory.getRootObject()
245+ adder = yield calculator.callRemote('get_adder')
246+ check = yield calculator.callRemote('check_adder', adder)
247+ self.assertTrue(check)
248+
249+
250+class TestNoConnetionTrackingTestCase(TCPServerTestCase):
251+ """Test using a server.
252+
253+ This test class is not testing the server and the client perse but testing
254+ that we can use the PbServerFactory and PbClientFactory in a way that the
255+ connection will be closed and some of the actions performed in the setup
256+ and not recorded.
257+ """
258+
259+ @defer.inlineCallbacks
260+ def setUp(self):
261+ """Set the diff tests."""
262+ yield super(TestNoConnetionTrackingTestCase, self).setUp()
263+ self.adder = Adder()
264+ self.calculator = Calculator(self.adder)
265+ # connect client and server
266+ self.listen_server(pb.PBServerFactory, self.calculator)
267+ self.connect_client(pb.PBClientFactory)
268+
269+ # ensure we are connected
270+ yield self.client_connected
271+
272+ self.first_number = 1
273+ self.second_number = 2
274+ self.setup_result = None
275+
276+ # perform some actions before the tests
277+ calculator = yield self.client_factory.getRootObject()
278+ adder = yield calculator.callRemote('get_adder')
279+ self.setup_result = yield adder.callRemote('add',
280+ self.first_number,
281+ self.second_number)
282+
283+ def test_deferreds(self):
284+ """Test that the deferreds are not broken."""
285+ self.assertFalse(self.client_disconnected.called)
286+ self.assertFalse(self.server_disconnected.called)
287+
288+ @defer.inlineCallbacks
289+ def test_addition(self):
290+ """Test adding numbers."""
291+ first_number = 1
292+ second_number = 2
293+ calculator = yield self.client_factory.getRootObject()
294+ adder = yield calculator.callRemote('get_adder')
295+ result = yield adder.callRemote('add', first_number, second_number)
296+ self.assertEqual(first_number + second_number, result)
297+ self.assertEqual(self.setup_result, result)
298+
299+ @defer.inlineCallbacks
300+ def test_check_adder(self):
301+ """Test comparing the adder."""
302+ calculator = yield self.client_factory.getRootObject()
303+ adder = yield calculator.callRemote('get_adder')
304+ check = yield calculator.callRemote('check_adder', adder)
305+ self.assertTrue(check)
306+
307+
308+class TestPlainPbTestCase(PbServerTestCase):
309+ """Test using a server.
310+
311+ This test class is not testing the server and client perse but testing
312+ that we can use a PbServerFactory and PbClientFactory in a way that the
313+ connection will be closed correctly.
314+ """
315+
316+ @defer.inlineCallbacks
317+ def setUp(self):
318+ """Set the diff tests."""
319+ yield super(TestPlainPbTestCase, self).setUp()
320+ self.adder = Adder()
321+ self.calculator = Calculator(self.adder)
322+ self.listen_server(self.calculator)
323+ self.connect_client()
324+ yield self.client_connected
325+
326+ @defer.inlineCallbacks
327+ def test_addition(self):
328+ """Test adding numbers."""
329+ first_number = 1
330+ second_number = 2
331+ calculator = yield self.client_factory.getRootObject()
332+ adder = yield calculator.callRemote('get_adder')
333+ result = yield adder.callRemote('add', first_number, second_number)
334+ self.assertEqual(first_number + second_number, result)
335+
336+ @defer.inlineCallbacks
337+ def test_check_adder(self):
338+ """Test comparing the adder."""
339+ calculator = yield self.client_factory.getRootObject()
340+ adder = yield calculator.callRemote('get_adder')
341+ check = yield calculator.callRemote('check_adder', adder)
342+ self.assertTrue(check)
343+
344+
345+class MultipleServersTestCase(TestCase):
346+ """Ensure that several servers can be ran."""
347+
348+ timeout = 2
349+
350+ @defer.inlineCallbacks
351+ def setUp(self):
352+ """Set the diff tests."""
353+ yield super(MultipleServersTestCase, self).setUp()
354+ self.first_tcp_server = TidyTCPServer()
355+ self.second_tcp_server = TidyTCPServer()
356+ self.adder = Adder()
357+ self.calculator = Calculator(self.adder)
358+ self.echoer = Echoer()
359+
360+ @defer.inlineCallbacks
361+ def test_single_server(self):
362+ """Test setting a single server."""
363+ first_number = 1
364+ second_number = 2
365+ self.first_tcp_server.listen_server(pb.PBServerFactory,
366+ self.calculator)
367+ self.addCleanup(self.first_tcp_server.clean_up)
368+ calculator_c = self.first_tcp_server.connect_client(pb.PBClientFactory)
369+ # ensure we do have connected
370+ yield calculator_c.testserver_on_connection_made
371+ calculator = yield calculator_c.getRootObject()
372+ adder = yield calculator.callRemote('get_adder')
373+ result = yield adder.callRemote('add', first_number, second_number)
374+ self.assertEqual(first_number + second_number, result)
375+
376+ @defer.inlineCallbacks
377+ def test_multiple_server(self):
378+ """Test setting multiple server."""
379+ first_number = 1
380+ second_number = 2
381+ # first server
382+ self.first_tcp_server.listen_server(pb.PBServerFactory,
383+ self.calculator)
384+ self.addCleanup(self.first_tcp_server.clean_up)
385+
386+ # second server
387+ self.second_tcp_server.listen_server(pb.PBServerFactory, self.echoer)
388+ self.addCleanup(self.second_tcp_server.clean_up)
389+
390+ # connect the diff clients
391+ calculator_c = self.first_tcp_server.connect_client(pb.PBClientFactory)
392+ echoer_c = self.second_tcp_server.connect_client(pb.PBClientFactory)
393+ # ensure we do have connected
394+ yield calculator_c.testserver_on_connection_made
395+ yield echoer_c.testserver_on_connection_made
396+
397+ calculator = yield calculator_c.getRootObject()
398+ adder = yield calculator.callRemote('get_adder')
399+ result = yield adder.callRemote('add', first_number, second_number)
400+ self.assertEqual(first_number + second_number, result)
401+ echoer = yield echoer_c.getRootObject()
402+ echo = yield echoer.callRemote('say', 'hello')
403+ self.assertEqual(self.echoer.remote_say('hello'), echo)
404+
405+ def test_no_single_client(self):
406+ """Test setting a single server no client."""
407+ # start server but do not connect a client
408+ self.first_tcp_server.listen_server(pb.PBServerFactory,
409+ self.calculator)
410+ self.addCleanup(self.first_tcp_server.clean_up)
411+
412+ def test_no_multiple_clients(self):
413+ """Test setting multiple servers no clients."""
414+ # first server
415+ self.first_tcp_server.listen_server(pb.PBServerFactory,
416+ self.calculator)
417+ self.addCleanup(self.first_tcp_server.clean_up)
418+
419+ # second server
420+ self.second_tcp_server.listen_server(pb.PBServerFactory, self.echoer)
421+ self.addCleanup(self.second_tcp_server.clean_up)
422
423=== added file 'ubuntuone/devtools/testcases/txtcpserver.py'
424--- ubuntuone/devtools/testcases/txtcpserver.py 1970-01-01 00:00:00 +0000
425+++ ubuntuone/devtools/testcases/txtcpserver.py 2012-04-13 17:08:21 +0000
426@@ -0,0 +1,230 @@
427+# -*- coding: utf-8 -*-
428+# Copyright 2012 Canonical Ltd.
429+#
430+# This program is free software: you can redistribute it and/or modify it
431+# under the terms of the GNU General Public License version 3, as published
432+# by the Free Software Foundation.
433+#
434+# This program is distributed in the hope that it will be useful, but
435+# WITHOUT ANY WARRANTY; without even the implied warranties of
436+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
437+# PURPOSE. See the GNU General Public License for more details.
438+#
439+# You should have received a copy of the GNU General Public License along
440+# with this program. If not, see <http://www.gnu.org/licenses/>.
441+#
442+# In addition, as a special exception, the copyright holders give
443+# permission to link the code of portions of this program with the
444+# OpenSSL library under certain conditions as described in each
445+# individual source file, and distribute linked combinations
446+# including the two.
447+# You must obey the GNU General Public License in all respects
448+# for all of the code used other than OpenSSL. If you modify
449+# file(s) with this exception, you may extend this exception to your
450+# version of the file(s), but you are not obligated to do so. If you
451+# do not wish to do so, delete this exception statement from your
452+# version. If you delete this exception statement from all source
453+# files in the program, then also delete it here.
454+
455+"""Base test case for twisted servers."""
456+
457+from twisted.internet import defer, protocol
458+from twisted.spread import pb
459+
460+from ubuntuone.devtools.testcases import BaseTestCase
461+
462+# no init method + twisted common warnings
463+# pylint: disable=W0232, C0103, E1101
464+
465+
466+def server_protocol_factory(cls):
467+ """Factory to create tidy protocols."""
468+
469+ if cls is None:
470+ cls = protocol.Protocol
471+
472+ class ServerTidyProtocol(cls):
473+ """A tidy protocol."""
474+
475+ def connectionLost(self, *args):
476+ """Lost the connection."""
477+ cls.connectionLost(self, *args)
478+ # lets tell everyone
479+ # pylint: disable=W0212
480+ if (self.factory._disconnecting
481+ and self.factory.testserver_on_connection_lost is not None
482+ and not self.factory.testserver_on_connection_lost.called):
483+ self.factory.testserver_on_connection_lost.callback(self)
484+ # pylint: enable=W0212
485+
486+ return ServerTidyProtocol
487+
488+
489+def client_protocol_factory(cls):
490+ """Factory to create tidy protocols."""
491+
492+ if cls is None:
493+ cls = protocol.Protocol
494+
495+ class ClientTidyProtocol(cls):
496+ """A tidy protocol."""
497+
498+ def connectionMade(self):
499+ """Connection made."""
500+ if (self.factory.testserver_on_connection_made is not None
501+ and not self.factory.testserver_on_connection_made.called):
502+ self.factory.testserver_on_connection_made.callback(self)
503+ cls.connectionMade(self)
504+
505+ def connectionLost(self, *a):
506+ """Connection list."""
507+ # pylint: disable=W0212
508+ if (self.factory._disconnecting
509+ and self.factory.testserver_on_connection_lost is not None
510+ and not self.factory.testserver_on_connection_lost.called):
511+ self.factory.testserver_on_connection_lost.callback(self)
512+ # pylint: enable=W0212
513+ cls.connectionLost(self, *a)
514+
515+ return ClientTidyProtocol
516+
517+
518+class TidyTCPServer(object):
519+ """Ensure that twisted servers are correctly managed in tests.
520+
521+ Closing a twisted server is a complicated matter. In order to do so you
522+ have to ensure that three different deferreds are fired:
523+
524+ 1. The server must stop listening.
525+ 2. The client connection must disconnect.
526+ 3. The server connection must disconnect.
527+
528+ This class allows to create a server and a client that will ensure that
529+ the reactor is left clean by following the pattern described at
530+ http://mumak.net/stuff/twisted-disconnect.html
531+ """
532+
533+ def __init__(self):
534+ """Create a new instance."""
535+ self.listener = None
536+ self.server_factory = None
537+
538+ self.connector = None
539+ self.client_factory = None
540+
541+ def listen_server(self, server_class, *args, **kwargs):
542+ """Start a server in a random port."""
543+
544+ from twisted.internet import reactor
545+ self.server_factory = server_class(*args, **kwargs)
546+ self.server_factory._disconnecting = False
547+ self.server_factory.testserver_on_connection_lost = defer.Deferred()
548+ self.server_factory.protocol = server_protocol_factory(
549+ self.server_factory.protocol)
550+ self.listener = reactor.listenTCP(0, self.server_factory)
551+ return self.server_factory
552+
553+ def connect_client(self, client_class, *args, **kwargs):
554+ """Conect a client to a given server."""
555+ from twisted.internet import reactor
556+
557+ if self.server_factory is None:
558+ raise ValueError('Server Factory was not provided.')
559+ if self.listener is None:
560+ raise ValueError('%s has not started listening.',
561+ self.server_factory)
562+
563+ self.client_factory = client_class(*args, **kwargs)
564+ self.client_factory._disconnecting = False
565+ self.client_factory.protocol = client_protocol_factory(
566+ self.client_factory.protocol)
567+ self.client_factory.testserver_on_connection_made = defer.Deferred()
568+ self.client_factory.testserver_on_connection_lost = defer.Deferred()
569+ self.connector = reactor.connectTCP('localhost',
570+ self.listener.getHost().port,
571+ self.client_factory)
572+ return self.client_factory
573+
574+ def clean_up(self):
575+ """Action to be performed for clean up."""
576+ if self.server_factory is None or self.listener is None:
577+ # nothing to clean
578+ return defer.succeed(None)
579+
580+ if self.listener and self.connector:
581+ # clean client and server
582+ self.server_factory._disconnecting = True
583+ self.client_factory._disconnecting = True
584+ d = defer.maybeDeferred(self.listener.stopListening)
585+ self.connector.disconnect()
586+ return defer.gatherResults([d,
587+ self.client_factory.testserver_on_connection_lost,
588+ self.server_factory.testserver_on_connection_lost])
589+ if self.listener:
590+ # just clean the server since there is no client
591+ self.server_factory._disconnecting = True
592+ return defer.maybeDeferred(self.listener.stopListening)
593+
594+
595+class TCPServerTestCase(BaseTestCase):
596+ """Test that uses a single twisted server."""
597+
598+ @defer.inlineCallbacks
599+ def setUp(self):
600+ """Set the diff tests."""
601+ yield super(TCPServerTestCase, self).setUp()
602+ self.server_runner = TidyTCPServer()
603+
604+ self.server_factory = None
605+ self.client_factory = None
606+ self.server_disconnected = None
607+ self.client_connected = None
608+ self.client_disconnected = None
609+ self.listener = None
610+ self.connector = None
611+ self.addCleanup(self.tear_down_server_client)
612+
613+ def listen_server(self, server_class, *args, **kwargs):
614+ """Listen a server.
615+
616+ The method takes the server class and the arguments that should be
617+ passed to the server constructor.
618+ """
619+ self.server_factory = self.server_runner.listen_server(server_class,
620+ *args, **kwargs)
621+ self.server_disconnected = \
622+ self.server_factory.testserver_on_connection_lost
623+ self.listener = self.server_runner.listener
624+
625+ def connect_client(self, client_class, *args, **kwargs):
626+ """Connect the client.
627+
628+ The method takes the client factory class and the arguments that
629+ should be passed to the client constructor.
630+ """
631+ self.client_factory = self.server_runner.connect_client(client_class,
632+ *args, **kwargs)
633+ self.client_connected = \
634+ self.client_factory.testserver_on_connection_made
635+ self.client_disconnected = \
636+ self.client_factory.testserver_on_connection_lost
637+ self.connector = self.server_runner.connector
638+
639+ def tear_down_server_client(self):
640+ """Clean the server and client."""
641+ if self.server_factory and self.client_factory:
642+ return self.server_runner.clean_up()
643+
644+
645+class PbServerTestCase(TCPServerTestCase):
646+ """Test a pb server."""
647+
648+ def listen_server(self, *args, **kwargs):
649+ """Listen a pb server."""
650+ super(PbServerTestCase, self).listen_server(pb.PBServerFactory,
651+ *args, **kwargs)
652+
653+ def connect_client(self, *args, **kwargs):
654+ """Connect a pb client."""
655+ super(PbServerTestCase, self).connect_client(pb.PBClientFactory,
656+ *args, **kwargs)

Subscribers

People subscribed via source and target branches

to all changes: