← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~gary/launchpad/bug856048 into lp:launchpad

 

Gary Poster has proposed merging lp:~gary/launchpad/bug856048 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #856048 in Launchpad itself: "SQLLogger and StormStatementRecorder duplicate functionality"
  https://bugs.launchpad.net/launchpad/+bug/856048

For more details, see:
https://code.launchpad.net/~gary/launchpad/bug856048/+merge/76597

Merge SQLLogger and StormStatementRecorder.

Per a comment from Robert, I'm getting rid of my recently introduced SQLLogger by merging it with StormStatementRecorder (which I had forgotten about and not found again).  This meant slightly adjusting a few tests (which meant addressing a few pieces of unrelated lint in these files).

The new StormStatementRecorder is now simpler and has a few more features.  In particular:

 * It collects accurate timings and database names.
 * It can collect tracebacks never (default), always or conditionally.
 * printing it gives you a view of what happened, including any gathered tracebacks, making it useful for interactive data-gathering work.

Lint is happy.

Thank you

Gary


-- 
https://code.launchpad.net/~gary/launchpad/bug856048/+merge/76597
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~gary/launchpad/bug856048 into lp:launchpad.
=== modified file 'lib/canonical/launchpad/webapp/adapter.py'
--- lib/canonical/launchpad/webapp/adapter.py	2011-09-19 13:23:38 +0000
+++ lib/canonical/launchpad/webapp/adapter.py	2011-09-22 14:51:54 +0000
@@ -10,7 +10,6 @@
 import logging
 import os
 import re
-import StringIO
 import sys
 import thread
 import threading
@@ -97,7 +96,6 @@
     'get_store_name',
     'print_queries',
     'soft_timeout_expired',
-    'SQLLogger',
     'start_sql_logging',
     'stop_sql_logging',
     'StoreSelector',
@@ -365,8 +363,10 @@
         warnings.warn('SQL logging already started')
         return
     _local.sql_logging_tracebacks_if = tracebacks_if
-    _local.sql_logging = []
+    result = []
+    _local.sql_logging = result
     _local.sql_logging_start = int(time() * 1000)
+    return result
 
 
 def stop_sql_logging():
@@ -380,29 +380,6 @@
     return result
 
 
-class SQLLogger:
-
-    def __init__(self, tracebacks_if=False):
-        self.tracebacks_if = tracebacks_if
-
-    queries = None
-
-    def __enter__(self):
-        self.queries = None
-        start_sql_logging(self.tracebacks_if)
-
-    def __exit__(self, exc_type, exc_value, tb):
-        self.queries = stop_sql_logging()
-
-    def __str__(self):
-        if self.queries is None:
-            return '(no queries)'
-        else:
-            out = StringIO.StringIO()
-            print_queries(self.queries, file=out)
-            return out.getvalue()
-
-
 def print_queries(queries, file=None):
     if file is None:
         file = sys.stdout

=== modified file 'lib/canonical/launchpad/webapp/tests/test_statementtracer.py'
--- lib/canonical/launchpad/webapp/tests/test_statementtracer.py	2011-09-13 05:23:16 +0000
+++ lib/canonical/launchpad/webapp/tests/test_statementtracer.py	2011-09-22 14:51:54 +0000
@@ -17,6 +17,7 @@
 from lp.services.timeline.requesttimeline import get_request_timeline
 from lp.testing import (
     person_logged_in,
+    StormStatementRecorder,
     TestCase,
     TestCaseWithFactory,
     )
@@ -217,16 +218,23 @@
                 "-" * 70 + "\n")
 
     def test_context_manager(self):
-        logger = da.SQLLogger()
-        with logger:
+        with StormStatementRecorder() as logger:
             self.execute()
-        self.assertEqual(1, len(logger.queries))
-        self.assertIs(None, logger.queries[0]['stack'])
-        self.assertIs(None, logger.queries[0]['exception'])
-        self.assertEqual(
-            (1, 2, 'SQL-stub-database', 'SELECT * FROM bar WHERE bing = 42'),
-            logger.queries[0]['sql'])
+        self.assertEqual(1, len(logger.query_data))
+        self.assertIs(None, logger.query_data[0]['stack'])
+        self.assertIs(None, logger.query_data[0]['exception'])
+        self.assertEqual(
+            (1, 2, 'SQL-stub-database', 'SELECT * FROM bar WHERE bing = 42'),
+            logger.query_data[0]['sql'])
+        self.assertEqual(
+            (1, 2, 'SQL-stub-database', 'SELECT * FROM bar WHERE bing = 42'),
+            logger.queries[0])
+        self.assertEqual(
+            'SELECT * FROM bar WHERE bing = 42',
+            logger.statements[0])
+        self.assertEqual(1, logger.count)
         with stdout() as file:
+            # Show that calling str does not actually print (bugfix).
             result = str(logger)
             self.assertEqual('', file.getvalue())
         self.assertEqual(
@@ -235,20 +243,18 @@
             result)
 
     def test_context_manager_with_stacktrace(self):
-        logger = da.SQLLogger(tracebacks_if=True)
-        with logger:
+        with StormStatementRecorder(tracebacks_if=True) as logger:
             self.execute()
-        self.assertEqual(1, len(logger.queries))
-        self.assertIsNot(None, logger.queries[0]['stack'])
+        self.assertEqual(1, len(logger.query_data))
+        self.assertIsNot(None, logger.query_data[0]['stack'])
 
     def test_sql_parameters(self):
-        logger = da.SQLLogger()
-        with logger:
+        with StormStatementRecorder() as logger:
             self.execute(statement='SELECT * FROM bar WHERE bing = %s',
                          params=(142,))
         self.assertEqual(
             (1, 2, 'SQL-stub-database', 'SELECT * FROM bar WHERE bing = 142'),
-            logger.queries[0]['sql'])
+            logger.query_data[0]['sql'])
 
 
 class TestLoggingWithinRequest(TestCaseWithFactory):
@@ -266,9 +272,8 @@
 
     def test_logger(self):
         tracer = da.LaunchpadStatementTracer()
-        logger = da.SQLLogger()
         with person_logged_in(self.person):
-            with logger:
+            with StormStatementRecorder() as logger:
                 tracer.connection_raw_execute(
                     self.connection, None,
                     'SELECT * FROM bar WHERE bing = 42', ())
@@ -287,7 +292,7 @@
                     'SELECT * FROM bar WHERE bing = 42', ())
                 self.assertIsNot(None, action.duration)
         self.assertEqual(
-            'SELECT * FROM surprise', logger.queries[0]['sql'][3])
+            'SELECT * FROM surprise', logger.query_data[0]['sql'][3])
 
     def test_stderr(self):
         with override_environ(LP_DEBUG_SQL='1'):

=== modified file 'lib/lp/bugs/browser/tests/test_expose.py'
--- lib/lp/bugs/browser/tests/test_expose.py	2011-06-03 20:52:27 +0000
+++ lib/lp/bugs/browser/tests/test_expose.py	2011-09-22 14:51:54 +0000
@@ -124,7 +124,7 @@
         # the team.  (If the team has a preferredemail, muting is not
         # possible).
         context = self.factory.makeProduct(owner=self.user)
-        team1 = self.factory.makeTeam(name='team-1', owner=self.user)
+        self.factory.makeTeam(name='team-1', owner=self.user)
         team2 = self.factory.makeTeam(name='team-2', owner=self.user)
         self.factory.makeEmail('foo@xxxxxxxxxxx',
                                team2,
@@ -186,7 +186,7 @@
                 absoluteURL=fake_absoluteURL)
         statements_for_admininstrated_teams = [
             statement for statement in recorder.statements
-            if statement.startswith("'SELECT *")]
+            if statement.startswith("SELECT *")]
         self.assertEqual(1, len(statements_for_admininstrated_teams))
 
         # Calling the function a second time does not require an
@@ -197,7 +197,7 @@
                 absoluteURL=fake_absoluteURL)
         statements_for_admininstrated_teams = [
             statement for statement in recorder.statements
-            if statement.startswith("'SELECT *")]
+            if statement.startswith("SELECT *")]
         self.assertEqual(0, len(statements_for_admininstrated_teams))
 
     def test_teams_owned_but_not_joined_are_not_included(self):
@@ -313,13 +313,13 @@
             sub = target.addBugSubscription(team, team.teamowner)
         expose_user_subscriptions_to_js(user, [sub], request)
         info = IJSONRequestCache(request).objects['subscription_info']
-        self.assertEqual(len(info), 1) # One target.
+        self.assertEqual(len(info), 1)  # One target.
         target_info = info[0]
         self.assertEqual(target_info['target_title'], target.title)
         self.assertEqual(
             target_info['target_url'], canonical_url(
                 target, rootsite='mainsite'))
-        self.assertEqual(len(target_info['filters']), 1) # One filter.
+        self.assertEqual(len(target_info['filters']), 1)  # One filter.
         filter_info = target_info['filters'][0]
         self.assertEqual(filter_info['filter'], sub.bug_filters[0])
         self.assertTrue(filter_info['subscriber_is_team'])
@@ -409,7 +409,7 @@
             expose_user_subscriptions_to_js(user, [sub], request)
         statements_for_admininstrated_teams = [
             statement for statement in recorder.statements
-            if statement.startswith("'SELECT *")]
+            if statement.startswith("SELECT *")]
         self.assertEqual(1, len(statements_for_admininstrated_teams))
 
         # Calling the function a second time does not require an
@@ -419,5 +419,5 @@
                 expose_user_subscriptions_to_js(user, [sub], request)
         statements_for_admininstrated_teams = [
             statement for statement in recorder.statements
-            if statement.startswith("'SELECT *")]
+            if statement.startswith("SELECT *")]
         self.assertEqual(0, len(statements_for_admininstrated_teams))

=== modified file 'lib/lp/bugs/model/tests/test_personsubscriptioninfo.py'
--- lib/lp/bugs/model/tests/test_personsubscriptioninfo.py	2011-09-01 16:30:29 +0000
+++ lib/lp/bugs/model/tests/test_personsubscriptioninfo.py	2011-09-22 14:51:54 +0000
@@ -8,7 +8,6 @@
 from testtools.matchers import LessThan
 from zope.security.proxy import removeSecurityProxy
 
-from canonical.launchpad.webapp.adapter import SQLLogger
 from canonical.testing import DatabaseFunctionalLayer
 from lp.bugs.interfaces.personsubscriptioninfo import (
     IRealSubscriptionInfo,
@@ -20,9 +19,13 @@
 from lp.registry.interfaces.teammembership import TeamMembershipStatus
 from lp.testing import (
     person_logged_in,
+    StormStatementRecorder,
     TestCaseWithFactory,
     )
-from lp.testing.matchers import Provides
+from lp.testing.matchers import (
+    HasQueryCount,
+    Provides,
+    )
 
 
 class TestPersonSubscriptionInfo(TestCaseWithFactory):
@@ -509,12 +512,11 @@
             team.addMember(self.subscriber, team.teamowner,
                            status=TeamMembershipStatus.ADMIN)
         self.makeDuplicates(count=1, subscriber=team)
-        logger = SQLLogger()
-        with logger:
+        with StormStatementRecorder() as recorder:
             self.subscriptions.reload()
         # This should produce a very small number of queries.
-        count_with_one_subscribed_duplicate = len(logger.queries)
-        self.assertThat(count_with_one_subscribed_duplicate, LessThan(5))
+        self.assertThat(recorder, HasQueryCount(LessThan(5)))
+        count_with_one_subscribed_duplicate = recorder.count
         # It should have the correct result.
         self.assertCollectionsAreEmpty(except_='from_duplicate')
         self.assertCollectionContents(
@@ -522,10 +524,10 @@
         # If we increase the number of duplicates subscribed via the team that
         # the user administers...
         self.makeDuplicates(count=4, subscriber=team)
-        with logger:
+        with StormStatementRecorder() as recorder:
             self.subscriptions.reload()
         # ...then the query count should remain the same.
-        count_with_five_subscribed_duplicates = len(logger.queries)
+        count_with_five_subscribed_duplicates = recorder.count
         self.assertEqual(
             count_with_one_subscribed_duplicate,
             count_with_five_subscribed_duplicates)

=== modified file 'lib/lp/bugs/tests/test_duplicate_handling.py'
--- lib/lp/bugs/tests/test_duplicate_handling.py	2011-08-12 11:37:08 +0000
+++ lib/lp/bugs/tests/test_duplicate_handling.py	2011-09-22 14:51:54 +0000
@@ -138,7 +138,7 @@
         target_heat_cache_statements = [
             statement for statement in recorder.statements
             if statement.startswith(
-                "'SELECT MAX(Bug.heat), SUM(Bug.heat), COUNT(Bug.id)")]
+                "SELECT MAX(Bug.heat), SUM(Bug.heat), COUNT(Bug.id)")]
         return len(target_heat_cache_statements)
 
     def test_move_duplicates_efficient_target_heat_cache_calculation(self):

=== modified file 'lib/lp/testing/__init__.py'
--- lib/lp/testing/__init__.py	2011-09-05 15:42:27 +0000
+++ lib/lp/testing/__init__.py	2011-09-22 14:51:54 +0000
@@ -88,12 +88,7 @@
 import oops_datedir_repo.serializer_rfc822
 import pytz
 import simplejson
-from storm.expr import Variable
 from storm.store import Store
-from storm.tracer import (
-    install_tracer,
-    remove_tracer_type,
-    )
 import subunit
 import testtools
 from testtools.content import Content
@@ -119,7 +114,10 @@
     errorlog,
     )
 from canonical.launchpad.webapp.adapter import (
+    print_queries,
     set_permit_timeout_from_features,
+    start_sql_logging,
+    stop_sql_logging,
     )
 from canonical.launchpad.webapp.errorlog import ErrorReportEvent
 from canonical.launchpad.webapp.interaction import ANONYMOUS
@@ -269,53 +267,43 @@
         do somestuff
     self.assertThat(recorder, HasQueryCount(LessThan(42)))
 
-    Note that due to the storm API used, only one of these recorders may be in
-    place at a time: all will be removed when the first one is removed (by
-    calling __exit__ or leaving the scope of a with statement).
+    This also can be useful for investigation, such as in make harness.
+    Try printing it after you have collected some queries.  You can
+    even collect tracebacks, passing True to "tracebacks_if" for tracebacks
+    of every SQL query, or a callable that takes the SQL query string and
+    returns a boolean decision as to whether a traceback is desired.
     """
-
-    def __init__(self):
-        self.statements = []
+    # Note that tests for this are in canonical.launchpad.webapp.tests.
+    # test_statementtracer, because this is really just a small wrapper of
+    # the functionality found there.
+
+    def __init__(self, tracebacks_if=False):
+        self.tracebacks_if = tracebacks_if
+        self.query_data = []
+
+    @property
+    def queries(self):
+        return [record['sql'] for record in self.query_data]
 
     @property
     def count(self):
-        return len(self.statements)
+        return len(self.query_data)
 
     @property
-    def queries(self):
-        """The statements executed as per get_request_statements."""
-        # Perhaps we could just consolidate this code with the request tracer
-        # code and not need a custom tracer at all - if we provided a context
-        # factory to the tracer, which in the production tracers would
-        # use the adapter magic, and in test created ones would log to a list.
-        # We would need to be able to remove just one tracer though, which I
-        # haven't looked into yet. RBC 20100831
-        result = []
-        for statement in self.statements:
-            result.append((0, 0, 'unknown', statement))
-        return result
+    def statements(self):
+        return [record['sql'][3] for record in self.query_data]
 
     def __enter__(self):
-        """Context manager protocol - return this object as the context."""
-        install_tracer(self)
+        self.query_data = start_sql_logging(self.tracebacks_if)
         return self
 
-    def __exit__(self, _ignored, _ignored2, _ignored3):
-        """Content manager protocol - do not swallow exceptions."""
-        remove_tracer_type(StormStatementRecorder)
-        return False
-
-    def connection_raw_execute(self, ignored, raw_cursor, statement, params):
-        """Increment the counter.  We don't care about the args."""
-
-        raw_params = []
-        for param in params:
-            if isinstance(param, Variable):
-                raw_params.append(param.get())
-            else:
-                raw_params.append(param)
-        raw_params = tuple(raw_params)
-        self.statements.append("%r, %r" % (statement, raw_params))
+    def __exit__(self, exc_type, exc_value, tb):
+        stop_sql_logging()
+
+    def __str__(self):
+        out = StringIO()
+        print_queries(self.query_data, file=out)
+        return out.getvalue()
 
 
 def record_statements(function, *args, **kwargs):