Merge lp:~jseutter/landscape-client/record-playback into lp:~landscape/landscape-client/trunk
- record-playback
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Thomas Herve |
Approved revision: | 347 |
Merge reported by: | Jerry Seutter |
Merged at revision: | not available |
Proposed branch: | lp:~jseutter/landscape-client/record-playback |
Merge into: | lp:~landscape/landscape-client/trunk |
Diff against target: |
646 lines (+258/-46) 9 files modified
landscape/broker/config.py (+7/-0) landscape/broker/service.py (+8/-3) landscape/broker/tests/test_store.py (+14/-8) landscape/broker/tests/test_transport.py (+139/-12) landscape/broker/transport.py (+72/-6) landscape/hal.py (+1/-1) landscape/lib/persist.py (+15/-14) landscape/package/taskhandler.py (+1/-1) scripts/landscape-dbus-proxy (+1/-1) |
To merge this branch: | bzr merge lp:~jseutter/landscape-client/record-playback |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Free Ekanayaka (community) | Approve | ||
Alberto Donato (community) | Approve | ||
Frank Wierzbicki | Pending | ||
Review via email: mp+65839@code.launchpad.net |
Commit message
Description of the change
With this change, landscape-client can now record the information that was sent from the client broker to landscape server. It adds --record and --record-dir command line options to the client broker.
I created a PayloadRecorder class that persists information to disk. I could not figure out how mocker could stub out the os module for unit testing, so some of the methods are not tested. Suggestions on how to improve the unit tests would be appreciated.
There are several lintian-triggered changes in the diff as well.
- 343. By Jerry Seutter
-
Cleanups.
- 344. By Jerry Seutter
-
- Implemented fixes suggested by ack.
- Improved unit test coverage.
Jerry Seutter (jseutter) wrote : | # |
Thanks ack. 1, 2 and 3 are fixed. Thank you for the pointers on mocker. The unit tests are much more thorough now.
Free Ekanayaka (free.ekanayaka) wrote : | # |
Hi Jerry, looks good, however please use mocking only for time.time.
[1]
+ parser.
+ help="Directory used to store server exchanges ("
+ "use with --record).")
Is there some use case where we want to set this to a particular directory? If not, I'd drop the option and go for something like:
class BrokerConfigura
@property
def record_
return os.path.
which will return /var/lib/
[2]
+ if self._payload_
+ self._payload_
Please check against None explicitly:
if self._payload_
[3]
+ file(self.
You can use landscape.
+ create_
[3]
+ if self.recording and self.destinatio
and
+ if self.recording:
Instead of duplicating the check for self.recording, I think you could drop self.recording entirely and delegate the decision upper in the stack in landscape.
[4]
+ self.persist = Persist(
This change doesn't seem to be needed (in landscape/
[5]
- self.assertEqua
+ self.assertEqua
+ "34")
Instead of breaking the line, you could make lint happy by s/assertEquals/
[6]
+class PayloadRecorder
+ def test_get_
Please put a blank line between the class and the test.
[7]
+ def __init__(self, url, pubkey=None, payload_
Please add a @param entry of the new parameter in the class docstring.
[8]
+ def __init__(self, recording, destination_dir):
Please add a @param for these two parameters in the class docstring.
[9]
+ self.destinatio
+ self.last_
+ self.recording = recording
It seems none of these attributes needs to be exposed by the PayloadRecorder interface, I'd suggest making them private.
[10]
+ for file in os.listdir(
+ if os.path.
+ os.unlink(
Please use os.path.join, and a temp variable to avoid duplication. Also I'd recommend s/file/filename/ as 'file' overrides Python's builtin type :
+ for filename in os.listdir(
+ path = os.path.
+ if os.path.
+ os.unlink(path)
[11]
+ recorder = PayloadRecorder
You can use self.makeDir() instead, it...
Alberto Donato (ack) wrote : | # |
Thanks for taking care of comments.
- 345. By Jerry Seutter
-
Changes based on review feedback.
Jerry Seutter (jseutter) wrote : | # |
Hi Free,
1, 2, 3, 3(2), 4 are fixed.
> [5]
>
> - self.assertEqua
> "34")
> + self.assertEqua
> + "34")
>
> Instead of breaking the line, you could make lint happy by
> s/assertEquals/
> couple of this)
assertEquals is used in several places. I will create a separate bug for this.
6, 7 are fixed.
> [8]
>
> + def __init__(self, recording, destination_dir):
>
> Please add a @param for these two parameters in the class docstring.
Are you sure? These are documented in the docstring for the method.
I will create a separate bug to fix this in other classes as well as this one.
9, 10, 11, 12, 13 are fixed.
- 346. By Jerry Seutter
-
Making docstring match code.
- 347. By Jerry Seutter
-
Fixing unit tests.
Free Ekanayaka (free.ekanayaka) wrote : | # |
Nice work, +1!
[14]
+ def record_
Please make it a @property for consistency with other similar methods in the same class, like exchange_store_path and message_store_path. Maybe also rename it to record_path, also for consistency.
[15]
+ if config.record is not None:
+ self.payload_
+ else:
+ self.payload_
Well done, moving this conditional up the stack looks better :)
[16]
+ self.assertEqua
You can use landscape.
[17]
+ @param payload_recorder: PayloadRecorder used for recording exchanges
+ with the server.
I'd append a "If C{None}, exchanges won't be recorded at all."
[18]
+ if self._payload_
+ self._payload_
This isn't unit-tested, afaics. It'd be good add a test to landscape.
Jerry Seutter (jseutter) wrote : | # |
> Nice work, +1!
>
> [14]
>
> + def record_
>
> Please make it a @property for consistency with other similar methods in the
> same class, like exchange_store_path and message_store_path. Maybe also rename
> it to record_path, also for consistency.
Oops. Fixed.
> [16]
>
> + self.assertEqua
>
> You can use landscape.
> descriptor.
Done.
> [17]
>
> + @param payload_recorder: PayloadRecorder used for recording exchanges
> + with the server.
>
> I'd append a "If C{None}, exchanges won't be recorded at all."
Good point. Added.
> [18]
>
> + if self._payload_
> + self._payload_
>
> This isn't unit-tested, afaics. It'd be good add a test to
> landscape.
> PayloadRecorder to the HTTPTransport constructor, running an exchange and
> asserting that the payload gets saved.
I added a test when payload_recorder is defined and a second test without payloa
d_recorder defined.
- 348. By Jerry Seutter
-
Addressing review comments.
Preview Diff
1 | === modified file 'landscape/broker/config.py' |
2 | --- landscape/broker/config.py 2011-01-12 16:30:14 +0000 |
3 | +++ landscape/broker/config.py 2011-06-29 17:53:31 +0000 |
4 | @@ -22,6 +22,10 @@ |
5 | def exchange_store_path(self): |
6 | return os.path.join(self.data_path, "exchange.database") |
7 | |
8 | + @property |
9 | + def record_directory(self): |
10 | + return os.path.join(self.data_path, "exchanges") |
11 | + |
12 | def make_parser(self): |
13 | """Parser factory for broker-specific options. |
14 | |
15 | @@ -39,6 +43,7 @@ |
16 | - C{https_proxy} |
17 | - C{cloud} |
18 | - C{otp} |
19 | + - C{record} |
20 | """ |
21 | parser = super(BrokerConfiguration, self).make_parser() |
22 | |
23 | @@ -76,6 +81,8 @@ |
24 | parser.add_option("--tags", |
25 | help="Comma separated list of tag names to be sent " |
26 | "to the server.") |
27 | + parser.add_option("--record", action="store_true", |
28 | + help="Record data sent to the server on filesystem ") |
29 | return parser |
30 | |
31 | @property |
32 | |
33 | === modified file 'landscape/broker/service.py' |
34 | --- landscape/broker/service.py 2011-03-25 08:42:48 +0000 |
35 | +++ landscape/broker/service.py 2011-06-29 17:53:31 +0000 |
36 | @@ -5,7 +5,7 @@ |
37 | from landscape.service import LandscapeService, run_landscape_service |
38 | from landscape.broker.registration import RegistrationHandler, Identity |
39 | from landscape.broker.config import BrokerConfiguration |
40 | -from landscape.broker.transport import HTTPTransport |
41 | +from landscape.broker.transport import HTTPTransport, PayloadRecorder |
42 | from landscape.broker.exchange import MessageExchange |
43 | from landscape.broker.exchangestore import ExchangeStore |
44 | from landscape.broker.ping import Pinger |
45 | @@ -48,8 +48,13 @@ |
46 | self.persist_filename = os.path.join( |
47 | config.data_path, "%s.bpickle" % (self.service_name,)) |
48 | super(BrokerService, self).__init__(config) |
49 | - self.transport = self.transport_factory(config.url, |
50 | - config.ssl_public_key) |
51 | + |
52 | + if config.record is not None: |
53 | + self.payload_recorder = PayloadRecorder(config.record_directory) |
54 | + else: |
55 | + self.payload_recorder = None |
56 | + self.transport = self.transport_factory( |
57 | + config.url, config.ssl_public_key, self.payload_recorder) |
58 | self.message_store = get_default_message_store( |
59 | self.persist, config.message_store_path) |
60 | self.identity = Identity(self.config, self.persist) |
61 | |
62 | === modified file 'landscape/broker/tests/test_store.py' |
63 | --- landscape/broker/tests/test_store.py 2011-06-16 20:05:54 +0000 |
64 | +++ landscape/broker/tests/test_store.py 2011-06-29 17:53:31 +0000 |
65 | @@ -4,7 +4,8 @@ |
66 | |
67 | from landscape.lib.persist import Persist |
68 | from landscape.broker.store import MessageStore |
69 | -from landscape.schema import InvalidError, Message, Int, String, UnicodeOrString |
70 | +from landscape.schema import ( |
71 | + InvalidError, Message, Int, String, UnicodeOrString) |
72 | |
73 | from landscape.tests.helpers import LandscapeTest |
74 | from landscape.tests.mocker import ANY |
75 | @@ -22,7 +23,8 @@ |
76 | |
77 | def create_store(self): |
78 | persist = Persist(filename=self.persist_filename) |
79 | - store = MessageStore(persist, self.temp_dir, 20, get_time=self.get_time) |
80 | + store = MessageStore(persist, self.temp_dir, 20, |
81 | + get_time=self.get_time) |
82 | store.set_accepted_types(["empty", "data"]) |
83 | store.add_schema(Message("empty", {})) |
84 | store.add_schema(Message("empty2", {})) |
85 | @@ -156,20 +158,23 @@ |
86 | |
87 | def test_unaccepted(self): |
88 | for i in range(10): |
89 | - self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i))) |
90 | + self.store.add(dict(type=["data", "unaccepted"][i % 2], |
91 | + data=str(i))) |
92 | il = [m["data"] for m in self.store.get_pending_messages(20)] |
93 | self.assertEquals(il, map(str, [0, 2, 4, 6, 8])) |
94 | |
95 | def test_unaccepted_with_offset(self): |
96 | for i in range(10): |
97 | - self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i))) |
98 | + self.store.add(dict(type=["data", "unaccepted"][i % 2], |
99 | + data=str(i))) |
100 | self.store.set_pending_offset(2) |
101 | il = [m["data"] for m in self.store.get_pending_messages(20)] |
102 | self.assertEquals(il, map(str, [4, 6, 8])) |
103 | |
104 | def test_unaccepted_reaccepted(self): |
105 | for i in range(10): |
106 | - self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i))) |
107 | + self.store.add(dict(type=["data", "unaccepted"][i % 2], |
108 | + data=str(i))) |
109 | self.store.set_pending_offset(2) |
110 | il = [m["data"] for m in self.store.get_pending_messages(2)] |
111 | self.store.set_accepted_types(["data", "unaccepted"]) |
112 | @@ -178,7 +183,8 @@ |
113 | |
114 | def test_accepted_unaccepted(self): |
115 | for i in range(10): |
116 | - self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i))) |
117 | + self.store.add(dict(type=["data", "unaccepted"][i % 2], |
118 | + data=str(i))) |
119 | # Setting pending offset here means that the first two |
120 | # messages, even though becoming unaccepted now, were already |
121 | # accepted before, so they shouldn't be marked for hold. |
122 | @@ -192,7 +198,8 @@ |
123 | |
124 | def test_accepted_unaccepted_old(self): |
125 | for i in range(10): |
126 | - self.store.add(dict(type=["data", "unaccepted"][i%2], data=str(i))) |
127 | + self.store.add(dict(type=["data", "unaccepted"][i % 2], |
128 | + data=str(i))) |
129 | self.store.set_pending_offset(2) |
130 | self.store.set_accepted_types(["unaccepted"]) |
131 | il = [m["data"] for m in self.store.get_pending_messages(20)] |
132 | @@ -323,7 +330,6 @@ |
133 | self.assertRaises(InvalidError, |
134 | self.store.add, {"type": "data", "data": 3}) |
135 | |
136 | - |
137 | def test_coercion_ignores_custom_api(self): |
138 | """ |
139 | If a custom 'api' key is specified in the message, it should |
140 | |
141 | === modified file 'landscape/broker/tests/test_transport.py' |
142 | --- landscape/broker/tests/test_transport.py 2009-03-16 22:10:37 +0000 |
143 | +++ landscape/broker/tests/test_transport.py 2011-06-29 17:53:31 +0000 |
144 | @@ -1,11 +1,13 @@ |
145 | import os |
146 | |
147 | from landscape import VERSION |
148 | -from landscape.broker.transport import HTTPTransport |
149 | +from landscape.broker.transport import HTTPTransport, PayloadRecorder |
150 | from landscape.lib.fetch import PyCurlError |
151 | +from landscape.lib.fs import create_file, read_file |
152 | from landscape.lib import bpickle |
153 | |
154 | -from landscape.tests.helpers import LandscapeTest, LogKeeperHelper |
155 | +from landscape.tests.helpers import ( |
156 | + LandscapeTest, LogKeeperHelper, MockerTestCase) |
157 | |
158 | from twisted.web import server, resource |
159 | from twisted.internet import reactor |
160 | @@ -25,6 +27,7 @@ |
161 | |
162 | class DataCollectingResource(resource.Resource): |
163 | request = content = None |
164 | + |
165 | def getChild(self, request, name): |
166 | return self |
167 | |
168 | @@ -67,13 +70,14 @@ |
169 | r = DataCollectingResource() |
170 | port = reactor.listenTCP(0, server.Site(r), interface="127.0.0.1") |
171 | self.ports.append(port) |
172 | - transport = HTTPTransport("http://localhost:%d/" |
173 | - % (port.getHost().port,)) |
174 | + transport = HTTPTransport( |
175 | + "http://localhost:%d/" % (port.getHost().port,)) |
176 | result = deferToThread(transport.exchange, "HI", computer_id="34", |
177 | message_api="X.Y") |
178 | |
179 | def got_result(ignored): |
180 | - self.assertEquals(r.request.received_headers["x-computer-id"], "34") |
181 | + self.assertEquals(r.request.received_headers["x-computer-id"], |
182 | + "34") |
183 | self.assertEquals(r.request.received_headers["user-agent"], |
184 | "landscape-client/%s" % (VERSION,)) |
185 | self.assertEquals(r.request.received_headers["x-message-api"], |
186 | @@ -89,19 +93,18 @@ |
187 | public key specified. |
188 | """ |
189 | r = DataCollectingResource() |
190 | - context_factory = DefaultOpenSSLContextFactory(PRIVKEY, |
191 | - PUBKEY) |
192 | + context_factory = DefaultOpenSSLContextFactory(PRIVKEY, PUBKEY) |
193 | port = reactor.listenSSL(0, server.Site(r), context_factory, |
194 | interface="127.0.0.1") |
195 | self.ports.append(port) |
196 | - transport = HTTPTransport("https://localhost:%d/" |
197 | - % (port.getHost().port,), |
198 | - pubkey=PUBKEY) |
199 | + transport = HTTPTransport( |
200 | + "https://localhost:%d/" % (port.getHost().port,), PUBKEY) |
201 | result = deferToThread(transport.exchange, "HI", computer_id="34", |
202 | message_api="X.Y") |
203 | |
204 | def got_result(ignored): |
205 | - self.assertEquals(r.request.received_headers["x-computer-id"], "34") |
206 | + self.assertEquals(r.request.received_headers["x-computer-id"], |
207 | + "34") |
208 | self.assertEquals(r.request.received_headers["user-agent"], |
209 | "landscape-client/%s" % (VERSION,)) |
210 | self.assertEquals(r.request.received_headers["x-message-api"], |
211 | @@ -110,7 +113,6 @@ |
212 | result.addCallback(got_result) |
213 | return result |
214 | |
215 | - |
216 | def test_ssl_verification_negative(self): |
217 | """ |
218 | If the SSL server provides a key which is not verified by the |
219 | @@ -130,6 +132,7 @@ |
220 | |
221 | result = deferToThread(transport.exchange, "HI", computer_id="34", |
222 | message_api="X.Y") |
223 | + |
224 | def got_result(ignored): |
225 | self.assertEquals(r.request, None) |
226 | self.assertEquals(r.content, None) |
227 | @@ -137,3 +140,127 @@ |
228 | in self.logfile.getvalue()) |
229 | result.addCallback(got_result) |
230 | return result |
231 | + |
232 | + def test_payload_recording_works(self): |
233 | + """ |
234 | + When C{HTTPTransport} is configured with a payload recorder, exchanges |
235 | + with the server should be saved to the filesystem. |
236 | + """ |
237 | + path = self.makeDir() |
238 | + recorder = PayloadRecorder(path) |
239 | + |
240 | + def static_filename(): |
241 | + return "filename" |
242 | + recorder.get_payload_filename = static_filename |
243 | + |
244 | + transport = HTTPTransport("http://localhost", |
245 | + payload_recorder=recorder) |
246 | + |
247 | + def fake_curl(param1, param2, param3): |
248 | + """Stub out the curl network call.""" |
249 | + class Curly(object): |
250 | + def getinfo(self, param1): |
251 | + return 200 |
252 | + return (Curly(), bpickle.dumps("pay load response")) |
253 | + transport._curl = fake_curl |
254 | + |
255 | + transport.exchange("pay load") |
256 | + |
257 | + file_path = os.path.join(path, static_filename()) |
258 | + self.assertEquals("pay load", bpickle.loads(read_file(file_path))) |
259 | + |
260 | + def test_exchange_works_without_payload_recording(self): |
261 | + """ |
262 | + When C{HTTPTransport} is configured without a payload recorder, |
263 | + exchanges with the server should still complete. |
264 | + """ |
265 | + transport = HTTPTransport("http://localhost") |
266 | + self.called = False |
267 | + |
268 | + def fake_curl(param1, param2, param3): |
269 | + """Stub out the curl network call.""" |
270 | + self.called = True |
271 | + class Curly(object): |
272 | + def getinfo(self, param1): |
273 | + return 200 |
274 | + return (Curly(), bpickle.dumps("pay load response")) |
275 | + transport._curl = fake_curl |
276 | + |
277 | + transport.exchange("pay load") |
278 | + |
279 | + self.assertTrue(self.called) |
280 | + |
281 | +class PayloadRecorderTest(MockerTestCase): |
282 | + |
283 | + def test_get_payload_filename(self): |
284 | + """ |
285 | + L{PayloadRecorder.get_payload_filename} should return a filename that |
286 | + is equal to the number of seconds since it was created. |
287 | + """ |
288 | + mock = self.mocker.replace("time.time") |
289 | + mock() |
290 | + self.mocker.result(0.0) |
291 | + mock() |
292 | + self.mocker.result(12.3456) |
293 | + self.mocker.replay() |
294 | + recorder = PayloadRecorder(None) |
295 | + |
296 | + payload_name = recorder.get_payload_filename() |
297 | + |
298 | + self.assertEquals("12.346", payload_name) |
299 | + |
300 | + def test_get_payload_filename_no_duplicates(self): |
301 | + """ |
302 | + L{PayloadRecorder.get_payload_filename} should not generate duplicate |
303 | + payload names. |
304 | + """ |
305 | + mock = self.mocker.replace("time.time") |
306 | + mock() |
307 | + self.mocker.result(0.0) |
308 | + mock() |
309 | + self.mocker.result(12.345) |
310 | + mock() |
311 | + self.mocker.result(12.345) |
312 | + self.mocker.replay() |
313 | + |
314 | + recorder = PayloadRecorder(None) |
315 | + |
316 | + payload_name_1 = recorder.get_payload_filename() |
317 | + payload_name_2 = recorder.get_payload_filename() |
318 | + |
319 | + self.assertEquals("12.345", payload_name_1) |
320 | + self.assertEquals("12.346", payload_name_2) |
321 | + |
322 | + def test_save(self): |
323 | + """L{PayloadRecorder.save} should save the payload to the filesystem. |
324 | + """ |
325 | + path = self.makeDir() |
326 | + recorder = PayloadRecorder(path) |
327 | + |
328 | + def static_filename(): |
329 | + return "filename" |
330 | + recorder.get_payload_filename = static_filename |
331 | + recorder.save("payload data") |
332 | + file_path = os.path.join(path, static_filename()) |
333 | + self.assertEquals("payload data", read_file(file_path)) |
334 | + |
335 | + def test_create_destination_dir(self): |
336 | + """ |
337 | + L{PayloadRecorder} should create the destination directory if it does |
338 | + not exist. |
339 | + """ |
340 | + path = self.makeDir() |
341 | + os.rmdir(path) |
342 | + PayloadRecorder(path) |
343 | + self.assertTrue(os.path.isdir(path)) |
344 | + |
345 | + def test_delete_old_payloads(self): |
346 | + """ |
347 | + L{PayloadRecorder} should remove all files from the destination |
348 | + directory before writing new files. |
349 | + """ |
350 | + path = self.makeDir() |
351 | + create_file(os.path.join(path, "one"), "one") |
352 | + create_file(os.path.join(path, "two"), "two") |
353 | + PayloadRecorder(path) |
354 | + self.assertEquals([], os.listdir(path)) |
355 | |
356 | === modified file 'landscape/broker/transport.py' |
357 | --- landscape/broker/transport.py 2010-09-09 16:10:07 +0000 |
358 | +++ landscape/broker/transport.py 2011-06-29 17:53:31 +0000 |
359 | @@ -1,4 +1,5 @@ |
360 | """Low-level server communication.""" |
361 | +import os |
362 | import time |
363 | import logging |
364 | import pprint |
365 | @@ -6,6 +7,7 @@ |
366 | import pycurl |
367 | |
368 | from landscape.lib.fetch import fetch |
369 | +from landscape.lib.fs import create_file |
370 | from landscape.lib import bpickle |
371 | from landscape.log import format_delta |
372 | from landscape import SERVER_API, VERSION |
373 | @@ -14,13 +16,16 @@ |
374 | class HTTPTransport(object): |
375 | """Transport makes a request to exchange message data over HTTP.""" |
376 | |
377 | - def __init__(self, url, pubkey=None): |
378 | + def __init__(self, url, pubkey=None, payload_recorder=None): |
379 | """ |
380 | @param url: URL of the remote Landscape server message system. |
381 | @param pubkey: SSH public key used for secure communication. |
382 | + @param payload_recorder: PayloadRecorder used for recording exchanges |
383 | + with the server. If C{None}, exchanges will not be recorded. |
384 | """ |
385 | self._url = url |
386 | self._pubkey = pubkey |
387 | + self._payload_recorder = payload_recorder |
388 | |
389 | def get_url(self): |
390 | """Get the URL of the remote message system.""" |
391 | @@ -31,9 +36,9 @@ |
392 | self._url = url |
393 | |
394 | def _curl(self, payload, computer_id, message_api): |
395 | - headers= {"X-Message-API": message_api, |
396 | - "User-Agent": "landscape-client/%s" % VERSION, |
397 | - "Content-Type": "application/octet-stream"} |
398 | + headers = {"X-Message-API": message_api, |
399 | + "User-Agent": "landscape-client/%s" % VERSION, |
400 | + "Content-Type": "application/octet-stream"} |
401 | if computer_id: |
402 | headers["X-Computer-ID"] = computer_id |
403 | curl = pycurl.Curl() |
404 | @@ -55,6 +60,8 @@ |
405 | |
406 | """ |
407 | spayload = bpickle.dumps(payload) |
408 | + if self._payload_recorder is not None: |
409 | + self._payload_recorder.save(spayload) |
410 | try: |
411 | start_time = time.time() |
412 | if logging.getLogger().getEffectiveLevel() <= logging.DEBUG: |
413 | @@ -85,11 +92,70 @@ |
414 | return response |
415 | |
416 | |
417 | +class PayloadRecorder(object): |
418 | + """ |
419 | + L{PayloadRecorder} records client exchanges with the server to disk for |
420 | + later playback. |
421 | + |
422 | + Exchange payloads will be stored one per file, where the file name is |
423 | + the elapsed time since the client was started. |
424 | + """ |
425 | + |
426 | + def __init__(self, destination_dir): |
427 | + """ |
428 | + @param destination_dir - The directory to record exchanges in. |
429 | + """ |
430 | + self._time_offset = time.time() |
431 | + self._destination_dir = destination_dir |
432 | + self._last_payload_time = -1 |
433 | + if self._destination_dir is not None: |
434 | + self._create_destination_dir(self._destination_dir) |
435 | + self._delete_old_payloads() |
436 | + |
437 | + def _create_destination_dir(self, destination_dir): |
438 | + """Create the destination directory if it does not exist. |
439 | + |
440 | + @param destination_dir: The directory to be created. |
441 | + """ |
442 | + if not os.path.exists(destination_dir): |
443 | + os.mkdir(destination_dir) |
444 | + |
445 | + def _delete_old_payloads(self): |
446 | + """Delete payloads lying around from a previous session.""" |
447 | + for filename in os.listdir(self._destination_dir): |
448 | + file_path = os.path.join(self._destination_dir, filename) |
449 | + if os.path.isfile(file_path): |
450 | + os.unlink(file_path) |
451 | + |
452 | + def save(self, payload): |
453 | + """Persist the given payload to disk. |
454 | + |
455 | + @param payload: The payload to be persisted. |
456 | + """ |
457 | + payload_name = self.get_payload_filename() |
458 | + create_file(os.path.join(self._destination_dir, payload_name), |
459 | + payload) |
460 | + |
461 | + def get_payload_filename(self): |
462 | + """ |
463 | + Generate a payload filename. This method ensures that payloads |
464 | + will have a unique name. |
465 | + """ |
466 | + payload_time = time.time() - self._time_offset |
467 | + last_payload_time = '%.3f' % self._last_payload_time |
468 | + this_payload_time = '%.3f' % payload_time |
469 | + if last_payload_time == this_payload_time: |
470 | + payload_time = payload_time + 0.001 |
471 | + self._last_payload_time = payload_time |
472 | + return '%.3f' % payload_time |
473 | + |
474 | + |
475 | class FakeTransport(object): |
476 | """Fake transport for testing purposes.""" |
477 | |
478 | - def __init__(self, url=None, pubkey=None): |
479 | + def __init__(self, url=None, pubkey=None, payload_recorder=None): |
480 | self._pubkey = pubkey |
481 | + self._payload_recorder = payload_recorder |
482 | self.payloads = [] |
483 | self.responses = [] |
484 | self._current_response = 0 |
485 | @@ -101,7 +167,7 @@ |
486 | |
487 | def get_url(self): |
488 | return self._url |
489 | - |
490 | + |
491 | def set_url(self, url): |
492 | self._url = url |
493 | |
494 | |
495 | === modified file 'landscape/hal.py' |
496 | --- landscape/hal.py 2010-06-11 13:23:00 +0000 |
497 | +++ landscape/hal.py 2011-06-29 17:53:31 +0000 |
498 | @@ -21,7 +21,7 @@ |
499 | """Returns a list of HAL devices. |
500 | |
501 | @note: If it wasn't possible to connect to HAL over DBus, then an |
502 | - emtpy list will be returned. This can happen if the HAL or DBus |
503 | + empty list will be returned. This can happen if the HAL or DBus |
504 | services are not running. |
505 | """ |
506 | if not self._manager: |
507 | |
508 | === modified file 'landscape/lib/persist.py' |
509 | --- landscape/lib/persist.py 2009-04-02 10:25:04 +0000 |
510 | +++ landscape/lib/persist.py 2011-06-29 17:53:31 +0000 |
511 | @@ -18,7 +18,8 @@ |
512 | # along with this Python module; if not, write to the Free Software |
513 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
514 | # |
515 | -import sys, os |
516 | +import os |
517 | +import sys |
518 | import copy |
519 | import re |
520 | |
521 | @@ -43,7 +44,7 @@ |
522 | |
523 | """Persist a hierarchical database of key=>value pairs. |
524 | |
525 | - There are three different kinds of opition maps, regarding the |
526 | + There are three different kinds of option maps, regarding the |
527 | persistence and priority that maps are queried. |
528 | |
529 | - hard - Options are persistent. |
530 | @@ -113,7 +114,7 @@ |
531 | try: |
532 | self._hardmap = self._backend.load(filepath) |
533 | except: |
534 | - filepathold = filepath+".old" |
535 | + filepathold = filepath + ".old" |
536 | if (os.path.isfile(filepathold) and |
537 | os.path.getsize(filepathold) > 0): |
538 | # warning("Broken configuration file at %s" % filepath) |
539 | @@ -139,7 +140,7 @@ |
540 | filepath = self.filename |
541 | filepath = os.path.expanduser(filepath) |
542 | if os.path.isfile(filepath): |
543 | - os.rename(filepath, filepath+".old") |
544 | + os.rename(filepath, filepath + ".old") |
545 | dirname = os.path.dirname(filepath) |
546 | if dirname and not os.path.isdir(dirname): |
547 | os.makedirs(dirname) |
548 | @@ -263,7 +264,7 @@ |
549 | current = self._traverse(map, path) |
550 | if type(current) is list and value in current: |
551 | return |
552 | - path = path+(sys.maxint,) |
553 | + path = path + (sys.maxint,) |
554 | self._traverse(map, path, setvalue=value) |
555 | |
556 | def remove(self, path, value=NOTHING, soft=False, weak=False): |
557 | @@ -360,49 +361,50 @@ |
558 | def has(self, path, value=NOTHING, soft=False, hard=False, weak=False): |
559 | if type(path) is str: |
560 | path = path_string_to_tuple(path) |
561 | - return self.parent.has(self.root+path, value, soft, hard, weak) |
562 | + return self.parent.has(self.root + path, value, soft, hard, weak) |
563 | |
564 | def keys(self, path, soft=False, hard=False, weak=False): |
565 | if type(path) is str: |
566 | path = path_string_to_tuple(path) |
567 | - return self.parent.keys(self.root+path, soft, hard, weak) |
568 | + return self.parent.keys(self.root + path, soft, hard, weak) |
569 | |
570 | def get(self, path, default=None, soft=False, hard=False, weak=False): |
571 | if type(path) is str: |
572 | path = path_string_to_tuple(path) |
573 | - return self.parent.get(self.root+path, default, soft, hard, weak) |
574 | + return self.parent.get(self.root + path, default, soft, hard, weak) |
575 | |
576 | def set(self, path, value, soft=False, weak=False): |
577 | if type(path) is str: |
578 | path = path_string_to_tuple(path) |
579 | - return self.parent.set(self.root+path, value, soft, weak) |
580 | + return self.parent.set(self.root + path, value, soft, weak) |
581 | |
582 | def add(self, path, value, unique=False, soft=False, weak=False): |
583 | if type(path) is str: |
584 | path = path_string_to_tuple(path) |
585 | - return self.parent.add(self.root+path, value, unique, soft, weak) |
586 | + return self.parent.add(self.root + path, value, unique, soft, weak) |
587 | |
588 | def remove(self, path, value=NOTHING, soft=False, weak=False): |
589 | if type(path) is str: |
590 | path = path_string_to_tuple(path) |
591 | - return self.parent.remove(self.root+path, value, soft, weak) |
592 | + return self.parent.remove(self.root + path, value, soft, weak) |
593 | |
594 | def move(self, oldpath, newpath, soft=False, weak=False): |
595 | if type(oldpath) is str: |
596 | oldpath = path_string_to_tuple(oldpath) |
597 | if type(newpath) is str: |
598 | newpath = path_string_to_tuple(newpath) |
599 | - return self.parent.move(self.root+oldpath, self.root+newpath, |
600 | + return self.parent.move(self.root + oldpath, self.root + newpath, |
601 | soft, weak) |
602 | |
603 | def root_at(self, path): |
604 | if type(path) is str: |
605 | path = path_string_to_tuple(path) |
606 | - return self.parent.root_at(self.root+path) |
607 | + return self.parent.root_at(self.root + path) |
608 | |
609 | |
610 | _splitpath = re.compile(r"(\[-?\d+\])|(?<!\\)\.").split |
611 | |
612 | + |
613 | def path_string_to_tuple(path): |
614 | if "." not in path and "[" not in path: |
615 | return (path,) |
616 | @@ -610,4 +612,3 @@ |
617 | return Backend.copy(self, value) |
618 | |
619 | # vim:ts=4:sw=4:et |
620 | - |
621 | |
622 | === modified file 'landscape/package/taskhandler.py' |
623 | --- landscape/package/taskhandler.py 2010-04-20 08:59:44 +0000 |
624 | +++ landscape/package/taskhandler.py 2011-06-29 17:53:31 +0000 |
625 | @@ -132,7 +132,7 @@ |
626 | failure.trap(PackageTaskError) |
627 | |
628 | def handle_task(self, task): |
629 | - """Handle a sigle task. |
630 | + """Handle a single task. |
631 | |
632 | Sub-classes must override this method in order to trigger task-specific |
633 | actions. |
634 | |
635 | === modified file 'scripts/landscape-dbus-proxy' |
636 | --- scripts/landscape-dbus-proxy 2010-07-09 14:51:01 +0000 |
637 | +++ scripts/landscape-dbus-proxy 2011-06-29 17:53:31 +0000 |
638 | @@ -33,7 +33,7 @@ |
639 | |
640 | |
641 | class BrokerDBusObject(Object): |
642 | - """A DBus-published object proxing L{RemoteBroker.send_message}. |
643 | + """A DBus-published object proxying L{RemoteBroker.send_message}. |
644 | |
645 | It is used when upgrading from a DBus-based version of the Landscape client |
646 | to the newer AMP-based one, for letting the old package-changer process |
+0. The branch is ok, but I'd like to see a few changes:
[1]
+ parser. add_option( "--record" , action= "store_ true",
+ help="Record data sent to the server on disk (see "
+ "--record-dir).")
Rather than "on disk" I would use something like "on filesystem".
[2]
+ if self.last_ payload_ time == payload_time:
+ payload_time = payload_time + .001
Can you please use 0.001 here?
[3]
+ self._time = time.time
+ self._time_offset = self._time()
and
+ payload_time = self._time() - self._time_offset
AFAICT saving the reference to the time function is only needed for tests.
self._time should be dropped.
You can use the functionalities provided by Mocker to override the time() function.
Something like this should do:
def test_get_ payload_ filename( self):
L{PayloadRecor der.get_ payload_ filename} should return a filename that replace( "time.time" )
self.mocker. result( 0.0)
self.mocker. result( 12.345)
self.mocker. count(1)
self.mocker. replay( )
"""
is equal to the number of seconds since it was created.
"""
mock = self.mocker.
mock()
mock()
recorder = PayloadRecorder (True, None)
Similarly for the other related test.
Thanks!