Merge lp:~free.ekanayaka/storm/catpure-tracer into lp:storm
- catpure-tracer
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Free Ekanayaka |
Approved revision: | 427 |
Merge reported by: | Free Ekanayaka |
Merged at revision: | not available |
Proposed branch: | lp:~free.ekanayaka/storm/catpure-tracer |
Merge into: | lp:storm |
Diff against target: |
428 lines (+189/-30) 3 files modified
NEWS (+10/-0) storm/tracer.py (+51/-5) tests/tracer.py (+128/-25) |
To merge this branch: | bzr merge lp:~free.ekanayaka/storm/catpure-tracer |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella | Approve | ||
Jamu Kakar (community) | Approve | ||
Review via email: mp+80025@code.launchpad.net |
Commit message
Description of the change
This branch adds a storm.tracer.
- 421. By Free Ekanayaka
-
Add a CaptureFixture as testing convenience
- 422. By Free Ekanayaka
-
Rename class
- 423. By Free Ekanayaka
-
Rename test
- 424. By Free Ekanayaka
-
Add a queries property
Gavin Panella (allenap) wrote : | # |
This looks good, though it's quite complicated. I'm not sure I
understand the need for being able to close a log other than to stop
tracing. Why does add_query need to raise a RuntimeError if it's
closed for example?
The add_query(), is_closed(), count() and queries() methods just seem
a little unnecessary. I don't think is_closed() or the copying in
queries() is really needed (we're all adults, etc.), so a plain list
would suffice.
In that vein, here's my go at something that does a similar job but
with fewer bells and whistles:
class CaptureTracer(
def __init__(self):
def setUp(self):
def _expanded_
# Fixtures can be used as context managers.
with CaptureTracer() as capture:
...
self.
No tests ;)
So, Needs Information for now, and over to you to argue for the
features you've created :)
[1]
+def remove_
+ _tracers.
If tracer is not in _tracers then this will blow up with
ValueError. Perhaps it's more friendly to suppress that?
- 425. By Free Ekanayaka
-
Make CaptureTracer a Fixture
Free Ekanayaka (free.ekanayaka) wrote : | # |
Thanks for reviewing Gavin.
Yeah, I initially made all those classes separate because I thought one might want to use one but not the others, e.g. for not depending on python-fixtures. As my use case are tests and I do want to use python-fixture, I'm fine with what you suggest and changed the code accordingly. If somebody needs support for other use cases it can be added.
[1]
Fixed as you suggest.
- 426. By Free Ekanayaka
-
Update NEWS file
Gavin Panella (allenap) wrote : | # |
The changes look great.
- 427. By Free Ekanayaka
-
Add missing test
Free Ekanayaka (free.ekanayaka) wrote : | # |
Merged. Thanks Jamu too, wherever you are :)
Preview Diff
1 | === modified file 'NEWS' |
2 | --- NEWS 2011-10-17 18:30:27 +0000 |
3 | +++ NEWS 2011-10-27 13:27:23 +0000 |
4 | @@ -4,6 +4,16 @@ |
5 | Improvements |
6 | ------------ |
7 | |
8 | +- A new CaptureTracer has been added to storm.tracer, for capturing all SQL |
9 | + statements executed by Storm. It can be used like this: |
10 | + |
11 | + with CaptureTracer() as tracer: |
12 | + # Run queries |
13 | + pass |
14 | + print tracer.queries # Print all queries run in the context manager block |
15 | + |
16 | + You will need the python-fixtures package in order to use this feature. |
17 | + |
18 | Bug fixes |
19 | --------- |
20 | |
21 | |
22 | === modified file 'storm/tracer.py' |
23 | --- storm/tracer.py 2011-09-13 07:07:09 +0000 |
24 | +++ storm/tracer.py 2011-10-27 13:27:23 +0000 |
25 | @@ -8,6 +8,12 @@ |
26 | from storm.exceptions import TimeoutError |
27 | from storm.expr import Variable |
28 | |
29 | +try: |
30 | + Fixture = object |
31 | + from fixtures import Fixture |
32 | +except ImportError: |
33 | + pass |
34 | + |
35 | |
36 | class DebugTracer(object): |
37 | |
38 | @@ -16,7 +22,8 @@ |
39 | stream = sys.stderr |
40 | self._stream = stream |
41 | |
42 | - def connection_raw_execute(self, connection, raw_cursor, statement, params): |
43 | + def connection_raw_execute(self, connection, raw_cursor, statement, |
44 | + params): |
45 | time = datetime.now().isoformat()[11:] |
46 | raw_params = [] |
47 | for param in params: |
48 | @@ -158,7 +165,7 @@ |
49 | |
50 | For more information on timelines see the module at |
51 | http://pypi.python.org/pypi/timeline. |
52 | - |
53 | + |
54 | The timeline to use is obtained by calling the timeline_factory supplied to |
55 | the constructor. This simple function takes no parameters and returns a |
56 | timeline to use. If it returns None, the tracer is bypassed. |
57 | @@ -190,7 +197,7 @@ |
58 | |
59 | def connection_raw_execute_success(self, connection, raw_cursor, |
60 | statement, params): |
61 | - |
62 | + |
63 | # action may be None if the tracer was installed after the statement |
64 | # was submitted. |
65 | action = getattr(self.threadinfo, 'action', None) |
66 | @@ -205,32 +212,71 @@ |
67 | connection, raw_cursor, statement, params) |
68 | |
69 | |
70 | +class CaptureTracer(BaseStatementTracer, Fixture): |
71 | + """Trace SQL statements appending them to a C{list}. |
72 | + |
73 | + Example: |
74 | + |
75 | + with CaptureTracer() as tracer: |
76 | + # Run queries |
77 | + print tracer.queries # Print the queries that have been run |
78 | + |
79 | + @note: This class requires the fixtures package to be available. |
80 | + """ |
81 | + |
82 | + def __init__(self): |
83 | + super(CaptureTracer, self).__init__() |
84 | + self.queries = [] |
85 | + |
86 | + def setUp(self): |
87 | + super(CaptureTracer, self).setUp() |
88 | + install_tracer(self) |
89 | + self.addCleanup(remove_tracer, self) |
90 | + |
91 | + def _expanded_raw_execute(self, conn, raw_cursor, statement): |
92 | + """Save the statement to the log.""" |
93 | + self.queries.append(statement) |
94 | + |
95 | + |
96 | _tracers = [] |
97 | |
98 | + |
99 | def trace(name, *args, **kwargs): |
100 | for tracer in _tracers: |
101 | attr = getattr(tracer, name, None) |
102 | if attr: |
103 | attr(*args, **kwargs) |
104 | |
105 | + |
106 | def install_tracer(tracer): |
107 | _tracers.append(tracer) |
108 | |
109 | + |
110 | def get_tracers(): |
111 | return _tracers[:] |
112 | |
113 | + |
114 | def remove_all_tracers(): |
115 | del _tracers[:] |
116 | |
117 | + |
118 | +def remove_tracer(tracer): |
119 | + try: |
120 | + _tracers.remove(tracer) |
121 | + except ValueError: |
122 | + pass # The tracer is not installed, succeed gracefully |
123 | + |
124 | + |
125 | def remove_tracer_type(tracer_type): |
126 | - for i in range(len(_tracers)-1, -1, -1): |
127 | + for i in range(len(_tracers) - 1, -1, -1): |
128 | if type(_tracers[i]) is tracer_type: |
129 | del _tracers[i] |
130 | |
131 | + |
132 | def debug(flag, stream=None): |
133 | remove_tracer_type(DebugTracer) |
134 | if flag: |
135 | install_tracer(DebugTracer(stream=stream)) |
136 | |
137 | -# Deal with circular import. |
138 | +# Deal with circular import. |
139 | from storm.database import convert_param_marks |
140 | |
141 | === modified file 'tests/tracer.py' |
142 | --- tests/tracer.py 2011-09-13 07:07:09 +0000 |
143 | +++ tests/tracer.py 2011-10-27 13:27:23 +0000 |
144 | @@ -3,15 +3,26 @@ |
145 | from unittest import TestCase |
146 | |
147 | try: |
148 | + # Optional dependency, if missing Fixture tests are skipped. |
149 | + TestWithFixtures = object |
150 | + from fixtures.testcase import TestWithFixtures |
151 | +except ImportError: |
152 | + has_fixtures = False |
153 | +else: |
154 | + has_fixtures = True |
155 | + |
156 | +try: |
157 | # Optional dependency, if missing TimelineTracer tests are skipped. |
158 | import timeline |
159 | + has_timeline = True |
160 | except ImportError: |
161 | - timeline = None |
162 | + has_timeline = False |
163 | |
164 | -from storm.tracer import (trace, install_tracer, get_tracers, |
165 | +from storm.tracer import (trace, install_tracer, get_tracers, remove_tracer, |
166 | remove_tracer_type, remove_all_tracers, debug, |
167 | BaseStatementTracer, DebugTracer, TimeoutTracer, |
168 | - TimelineTracer, TimeoutError, _tracers) |
169 | + TimelineTracer, TimeoutError, CaptureTracer, |
170 | + _tracers) |
171 | from storm.database import Connection |
172 | from storm.expr import Variable |
173 | |
174 | @@ -36,9 +47,28 @@ |
175 | remove_all_tracers() |
176 | self.assertEquals(get_tracers(), []) |
177 | |
178 | + def test_remove_tracer(self): |
179 | + """The C{remote_tracer} function removes a specific tracer.""" |
180 | + tracer1 = object() |
181 | + tracer2 = object() |
182 | + install_tracer(tracer1) |
183 | + install_tracer(tracer2) |
184 | + remove_tracer(tracer1) |
185 | + self.assertEquals(get_tracers(), [tracer2]) |
186 | + |
187 | + def test_remove_tracer_with_not_installed_tracer(self): |
188 | + """C{remote_tracer} exits gracefully if the tracer is not installed.""" |
189 | + tracer = object() |
190 | + remove_tracer(tracer) |
191 | + self.assertEquals(get_tracers(), []) |
192 | + |
193 | def test_remove_tracer_type(self): |
194 | - class C(object): pass |
195 | - class D(C): pass |
196 | + class C(object): |
197 | + pass |
198 | + |
199 | + class D(C): |
200 | + pass |
201 | + |
202 | c = C() |
203 | d1 = D() |
204 | d2 = D() |
205 | @@ -69,9 +99,11 @@ |
206 | |
207 | def test_trace(self): |
208 | stash = [] |
209 | + |
210 | class Tracer(object): |
211 | def m1(_, *args, **kwargs): |
212 | stash.extend(["m1", args, kwargs]) |
213 | + |
214 | def m2(_, *args, **kwargs): |
215 | stash.extend(["m2", args, kwargs]) |
216 | |
217 | @@ -82,7 +114,6 @@ |
218 | self.assertEquals(stash, ["m1", (1, 2), {"c": 3}, "m2", (), {}]) |
219 | |
220 | |
221 | - |
222 | class MockVariable(Variable): |
223 | |
224 | def __init__(self, value): |
225 | @@ -101,7 +132,7 @@ |
226 | |
227 | datetime_mock = self.mocker.replace("datetime.datetime") |
228 | datetime_mock.now() |
229 | - self.mocker.result(datetime.datetime(1,2,3,4,5,6,7)) |
230 | + self.mocker.result(datetime.datetime(1, 2, 3, 4, 5, 6, 7)) |
231 | self.mocker.count(0, 1) |
232 | |
233 | self.variable = MockVariable("PARAM") |
234 | @@ -192,7 +223,9 @@ |
235 | |
236 | # Some data is kept in the connection, so we use a proxy to |
237 | # allow things we don't care about here to happen. |
238 | - class Connection(object): pass |
239 | + class Connection(object): |
240 | + pass |
241 | + |
242 | self.connection = self.mocker.proxy(Connection()) |
243 | |
244 | def tearDown(self): |
245 | @@ -330,6 +363,14 @@ |
246 | self.execute() |
247 | |
248 | |
249 | +class StubConnection(Connection): |
250 | + |
251 | + def __init__(self): |
252 | + self._database = None |
253 | + self._event = None |
254 | + self._raw_connection = None |
255 | + |
256 | + |
257 | class BaseStatementTracerTest(TestCase): |
258 | |
259 | class LoggingBaseStatementTracer(BaseStatementTracer): |
260 | @@ -337,13 +378,6 @@ |
261 | self.__dict__.setdefault('calls', []).append( |
262 | (connection, raw_cursor, statement)) |
263 | |
264 | - class StubConnection(Connection): |
265 | - |
266 | - def __init__(self): |
267 | - self._database = None |
268 | - self._event = None |
269 | - self._raw_connection = None |
270 | - |
271 | def test_no_params(self): |
272 | """With no parameters the statement is passed through verbatim.""" |
273 | tracer = self.LoggingBaseStatementTracer() |
274 | @@ -352,7 +386,7 @@ |
275 | |
276 | def test_params_substituted_pyformat(self): |
277 | tracer = self.LoggingBaseStatementTracer() |
278 | - conn = self.StubConnection() |
279 | + conn = StubConnection() |
280 | conn.param_mark = '%s' |
281 | var1 = MockVariable(u'VAR1') |
282 | tracer.connection_raw_execute( |
283 | @@ -364,7 +398,7 @@ |
284 | def test_params_substituted_single_string(self): |
285 | """String parameters are formatted as a single quoted string.""" |
286 | tracer = self.LoggingBaseStatementTracer() |
287 | - conn = self.StubConnection() |
288 | + conn = StubConnection() |
289 | var1 = MockVariable(u'VAR1') |
290 | tracer.connection_raw_execute( |
291 | conn, 'cursor', 'SELECT * FROM person where name = ?', [var1]) |
292 | @@ -375,7 +409,7 @@ |
293 | def test_qmark_percent_s_literal_preserved(self): |
294 | """With ? parameters %s in the statement can be kept intact.""" |
295 | tracer = self.LoggingBaseStatementTracer() |
296 | - conn = self.StubConnection() |
297 | + conn = StubConnection() |
298 | var1 = MockVariable(1) |
299 | tracer.connection_raw_execute( |
300 | conn, 'cursor', |
301 | @@ -388,7 +422,7 @@ |
302 | def test_int_variable_as_int(self): |
303 | """Int parameters are formatted as an int literal.""" |
304 | tracer = self.LoggingBaseStatementTracer() |
305 | - conn = self.StubConnection() |
306 | + conn = StubConnection() |
307 | var1 = MockVariable(1) |
308 | tracer.connection_raw_execute( |
309 | conn, 'cursor', "SELECT * FROM person where id = ?", [var1]) |
310 | @@ -399,7 +433,7 @@ |
311 | def test_like_clause_preserved(self): |
312 | """% operators in LIKE statements are preserved.""" |
313 | tracer = self.LoggingBaseStatementTracer() |
314 | - conn = self.StubConnection() |
315 | + conn = StubConnection() |
316 | var1 = MockVariable(u'substring') |
317 | tracer.connection_raw_execute( |
318 | conn, 'cursor', |
319 | @@ -412,7 +446,7 @@ |
320 | |
321 | def test_unformattable_statements_are_handled(self): |
322 | tracer = self.LoggingBaseStatementTracer() |
323 | - conn = self.StubConnection() |
324 | + conn = StubConnection() |
325 | var1 = MockVariable(u'substring') |
326 | tracer.connection_raw_execute( |
327 | conn, 'cursor', "%s %s", |
328 | @@ -426,14 +460,14 @@ |
329 | class TimelineTracerTest(TestHelper): |
330 | |
331 | def is_supported(self): |
332 | - return timeline is not None |
333 | + return has_timeline |
334 | |
335 | def factory(self): |
336 | self.timeline = timeline.Timeline() |
337 | return self.timeline |
338 | |
339 | def test_separate_tracers_own_state(self): |
340 | - """"Check that multiple TimelineTracer's could be used at once.""" |
341 | + """Check that multiple TimelineTracer's could be used at once.""" |
342 | tracer1 = TimelineTracer(self.factory) |
343 | tracer2 = TimelineTracer(self.factory) |
344 | tracer1.threadinfo.action = 'foo' |
345 | @@ -457,8 +491,7 @@ |
346 | |
347 | def test_finds_timeline_from_factory(self): |
348 | factory_result = timeline.Timeline() |
349 | - factory = lambda:factory_result |
350 | - tracer = TimelineTracer(lambda:factory_result) |
351 | + tracer = TimelineTracer(lambda: factory_result) |
352 | tracer._expanded_raw_execute('conn', 'cursor', 'statement') |
353 | self.assertEqual(1, len(factory_result.actions)) |
354 | |
355 | @@ -491,3 +524,73 @@ |
356 | tracer = TimelineTracer(self.factory) |
357 | tracer._expanded_raw_execute('conn', 'cursor', 'statement') |
358 | self.assertEqual('SQL-<unknown>', self.timeline.actions[-1].category) |
359 | + |
360 | + |
361 | +class CaptureTracerTest(TestHelper, TestWithFixtures): |
362 | + |
363 | + def is_supported(self): |
364 | + return has_fixtures |
365 | + |
366 | + def tearDown(self): |
367 | + super(CaptureTracerTest, self).tearDown() |
368 | + del _tracers[:] |
369 | + |
370 | + def test_capture(self): |
371 | + """ |
372 | + Using the L{CaptureTracer} fixture starts capturing queries and stops |
373 | + removes the tracer upon cleanup. |
374 | + """ |
375 | + tracer = self.useFixture(CaptureTracer()) |
376 | + self.assertEqual([tracer], get_tracers()) |
377 | + conn = StubConnection() |
378 | + conn.param_mark = '%s' |
379 | + var = MockVariable(u"var") |
380 | + tracer.connection_raw_execute(conn, "cursor", "select %s", [var]) |
381 | + self.assertEqual(["select 'var'"], tracer.queries) |
382 | + |
383 | + def check(): |
384 | + self.assertEqual([], get_tracers()) |
385 | + |
386 | + self.addCleanup(check) |
387 | + |
388 | + def test_capture_as_context_manager(self): |
389 | + """{CaptureTracer}s can be used as context managers.""" |
390 | + conn = StubConnection() |
391 | + with CaptureTracer() as tracer: |
392 | + self.assertEqual([tracer], get_tracers()) |
393 | + tracer.connection_raw_execute(conn, "cursor", "select", []) |
394 | + self.assertEqual([], get_tracers()) |
395 | + self.assertEqual(["select"], tracer.queries) |
396 | + |
397 | + def test_capture_multiple(self): |
398 | + """L{CaptureTracer}s can be used as nested context managers.""" |
399 | + |
400 | + conn = StubConnection() |
401 | + |
402 | + def trace(statement): |
403 | + for tracer in get_tracers(): |
404 | + tracer.connection_raw_execute(conn, "cursor", statement, []) |
405 | + |
406 | + with CaptureTracer() as tracer1: |
407 | + trace("one") |
408 | + with CaptureTracer() as tracer2: |
409 | + trace("two") |
410 | + trace("three") |
411 | + |
412 | + self.assertEqual([], get_tracers()) |
413 | + self.assertEqual(["one", "two", "three"], tracer1.queries) |
414 | + self.assertEqual(["two"], tracer2.queries) |
415 | + |
416 | + def test_capture_with_exception(self): |
417 | + """ |
418 | + L{CaptureTracer}s re-raise any error when used as context managers. |
419 | + """ |
420 | + errors = [] |
421 | + try: |
422 | + with CaptureTracer(): |
423 | + raise RuntimeError("boom") |
424 | + except RuntimeError, error: |
425 | + errors.append(error) |
426 | + [error] = errors |
427 | + self.assertEqual("boom", str(error)) |
428 | + self.assertEqual([], get_tracers()) |
Nice work, +1!