Merge lp:~free.ekanayaka/landscape-client/method-call-defer into lp:~landscape/landscape-client/amp-trunk
- method-call-defer
- Merge into amp-trunk
Status: | Merged |
---|---|
Approved by: | Sidnei da Silva |
Approved revision: | not available |
Merge reported by: | Free Ekanayaka |
Merged at revision: | not available |
Proposed branch: | lp:~free.ekanayaka/landscape-client/method-call-defer |
Merge into: | lp:~landscape/landscape-client/amp-trunk |
Diff against target: |
412 lines (+227/-30) 2 files modified
landscape/lib/amp.py (+123/-13) landscape/lib/tests/test_amp.py (+104/-17) |
To merge this branch: | bzr merge lp:~free.ekanayaka/landscape-client/method-call-defer |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kapil Thangavelu (community) | Approve | ||
Duncan McGreggor (community) | Approve | ||
Review via email:
|
This proposal supersedes a proposal from 2010-01-20.
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Free Ekanayaka (free.ekanayaka) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Duncan McGreggor (oubiwann) wrote : | # |
This is super-cool, Free -- so glad you did this :-)
[1] 1661 tests, all passing -- nice!
[2] This isn't part of this particular change, but there's a bare exception before the deferred check:
| try:
| result = getattr(
| except Exception, error:
| raise MethodCallError
It'd be nice if expected exceptions were handled explicitly here.
[3] You'll want to get Thomas' feedback on this suggestion... but one thing you might want to consider is actually having receive_method_call always return a deferred (with return maybeDeferred(
+1 for merge!
- 216. By Free Ekanayaka
-
Use maybeDeferred to simplify receive_method_call logic (Duncan [3])
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Free Ekanayaka (free.ekanayaka) wrote : | # |
Thanks Duncan! It was awesome to get your Landscape "goodbye review" :)
[1]
Yay, thanks buildbot.
[2]
The thing is that this is a very general method call, so in principle any exception could be raised. However whatever gets raised is wrapped around a MethodCallError, so the remote caller can possibly cope with it.
[3]
Great idea! That indeed simplified the logic quite a bit. Thanks.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Kapil Thangavelu (hazmat) wrote : | # |
Nice implementation, thorough test coverage. Looks good to me. +1
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Free Ekanayaka (free.ekanayaka) wrote : | # |
Thanks Kapil, that's merged in r185 of amp-trunk.
Preview Diff
1 | === modified file 'landscape/lib/amp.py' | |||
2 | --- landscape/lib/amp.py 2010-01-20 08:33:12 +0000 | |||
3 | +++ landscape/lib/amp.py 2010-01-25 08:30:27 +0000 | |||
4 | @@ -1,8 +1,10 @@ | |||
5 | 1 | """Expose the methods of a remote object over AMP.""" | 1 | """Expose the methods of a remote object over AMP.""" |
6 | 2 | 2 | ||
8 | 3 | from twisted.internet.defer import Deferred | 3 | from uuid import uuid4 |
9 | 4 | from twisted.internet.defer import Deferred, maybeDeferred | ||
10 | 4 | from twisted.internet.protocol import ServerFactory, ClientFactory | 5 | from twisted.internet.protocol import ServerFactory, ClientFactory |
11 | 5 | from twisted.protocols.amp import Argument, String, Command, AMP | 6 | from twisted.protocols.amp import Argument, String, Command, AMP |
12 | 7 | from twisted.python.failure import Failure | ||
13 | 6 | 8 | ||
14 | 7 | from landscape.lib.bpickle import loads, dumps, dumps_table | 9 | from landscape.lib.bpickle import loads, dumps, dumps_table |
15 | 8 | 10 | ||
16 | @@ -35,11 +37,21 @@ | |||
17 | 35 | ("args", MethodCallArgument()), | 37 | ("args", MethodCallArgument()), |
18 | 36 | ("kwargs", MethodCallArgument())] | 38 | ("kwargs", MethodCallArgument())] |
19 | 37 | 39 | ||
21 | 38 | response = [("result", MethodCallArgument())] | 40 | response = [("result", MethodCallArgument()), |
22 | 41 | ("deferred", String(optional=True))] | ||
23 | 39 | 42 | ||
24 | 40 | errors = {MethodCallError: "METHOD_CALL_ERROR"} | 43 | errors = {MethodCallError: "METHOD_CALL_ERROR"} |
25 | 41 | 44 | ||
26 | 42 | 45 | ||
27 | 46 | class DeferredResponse(Command): | ||
28 | 47 | """Fire a L{Deferred} associated with an outstanding method call result.""" | ||
29 | 48 | |||
30 | 49 | arguments = [("uuid", String()), | ||
31 | 50 | ("result", MethodCallArgument(optional=True)), | ||
32 | 51 | ("failure", String(optional=True))] | ||
33 | 52 | requiresAnswer = False | ||
34 | 53 | |||
35 | 54 | |||
36 | 43 | class MethodCallServerProtocol(AMP): | 55 | class MethodCallServerProtocol(AMP): |
37 | 44 | """Expose methods of a local object over AMP. | 56 | """Expose methods of a local object over AMP. |
38 | 45 | 57 | ||
39 | @@ -68,17 +80,109 @@ | |||
40 | 68 | if not method in self.methods: | 80 | if not method in self.methods: |
41 | 69 | raise MethodCallError("Forbidden method '%s'" % method) | 81 | raise MethodCallError("Forbidden method '%s'" % method) |
42 | 70 | 82 | ||
47 | 71 | try: | 83 | method_func = getattr(self.factory.object, method) |
48 | 72 | result = getattr(self.factory.object, method)(*args, **kwargs) | 84 | result = maybeDeferred(method_func, *args, **kwargs) |
49 | 73 | except Exception, error: | 85 | |
50 | 74 | raise MethodCallError("Remote exception %s" % str(error)) | 86 | # If the Deferred was already fired, we can return its result |
51 | 87 | if result.called: | ||
52 | 88 | if isinstance(result.result, Failure): | ||
53 | 89 | failure = str(result.result.value) | ||
54 | 90 | result.addErrback(lambda error: None) # Stop propagating | ||
55 | 91 | raise MethodCallError(failure) | ||
56 | 92 | return {"result": self._check_result(result.result)} | ||
57 | 93 | |||
58 | 94 | uuid = str(uuid4()) | ||
59 | 95 | result.addBoth(self.send_deferred_response, uuid) | ||
60 | 96 | return {"result": None, "deferred": uuid} | ||
61 | 97 | |||
62 | 98 | def _check_result(self, result): | ||
63 | 99 | """Check that the C{result} we're about to return is serializable. | ||
64 | 100 | |||
65 | 101 | @return: The C{result} itself if valid. | ||
66 | 102 | @raises: L{MethodCallError} if C{result} is not serializable. | ||
67 | 103 | """ | ||
68 | 75 | if not MethodCallArgument.check(result): | 104 | if not MethodCallArgument.check(result): |
69 | 76 | raise MethodCallError("Non-serializable result") | 105 | raise MethodCallError("Non-serializable result") |
71 | 77 | return {"result": result} | 106 | return result |
72 | 107 | |||
73 | 108 | def send_deferred_response(self, result, uuid): | ||
74 | 109 | """Send a L{DeferredResponse} for the deferred with given C{uuid}. | ||
75 | 110 | |||
76 | 111 | This is called when the result of a L{Deferred} returned by an | ||
77 | 112 | object's method becomes available. A L{DeferredResponse} notifying | ||
78 | 113 | such result (either success or failure) is sent to the peer. | ||
79 | 114 | """ | ||
80 | 115 | kwargs = {"uuid": uuid} | ||
81 | 116 | if isinstance(result, Failure): | ||
82 | 117 | kwargs["failure"] = str(result.value) | ||
83 | 118 | else: | ||
84 | 119 | kwargs["result"] = self._check_result(result) | ||
85 | 120 | self.callRemote(DeferredResponse, **kwargs) | ||
86 | 78 | 121 | ||
87 | 79 | 122 | ||
88 | 80 | class MethodCallClientProtocol(AMP): | 123 | class MethodCallClientProtocol(AMP): |
90 | 81 | """Calls methods of a remote object over L{AMP}.""" | 124 | """Calls methods of a remote object over L{AMP}. |
91 | 125 | |||
92 | 126 | @note: If the remote method returns a deferred, the associated local | ||
93 | 127 | deferred returned by L{send_method_call} will result in the same | ||
94 | 128 | callback value of the remote deferred. | ||
95 | 129 | @cvar timeout: A timeout for remote methods returning L{Deferred}s, if a | ||
96 | 130 | response for the deferred is not received within this amount of | ||
97 | 131 | seconds, the remote method call will errback with a L{MethodCallError}. | ||
98 | 132 | """ | ||
99 | 133 | timeout = 60 | ||
100 | 134 | |||
101 | 135 | def __init__(self): | ||
102 | 136 | AMP.__init__(self) | ||
103 | 137 | self._pending_responses = {} | ||
104 | 138 | |||
105 | 139 | @DeferredResponse.responder | ||
106 | 140 | def receive_deferred_response(self, uuid, result, failure): | ||
107 | 141 | """Receive the deferred L{MethodCall} response. | ||
108 | 142 | |||
109 | 143 | @param uuid: The id of the L{MethodCall} we're getting the result of. | ||
110 | 144 | @param result: The result of the associated deferred if successful. | ||
111 | 145 | @param failure: The failure message of the deferred if it failed. | ||
112 | 146 | """ | ||
113 | 147 | self.fire_pending_response(uuid, result, failure) | ||
114 | 148 | return {} | ||
115 | 149 | |||
116 | 150 | def fire_pending_response(self, uuid, result, failure): | ||
117 | 151 | """Fire the L{Deferred} associated with a pending response. | ||
118 | 152 | |||
119 | 153 | @param uuid: The id of the L{MethodCall} we're getting the result of. | ||
120 | 154 | @param result: The result of the associated deferred if successful. | ||
121 | 155 | @param failure: The failure message of the deferred if it failed. | ||
122 | 156 | """ | ||
123 | 157 | try: | ||
124 | 158 | deferred, call = self._pending_responses.pop(uuid) | ||
125 | 159 | except KeyError: | ||
126 | 160 | # Late response for a request that has timeout, just ignore it | ||
127 | 161 | return | ||
128 | 162 | if not call.called: | ||
129 | 163 | call.cancel() | ||
130 | 164 | if failure is None: | ||
131 | 165 | deferred.callback({"result": result}) | ||
132 | 166 | else: | ||
133 | 167 | deferred.errback(MethodCallError(failure)) | ||
134 | 168 | |||
135 | 169 | def handle_response(self, response): | ||
136 | 170 | """Handle a L{MethodCall} response. | ||
137 | 171 | |||
138 | 172 | If the response is tagged as deferred, it will be queued as pending, | ||
139 | 173 | and a L{Deferred} is returned, which will be fired as soon as the | ||
140 | 174 | final response becomes available, or the timeout is reached. | ||
141 | 175 | """ | ||
142 | 176 | if response["deferred"]: | ||
143 | 177 | uuid = response["deferred"] | ||
144 | 178 | deferred = Deferred() | ||
145 | 179 | call = self.factory.reactor.callLater(self.timeout, | ||
146 | 180 | self.fire_pending_response, | ||
147 | 181 | uuid, None, "timeout") | ||
148 | 182 | self._pending_responses[uuid] = (deferred, call) | ||
149 | 183 | return deferred | ||
150 | 184 | |||
151 | 185 | return response | ||
152 | 82 | 186 | ||
153 | 83 | def send_method_call(self, method, args=[], kwargs={}): | 187 | def send_method_call(self, method, args=[], kwargs={}): |
154 | 84 | """Send a L{MethodCall} command with the given arguments. | 188 | """Send a L{MethodCall} command with the given arguments. |
155 | @@ -87,8 +191,12 @@ | |||
156 | 87 | @param args: The positional arguments to pass to the remote method. | 191 | @param args: The positional arguments to pass to the remote method. |
157 | 88 | @param kwargs: The keyword arguments to pass to the remote method. | 192 | @param kwargs: The keyword arguments to pass to the remote method. |
158 | 89 | """ | 193 | """ |
161 | 90 | return self.callRemote(MethodCall, | 194 | result = self.callRemote(MethodCall, |
162 | 91 | method=method, args=args, kwargs=kwargs) | 195 | method=method, args=args, kwargs=kwargs) |
163 | 196 | # The result can be C{None} only if the requested command is a | ||
164 | 197 | # DeferredResponse, which has requiresAnswer set to False | ||
165 | 198 | if result is not None: | ||
166 | 199 | return result.addCallback(self.handle_response) | ||
167 | 92 | 200 | ||
168 | 93 | 201 | ||
169 | 94 | class MethodCallServerFactory(ServerFactory): | 202 | class MethodCallServerFactory(ServerFactory): |
170 | @@ -111,16 +219,18 @@ | |||
171 | 111 | 219 | ||
172 | 112 | def __init__(self, reactor, notifier): | 220 | def __init__(self, reactor, notifier): |
173 | 113 | """ | 221 | """ |
175 | 114 | @param reactor: The reactor used to schedule connection callbacks. | 222 | @param reactor: The reactor used by the created protocols to schedule |
176 | 223 | notifications and timeouts. | ||
177 | 115 | @param notifier: A function that will be called when the connection is | 224 | @param notifier: A function that will be called when the connection is |
178 | 116 | established. It will be passed the protocol instance as argument. | 225 | established. It will be passed the protocol instance as argument. |
179 | 117 | """ | 226 | """ |
181 | 118 | self._reactor = reactor | 227 | self.reactor = reactor |
182 | 119 | self._notifier = notifier | 228 | self._notifier = notifier |
183 | 120 | 229 | ||
184 | 121 | def buildProtocol(self, addr): | 230 | def buildProtocol(self, addr): |
185 | 122 | protocol = self.protocol() | 231 | protocol = self.protocol() |
187 | 123 | self._reactor.callLater(0, self._notifier, protocol) | 232 | protocol.factory = self |
188 | 233 | self.reactor.callLater(0, self._notifier, protocol) | ||
189 | 124 | return protocol | 234 | return protocol |
190 | 125 | 235 | ||
191 | 126 | 236 | ||
192 | 127 | 237 | ||
193 | === modified file 'landscape/lib/tests/test_amp.py' | |||
194 | --- landscape/lib/tests/test_amp.py 2010-01-20 08:33:12 +0000 | |||
195 | +++ landscape/lib/tests/test_amp.py 2010-01-25 08:30:27 +0000 | |||
196 | @@ -1,9 +1,11 @@ | |||
197 | 1 | from twisted.internet import reactor | 1 | from twisted.internet import reactor |
198 | 2 | from twisted.internet.defer import Deferred | ||
199 | 2 | from twisted.internet.protocol import ClientCreator | 3 | from twisted.internet.protocol import ClientCreator |
200 | 3 | 4 | ||
201 | 4 | from landscape.lib.amp import ( | 5 | from landscape.lib.amp import ( |
204 | 5 | MethodCallError, MethodCallServerProtocol, MethodCallClientProtocol, | 6 | MethodCallError, MethodCallServerProtocol, |
205 | 6 | MethodCallServerFactory, RemoteObject, RemoteObjectCreator) | 7 | MethodCallClientProtocol, MethodCallServerFactory, |
206 | 8 | MethodCallClientFactory, RemoteObject, RemoteObjectCreator) | ||
207 | 7 | from landscape.tests.helpers import LandscapeTest | 9 | from landscape.tests.helpers import LandscapeTest |
208 | 8 | 10 | ||
209 | 9 | 11 | ||
210 | @@ -60,6 +62,24 @@ | |||
211 | 60 | def translate(self, word): | 62 | def translate(self, word): |
212 | 61 | raise WordsException("Unknown word") | 63 | raise WordsException("Unknown word") |
213 | 62 | 64 | ||
214 | 65 | def google(self, word): | ||
215 | 66 | deferred = Deferred() | ||
216 | 67 | if word == "Landscape": | ||
217 | 68 | reactor.callLater(0.01, lambda: deferred.callback("Cool!")) | ||
218 | 69 | elif word == "Easy query": | ||
219 | 70 | deferred.callback("Done!") | ||
220 | 71 | elif word == "Weird stuff": | ||
221 | 72 | reactor.callLater(0.01, lambda: deferred.errback(Exception("bad"))) | ||
222 | 73 | elif word == "Censored": | ||
223 | 74 | deferred.errback(Exception("very bad")) | ||
224 | 75 | elif word == "Long query": | ||
225 | 76 | # Do nothing, the deferred won't be fired at all | ||
226 | 77 | pass | ||
227 | 78 | elif word == "Slowish query": | ||
228 | 79 | # Fire the result after a while. | ||
229 | 80 | reactor.callLater(0.05, lambda: deferred.callback("Done!")) | ||
230 | 81 | return deferred | ||
231 | 82 | |||
232 | 63 | 83 | ||
233 | 64 | class WordsProtocol(MethodCallServerProtocol): | 84 | class WordsProtocol(MethodCallServerProtocol): |
234 | 65 | 85 | ||
235 | @@ -72,7 +92,8 @@ | |||
236 | 72 | "multiply_alphabetically", | 92 | "multiply_alphabetically", |
237 | 73 | "translate", | 93 | "translate", |
238 | 74 | "meaning_of_life", | 94 | "meaning_of_life", |
240 | 75 | "guess"] | 95 | "guess", |
241 | 96 | "google"] | ||
242 | 76 | 97 | ||
243 | 77 | 98 | ||
244 | 78 | class MethodCallProtocolTest(LandscapeTest): | 99 | class MethodCallProtocolTest(LandscapeTest): |
245 | @@ -114,7 +135,8 @@ | |||
246 | 114 | result = self.protocol.send_method_call(method="empty", | 135 | result = self.protocol.send_method_call(method="empty", |
247 | 115 | args=[], | 136 | args=[], |
248 | 116 | kwargs={}) | 137 | kwargs={}) |
250 | 117 | return self.assertSuccess(result, {"result": None}) | 138 | return self.assertSuccess(result, {"result": None, |
251 | 139 | "deferred": None}) | ||
252 | 118 | 140 | ||
253 | 119 | def test_with_return_value(self): | 141 | def test_with_return_value(self): |
254 | 120 | """ | 142 | """ |
255 | @@ -124,7 +146,8 @@ | |||
256 | 124 | result = self.protocol.send_method_call(method="motd", | 146 | result = self.protocol.send_method_call(method="motd", |
257 | 125 | args=[], | 147 | args=[], |
258 | 126 | kwargs={}) | 148 | kwargs={}) |
260 | 127 | return self.assertSuccess(result, {"result": "Words are cool"}) | 149 | return self.assertSuccess(result, {"result": "Words are cool", |
261 | 150 | "deferred": None}) | ||
262 | 128 | 151 | ||
263 | 129 | def test_with_one_argument(self): | 152 | def test_with_one_argument(self): |
264 | 130 | """ | 153 | """ |
265 | @@ -134,7 +157,8 @@ | |||
266 | 134 | result = self.protocol.send_method_call(method="capitalize", | 157 | result = self.protocol.send_method_call(method="capitalize", |
267 | 135 | args=["john"], | 158 | args=["john"], |
268 | 136 | kwargs={}) | 159 | kwargs={}) |
270 | 137 | return self.assertSuccess(result, {"result": "John"}) | 160 | return self.assertSuccess(result, {"result": "John", |
271 | 161 | "deferred": None}) | ||
272 | 138 | 162 | ||
273 | 139 | def test_with_boolean_return_value(self): | 163 | def test_with_boolean_return_value(self): |
274 | 140 | """ | 164 | """ |
275 | @@ -143,7 +167,8 @@ | |||
276 | 143 | result = self.protocol.send_method_call(method="is_short", | 167 | result = self.protocol.send_method_call(method="is_short", |
277 | 144 | args=["hi"], | 168 | args=["hi"], |
278 | 145 | kwargs={}) | 169 | kwargs={}) |
280 | 146 | return self.assertSuccess(result, {"result": True}) | 170 | return self.assertSuccess(result, {"result": True, |
281 | 171 | "deferred": None}) | ||
282 | 147 | 172 | ||
283 | 148 | def test_with_many_arguments(self): | 173 | def test_with_many_arguments(self): |
284 | 149 | """ | 174 | """ |
285 | @@ -152,7 +177,8 @@ | |||
286 | 152 | result = self.protocol.send_method_call(method="concatenate", | 177 | result = self.protocol.send_method_call(method="concatenate", |
287 | 153 | args=["You ", "rock"], | 178 | args=["You ", "rock"], |
288 | 154 | kwargs={}) | 179 | kwargs={}) |
290 | 155 | return self.assertSuccess(result, {"result": "You rock"}) | 180 | return self.assertSuccess(result, {"result": "You rock", |
291 | 181 | "deferred": None}) | ||
292 | 156 | 182 | ||
293 | 157 | def test_with_default_arguments(self): | 183 | def test_with_default_arguments(self): |
294 | 158 | """ | 184 | """ |
295 | @@ -162,7 +188,8 @@ | |||
296 | 162 | result = self.protocol.send_method_call(method="lower_case", | 188 | result = self.protocol.send_method_call(method="lower_case", |
297 | 163 | args=["OHH"], | 189 | args=["OHH"], |
298 | 164 | kwargs={}) | 190 | kwargs={}) |
300 | 165 | return self.assertSuccess(result, {"result": "ohh"}) | 191 | return self.assertSuccess(result, {"result": "ohh", |
301 | 192 | "deferred": None}) | ||
302 | 166 | 193 | ||
303 | 167 | def test_with_overriden_default_arguments(self): | 194 | def test_with_overriden_default_arguments(self): |
304 | 168 | """ | 195 | """ |
305 | @@ -173,7 +200,8 @@ | |||
306 | 173 | result = self.protocol.send_method_call(method="lower_case", | 200 | result = self.protocol.send_method_call(method="lower_case", |
307 | 174 | args=["OHH"], | 201 | args=["OHH"], |
308 | 175 | kwargs={"index": 2}) | 202 | kwargs={"index": 2}) |
310 | 176 | return self.assertSuccess(result, {"result": "OHh"}) | 203 | return self.assertSuccess(result, {"result": "OHh", |
311 | 204 | "deferred": None}) | ||
312 | 177 | 205 | ||
313 | 178 | def test_with_dictionary_arguments(self): | 206 | def test_with_dictionary_arguments(self): |
314 | 179 | """ | 207 | """ |
315 | @@ -183,7 +211,8 @@ | |||
316 | 183 | "alphabetically", | 211 | "alphabetically", |
317 | 184 | args=[{"foo": 2, "bar": 3}], | 212 | args=[{"foo": 2, "bar": 3}], |
318 | 185 | kwargs={}) | 213 | kwargs={}) |
320 | 186 | return self.assertSuccess(result, {"result": "barbarbarfoofoo"}) | 214 | return self.assertSuccess(result, {"result": "barbarbarfoofoo", |
321 | 215 | "deferred": None}) | ||
322 | 187 | 216 | ||
323 | 188 | def test_with_non_serializable_return_value(self): | 217 | def test_with_non_serializable_return_value(self): |
324 | 189 | """ | 218 | """ |
325 | @@ -211,17 +240,19 @@ | |||
326 | 211 | def setUp(self): | 240 | def setUp(self): |
327 | 212 | super(RemoteObjectTest, self).setUp() | 241 | super(RemoteObjectTest, self).setUp() |
328 | 213 | socket = self.mktemp() | 242 | socket = self.mktemp() |
332 | 214 | factory = MethodCallServerFactory(Words()) | 243 | server_factory = MethodCallServerFactory(Words()) |
333 | 215 | factory.protocol = WordsProtocol | 244 | server_factory.protocol = WordsProtocol |
334 | 216 | self.port = reactor.listenUNIX(socket, factory) | 245 | self.port = reactor.listenUNIX(socket, server_factory) |
335 | 217 | 246 | ||
336 | 218 | def set_remote(protocol): | 247 | def set_remote(protocol): |
337 | 219 | self.protocol = protocol | 248 | self.protocol = protocol |
338 | 220 | self.words = RemoteObject(protocol) | 249 | self.words = RemoteObject(protocol) |
339 | 221 | 250 | ||
343 | 222 | connector = ClientCreator(reactor, MethodCallClientProtocol) | 251 | connected = Deferred() |
344 | 223 | connected = connector.connectUNIX(socket) | 252 | connected.addCallback(set_remote) |
345 | 224 | return connected.addCallback(set_remote) | 253 | client_factory = MethodCallClientFactory(reactor, connected.callback) |
346 | 254 | reactor.connectUNIX(socket, client_factory) | ||
347 | 255 | return connected | ||
348 | 225 | 256 | ||
349 | 226 | def tearDown(self): | 257 | def tearDown(self): |
350 | 227 | self.protocol.transport.loseConnection() | 258 | self.protocol.transport.loseConnection() |
351 | @@ -319,6 +350,62 @@ | |||
352 | 319 | result = self.words.guess("word", "cool", value=4) | 350 | result = self.words.guess("word", "cool", value=4) |
353 | 320 | return self.assertSuccess(result, "Guessed!") | 351 | return self.assertSuccess(result, "Guessed!") |
354 | 321 | 352 | ||
355 | 353 | def test_with_success_full_deferred(self): | ||
356 | 354 | """ | ||
357 | 355 | If the target object method returns a L{Deferred}, it is handled | ||
358 | 356 | transparently. | ||
359 | 357 | """ | ||
360 | 358 | result = self.words.google("Landscape") | ||
361 | 359 | return self.assertSuccess(result, "Cool!") | ||
362 | 360 | |||
363 | 361 | def test_with_failing_deferred(self): | ||
364 | 362 | """ | ||
365 | 363 | If the target object method returns a failing L{Deferred}, a | ||
366 | 364 | L{MethodCallError} is raised. | ||
367 | 365 | """ | ||
368 | 366 | result = self.words.google("Weird stuff") | ||
369 | 367 | return self.assertFailure(result, MethodCallError) | ||
370 | 368 | |||
371 | 369 | def test_with_already_callback_deferred(self): | ||
372 | 370 | """ | ||
373 | 371 | The target object method can return an already fired L{Deferred}. | ||
374 | 372 | """ | ||
375 | 373 | result = self.words.google("Easy query") | ||
376 | 374 | return self.assertSuccess(result, "Done!") | ||
377 | 375 | |||
378 | 376 | def test_with_already_errback_deferred(self): | ||
379 | 377 | """ | ||
380 | 378 | If the target object method can return an already failed L{Deferred}. | ||
381 | 379 | """ | ||
382 | 380 | result = self.words.google("Censored") | ||
383 | 381 | return self.assertFailure(result, MethodCallError) | ||
384 | 382 | |||
385 | 383 | def test_with_deferred_timeout(self): | ||
386 | 384 | """ | ||
387 | 385 | If the peer protocol doesn't send a response for a deferred within | ||
388 | 386 | the given timeout, the method call fails. | ||
389 | 387 | """ | ||
390 | 388 | self.protocol.timeout = 0.1 | ||
391 | 389 | result = self.words.google("Long query") | ||
392 | 390 | return self.assertFailure(result, MethodCallError) | ||
393 | 391 | |||
394 | 392 | def test_with_late_response(self): | ||
395 | 393 | """ | ||
396 | 394 | If the peer protocol sends a late response for a request that has | ||
397 | 395 | already timeout, that response is ignored. | ||
398 | 396 | """ | ||
399 | 397 | self.protocol.timeout = 0.01 | ||
400 | 398 | result = self.words.google("Slowish query") | ||
401 | 399 | self.assertFailure(result, MethodCallError) | ||
402 | 400 | |||
403 | 401 | def assert_late_response_is_handled(ignored): | ||
404 | 402 | deferred = Deferred() | ||
405 | 403 | # We wait a bit to be sure that the late response gets delivered | ||
406 | 404 | reactor.callLater(0.1, lambda: deferred.callback(None)) | ||
407 | 405 | return deferred | ||
408 | 406 | |||
409 | 407 | return result.addCallback(assert_late_response_is_handled) | ||
410 | 408 | |||
411 | 322 | 409 | ||
412 | 323 | class RemoteObjectCreatorTest(LandscapeTest): | 410 | class RemoteObjectCreatorTest(LandscapeTest): |
413 | 324 | 411 |
Please note that this mechanism is mainly meant for unit-testing. In general our code will not really use this feature or rely on it, because we don't generally wait at all for remote calls that return deferreds. However when unit-testing it is often needed to wait for a remote call to complete, and this branch makes it easy.