Merge lp:~allenap/maas/rpc-alt-cluster-rpc-fixture into lp:~maas-committers/maas/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: no longer in the source branch.
Merged at revision: 2664
Proposed branch: lp:~allenap/maas/rpc-alt-cluster-rpc-fixture
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 241 lines (+117/-11)
4 files modified
src/maasserver/rpc/testing/fixtures.py (+107/-2)
src/provisioningserver/rpc/testing/__init__.py (+4/-3)
src/provisioningserver/rpc/tests/test_power.py (+4/-4)
src/provisioningserver/tests/test_events.py (+2/-2)
To merge this branch: bzr merge lp:~allenap/maas/rpc-alt-cluster-rpc-fixture
Reviewer Review Type Date Requested Status
Jeroen T. Vermeulen (community) Approve
Blake Rouse (community) Approve
Review via email: mp+230109@code.launchpad.net

Commit message

New fixture AlternativeClusterRPCFixture that enables end-to-end testing against a stub cluster RPC implementation.

The existing ClusterRPCFixture wires clients up to the "real" cluster RPC implementation, which was a bit short-sighted of me; it is increasingly dependent on external data, which will entail ever more elaborate test fixtures. It's better to work against an accurately stubbed RPC implementation and lean on CI/QA to exercise end-to-end.

To post a comment you must log in.
Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good. Just a couple of comments inline, no real blockers.

review: Approve
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

From your description, this sounds like a substantial improvement. But can you find the “mot juste” (as the French would call it; English doesn't quite seem to have the right word) for the new fixture's name? “Alternative” tells me nothing more than that I'm probably looking at the second-best choice out of two.

.

Have you checked for lint? The fixture's docstring looks to me as if it's just slightly too wide.

.

Also in the docstring, why is the io.flush() needed? Is that something you'd call with the real thing?

.

Does it make sense to break from PEP8 style for addCluster and makeCluster?

.

In makeCluster, could you document the return value, and also say in a comment why the cluster needs Identify added to it?

.

I think this fixture gets to a point where it would be helpful, if possible, to have a test for the fixture itself.

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks for your review Blake. I've replied to your comments.

Next: Jeroen.

Revision history for this message
Gavin Panella (allenap) wrote :

> From your description, this sounds like a substantial improvement.  But can
> you find the “mot juste” (as the French would call it; English doesn't quite
> seem to have the right word) for the new fixture's name?  “Alternative” tells
> me nothing more than that I'm probably looking at the second-best choice out
> of two.

I've changed it to RegionToClusterRPCFixture now.

>
> .
>
> Have you checked for lint?  The fixture's docstring looks to me as if it's
> just slightly too wide.

It's clean; the line is right up into the 78th column.

>
> .
>
> Also in the docstring, why is the io.flush() needed?  Is that something you'd
> call with the real thing?

This is needed. The iosim.connect() call wires up the client and server
outside of the reactor's control. If you need data to move between these
you need to pump it "by hand". Once you get the hang of it it's quite
easy, and powerful too; by setting debug=True on the pump or passing it
into the flush() or pump() methods you get a log of the IO passing back
and forth.

>
> .
>
> Does it make sense to break from PEP8 style for addCluster and makeCluster?

It's not strictly breaking from PEP8:

  Function names should be lowercase, with words separated by
  underscores as necessary to improve readability.

  mixedCase is allowed only in contexts where that's already the
  prevailing style (e.g. threading.py), to retain backwards
  compatibility.

In fact, we've broken from PEP8 by using underscores in all of our test
names. The fixtures library itself uses a mix of lower_case and
mixedCase names.

We have a mix of both in MAAS, and I think we have to accept it. Twisted
is all mixedCase. Plus, with tab-completion it's hardly a burden.

>
> .
>
> In makeCluster, could you document the return value, and also say in a comment
> why the cluster needs Identify added to it?

Done.

>
> .
>
> I think this fixture gets to a point where it would be helpful, if possible,
> to have a test for the fixture itself.

Agreed. I have just been yak shaving for too long and can't face any
more right now!

Thanks for the review!

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On 09/08/14 02:02, Gavin Panella wrote:
>> From your description, this sounds like a substantial improvement. But can
>> > you find the “mot juste” (as the French would call it; English doesn't quite
>> > seem to have the right word) for the new fixture's name? “Alternative” tells
>> > me nothing more than that I'm probably looking at the second-best choice out
>> > of two.
> I've changed it to RegionToClusterRPCFixture now.
>

I know it's easy to bikeshed over names, but they are important.

We now have

 - ClusterRPCFixture
 - RegionToClusterRPCFixture

with no real indication of the difference from the names. Especially
since they are both Region-To-Cluster!

Can I suggest this new one is MockClusterRPCFixture? I don't think the
"RegionToCluster" bit adds anything.

Revision history for this message
Gavin Panella (allenap) wrote :

> I know it's easy to bikeshed over names, but they are important.
>
> We now have
>
> - ClusterRPCFixture
> - RegionToClusterRPCFixture
>
> with no real indication of the difference from the names. Especially
> since they are both Region-To-Cluster!
>
> Can I suggest this new one is MockClusterRPCFixture? I don't think the
> "RegionToCluster" bit adds anything.

The absence of direction in the name has confused a couple of people
that I know of so far (not seriously confused, just a double-take), so
I think the longer name has some value. ClusterRPCFixture is also
deprecated now, and I'll try to remove it when I can. It's used in
only one module so shouldn't be too much of a chore.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Monday 11 Aug 2014 08:27:22 you wrote:
> > I know it's easy to bikeshed over names, but they are important.
> >
> > We now have
> >
> > - ClusterRPCFixture
> > - RegionToClusterRPCFixture
> >
> > with no real indication of the difference from the names. Especially
> > since they are both Region-To-Cluster!
> >
> > Can I suggest this new one is MockClusterRPCFixture? I don't think the
> > "RegionToCluster" bit adds anything.
>
> The absence of direction in the name has confused a couple of people
> that I know of so far (not seriously confused, just a double-take), so
> I think the longer name has some value. ClusterRPCFixture is also
> deprecated now, and I'll try to remove it when I can. It's used in
> only one module so shouldn't be too much of a chore.

Can we at least get a Mock in there so we know it's been stubbed out?

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/maasserver/rpc/testing/fixtures.py'
2--- src/maasserver/rpc/testing/fixtures.py 2014-07-21 11:21:00 +0000
3+++ src/maasserver/rpc/testing/fixtures.py 2014-08-08 16:03:07 +0000
4@@ -14,19 +14,31 @@
5 __metaclass__ = type
6 __all__ = [
7 "ClusterRPCFixture",
8+ "RegionToClusterRPCFixture",
9 ]
10
11 from collections import defaultdict
12+from warnings import warn
13
14 from crochet import run_in_reactor
15 import fixtures
16 from maasserver import eventloop
17 from maasserver.enum import NODEGROUP_STATUS
18 from maasserver.models.nodegroup import NodeGroup
19-from provisioningserver.rpc import clusterservice
20+from maasserver.rpc.regionservice import RegionServer
21+from provisioningserver.rpc import (
22+ cluster,
23+ clusterservice,
24+ )
25 from provisioningserver.rpc.interfaces import IConnection
26-from provisioningserver.rpc.testing import call_responder
27+from provisioningserver.rpc.testing import (
28+ call_responder,
29+ make_amp_protocol_factory,
30+ )
31 from testtools.monkey import patch
32+from twisted.internet import defer
33+from twisted.internet.protocol import Factory
34+from twisted.test import iosim
35 from zope.interface import implementer
36
37
38@@ -49,6 +61,24 @@
39
40
41 class ClusterRPCFixture(fixtures.Fixture):
42+ """Deprecated: use :py:class:`RegionToClusterRPCFixture` instead.
43+
44+ This creates connections to the "real" cluster RPC implementation,
45+ but this relies on real data. This makes tests fragile, and, as time
46+ progresses, will result in ever more elaborate test fixtures to get
47+ that data into place.
48+
49+ Instead, use :py:class:`RegionToClusterRPCFixture`, which helps you
50+ stub out those RPC calls that you're testing against, and verifies
51+ each call's arguments *and* response against the RPC schema.
52+ """
53+
54+ def __init__(self):
55+ super(ClusterRPCFixture, self).__init__()
56+ warn(
57+ ("ClusterRPCFixture is deprecated; use "
58+ "RegionToClusterRPCFixture instead."),
59+ DeprecationWarning)
60
61 def setUp(self):
62 super(ClusterRPCFixture, self).setUp()
63@@ -70,3 +100,78 @@
64 fake_connections[connection.ident].add(connection)
65 # Patch the fake connections into place for this fixture's lifetime.
66 self.addCleanup(patch(rpc_service, "connections", fake_connections))
67+
68+
69+class RegionToClusterRPCFixture(fixtures.Fixture):
70+ """Patch in a stub cluster RPC implementation to enable end-to-end testing.
71+
72+ Use this in *region* tests.
73+
74+ Example usage::
75+
76+ nodegroup = factory.make_node_group()
77+ fixture = self.useFixture(RegionToClusterRPCFixture())
78+ protocol, io = fixture.makeCluster(nodegroup, region.Identify)
79+ protocol.Identify.return_value = defer.succeed({"ident": "foobar"})
80+
81+ client = getClientFor(nodegroup.uuid)
82+ result = client(region.Identify)
83+ io.flush() # Call this in the reactor thread.
84+
85+ self.assertThat(result, ...)
86+
87+ """
88+
89+ def setUp(self):
90+ super(RegionToClusterRPCFixture, self).setUp()
91+ # We need the event-loop up and running.
92+ if not eventloop.loop.running:
93+ raise RuntimeError(
94+ "Please start the event-loop before using this fixture.")
95+ self.rpc = get_service_in_eventloop("rpc").wait(10)
96+ # The RPC service uses a defaultdict(set) to manage connections, but
97+ # let's check those assumptions.
98+ assert isinstance(self.rpc.connections, defaultdict)
99+ assert self.rpc.connections.default_factory is set
100+ # Patch a fake connections dict into place for this fixture's lifetime.
101+ self.addCleanup(patch(self.rpc, "connections", defaultdict(set)))
102+
103+ def addCluster(self, protocol):
104+ """Add a new stub cluster using the given `protocol`.
105+
106+ The `protocol` should be an instance of `amp.AMP`.
107+
108+ :returns: py:class:`twisted.test.iosim.IOPump`
109+ """
110+ server_factory = Factory.forProtocol(RegionServer)
111+ server_factory.service = self.rpc
112+ server = server_factory.buildProtocol(addr=None)
113+ return iosim.connect(
114+ server, iosim.makeFakeServer(server),
115+ protocol, iosim.makeFakeClient(protocol),
116+ debug=False, # Debugging is useful, but too noisy by default.
117+ )
118+
119+ def makeCluster(self, nodegroup, *commands):
120+ """Make and add a new stub cluster connection with the `commands`.
121+
122+ See `make_amp_protocol_factory` for details.
123+
124+ Note that if the ``Identify`` call is not amongst `commands`, it will
125+ be added. In addition, its return value is also set to return the UUID
126+ of `nodegroup`. There's a good reason: the first thing that
127+ `RegionServer` does when a connection is made is call `Identify`. This
128+ has to succeed or the connection will never been added to the RPC
129+ service's list of connections.
130+
131+ :returns: A 2-tuple of the protocol instance created and the
132+ py:class:`twisted.test.iosim.IOPump` as returned by `addCluster`.
133+ """
134+ if cluster.Identify not in commands:
135+ commands = commands + (cluster.Identify,)
136+ protocol_factory = make_amp_protocol_factory(*commands)
137+ protocol = protocol_factory()
138+ ident_response = {"ident": nodegroup.uuid.decode("ascii")}
139+ protocol.Identify.side_effect = (
140+ lambda protocol: defer.succeed(ident_response.copy()))
141+ return protocol, self.addCluster(protocol)
142
143=== modified file 'src/provisioningserver/rpc/testing/__init__.py'
144--- src/provisioningserver/rpc/testing/__init__.py 2014-07-29 12:21:29 +0000
145+++ src/provisioningserver/rpc/testing/__init__.py 2014-08-08 16:03:07 +0000
146@@ -15,6 +15,7 @@
147 __all__ = [
148 "are_valid_tls_parameters",
149 "call_responder",
150+ "ClusterToRegionRPCFixture",
151 "make_amp_protocol_factory",
152 "TwistedLoggerFixture",
153 ]
154@@ -106,14 +107,14 @@
155 })
156
157
158-class RegionRPCFixture(fixtures.Fixture):
159+class ClusterToRegionRPCFixture(fixtures.Fixture):
160 """Patch in a stub region RPC implementation to enable end-to-end testing.
161
162 Use this in *cluster* tests.
163
164 Example usage::
165
166- fixture = self.useFixture(RegionRPCFixture())
167+ fixture = self.useFixture(ClusterToRegionRPCFixture())
168 protocol, io = fixture.makeEventLoop(region.Identify)
169 protocol.Identify.return_value = defer.succeed({"ident": "foobar"})
170
171@@ -126,7 +127,7 @@
172 """
173
174 def setUp(self):
175- super(RegionRPCFixture, self).setUp()
176+ super(ClusterToRegionRPCFixture, self).setUp()
177 # If services are running, what do we do with any existing RPC
178 # service? Do we shut it down and patch in? Do we just patch in and
179 # move the running service aside? If it's not running, do we patch
180
181=== modified file 'src/provisioningserver/rpc/tests/test_power.py'
182--- src/provisioningserver/rpc/tests/test_power.py 2014-08-05 15:48:54 +0000
183+++ src/provisioningserver/rpc/tests/test_power.py 2014-08-08 16:03:07 +0000
184@@ -36,7 +36,7 @@
185 power,
186 region,
187 )
188-from provisioningserver.rpc.testing import RegionRPCFixture
189+from provisioningserver.rpc.testing import ClusterToRegionRPCFixture
190 from testtools.deferredruntest import (
191 assert_fails_with,
192 AsynchronousDeferredRunTest,
193@@ -50,7 +50,7 @@
194 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
195
196 def patch_rpc_methods(self):
197- fixture = self.useFixture(RegionRPCFixture())
198+ fixture = self.useFixture(ClusterToRegionRPCFixture())
199 protocol, io = fixture.makeEventLoop(
200 region.MarkNodeBroken, region.UpdateNodePowerState,
201 region.SendEvent)
202@@ -205,7 +205,7 @@
203 return power_action, power_action_obj_execute
204
205 def patch_rpc_methods(self, return_value={}, side_effect=None):
206- fixture = self.useFixture(RegionRPCFixture())
207+ fixture = self.useFixture(ClusterToRegionRPCFixture())
208 protocol, io = fixture.makeEventLoop(
209 region.MarkNodeBroken, region.UpdateNodePowerState,
210 region.SendEvent)
211@@ -434,7 +434,7 @@
212 return power_action, power_action_obj_execute
213
214 def patch_rpc_methods(self, return_value={}, side_effect=None):
215- fixture = self.useFixture(RegionRPCFixture())
216+ fixture = self.useFixture(ClusterToRegionRPCFixture())
217 protocol, io = fixture.makeEventLoop(
218 region.MarkNodeBroken, region.SendEvent)
219 protocol.MarkNodeBroken.return_value = return_value
220
221=== modified file 'src/provisioningserver/tests/test_events.py'
222--- src/provisioningserver/tests/test_events.py 2014-07-30 14:57:14 +0000
223+++ src/provisioningserver/tests/test_events.py 2014-08-08 16:03:07 +0000
224@@ -31,7 +31,7 @@
225 )
226 from provisioningserver.rpc import region
227 from provisioningserver.rpc.exceptions import NoSuchEventType
228-from provisioningserver.rpc.testing import RegionRPCFixture
229+from provisioningserver.rpc.testing import ClusterToRegionRPCFixture
230 from provisioningserver.utils import map_enum
231 from testtools.deferredruntest import AsynchronousDeferredRunTest
232 from testtools.matchers import (
233@@ -54,7 +54,7 @@
234 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
235
236 def patch_rpc_methods(self, side_effect=None):
237- fixture = self.useFixture(RegionRPCFixture())
238+ fixture = self.useFixture(ClusterToRegionRPCFixture())
239 protocol, io = fixture.makeEventLoop(
240 region.SendEvent, region.RegisterEventType)
241 protocol.SendEvent.side_effect = side_effect