Merge lp:~free.ekanayaka/storm/catpure-tracer into lp:storm

Proposed by Free Ekanayaka
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
Reviewer Review Type Date Requested Status
Gavin Panella Approve
Jamu Kakar (community) Approve
Review via email: mp+80025@code.launchpad.net

Description of the change

This branch adds a storm.tracer.capture function that supports capturing SQL statements into an in-memory log.

To post a comment you must log in.
Revision history for this message
Jamu Kakar (jkakar) wrote :

Nice work, +1!

review: Approve
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

Revision history for this message
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(Fixture, BaseStatementTracer):

        def __init__(self):
            super(CaptureTracer, self).__init__()
            self.queries = []

        def setUp(self):
            super(CaptureTracer, self).setUp()
            install_tracer(self)
            self.addCleanup(remove_tracer, self)

        def _expanded_raw_execute(self, conn, raw_cursor, statement):
            self.queries.append(statement)

    # Fixtures can be used as context managers.
    with CaptureTracer() as capture:
         ...
    self.assertEqual([...], capture.queries)

No tests ;)

So, Needs Information for now, and over to you to argue for the
features you've created :)

[1]

+def remove_tracer(tracer):
+ _tracers.remove(tracer)

If tracer is not in _tracers then this will blow up with
ValueError. Perhaps it's more friendly to suppress that?

review: Needs Information
425. By Free Ekanayaka

Make CaptureTracer a Fixture

Revision history for this message
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

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

The changes look great.

review: Approve
427. By Free Ekanayaka

Add missing test

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Merged. Thanks Jamu too, wherever you are :)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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())

Subscribers

People subscribed via source and target branches

to status/vote changes: