Merge lp:~benji/landscape-client/better-self-signed-cert-ux-3 into lp:~landscape/landscape-client/trunk
- better-self-signed-cert-ux-3
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Benji York |
Approved revision: | 831 |
Merged at revision: | 812 |
Proposed branch: | lp:~benji/landscape-client/better-self-signed-cert-ux-3 |
Merge into: | lp:~landscape/landscape-client/trunk |
Diff against target: |
1345 lines (+604/-570) 4 files modified
landscape/broker/amp.py (+15/-0) landscape/configuration.py (+108/-90) landscape/tests/helpers.py (+1/-0) landscape/tests/test_configuration.py (+480/-480) |
To merge this branch: | bzr merge lp:~benji/landscape-client/better-self-signed-cert-ux-3 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Björn Tillenius (community) | Approve | ||
Chris Glass (community) | Abstain | ||
Данило Шеган (community) | Approve | ||
Review via email:
|
Commit message
This branch is a pure refactoring of the way the client configuration
communicates so as to separate the UI and network code in preparation
for better SSL error handling and user interaction.
Tests were then able to be improved -- but not as far as we would like.
Notes from Chris:
Most of the code breaks out nested function declaration to make them
easier to test, using functools.partial to keep the invocation sane in a
twisted context (test them "flat", use functools to pass closures to
event handlers).
We used manual stubs instead of mocks for readability and personal
preference [of both of us].
Description of the change
This branch is a pure refactoring of the way the client configuration communicates so as to separate the UI and network code in preparation for better SSL error handling and user interaction.
Tests were then able to be improved -- but not as far as we would like.
Notes from Chris:
Most of the code breaks out nested function declaration to make them easier to test, using functools.partial to keep the invocation sane in a twisted context (test them "flat", use functools to pass closures to event handlers).
We used manual stubs instead of mocks for readability and personal preference (of both of us).
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Chris Glass (tribaal) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
> Looks good, and seems to work.
>
> The merge description is wrong however
I've fixed the MP description and incorporated your notes below.
> I'll comment again when I will have performed actual manual tests (give me a
> couple of hours :) )
/me crosses fingers.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Chris Glass (tribaal) wrote : | # |
Summary testing succeeded.
+1
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Данило Шеган (danilo) wrote : | # |
Seems to work fine against my self-signed Landscape. A few comments inline.
I am not too happy with the tests, since you seem to be mostly testing the wiring, and never that it actually works well together. I do understand that coming up with proper tests would require even more work, and with the understanding that this does not make the code base any worse, I am approving it.
Finally, I am not sure it's such a great idea to have Chris do one of the reviews _if_ he has contributed a lot of code/opinions while you were developing this branch. But you guys know best how much his mind is already "tainted" with the approach :)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Chris Glass (tribaal) wrote : | # |
I will abstain from this one since I peer-programmed some of it.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Chris Glass (tribaal) wrote : | # |
Good point Danilo, I added another review slot and abstained.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Björn Tillenius (bjornt) wrote : | # |
Why couldn't this be broken up into smaller branches?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Free Ekanayaka (free.ekanayaka) wrote : | # |
> Why couldn't this be broken up into smaller branches?
Agreed. I didn't look at the code in detail, but I was expecting to see at least 2 branches: one for the pure refactoring of the registration code and one for adding the new functionality.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Chris Glass (tribaal) wrote : | # |
Free, this *is* the pure refactoring branch.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Björn Tillenius (bjornt) wrote : | # |
> Free, this *is* the pure refactoring branch.
In that case, the MP description is wrong. It says that more info is provided to the user and more error conditions are handled.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
> > Free, this *is* the pure refactoring branch.
>
> In that case, the MP description is wrong. It says that more info is provided
> to the user and more error conditions are handled.
I had some sort of edit failure and didn't get the MP description corrected. It is right now.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
> Why couldn't this be broken up into smaller branches?
It surely could have been, but given the nigh untestable nature of the current code structure (even after our refactoring), one large branch was the least risky way to proceed without undue exploratory testing, especially given Chris' upcoming disconnectedness.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
> Seems to work fine against my self-signed Landscape. A few comments inline.
>
> I am not too happy with the tests
As are we. Unfortunately, better tests would have required much more refactoring as well as having some place to put integration tests, which were expressly forbid when the idea of adding some to the existing test base was floated.
> Finally, I am not sure it's such a great idea to have Chris do one of the
> reviews _if_ he has contributed a lot of code/opinions while you were
> developing this branch.
Christ has recused himself from the review.
(I am of the opinion that his review would have been fine, given that I am not big on two-person reviews in the first place, and more so if a branch is the result of pair programming -- which this was.)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Данило Шеган (danilo) wrote : | # |
Yes, agreed re better tests: I don't think it's worth it.
I am happy about the Christ not having an opinion on this branch :)
As for Chris, pair programming usually does lead to better code, but at the same time can result in two minds agreeing to decisions and never re-questioning them after the original agreement (I've been guilty of the same thing).
Anyway, my overall assesement is that this is improvement over the existing code (and tests are no worse) which is reflected in my approval :)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
Thanks for the good review Danilo. I fixed the things you pointed out inline. I also noticed some of the pre-existing tests were generating output to stdout, so I corrected those.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Björn Tillenius (bjornt) wrote : | # |
Sorry, I won't have time to finish this review before my EOD. I added some initial comments inline.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Björn Tillenius (bjornt) wrote : | # |
Ok, I have had time to look at this branch more closely now. How would you feel if I put it in Needs Fixing and requested you to rewrite the tests?
I understand that it's some work doing that, and that fakes are a small improvement over mocking, but the emphasis is on small here. The way you use fakes makes it very similar to mocking. It's still easier to read, but you're still testing the that the code is written in the same way the tests expects them to be.
I would rather that the tests were a bit more functional, in that you set up the conditions, then call register() and check what it returns. I don't care how the deferreds are set up internally. What I care about is that register() returns "success" when the registration succeeds.
It's not quite trivial, but here's an example of a test that checks that register() returns "success" when the registration is successful:
https:/
What do you think?
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
> What do you think?
I haven't taken the time to look deeply into your proposed test approach, but I agree with everything you said. Unfortunately, the amount of time invested in this branch thus far means that the size of the marginal improvement must be very large for it to be worth it and regrettably, that's not the case.
I plan on keeping the test approach in mind for the next time we work on this code (which I hope is soon, it needs quite a bit of help).
Thank you for the review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Björn Tillenius (bjornt) wrote : | # |
Ok, I won't push too hard on adding better tests, since the existing tests weren't great either. But the old tests did actually tests that the async machinery really works, which your tests don't. So I would like you to at least add the test that I pasted, so you have one test that does end-to-end testing.
I'd also like you to respond to the inline comments I added here, as well as those I added previously, and push the latest revision please. I still don't see the changes you did in response to Danilo's comments.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) : | # |
- 827. By Benji York
-
docstring all the things
- 828. By Benji York
-
remove duplicate and redundant assertion
- 829. By Benji York
-
- add tests for reporting registration result to the user
- fix up tests that print to stdout so they won't
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
> [...] So I would like you to at
> least add the test that I pasted, so you have one test that does end-to-end
> testing.
Done. (BTW, that diff was odd. I'm curious how you generated it. I had to hand apply it.)
> I'd also like you to respond to the inline comments I added here, as well as
> those I added previously
Done.
> and push the latest revision please.
Done.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Björn Tillenius (bjornt) wrote : | # |
Sorry for being a pain :) But the latest diff isn't what I would expect it to be. It lacks at least one change that you said you would do (I added an inline comment), and the test I added isn't there. Instead you added a bunch of other tests that do mocking and seem redundant?
BTW, maybe the patch was odd because you looked at the wrong pastebin? ;) Although, the patch could look odd since I based it on what you pushed. Actually, looking at what you pushed, it's quite clear that you didn't push the changes you did in reply to my comments. You pushed revisions from Feb 26.
Please make it a habit to always push the latest code to LP. It's a bit complicated to review the branch when you know that there are other changes that you can't see.
- 830. By Benji York
-
- add a test that runs register() in a more full-boddied way
- use Twisted parameter passing functionality instead of partials
- tweak parameter order to account for different calling style
- add and improve motivational comments around test assertions
- fix a test assertion that was unreachable
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
> Sorry for being a pain :) But the latest diff isn't what I would expect it to
> be.
I pushed, but apparently forgot to commit beforehand.
> BTW, maybe the patch was odd because you looked at the wrong pastebin? ;)
This is the pastebin I was referring to: https:/
The oddities I see are the blank lines with a dot in the indicator position and the lack of whitespace after the file name in the headers.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Björn Tillenius (bjornt) wrote : | # |
Ok, let's get this branch landed now!
> This is the pastebin I was referring to: https:/
>
> The oddities I see are the blank lines with a dot in the indicator position and the lack of
> whitespace after the file name in the headers.
Oh, that is odd indeed. I've never seen that before, but it seems to be since I copied
it from 'bzr diff | vim -' and the diff highlighter adds those periods.
- 831. By Benji York
-
merge from trunk
Preview Diff
1 | === modified file 'landscape/broker/amp.py' |
2 | --- landscape/broker/amp.py 2015-01-12 12:10:05 +0000 |
3 | +++ landscape/broker/amp.py 2015-03-04 14:53:14 +0000 |
4 | @@ -65,6 +65,21 @@ |
5 | return maybeDeferred(callable, *args) |
6 | return succeed(None) |
7 | |
8 | + def call_on_event(self, handlers): |
9 | + """Call a given handler as soon as a certain event occurs. |
10 | + |
11 | + @param handlers: A dictionary mapping event types to callables, where |
12 | + an event type is string (the name of the event). When the first of |
13 | + the given event types occurs in the broker reactor, the associated |
14 | + callable will be fired. |
15 | + """ |
16 | + result = self.broker_server.listen_events(handlers.keys()) |
17 | + return result.addCallback( |
18 | + lambda (event_type, kwargs): handlers[event_type](**kwargs)) |
19 | + |
20 | + def register(self): |
21 | + return succeed(None) |
22 | + |
23 | |
24 | class RemoteBrokerConnector(ComponentConnector): |
25 | """Helper to create connections with the L{BrokerServer}.""" |
26 | |
27 | === modified file 'landscape/configuration.py' |
28 | --- landscape/configuration.py 2015-01-12 09:27:02 +0000 |
29 | +++ landscape/configuration.py 2015-03-04 14:53:14 +0000 |
30 | @@ -4,12 +4,14 @@ |
31 | for the C{landscape-config} script. |
32 | """ |
33 | |
34 | +from __future__ import print_function |
35 | + |
36 | +from functools import partial |
37 | import base64 |
38 | -import time |
39 | -import sys |
40 | +import getpass |
41 | import os |
42 | -import getpass |
43 | import pwd |
44 | +import sys |
45 | |
46 | from StringIO import StringIO |
47 | |
48 | @@ -35,6 +37,8 @@ |
49 | |
50 | |
51 | def print_text(text, end="\n", error=False): |
52 | + """Display the given text to the user, using stderr if flagged as an error. |
53 | + """ |
54 | if error: |
55 | stream = sys.stderr |
56 | else: |
57 | @@ -584,8 +588,60 @@ |
58 | return key_filename |
59 | |
60 | |
61 | -def register(config, on_message=print_text, on_error=sys.exit, reactor=None, |
62 | - max_retries=14): |
63 | +def failure(add_result): |
64 | + """Handle a failed communication by recording the kind of failure.""" |
65 | + add_result("failure") |
66 | + |
67 | + |
68 | +def exchange_failure(add_result, ssl_error=False): |
69 | + """Handle a failed call by recording if the failure was SSL-related.""" |
70 | + if ssl_error: |
71 | + add_result("ssl-error") |
72 | + else: |
73 | + add_result("non-ssl-error") |
74 | + |
75 | + |
76 | +def handle_registration_errors(add_result, connector, failure): |
77 | + """HAndle a failed registration by recording the kind of failure.""" |
78 | + # We'll get invalid credentials through the signal. |
79 | + failure.trap(InvalidCredentialsError, MethodCallError) |
80 | + add_result("registration-error") |
81 | + connector.disconnect() |
82 | + |
83 | + |
84 | +def success(add_result): |
85 | + """Handle a successful communication by recording the fact.""" |
86 | + add_result("success") |
87 | + |
88 | + |
89 | +def done(ignored_result, connector, reactor): |
90 | + """Clean up after communicating with the server.""" |
91 | + connector.disconnect() |
92 | + reactor.stop() |
93 | + |
94 | + |
95 | +def got_connection(add_result, connector, reactor, remote): |
96 | + """Handle becomming connected to a broker.""" |
97 | + handlers = {"registration-done": partial(success, add_result), |
98 | + "registration-failed": partial(failure, add_result), |
99 | + "exchange-failed": partial(exchange_failure, add_result)} |
100 | + deferreds = [ |
101 | + remote.call_on_event(handlers), |
102 | + remote.register().addErrback( |
103 | + handle_registration_errors, add_result, connector)] |
104 | + results = gather_results(deferreds) |
105 | + results.addCallback(done, connector, reactor) |
106 | + return results |
107 | + |
108 | + |
109 | +def got_error(failure, print=print): |
110 | + """...from broker.""" |
111 | + print(failure.getTraceback(), file=sys.stderr) |
112 | + raise SystemExit |
113 | + |
114 | + |
115 | +def register(config, reactor, connector_factory=RemoteBrokerConnector, |
116 | + got_connection=got_connection, max_retries=14, results=None): |
117 | """Instruct the Landscape Broker to register the client. |
118 | |
119 | The broker will be instructed to reload its configuration and then to |
120 | @@ -605,91 +661,48 @@ |
121 | max_retries = 14 |
122 | |
123 | 0.05 * (1 - 1.62 ** 14) / (1 - 1.62) = 69 seconds |
124 | - """ |
125 | - if reactor is None: |
126 | - reactor = LandscapeReactor() |
127 | - exit_with_error = [] |
128 | - |
129 | - def stop(errors): |
130 | - if not config.ok_no_register: |
131 | - for error in errors: |
132 | - if error is not None: |
133 | - exit_with_error.append(error) |
134 | - connector.disconnect() |
135 | - reactor.stop() |
136 | - |
137 | - def failure(): |
138 | - on_message("Invalid account name or " |
139 | - "registration key.", error=True) |
140 | - return 2 |
141 | - |
142 | - def success(): |
143 | - on_message("System successfully registered.") |
144 | - |
145 | - def exchange_failure(ssl_error=False): |
146 | - if ssl_error: |
147 | - message = ("\nThe server's SSL information is incorrect, or fails " |
148 | - "signature verification!\n" |
149 | - "If the server is using a self-signed certificate, " |
150 | - "please ensure you supply it with the --ssl-public-key " |
151 | - "parameter.") |
152 | - else: |
153 | - message = ("\nWe were unable to contact the server.\n" |
154 | - "Your internet connection may be down. " |
155 | - "The landscape client will continue to try and contact " |
156 | - "the server periodically.") |
157 | - |
158 | - on_message(message, error=True) |
159 | - return 2 |
160 | - |
161 | - def handle_registration_errors(failure): |
162 | - # We'll get invalid credentials through the signal. |
163 | - failure.trap(InvalidCredentialsError, MethodCallError) |
164 | - connector.disconnect() |
165 | - |
166 | - def catch_all(failure): |
167 | - on_message(failure.getTraceback(), error=True) |
168 | - on_message("Unknown error occurred.", error=True) |
169 | - return [2] |
170 | - |
171 | - on_message("Please wait... ", "") |
172 | - |
173 | - time.sleep(2) |
174 | - |
175 | - def got_connection(remote): |
176 | - handlers = {"registration-done": success, |
177 | - "registration-failed": failure, |
178 | - "exchange-failed": exchange_failure} |
179 | - deferreds = [ |
180 | - remote.call_on_event(handlers), |
181 | - remote.register().addErrback(handle_registration_errors)] |
182 | - # We consume errors here to ignore errors after the first one. |
183 | - # catch_all will be called for the very first deferred that fails. |
184 | - results = gather_results(deferreds, consume_errors=True) |
185 | - results.addErrback(catch_all) |
186 | - results.addCallback(stop) |
187 | - |
188 | - def got_error(failure): |
189 | - on_message("There was an error communicating with the Landscape" |
190 | - " client.", error=True) |
191 | - on_message("This machine will be registered with the provided " |
192 | - "details when the client runs.", error=True) |
193 | - stop([2]) |
194 | - |
195 | - connector = RemoteBrokerConnector(reactor, config) |
196 | - result = connector.connect(max_retries=max_retries, quiet=True) |
197 | - result.addCallback(got_connection) |
198 | - result.addErrback(got_error) |
199 | - |
200 | + """ |
201 | + if results is None: |
202 | + results = [] |
203 | + add_result = results.append |
204 | + |
205 | + connector = connector_factory(reactor, config) |
206 | + connection = connector.connect(max_retries=max_retries, quiet=True) |
207 | + connection.addCallback( |
208 | + partial(got_connection, add_result, connector, reactor)) |
209 | + connection.addErrback(got_error) |
210 | reactor.run() |
211 | |
212 | - if exit_with_error: |
213 | - on_error(exit_with_error[0]) |
214 | - |
215 | - return result |
216 | - |
217 | - |
218 | -def main(args): |
219 | + assert len(results) == 1, "We expect exactly one result." |
220 | + # Results will be things like "success" or "ssl-error". |
221 | + return results[0] |
222 | + |
223 | + |
224 | +def report_registration_outcome(what_happened, print=print): |
225 | + """ |
226 | + Report the registrtion interaction outcome to the user in human-readable |
227 | + form. |
228 | + """ |
229 | + if what_happened == "success": |
230 | + print("System successfully registered.") |
231 | + elif what_happened == "failure": |
232 | + print("Invalid account name or registration key.", file=sys.stderr) |
233 | + elif what_happened == "ssl-error": |
234 | + print("\nThe server's SSL information is incorrect, or fails " |
235 | + "signature verification!\n" |
236 | + "If the server is using a self-signed certificate, " |
237 | + "please ensure you supply it with the --ssl-public-key " |
238 | + "parameter.", file=sys.stderr) |
239 | + elif what_happened == "non-ssl-error": |
240 | + print("\nWe were unable to contact the server.\n" |
241 | + "Your internet connection may be down. " |
242 | + "The landscape client will continue to try and contact " |
243 | + "the server periodically.", file=sys.stderr) |
244 | + |
245 | + |
246 | +def main(args, print=print): |
247 | + """Interact with the user and the server to set up client configuration.""" |
248 | + |
249 | config = LandscapeSetupConfiguration() |
250 | try: |
251 | config.load(args) |
252 | @@ -716,11 +729,16 @@ |
253 | print_text(str(e)) |
254 | sys.exit("Aborting Landscape configuration") |
255 | |
256 | + print("Please wait...") |
257 | + |
258 | # Attempt to register the client. |
259 | + reactor = LandscapeReactor() |
260 | if config.silent: |
261 | - register(config) |
262 | + result = register(config, reactor) |
263 | + report_registration_outcome(result, print=print) |
264 | else: |
265 | answer = raw_input("\nRequest a new registration for " |
266 | "this computer now? (Y/n): ") |
267 | if not answer.upper().startswith("N"): |
268 | - register(config) |
269 | + result = register(config, reactor) |
270 | + report_registration_outcome(result, print=print) |
271 | |
272 | === modified file 'landscape/tests/helpers.py' |
273 | --- landscape/tests/helpers.py 2013-09-25 08:43:17 +0000 |
274 | +++ landscape/tests/helpers.py 2015-03-04 14:53:14 +0000 |
275 | @@ -359,6 +359,7 @@ |
276 | transport_factory = FakeTransport |
277 | |
278 | test_case.broker_service = FakeBrokerService(config) |
279 | + test_case.reactor = test_case.broker_service.reactor |
280 | test_case.remote = FakeRemoteBroker( |
281 | test_case.broker_service.exchanger, |
282 | test_case.broker_service.message_store, |
283 | |
284 | === modified file 'landscape/tests/test_configuration.py' |
285 | --- landscape/tests/test_configuration.py 2015-01-12 09:29:17 +0000 |
286 | +++ landscape/tests/test_configuration.py 2015-03-04 14:53:14 +0000 |
287 | @@ -1,27 +1,29 @@ |
288 | +from __future__ import print_function |
289 | + |
290 | +from ConfigParser import ConfigParser |
291 | +from cStringIO import StringIO |
292 | +from getpass import getpass |
293 | import os |
294 | import sys |
295 | -from getpass import getpass |
296 | -from ConfigParser import ConfigParser |
297 | -from cStringIO import StringIO |
298 | - |
299 | -from twisted.internet.defer import succeed, fail |
300 | -from twisted.internet.task import Clock |
301 | - |
302 | -from landscape.lib.amp import MethodCallSender |
303 | -from landscape.reactor import LandscapeReactor, FakeReactor |
304 | +import unittest |
305 | + |
306 | +from twisted.internet.defer import succeed |
307 | + |
308 | +from landscape.broker.registration import InvalidCredentialsError |
309 | +from landscape.tests.helpers import FakeBrokerServiceHelper |
310 | +from landscape.broker.tests.helpers import RemoteBrokerHelper |
311 | +from landscape.lib.amp import MethodCallError |
312 | from landscape.lib.fetch import HTTPCodeError, PyCurlError |
313 | from landscape.configuration import ( |
314 | print_text, LandscapeSetupScript, LandscapeSetupConfiguration, |
315 | register, setup, main, setup_init_script_and_start_client, |
316 | stop_client_and_disable_init_script, ConfigurationError, |
317 | ImportOptionError, store_public_key_data, |
318 | - bootstrap_tree) |
319 | -from landscape.broker.registration import InvalidCredentialsError |
320 | + bootstrap_tree, got_connection, success, failure, exchange_failure, |
321 | + handle_registration_errors, done, got_error, report_registration_outcome) |
322 | from landscape.sysvconfig import SysVConfig, ProcessError |
323 | -from landscape.tests.helpers import ( |
324 | - LandscapeTest, BrokerServiceHelper, EnvironSaverHelper) |
325 | -from landscape.tests.mocker import ARGS, ANY, MATCH, CONTAINS, expect |
326 | -from landscape.broker.amp import RemoteBroker |
327 | +from landscape.tests.helpers import LandscapeTest, EnvironSaverHelper |
328 | +from landscape.tests.mocker import ANY, MATCH, CONTAINS, expect |
329 | |
330 | |
331 | class LandscapeConfigurationTest(LandscapeTest): |
332 | @@ -41,6 +43,113 @@ |
333 | return config |
334 | |
335 | |
336 | +class SuccessTests(unittest.TestCase): |
337 | + def test_success(self): |
338 | + """The success handler records the success.""" |
339 | + results = [] |
340 | + success(results.append) |
341 | + self.assertEqual(["success"], results) |
342 | + |
343 | + |
344 | +class FailureTests(unittest.TestCase): |
345 | + def test_failure(self): |
346 | + """The failure handler records the failure and returns non-zero.""" |
347 | + results = [] |
348 | + self.assertNotEqual(0, failure(results.append)) |
349 | + self.assertEqual(["failure"], results) |
350 | + |
351 | + |
352 | +class ExchangeFailureTests(unittest.TestCase): |
353 | + |
354 | + def test_exchange_failure_ssl(self): |
355 | + """The exchange_failure() handler records whether or not the failure |
356 | + involved SSL or not and returns non-zero.""" |
357 | + results = [] |
358 | + self.assertNotEqual(0, |
359 | + exchange_failure(results.append, ssl_error=True)) |
360 | + self.assertEqual(["ssl-error"], results) |
361 | + |
362 | + def test_exchange_failure_non_ssl(self): |
363 | + """ |
364 | + The exchange_failure() handler records whether or not the failure |
365 | + involved SSL or not and returns non-zero. |
366 | + """ |
367 | + results = [] |
368 | + self.assertNotEqual(0, |
369 | + exchange_failure(results.append, ssl_error=False)) |
370 | + self.assertEqual(["non-ssl-error"], results) |
371 | + |
372 | + |
373 | +class HandleRegistrationErrorsTests(unittest.TestCase): |
374 | + |
375 | + def test_handle_registration_errors(self): |
376 | + """ |
377 | + The handle_registration_errors() function handles |
378 | + InvalidCredentialsError and MethodCallError errors and records the |
379 | + type of failure and disconnects. |
380 | + """ |
381 | + class FauxFailure(object): |
382 | + def trap(self, *trapped): |
383 | + self.trapped_exceptions = trapped |
384 | + |
385 | + faux_connector = FauxConnector() |
386 | + faux_failure = FauxFailure() |
387 | + |
388 | + results = [] |
389 | + self.assertNotEqual(0, |
390 | + handle_registration_errors( |
391 | + results.append, faux_connector, faux_failure)) |
392 | + self.assertEqual(["registration-error"], results) |
393 | + self.assertTrue(faux_connector.was_disconnected) |
394 | + self.assertTrue( |
395 | + [InvalidCredentialsError, MethodCallError], |
396 | + faux_failure.trapped_exceptions) |
397 | + |
398 | + |
399 | +class DoneTests(unittest.TestCase): |
400 | + |
401 | + def test_done(self): |
402 | + """The done() function handles cleaning up.""" |
403 | + class FauxConnector(object): |
404 | + was_disconnected = False |
405 | + |
406 | + def disconnect(self): |
407 | + self.was_disconnected = True |
408 | + |
409 | + class FauxReactor(object): |
410 | + was_stopped = False |
411 | + |
412 | + def stop(self): |
413 | + self.was_stopped = True |
414 | + |
415 | + faux_connector = FauxConnector() |
416 | + faux_reactor = FauxReactor() |
417 | + |
418 | + done(None, faux_connector, faux_reactor) |
419 | + self.assertTrue(faux_connector.was_disconnected) |
420 | + self.assertTrue(faux_reactor.was_stopped) |
421 | + |
422 | + |
423 | +class GotErrorTests(unittest.TestCase): |
424 | + |
425 | + def test_got_error(self): |
426 | + """The got_error() function handles displaying errors and exiting.""" |
427 | + class FauxFailure(object): |
428 | + |
429 | + def getTraceback(self): |
430 | + return "traceback" |
431 | + |
432 | + printed = [] |
433 | + |
434 | + def faux_print(text, file): |
435 | + printed.append((text, file)) |
436 | + |
437 | + with self.assertRaises(SystemExit): |
438 | + got_error(FauxFailure(), print=faux_print) |
439 | + |
440 | + self.assertEqual([('traceback', sys.stderr)], printed) |
441 | + |
442 | + |
443 | class PrintTextTest(LandscapeTest): |
444 | |
445 | def test_default(self): |
446 | @@ -683,6 +792,11 @@ |
447 | self.assertTrue(os.path.isdir(annotations_path)) |
448 | |
449 | |
450 | +def noop_print(*args, **kws): |
451 | + """A print that doesn't do anything.""" |
452 | + pass |
453 | + |
454 | + |
455 | class ConfigurationFunctionsTest(LandscapeConfigurationTest): |
456 | |
457 | helpers = [EnvironSaverHelper] |
458 | @@ -1022,12 +1136,12 @@ |
459 | |
460 | # This must not be called. |
461 | register_mock = self.mocker.replace(register, passthrough=False) |
462 | - register_mock(ANY) |
463 | + register_mock(ANY, ANY) |
464 | self.mocker.count(0) |
465 | |
466 | self.mocker.replay() |
467 | |
468 | - main(["-c", self.make_working_config()]) |
469 | + main(["-c", self.make_working_config()], print=noop_print) |
470 | |
471 | def test_main_silent(self): |
472 | """ |
473 | @@ -1038,7 +1152,7 @@ |
474 | setup_mock(ANY) |
475 | |
476 | register_mock = self.mocker.replace(register, passthrough=False) |
477 | - register_mock(ANY) |
478 | + register_mock(ANY, ANY) |
479 | self.mocker.count(1) |
480 | |
481 | self.mocker.replay() |
482 | @@ -1049,7 +1163,121 @@ |
483 | "account_name = Old Name\n" |
484 | "registration_key = Old Password\n" |
485 | ) |
486 | - main(["-c", config_filename, "--silent"]) |
487 | + main(["-c", config_filename, "--silent"], print=noop_print) |
488 | + |
489 | + def test_main_user_interaction_success(self): |
490 | + """The successful result of register() is communicated to the user.""" |
491 | + setup_mock = self.mocker.replace(setup) |
492 | + setup_mock(ANY) |
493 | + |
494 | + raw_input_mock = self.mocker.replace(raw_input) |
495 | + raw_input_mock("\nRequest a new registration for " |
496 | + "this computer now? (Y/n): ") |
497 | + self.mocker.result("y") |
498 | + |
499 | + # The register() function will be called. |
500 | + register_mock = self.mocker.replace(register, passthrough=False) |
501 | + register_mock(ANY, ANY) |
502 | + self.mocker.result("success") |
503 | + |
504 | + self.mocker.replay() |
505 | + |
506 | + printed = [] |
507 | + |
508 | + def faux_print(string, file=sys.stdout): |
509 | + printed.append((string, file)) |
510 | + |
511 | + main(["-c", self.make_working_config()], print=faux_print) |
512 | + self.assertEqual( |
513 | + [("Please wait...", sys.stdout), |
514 | + ("System successfully registered.", sys.stdout)], |
515 | + printed) |
516 | + |
517 | + def test_main_user_interaction_failure(self): |
518 | + """The failed result of register() is communicated to the user.""" |
519 | + setup_mock = self.mocker.replace(setup) |
520 | + setup_mock(ANY) |
521 | + |
522 | + raw_input_mock = self.mocker.replace(raw_input) |
523 | + raw_input_mock("\nRequest a new registration for " |
524 | + "this computer now? (Y/n): ") |
525 | + self.mocker.result("y") |
526 | + |
527 | + # The register() function will be called. |
528 | + register_mock = self.mocker.replace(register, passthrough=False) |
529 | + register_mock(ANY, ANY) |
530 | + self.mocker.result("failure") |
531 | + |
532 | + self.mocker.replay() |
533 | + |
534 | + printed = [] |
535 | + |
536 | + def faux_print(string, file=sys.stdout): |
537 | + printed.append((string, file)) |
538 | + |
539 | + main(["-c", self.make_working_config()], print=faux_print) |
540 | + # Note that the error is output via sys.stderr. |
541 | + self.assertEqual( |
542 | + [("Please wait...", sys.stdout), |
543 | + ("Invalid account name or registration key.", sys.stderr)], |
544 | + printed) |
545 | + |
546 | + def test_main_user_interaction_success_silent(self): |
547 | + """A successful result is communicated to the user even with --silent. |
548 | + """ |
549 | + setup_mock = self.mocker.replace(setup) |
550 | + setup_mock(ANY) |
551 | + |
552 | + raw_input_mock = self.mocker.replace(raw_input) |
553 | + raw_input_mock(ANY) |
554 | + self.mocker.count(0) |
555 | + |
556 | + # The register() function will be called. |
557 | + register_mock = self.mocker.replace(register, passthrough=False) |
558 | + register_mock(ANY, ANY) |
559 | + self.mocker.result("success") |
560 | + |
561 | + self.mocker.replay() |
562 | + |
563 | + printed = [] |
564 | + |
565 | + def faux_print(string, file=sys.stdout): |
566 | + printed.append((string, file)) |
567 | + |
568 | + main(["--silent", "-c", self.make_working_config()], print=faux_print) |
569 | + self.assertEqual( |
570 | + [("Please wait...", sys.stdout), |
571 | + ("System successfully registered.", sys.stdout)], |
572 | + printed) |
573 | + |
574 | + def test_main_user_interaction_failure_silent(self): |
575 | + """A failure result is communicated to the user even with --silent. |
576 | + """ |
577 | + setup_mock = self.mocker.replace(setup) |
578 | + setup_mock(ANY) |
579 | + |
580 | + raw_input_mock = self.mocker.replace(raw_input) |
581 | + raw_input_mock(ANY) |
582 | + self.mocker.count(0) |
583 | + |
584 | + # The register() function will be called. |
585 | + register_mock = self.mocker.replace(register, passthrough=False) |
586 | + register_mock(ANY, ANY) |
587 | + self.mocker.result("failure") |
588 | + |
589 | + self.mocker.replay() |
590 | + |
591 | + printed = [] |
592 | + |
593 | + def faux_print(string, file=sys.stdout): |
594 | + printed.append((string, file)) |
595 | + |
596 | + main(["--silent", "-c", self.make_working_config()], print=faux_print) |
597 | + # Note that the error is output via sys.stderr. |
598 | + self.assertEqual( |
599 | + [("Please wait...", sys.stdout), |
600 | + ("Invalid account name or registration key.", sys.stderr)], |
601 | + printed) |
602 | |
603 | def make_working_config(self): |
604 | return self.makeFile("[client]\n" |
605 | @@ -1081,10 +1309,10 @@ |
606 | self.mocker.result("") |
607 | |
608 | register_mock = self.mocker.replace(register, passthrough=False) |
609 | - register_mock(ANY) |
610 | + register_mock(ANY, ANY) |
611 | |
612 | self.mocker.replay() |
613 | - main(["--config", self.make_working_config()]) |
614 | + main(["--config", self.make_working_config()], print=noop_print) |
615 | |
616 | def test_errors_from_restart_landscape(self): |
617 | """ |
618 | @@ -1141,10 +1369,10 @@ |
619 | self.mocker.result("") |
620 | |
621 | register_mock = self.mocker.replace(register, passthrough=False) |
622 | - register_mock(ANY) |
623 | + register_mock(ANY, ANY) |
624 | |
625 | self.mocker.replay() |
626 | - main(["-c", self.make_working_config()]) |
627 | + main(["-c", self.make_working_config()], print=noop_print) |
628 | |
629 | def test_setup_init_script_and_start_client(self): |
630 | sysvconfig_mock = self.mocker.patch(SysVConfig) |
631 | @@ -1178,11 +1406,11 @@ |
632 | # The registration logic should be called and passed the configuration |
633 | # file. |
634 | register_mock = self.mocker.replace(register, passthrough=False) |
635 | - register_mock(ANY) |
636 | + register_mock(ANY, ANY) |
637 | |
638 | self.mocker.replay() |
639 | |
640 | - main(["--silent", "-c", self.make_working_config()]) |
641 | + main(["--silent", "-c", self.make_working_config()], print=noop_print) |
642 | |
643 | def test_disable(self): |
644 | stop_client_and_disable_init_script_mock = self.mocker.replace( |
645 | @@ -1672,469 +1900,203 @@ |
646 | # we care about are done. |
647 | |
648 | |
649 | +class FakeConnectorFactory(object): |
650 | + |
651 | + def __init__(self, remote): |
652 | + self.remote = remote |
653 | + |
654 | + def __call__(self, reactor, config): |
655 | + self.reactor = reactor |
656 | + self.config = config |
657 | + return self |
658 | + |
659 | + def connect(self, max_retries=None, quiet=False): |
660 | + return succeed(self.remote) |
661 | + |
662 | + def disconnect(self): |
663 | + return succeed(None) |
664 | + |
665 | + |
666 | +class RegisterRealFunctionTest(LandscapeConfigurationTest): |
667 | + |
668 | + helpers = [FakeBrokerServiceHelper] |
669 | + |
670 | + def setUp(self): |
671 | + super(RegisterRealFunctionTest, self).setUp() |
672 | + self.config = LandscapeSetupConfiguration() |
673 | + self.config.load(["-c", self.config_filename]) |
674 | + |
675 | + def test_register_success(self): |
676 | + self.reactor.call_later(0, self.reactor.fire, "registration-done") |
677 | + connector_factory = FakeConnectorFactory(self.remote) |
678 | + result = register( |
679 | + self.config, self.reactor, connector_factory, max_retries=99) |
680 | + self.assertEqual("success", result) |
681 | + |
682 | + |
683 | +class FauxConnection(object): |
684 | + def __init__(self): |
685 | + self.callbacks = [] |
686 | + self.errbacks = [] |
687 | + |
688 | + def addCallback(self, func, *args, **kws): |
689 | + self.callbacks.append(func) |
690 | + |
691 | + def addErrback(self, func, *args, **kws): |
692 | + self.errbacks.append(func) |
693 | + |
694 | + |
695 | +class FauxConnector(object): |
696 | + |
697 | + was_disconnected = False |
698 | + |
699 | + def __init__(self, reactor=None, config=None): |
700 | + self.reactor = reactor |
701 | + self.config = config |
702 | + |
703 | + def connect(self, max_retries, quiet): |
704 | + self.max_retries = max_retries |
705 | + self.connection = FauxConnection() |
706 | + return self.connection |
707 | + |
708 | + def disconnect(self): |
709 | + self.was_disconnected = True |
710 | + |
711 | + |
712 | class RegisterFunctionTest(LandscapeConfigurationTest): |
713 | |
714 | - helpers = [BrokerServiceHelper] |
715 | + helpers = [RemoteBrokerHelper] |
716 | |
717 | def setUp(self): |
718 | super(RegisterFunctionTest, self).setUp() |
719 | self.config = LandscapeSetupConfiguration() |
720 | self.config.load(["-c", self.config_filename]) |
721 | |
722 | - def test_register_success(self): |
723 | - service = self.broker_service |
724 | - |
725 | - registration_mock = self.mocker.replace(service.registration) |
726 | - print_text_mock = self.mocker.replace(print_text) |
727 | - reactor_mock = self.mocker.patch(FakeReactor) |
728 | - |
729 | - # This must necessarily happen in the following order. |
730 | - self.mocker.order() |
731 | - |
732 | - # This very informative message is printed out. |
733 | - print_text_mock("Please wait... ", "") |
734 | - |
735 | - time_mock = self.mocker.replace("time") |
736 | - time_mock.sleep(ANY) |
737 | - self.mocker.count(1) |
738 | - |
739 | - # The register() method is called. We fire the "registration-done" |
740 | - # event after it's done, so that it cascades into a deferred callback. |
741 | - |
742 | - def register_done(): |
743 | - service.reactor.fire("registration-done") |
744 | - |
745 | - registration_mock.register() |
746 | - self.mocker.call(register_done) |
747 | - |
748 | - # The deferred callback finally prints out this message. |
749 | - print_text_mock("System successfully registered.") |
750 | - |
751 | - reactor_mock.stop() |
752 | - |
753 | - # This is actually called after everything else since all deferreds |
754 | - # are synchronous and callbacks will be executed immediately. |
755 | - reactor_mock.run() |
756 | - |
757 | - # Nothing else is printed! |
758 | - print_text_mock(ANY) |
759 | - self.mocker.count(0) |
760 | - |
761 | - self.mocker.replay() |
762 | - |
763 | - # DO IT! |
764 | - return register(self.config, print_text, sys.exit, |
765 | - reactor=FakeReactor()) |
766 | - |
767 | - def test_register_failure(self): |
768 | - """ |
769 | - When registration fails because of invalid credentials, a message will |
770 | - be printed to the console and the program will exit. |
771 | - """ |
772 | - service = self.broker_service |
773 | - |
774 | - self.log_helper.ignore_errors(InvalidCredentialsError) |
775 | - registration_mock = self.mocker.replace(service.registration) |
776 | - print_text_mock = self.mocker.replace(print_text) |
777 | - reactor_mock = self.mocker.patch(FakeReactor) |
778 | - |
779 | - # This must necessarily happen in the following order. |
780 | - self.mocker.order() |
781 | - |
782 | - # This very informative message is printed out. |
783 | - print_text_mock("Please wait... ", "") |
784 | - |
785 | - time_mock = self.mocker.replace("time") |
786 | - time_mock.sleep(ANY) |
787 | - self.mocker.count(1) |
788 | - |
789 | - # The register() method is called. We fire the "registration-failed" |
790 | - # event after it's done, so that it cascades into a deferred errback. |
791 | - |
792 | - def register_done(): |
793 | - service.reactor.fire("registration-failed") |
794 | - |
795 | - registration_mock.register() |
796 | - self.mocker.call(register_done) |
797 | - |
798 | - # The deferred errback finally prints out this message. |
799 | - print_text_mock("Invalid account name or registration key.", |
800 | - error=True) |
801 | - |
802 | - reactor_mock.stop() |
803 | - |
804 | - # This is actually called after everything else since all deferreds |
805 | - # are synchronous and callbacks will be executed immediately. |
806 | - reactor_mock.run() |
807 | - |
808 | - # Nothing else is printed! |
809 | - print_text_mock(ANY) |
810 | - self.mocker.count(0) |
811 | - |
812 | - self.mocker.replay() |
813 | - |
814 | - # DO IT! |
815 | - exit = [] |
816 | - register(self.config, print_text, exit.append, reactor=FakeReactor()) |
817 | - self.assertEqual([2], exit) |
818 | - |
819 | - def test_register_exchange_failure(self): |
820 | - """ |
821 | - When registration fails because the server couldn't be contacted, a |
822 | - message is printed and the program quits. |
823 | - """ |
824 | - service = self.broker_service |
825 | - |
826 | - registration_mock = self.mocker.replace(service.registration) |
827 | - print_text_mock = self.mocker.replace(print_text) |
828 | - reactor_mock = self.mocker.patch(FakeReactor) |
829 | - |
830 | - # This must necessarily happen in the following order. |
831 | - self.mocker.order() |
832 | - |
833 | - # This very informative message is printed out. |
834 | - print_text_mock("Please wait... ", "") |
835 | - |
836 | - time_mock = self.mocker.replace("time") |
837 | - time_mock.sleep(ANY) |
838 | - self.mocker.count(1) |
839 | - |
840 | - def register_done(): |
841 | - service.reactor.fire("exchange-failed") |
842 | - registration_mock.register() |
843 | - self.mocker.call(register_done) |
844 | - |
845 | - # The deferred errback finally prints out this message. |
846 | - print_text_mock("\nWe were unable to contact the server.\n" |
847 | - "Your internet connection may be down. " |
848 | - "The landscape client will continue to try and " |
849 | - "contact the server periodically.", |
850 | - error=True) |
851 | - |
852 | - reactor_mock.stop() |
853 | - |
854 | - # This is actually called after everything else since all deferreds |
855 | - # are synchronous and callbacks will be executed immediately. |
856 | - reactor_mock.run() |
857 | - |
858 | - # Nothing else is printed! |
859 | - print_text_mock(ANY) |
860 | - self.mocker.count(0) |
861 | - |
862 | - self.mocker.replay() |
863 | - |
864 | - # DO IT! |
865 | - exit = [] |
866 | - register(self.config, print_text, exit.append, reactor=FakeReactor()) |
867 | - self.assertEqual([2], exit) |
868 | - |
869 | - def test_register_exchange_SSL_failure(self): |
870 | - """ |
871 | - When registration fails because the server's SSL certificate could not |
872 | - be validated, a message is printed and the program quits. |
873 | - """ |
874 | - service = self.broker_service |
875 | - |
876 | - registration_mock = self.mocker.replace(service.registration) |
877 | - print_text_mock = self.mocker.replace(print_text) |
878 | - reactor_mock = self.mocker.patch(FakeReactor) |
879 | - |
880 | - # This must necessarily happen in the following order. |
881 | - self.mocker.order() |
882 | - |
883 | - # This very informative message is printed out. |
884 | - print_text_mock("Please wait... ", "") |
885 | - |
886 | - time_mock = self.mocker.replace("time") |
887 | - time_mock.sleep(ANY) |
888 | - self.mocker.count(1) |
889 | - |
890 | - def register_done(): |
891 | - service.reactor.fire("exchange-failed", ssl_error=True) |
892 | - registration_mock.register() |
893 | - self.mocker.call(register_done) |
894 | - |
895 | - # The deferred errback finally prints out this message. |
896 | - print_text_mock("\nThe server's SSL information is incorrect, or " |
897 | - "fails signature verification!\n" |
898 | - "If the server is using a self-signed certificate, " |
899 | - "please ensure you supply it with the " |
900 | - "--ssl-public-key parameter.", |
901 | - error=True) |
902 | - |
903 | - reactor_mock.stop() |
904 | - |
905 | - # This is actually called after everything else since all deferreds |
906 | - # are synchronous and callbacks will be executed immediately. |
907 | - reactor_mock.run() |
908 | - |
909 | - # Nothing else is printed! |
910 | - print_text_mock(ANY) |
911 | - self.mocker.count(0) |
912 | - |
913 | - self.mocker.replay() |
914 | - |
915 | - # DO IT! |
916 | - exit = [] |
917 | - register(self.config, print_text, exit.append, reactor=FakeReactor()) |
918 | - self.assertEqual([2], exit) |
919 | - |
920 | - def test_register_timeout_failure(self): |
921 | - service = self.broker_service |
922 | - |
923 | - registration_mock = self.mocker.replace(service.registration) |
924 | - print_text_mock = self.mocker.replace(print_text) |
925 | - reactor_mock = self.mocker.patch(FakeReactor) |
926 | - remote_mock = self.mocker.patch(RemoteBroker) |
927 | - |
928 | - protocol_mock = self.mocker.patch(MethodCallSender) |
929 | - protocol_mock.timeout |
930 | - self.mocker.result(0.1) |
931 | - self.mocker.count(0, None) |
932 | - |
933 | - # This must necessarily happen in the following order. |
934 | - self.mocker.order() |
935 | - |
936 | - # This very informative message is printed out. |
937 | - print_text_mock("Please wait... ", "") |
938 | - |
939 | - time_mock = self.mocker.replace("time") |
940 | - time_mock.sleep(ANY) |
941 | - self.mocker.count(1) |
942 | - |
943 | - remote_mock.call_on_event(ANY) |
944 | - self.mocker.result(succeed(None)) |
945 | - |
946 | - registration_mock.register() |
947 | - self.mocker.passthrough() |
948 | - |
949 | - # This is actually called after everything else since all deferreds |
950 | - # are synchronous and callbacks will be executed immediately. |
951 | - reactor_mock.run() |
952 | - |
953 | - # Nothing else is printed! |
954 | - print_text_mock(ANY) |
955 | - self.mocker.count(0) |
956 | - |
957 | - self.mocker.replay() |
958 | - |
959 | - # DO IT! |
960 | - fake_reactor = FakeReactor() |
961 | - fake_reactor._reactor = Clock() |
962 | - deferred = register(self.config, print_text, sys.exit, |
963 | - reactor=fake_reactor) |
964 | - fake_reactor._reactor.advance(100) |
965 | - return deferred |
966 | - |
967 | - def test_register_bus_connection_failure(self): |
968 | - """ |
969 | - If the socket can't be connected to, landscape-config will print an |
970 | - explanatory message and exit cleanly. |
971 | - """ |
972 | - # This will make the RemoteBrokerConnector.connect call fail |
973 | - print_text_mock = self.mocker.replace(print_text) |
974 | - time_mock = self.mocker.replace("time") |
975 | - sys_mock = self.mocker.replace("sys") |
976 | - reactor_mock = self.mocker.patch(LandscapeReactor) |
977 | - |
978 | - connector_factory = self.mocker.replace( |
979 | - "landscape.broker.amp.RemoteBrokerConnector", passthrough=False) |
980 | - connector = connector_factory(ANY, ANY) |
981 | - connector.connect(max_retries=0, quiet=True) |
982 | - self.mocker.result(fail(ZeroDivisionError)) |
983 | - |
984 | - print_text_mock(ARGS) |
985 | - time_mock.sleep(ANY) |
986 | - reactor_mock.run() |
987 | - |
988 | - print_text_mock( |
989 | - CONTAINS("There was an error communicating with the " |
990 | - "Landscape client"), |
991 | - error=True) |
992 | - print_text_mock(CONTAINS("This machine will be registered"), |
993 | - error=True) |
994 | - |
995 | - sys_mock.exit(2) |
996 | - connector.disconnect() |
997 | - reactor_mock.stop() |
998 | - |
999 | - self.mocker.replay() |
1000 | - |
1001 | - config = self.get_config(["-a", "accountname", "--silent"]) |
1002 | - return register(config, print_text, sys.exit, max_retries=0) |
1003 | - |
1004 | - def test_register_bus_connection_failure_ok_no_register(self): |
1005 | - """ |
1006 | - Exit code 0 will be returned if we can't contact Landscape via DBus and |
1007 | - --ok-no-register was passed. |
1008 | - """ |
1009 | - print_text_mock = self.mocker.replace(print_text) |
1010 | - time_mock = self.mocker.replace("time") |
1011 | - reactor_mock = self.mocker.patch(LandscapeReactor) |
1012 | - |
1013 | - print_text_mock(ARGS) |
1014 | - time_mock.sleep(ANY) |
1015 | - reactor_mock.run() |
1016 | - reactor_mock.stop() |
1017 | - |
1018 | - print_text_mock( |
1019 | - CONTAINS("There was an error communicating with the " |
1020 | - "Landscape client"), |
1021 | - error=True) |
1022 | - print_text_mock(CONTAINS("This machine will be registered"), |
1023 | - error=True) |
1024 | - |
1025 | - self.mocker.replay() |
1026 | - |
1027 | - config = self.get_config( |
1028 | - ["-a", "accountname", "--silent", "--ok-no-register"]) |
1029 | - return self.assertSuccess(register(config, print_text, sys.exit, |
1030 | - max_retries=0)) |
1031 | - |
1032 | - |
1033 | -class RegisterFunctionRetryTest(LandscapeConfigurationTest): |
1034 | - |
1035 | - helpers = [BrokerServiceHelper] |
1036 | - |
1037 | - def setUp(self): |
1038 | - super(RegisterFunctionRetryTest, self).setUp() |
1039 | - self.config = LandscapeSetupConfiguration() |
1040 | - self.config.load(["-c", self.config_filename]) |
1041 | - |
1042 | - def test_register_with_retry_parameters(self): |
1043 | - """ |
1044 | - Retry parameters are passed to the L{connect} method of the connector. |
1045 | - """ |
1046 | - print_text_mock = self.mocker.replace(print_text) |
1047 | - time_mock = self.mocker.replace("time") |
1048 | - sys_mock = self.mocker.replace("sys") |
1049 | - reactor_mock = self.mocker.patch(LandscapeReactor) |
1050 | - |
1051 | - connector_factory = self.mocker.replace( |
1052 | - "landscape.broker.amp.RemoteBrokerConnector", passthrough=False) |
1053 | - connector = connector_factory(ANY, ANY) |
1054 | - connector.connect(quiet=True, max_retries=12) |
1055 | - |
1056 | - self.mocker.result(succeed(None)) |
1057 | - |
1058 | - print_text_mock(ARGS) |
1059 | - time_mock.sleep(ANY) |
1060 | - reactor_mock.run() |
1061 | - |
1062 | - print_text_mock( |
1063 | - CONTAINS("There was an error communicating with the " |
1064 | - "Landscape client"), |
1065 | - error=True) |
1066 | - print_text_mock(CONTAINS("This machine will be registered"), |
1067 | - error=True) |
1068 | - |
1069 | - sys_mock.exit(2) |
1070 | - connector.disconnect() |
1071 | - reactor_mock.stop() |
1072 | - |
1073 | - self.mocker.replay() |
1074 | - |
1075 | - config = self.get_config(["-a", "accountname", "--silent"]) |
1076 | - return register(config, print_text, sys.exit, max_retries=12) |
1077 | - |
1078 | - def test_register_with_default_retry_parameters(self): |
1079 | - """ |
1080 | - max_retries has reasonable default behavior - retry 14 times which |
1081 | - will result in a wait of about 60 seconds, until the broker has time |
1082 | - to start on heavily loaded systems. |
1083 | - |
1084 | - initialDelay = 0.05 |
1085 | - factor = 1.62 |
1086 | - maxDelay = 30 |
1087 | - max_retries = 14 |
1088 | - |
1089 | - 0.05 * (1 - 1.62 ** 14) / (1 - 1.62) = 69 seconds |
1090 | - """ |
1091 | - print_text_mock = self.mocker.replace(print_text) |
1092 | - time_mock = self.mocker.replace("time") |
1093 | - sys_mock = self.mocker.replace("sys") |
1094 | - reactor_mock = self.mocker.patch(LandscapeReactor) |
1095 | - |
1096 | - connector_factory = self.mocker.replace( |
1097 | - "landscape.broker.amp.RemoteBrokerConnector", passthrough=False) |
1098 | - connector = connector_factory(ANY, ANY) |
1099 | - connector.connect(quiet=True, max_retries=14) |
1100 | - |
1101 | - self.mocker.result(succeed(None)) |
1102 | - |
1103 | - print_text_mock(ARGS) |
1104 | - time_mock.sleep(ANY) |
1105 | - reactor_mock.run() |
1106 | - |
1107 | - print_text_mock( |
1108 | - CONTAINS("There was an error communicating with the " |
1109 | - "Landscape client"), |
1110 | - error=True) |
1111 | - print_text_mock(CONTAINS("This machine will be registered"), |
1112 | - error=True) |
1113 | - |
1114 | - sys_mock.exit(2) |
1115 | - connector.disconnect() |
1116 | - reactor_mock.stop() |
1117 | - |
1118 | - self.mocker.replay() |
1119 | - |
1120 | - config = self.get_config(["-a", "accountname", "--silent"]) |
1121 | - return register(config, print_text, sys.exit) |
1122 | - |
1123 | - |
1124 | -class RegisterFunctionNoServiceTest(LandscapeTest): |
1125 | - |
1126 | - def test_register_unknown_error(self): |
1127 | - """ |
1128 | - When registration fails because of an unknown error, a message is |
1129 | - printed and the program exits. |
1130 | - """ |
1131 | - configuration = LandscapeSetupConfiguration() |
1132 | - |
1133 | - # We'll just mock the remote here to have it raise an exception. |
1134 | - connector_factory = self.mocker.replace( |
1135 | - "landscape.broker.amp.RemoteBrokerConnector", passthrough=False) |
1136 | - remote_broker = self.mocker.mock() |
1137 | - |
1138 | - print_text_mock = self.mocker.replace(print_text) |
1139 | - reactor_mock = self.mocker.patch(FakeReactor) |
1140 | - |
1141 | - # This is unordered. It's just way too much of a pain. |
1142 | - print_text_mock("Please wait... ", "") |
1143 | - time_mock = self.mocker.replace("time") |
1144 | - time_mock.sleep(ANY) |
1145 | - self.mocker.count(1) |
1146 | - |
1147 | - # SNORE |
1148 | - connector = connector_factory(ANY, configuration) |
1149 | - connector.connect(max_retries=0, quiet=True) |
1150 | - self.mocker.result(succeed(remote_broker)) |
1151 | - remote_broker.call_on_event(ANY) |
1152 | - self.mocker.result(succeed(None)) |
1153 | - |
1154 | - # here it is! |
1155 | - remote_broker.register() |
1156 | - self.mocker.result(fail(ZeroDivisionError)) |
1157 | - |
1158 | - print_text_mock(ANY, error=True) |
1159 | - |
1160 | - def check_logged_failure(text, error): |
1161 | - self.assertTrue("ZeroDivisionError" in text) |
1162 | - self.mocker.call(check_logged_failure) |
1163 | - print_text_mock("Unknown error occurred.", error=True) |
1164 | - |
1165 | - # WHOAH DUDE. This waits for callLater(0, reactor.stop). |
1166 | - connector.disconnect() |
1167 | - reactor_mock.stop() |
1168 | - reactor_mock.run() |
1169 | - |
1170 | - self.mocker.replay() |
1171 | - |
1172 | - exit = [] |
1173 | - register(configuration, print_text, exit.append, max_retries=0, |
1174 | - reactor=FakeReactor()) |
1175 | - self.assertEqual(exit, [2]) |
1176 | + def test_register(self): |
1177 | + """Is the async machinery wired up properly?""" |
1178 | + |
1179 | + class FauxFailure(object): |
1180 | + def getTraceback(self): |
1181 | + return 'traceback' |
1182 | + |
1183 | + class FauxReactor(object): |
1184 | + def run(self): |
1185 | + self.was_run = True |
1186 | + |
1187 | + def stop(self, *args): |
1188 | + self.was_stopped = True |
1189 | + |
1190 | + reactor = FauxReactor() |
1191 | + connector = FauxConnector(reactor, self.config) |
1192 | + |
1193 | + def connector_factory(reactor, config): |
1194 | + return connector |
1195 | + |
1196 | + # We pre-seed a success because no actual result will be generated. |
1197 | + register(self.config, reactor, connector_factory, max_retries=99, |
1198 | + results=['success']) |
1199 | + self.assertTrue(reactor.was_run) |
1200 | + # Only a single callback is registered, it does the real work when a |
1201 | + # connection is established. |
1202 | + self.assertTrue(1, len(connector.connection.callbacks)) |
1203 | + self.assertEqual( |
1204 | + 'got_connection', |
1205 | + connector.connection.callbacks[0].func.__name__) |
1206 | + # Should something go wrong, there is an error handler registered. |
1207 | + self.assertTrue(1, len(connector.connection.errbacks)) |
1208 | + self.assertEqual( |
1209 | + 'got_error', |
1210 | + connector.connection.errbacks[0].__name__) |
1211 | + # We ask for retries because networks aren't reliable. |
1212 | + self.assertEqual(99, connector.max_retries) |
1213 | + |
1214 | + def test_got_connection(self): |
1215 | + """got_connection() adds deferreds and callbacks.""" |
1216 | + |
1217 | + def faux_got_connection(add_result, remote, connector, reactor): |
1218 | + pass |
1219 | + |
1220 | + class FauxRemote(object): |
1221 | + handlers = None |
1222 | + deferred = None |
1223 | + |
1224 | + def call_on_event(self, handlers): |
1225 | + assert not self.handlers, "Called twice" |
1226 | + self.handlers = handlers |
1227 | + self.call_on_event_deferred = FauxCallOnEventDeferred() |
1228 | + return self.call_on_event_deferred |
1229 | + |
1230 | + def register(self): |
1231 | + assert not self.deferred, "Called twice" |
1232 | + self.register_deferred = FauxRegisterDeferred() |
1233 | + return self.register_deferred |
1234 | + |
1235 | + class FauxCallOnEventDeferred(object): |
1236 | + def __init__(self): |
1237 | + self.callbacks = [] |
1238 | + self.errbacks = [] |
1239 | + |
1240 | + def addCallbacks(self, *funcs, **kws): |
1241 | + self.callbacks.extend(funcs) |
1242 | + |
1243 | + class FauxRegisterDeferred(object): |
1244 | + def __init__(self): |
1245 | + self.callbacks = [] |
1246 | + self.errbacks = [] |
1247 | + |
1248 | + def addCallback(self, func): |
1249 | + assert func.__name__ == "got_connection", "Wrong callback." |
1250 | + self.callbacks.append(faux_got_connection) |
1251 | + self.gather_results_deferred = GatherResultsDeferred() |
1252 | + return self.gather_results_deferred |
1253 | + |
1254 | + def addCallbacks(self, *funcs, **kws): |
1255 | + self.callbacks.extend(funcs) |
1256 | + |
1257 | + def addErrback(self, func, *args, **kws): |
1258 | + self.errbacks.append(func) |
1259 | + return self |
1260 | + |
1261 | + class GatherResultsDeferred(object): |
1262 | + def __init__(self): |
1263 | + self.callbacks = [] |
1264 | + self.errbacks = [] |
1265 | + |
1266 | + def addCallbacks(self, *funcs, **kws): |
1267 | + self.callbacks.extend(funcs) |
1268 | + |
1269 | + faux_connector = FauxConnector(self.reactor, self.config) |
1270 | + |
1271 | + status_results = [] |
1272 | + faux_remote = FauxRemote() |
1273 | + results = got_connection( |
1274 | + status_results.append, faux_connector, self.reactor, faux_remote) |
1275 | + # We set up two deferreds, one for the RPC call and one for event |
1276 | + # handlers. |
1277 | + self.assertEqual(2, len(results.resultList)) |
1278 | + # Handlers are registered for the events we are interested in. |
1279 | + self.assertEqual( |
1280 | + ['registration-failed', 'exchange-failed', 'registration-done'], |
1281 | + faux_remote.handlers.keys()) |
1282 | + self.assertEqual( |
1283 | + ['failure', 'exchange_failure', 'success'], |
1284 | + [handler.func.__name__ |
1285 | + for handler in faux_remote.handlers.values()]) |
1286 | + # We include a single error handler to react to exchange errors. |
1287 | + self.assertTrue(1, len(faux_remote.register_deferred.errbacks)) |
1288 | + self.assertEqual( |
1289 | + 'handle_registration_errors', |
1290 | + faux_remote.register_deferred.errbacks[0].__name__) |
1291 | + |
1292 | + def test_register_happy_path(self): |
1293 | + """A successful result provokes no exceptions.""" |
1294 | + def faux_got_connection(add_result, remote, connector, reactor): |
1295 | + add_result('success') |
1296 | + self.reactor.call_later(1, self.reactor.stop) |
1297 | + self.assertEqual( |
1298 | + "success", |
1299 | + register(self.config, reactor=self.reactor, |
1300 | + got_connection=faux_got_connection)) |
1301 | |
1302 | |
1303 | class SSLCertificateDataTest(LandscapeConfigurationTest): |
1304 | @@ -2159,3 +2121,41 @@ |
1305 | self.assertEqual(key_filename, |
1306 | store_public_key_data(config, "123456789")) |
1307 | self.assertEqual("123456789", open(key_filename, "r").read()) |
1308 | + |
1309 | + |
1310 | +class ReportRegistrationOutcomeTest(unittest.TestCase): |
1311 | + |
1312 | + def setUp(self): |
1313 | + self.result = [] |
1314 | + self.output = [] |
1315 | + |
1316 | + def record_result(self, result, file=sys.stdout): |
1317 | + self.result.append(result) |
1318 | + self.output.append(file.name) |
1319 | + |
1320 | + def test_success_case(self): |
1321 | + report_registration_outcome("success", print=self.record_result) |
1322 | + self.assertIn("System successfully registered.", self.result) |
1323 | + self.assertIn(sys.stdout.name, self.output) |
1324 | + |
1325 | + def test_failure_case(self): |
1326 | + report_registration_outcome("failure", print=self.record_result) |
1327 | + self.assertIn("Invalid account name or registration key.", self.result) |
1328 | + self.assertIn(sys.stderr.name, self.output) |
1329 | + |
1330 | + def test_ssl_error_case(self): |
1331 | + report_registration_outcome("ssl-error", print=self.record_result) |
1332 | + self.assertIn("\nThe server's SSL information is incorrect, or fails " |
1333 | + "signature verification!\n" |
1334 | + "If the server is using a self-signed certificate, " |
1335 | + "please ensure you supply it with the --ssl-public-key " |
1336 | + "parameter.", self.result) |
1337 | + self.assertIn(sys.stderr.name, self.output) |
1338 | + |
1339 | + def test_non_ssl_error_case(self): |
1340 | + report_registration_outcome("non-ssl-error", print=self.record_result) |
1341 | + self.assertIn("\nWe were unable to contact the server.\n" |
1342 | + "Your internet connection may be down. " |
1343 | + "The landscape client will continue to try and contact " |
1344 | + "the server periodically.", self.result) |
1345 | + self.assertIn(sys.stderr.name, self.output) |
Looks good, and seems to work.
The merge description is wrong however - this branch "only" refactors the current code to make it easier to grok and test, but there is currently no functional improvement (that should come in a subsequent branch).
Note (can be used in the change description):
Most of the code breaks out nested function declaration to make them easier to test, using functools.partial to keep the invocation sane in a twisted context (test them "flat", use functools to pass closures to event handlers).
Note 2:
We used manual stubs instead of mocks for readability and personal preference (of both of us).
I'll comment again when I will have performed actual manual tests (give me a couple of hours :) )