PageRenderTime 73ms CodeModel.GetById 4ms app.highlight 61ms RepoModel.GetById 1ms app.codeStats 0ms

/johnny/tests/cache.py

https://bitbucket.org/jmoiron/johnny-cache/
Python | 961 lines | 938 code | 17 blank | 6 comment | 9 complexity | b6906ecacd8e833339969a76477d1196 MD5 | raw file
  1#!/usr/bin/env python
  2# -*- coding: utf-8 -*-
  3
  4"""Tests for the QueryCache functionality of johnny."""
  5
  6import django
  7from django.conf import settings
  8from django.db import connection
  9try:
 10    from django.db import connections
 11except:
 12    connections = None
 13from johnny import middleware
 14from johnny import settings as johnny_settings
 15import base
 16
 17try:
 18    any
 19except NameError:
 20    def any(iterable):
 21        for i in iterable:
 22            if i: return True
 23        return False
 24
 25# put tests in here to be included in the testing suite
 26__all__ = ['MultiDbTest', 'SingleModelTest', 'MultiModelTest', 'TransactionSupportTest', 'BlackListTest', 'TransactionManagerTestCase']
 27
 28def _pre_setup(self):
 29    self.saved_DISABLE_SETTING = getattr(johnny_settings, 'DISABLE_QUERYSET_CACHE', False)
 30    johnny_settings.DISABLE_QUERYSET_CACHE = False
 31    self.middleware = middleware.QueryCacheMiddleware()
 32
 33def _post_teardown(self):
 34    self.middleware.unpatch()
 35    johnny_settings.DISABLE_QUERYSET_CACHE = self.saved_DISABLE_SETTING
 36
 37class QueryCacheBase(base.JohnnyTestCase):
 38    def _pre_setup(self):
 39        _pre_setup(self)
 40        super(QueryCacheBase, self)._pre_setup()
 41
 42    def _post_teardown(self):
 43        _post_teardown(self)
 44        super(QueryCacheBase, self)._post_teardown()
 45
 46class TransactionQueryCacheBase(base.TransactionJohnnyTestCase):
 47    def _pre_setup(self):
 48        _pre_setup(self)
 49        super(TransactionQueryCacheBase, self)._pre_setup()
 50
 51    def _post_teardown(self):
 52        from django.db import transaction
 53        _post_teardown(self)
 54        super(TransactionQueryCacheBase, self)._post_teardown()
 55        if transaction.is_managed():
 56            transaction.managed(False)
 57
 58class BlackListTest(QueryCacheBase):
 59    fixtures = base.johnny_fixtures
 60
 61    def test_basic_blacklist(self):
 62        from johnny import cache, settings
 63        from testapp.models import Genre, Book
 64        q = base.message_queue()
 65        old = johnny_settings.BLACKLIST
 66        johnny_settings.BLACKLIST = set(['testapp_genre'])
 67        connection.queries = []
 68        Book.objects.get(id=1)
 69        Book.objects.get(id=1)
 70        self.failUnless((False, True) == (q.get_nowait(), q.get_nowait()))
 71        list(Genre.objects.all())
 72        list(Genre.objects.all())
 73        self.failUnless(not any((q.get_nowait(), q.get_nowait())))
 74        johnny_settings.BLACKLIST = old
 75
 76
 77class MultiDbTest(TransactionQueryCacheBase):
 78    multi_db = True
 79    fixtures = ['genres.json', 'genres.second.json']
 80
 81    def _run_threaded(self, query, queue):
 82        """Runs a query (as a string) from testapp in another thread and
 83        puts (hit?, result) on the provided queue."""
 84        from threading import Thread
 85        def _inner(_query):
 86            from testapp.models import Genre, Book, Publisher, Person
 87            from johnny.signals import qc_hit, qc_miss
 88            from johnny.cache import local
 89            from django.db import transaction
 90            msg = []
 91            def hit(*args, **kwargs):
 92                msg.append(True)
 93            def miss(*args, **kwargs):
 94                msg.append(False)
 95            qc_hit.connect(hit)
 96            qc_miss.connect(miss)
 97            obj = eval(_query)
 98            msg.append(obj)
 99            queue.put(msg)
100        t = Thread(target=_inner, args=(query,))
101        t.start()
102        t.join()
103
104    def _other(self, cmd, q):
105        def _innter(cmd):
106            q.put(eval(cmd))
107        t = Thread(target=_inner, args=(cmd,))
108        t.start()
109        t.join()
110
111    def test_basic_queries(self):
112        """Tests basic queries and that the cache is working for multiple db's"""
113        if len(getattr(settings, "DATABASES", [])) <= 1:
114            print "\n  Skipping multi database tests"
115            return
116
117        from pprint import pformat
118        from testapp.models import Genre, Book, Publisher, Person
119        from django.db import connections
120
121        self.failUnless("default" in getattr(settings, "DATABASES"))
122        self.failUnless("second" in getattr(settings, "DATABASES"))
123
124        g1 = Genre.objects.using("default").get(pk=1)
125        g1.title = "A default database"
126        g1.save(using='default')
127        g2 = Genre.objects.using("second").get(pk=1)
128        g2.title = "A second database"
129        g2.save(using='second')
130        for c in connections:
131            connections[c].queries = []
132        #fresh from cache since we saved each
133        g1 = Genre.objects.using('default').get(pk=1)
134        g2 = Genre.objects.using('second').get(pk=1)
135        for c in connections:
136            self.failUnless(len(connections[c].queries) == 1)
137        self.failUnless(g1.title == "A default database")
138        self.failUnless(g2.title == "A second database")
139        #should be a cache hit
140        g1 = Genre.objects.using('default').get(pk=1)
141        g2 = Genre.objects.using('second').get(pk=1)
142        for c in connections:
143            self.failUnless(len(connections[c].queries) == 1)
144
145    def test_cache_key_setting(self):
146        """Tests that two databases use a single cached object when given the same DB cache key"""
147        if len(getattr(settings, "DATABASES", [])) <= 1:
148            print "\n  Skipping multi database tests"
149            return
150
151        from testapp.models import Genre
152        from django.db import connections
153
154        self.failUnless("default" in getattr(settings, "DATABASES"))
155        self.failUnless("second" in getattr(settings, "DATABASES"))
156
157        old_cache_keys = johnny_settings.DB_CACHE_KEYS
158        johnny_settings.DB_CACHE_KEYS = {'default': 'default', 'second': 'default'}
159
160        g1 = Genre.objects.using("default").get(pk=1)
161        g1.title = "A default database"
162        g1.save(using='default')
163        g2 = Genre.objects.using("second").get(pk=1)
164        g2.title = "A second database"
165        g2.save(using='second')
166        for c in connections:
167            connections[c].queries = []
168        #fresh from cache since we saved each
169        g1 = Genre.objects.using('default').get(pk=1)
170        g2 = Genre.objects.using('second').get(pk=1)
171        johnny_settings.DB_CACHE_KEYS = old_cache_keys
172        total_queries = sum([len(connections[c].queries)
173                             for c in connections])
174        self.assertEqual(total_queries, 1)
175
176    def test_transactions(self):
177        """Tests transaction rollbacks and local cache for multiple dbs"""
178
179        if len(getattr(settings, "DATABASES", [])) <= 1:
180            print "\n  Skipping multi database tests"
181            return
182        if hasattr(settings, 'DATABASE_ENGINE'):
183            if settings.DATABASE_ENGINE == 'sqlite3':
184                print "\n  Skipping test requiring multiple threads."
185                return
186        else:
187            from django.db import connections, transaction
188            for db in settings.DATABASES.values():
189                if db['ENGINE'] == 'sqlite3':
190                    print "\n  Skipping test requiring multiple threads."
191                    return
192
193            for conname in connections:
194                con = connections[conname]
195                if not base.supports_transactions(con):
196                    print "\n  Skipping test requiring transactions."
197                    return
198
199        from django.db import connections, transaction
200        from johnny import cache as c
201        from Queue import Queue as queue
202        q = queue()
203        other = lambda x: self._run_threaded(x, q)
204
205        from testapp.models import Genre
206
207
208        # sanity check 
209        self.failUnless(transaction.is_managed() == False)
210        self.failUnless(transaction.is_dirty() == False)
211        self.failUnless("default" in getattr(settings, "DATABASES"))
212        self.failUnless("second" in getattr(settings, "DATABASES"))
213
214        # this should seed this fetch in the global cache
215        g1 = Genre.objects.using("default").get(pk=1)
216        g2 = Genre.objects.using("second").get(pk=1)
217        start_g1 = g1.title
218
219        transaction.enter_transaction_management(using='default')
220        transaction.managed(using='default')
221        transaction.enter_transaction_management(using='second')
222        transaction.managed(using='second')
223
224        g1.title = "Testing a rollback"
225        g2.title = "Testing a commit"
226        g1.save()
227        g2.save()
228
229        # test outside of transaction, should be cache hit and 
230        # not contain the local changes
231        other("Genre.objects.using('default').get(pk=1)")
232        hit, ostart = q.get()
233        self.failUnless(ostart.title == start_g1)
234        self.failUnless(hit)
235
236        transaction.rollback(using='default')
237        transaction.commit(using='second')
238        transaction.managed(False, "default")
239        transaction.managed(False, "second")
240
241        #other thread should have seen rollback
242        other("Genre.objects.using('default').get(pk=1)")
243        hit, ostart = q.get()
244        self.failUnless(ostart.title == start_g1)
245        self.failUnless(hit)
246
247        connections['default'].queries = []
248        connections['second'].queries = []
249        #should be a cache hit due to rollback
250        g1 = Genre.objects.using("default").get(pk=1)
251        #should be a db hit due to commit
252        g2 = Genre.objects.using("second").get(pk=1)
253        self.failUnless(connections['default'].queries == [])
254        self.failUnless(len(connections['second'].queries) == 1)
255
256        #other thread sould now be accessing the cache after the get
257        #from the commit.
258        other("Genre.objects.using('second').get(pk=1)")
259        hit, ostart = q.get()
260        self.failUnless(ostart.title == g2.title)
261        self.failUnless(hit)
262
263        self.failUnless(g1.title == start_g1)
264        self.failUnless(g2.title == "Testing a commit")
265        transaction.leave_transaction_management("default")
266        transaction.leave_transaction_management("second")
267
268    def test_savepoints(self):
269        """tests savepoints for multiple db's"""
270        from Queue import Queue as queue
271        q = queue()
272        other = lambda x: self._run_threaded(x, q)
273
274        from testapp.models import Genre
275        try:
276            from django.db import connections, transaction
277        except ImportError:
278            # connections doesn't exist in 1.1 and under
279            print"\n  Skipping multi database tests"
280
281        if len(getattr(settings, "DATABASES", [])) <= 1:
282            print "\n  Skipping multi database tests"
283            return
284        for name, db in settings.DATABASES.items():
285            if name in ('default', 'second'):
286                if 'sqlite' in db['ENGINE']:
287                    print "\n  Skipping test requiring multiple threads."
288                    return
289                con = connections[name]
290                if not con.features.uses_savepoints:
291                    print "\n  Skipping test requiring savepoints."
292                    return
293
294        # sanity check 
295        self.failUnless(transaction.is_managed() == False)
296        self.failUnless(transaction.is_dirty() == False)
297        self.failUnless("default" in getattr(settings, "DATABASES"))
298        self.failUnless("second" in getattr(settings, "DATABASES"))
299
300        g1 = Genre.objects.using("default").get(pk=1)
301        start_g1 = g1.title
302        g2 = Genre.objects.using("second").get(pk=1)
303
304        transaction.enter_transaction_management(using='default')
305        transaction.managed(using='default')
306        transaction.enter_transaction_management(using='second')
307        transaction.managed(using='second')
308
309        g1.title = "Rollback savepoint"
310        g1.save()
311
312        g2.title = "Committed savepoint"
313        g2.save(using="second")
314        sid2 = transaction.savepoint(using="second")
315
316        sid = transaction.savepoint(using="default")
317        g1.title = "Dirty text"
318        g1.save()
319
320        #other thread should see the original key and cache object from memcache,
321        #not the local cache version
322        other("Genre.objects.using('default').get(pk=1)")
323        hit, ostart = q.get()
324        self.failUnless(hit)
325        self.failUnless(ostart.title == start_g1)
326        #should not be a hit due to rollback
327        connections["default"].queries = []
328        transaction.savepoint_rollback(sid, using="default")
329        g1 = Genre.objects.using("default").get(pk=1)
330
331        # i think it should be "Rollback Savepoint" here
332        self.failUnless(g1.title == start_g1)
333
334        #will be pushed to dirty in commit
335        g2 = Genre.objects.using("second").get(pk=1)
336        self.failUnless(g2.title == "Committed savepoint")
337        transaction.savepoint_commit(sid2, using="second")
338
339        #other thread should still see original version even 
340        #after savepoint commit
341        other("Genre.objects.using('second').get(pk=1)")
342        hit, ostart = q.get()
343        self.failUnless(hit)
344        self.failUnless(ostart.title == start_g1)
345
346        connections["second"].queries = []
347        g2 = Genre.objects.using("second").get(pk=1)
348        self.failUnless(connections["second"].queries == [])
349
350        transaction.commit(using="second")
351        transaction.managed(False, "second")
352
353        g2 = Genre.objects.using("second").get(pk=1)
354        self.failUnless(connections["second"].queries == [])
355        self.failUnless(g2.title == "Committed savepoint")
356
357        #now committed and cached, other thread should reflect new title
358        #without a hit to the db
359        other("Genre.objects.using('second').get(pk=1)")
360        hit, ostart = q.get()
361        self.failUnless(ostart.title == g2.title)
362        self.failUnless(hit)
363
364        transaction.managed(False, "default")
365        transaction.leave_transaction_management("default")
366        transaction.leave_transaction_management("second")
367
368
369class SingleModelTest(QueryCacheBase):
370    fixtures = base.johnny_fixtures
371
372    def test_multi_where_cache_coherency(self):
373        """A test to detect the issue described in bitbucket #24:
374        https://bitbucket.org/jmoiron/johnny-cache/issue/24/
375        """
376        from testapp.models import Issue24Model as i24m
377
378        i24m.objects.get_or_create(one=1, two=1)
379        i24m.objects.get_or_create(one=1, two=2)
380        i24m.objects.get_or_create(one=2, two=1)
381        i24m.objects.get_or_create(one=2, two=2)
382
383        ones = i24m.objects.filter(one=1)
384        twos = i24m.objects.filter(two=1)
385
386        res = i24m.objects.filter(one__in=ones).exclude(two=twos).all()
387        # XXX: I'm afraid I don't even understand what this is supposed
388        # to be doing here, and in any case this test case fails.  I've
389        # included something similar to the patch in #24, if someone knows
390        # how to write a test case to create that condition please do so here
391
392    def test_exists_hit(self):
393        """Tests that an exist failure caches properly"""
394        from testapp.models import Publisher
395        if django.VERSION[:2] < (1, 2):
396            # django 1.1.x does not have exists()
397            return
398        connection.queries = []
399
400        Publisher.objects.filter(title="Doesn't Exist").exists()
401        Publisher.objects.filter(title="Doesn't Exist").exists()
402
403        self.assertEqual(len(connection.queries), 1)
404
405    def test_basic_querycaching(self):
406        """A basic test that querycaching is functioning properly and is
407        being invalidated properly on singular table reads & writes."""
408        from testapp.models import Publisher, Genre
409        from django.db.models import Q
410        connection.queries = []
411        starting_count = Publisher.objects.count()
412        starting_count = Publisher.objects.count()
413        # make sure that doing this twice doesn't hit the db twice
414        self.failUnless(len(connection.queries) == 1)
415        self.failUnless(starting_count == 1)
416        # this write should invalidate the key we have
417        Publisher(title='Harper Collins', slug='harper-collins').save()
418        connection.queries = []
419        new_count = Publisher.objects.count()
420        self.failUnless(len(connection.queries) == 1)
421        self.failUnless(new_count == 2)
422        # this tests the codepath after 'except EmptyResultSet' where
423        # result_type == MULTI
424        self.failUnless(not list(Publisher.objects.filter(title__in=[])))
425        # test for a regression on the WhereNode, bitbucket #20
426        g1 = Genre.objects.get(pk=1)
427        g1.title = "Survival Horror"
428        g1.save()
429        g1 = Genre.objects.get(Q(title__iexact="Survival Horror"))
430
431    def test_querycache_return_results(self):
432        """Test that the return results from the query cache are what we
433        expect;  single items are single items, etc."""
434        from testapp.models import Publisher
435        connection.queries = []
436        pub = Publisher.objects.get(id=1)
437        pub2 = Publisher.objects.get(id=1)
438        self.failUnless(pub == pub2)
439        self.failUnless(len(connection.queries) == 1)
440        pubs = list(Publisher.objects.all())
441        pubs2 = list(Publisher.objects.all())
442        self.failUnless(pubs == pubs2)
443        self.failUnless(len(connection.queries) == 2)
444
445    def test_delete(self):
446        """Test that a database delete clears a table cache."""
447        from testapp.models import Genre
448        g1 = Genre.objects.get(pk=1)
449        begin = Genre.objects.all().count()
450        g1.delete()
451        self.assertRaises(Genre.DoesNotExist, lambda: Genre.objects.get(pk=1))
452        connection.queries = []
453        self.failUnless(Genre.objects.all().count() == (begin -1))
454        self.failUnless(len(connection.queries) == 1)
455        Genre(title='Science Fiction', slug='scifi').save()
456        Genre(title='Fantasy', slug='rubbish').save()
457        Genre(title='Science Fact', slug='scifact').save()
458        count = Genre.objects.count()
459        Genre.objects.get(title='Fantasy')
460        q = base.message_queue()
461        Genre.objects.filter(title__startswith='Science').delete()
462        # this should not be cached
463        Genre.objects.get(title='Fantasy')
464        self.failUnless(not q.get_nowait())
465
466    def test_update(self):
467        from testapp.models import Genre
468        connection.queries = []
469        g1 = Genre.objects.get(pk=1)
470        Genre.objects.all().update(title="foo")
471        g2 = Genre.objects.get(pk=1)
472        self.failUnless(g1.title != g2.title)
473        self.failUnless(g2.title == "foo")
474        self.failUnless(len(connection.queries) == 3)
475
476    def test_empty_count(self):
477        """Test for an empty count aggregate query with an IN"""
478        from testapp.models import Genre
479        books = Genre.objects.filter(id__in=[])
480        count = books.count()
481        self.failUnless(count == 0)
482
483    def test_aggregate_annotation(self):
484        """Test aggregating an annotation """
485        from django.db.models import Count
486        from django.db.models import Sum
487        from testapp.models import Book
488        from django.core.paginator import Paginator
489        author_count = Book.objects.annotate(author_count=Count('authors')).aggregate(Sum('author_count'))
490        self.assertEquals(author_count['author_count__sum'],2)
491        # also test using the paginator, although this shouldn't be a big issue..
492        books = Book.objects.all().annotate(num_authors=Count('authors'))
493        paginator = Paginator(books, 25)
494        list_page = paginator.page(1)
495
496    def test_queryset_laziness(self):
497        """This test exists to model the laziness of our queries;  the
498        QuerySet cache should not alter the laziness of QuerySets."""
499        from testapp.models import Genre
500        connection.queries = []
501        qs = Genre.objects.filter(title__startswith='A')
502        qs = qs.filter(pk__lte=1)
503        qs = qs.order_by('pk')
504        # we should only execute the query at this point
505        arch = qs[0]
506        self.failUnless(len(connection.queries) == 1)
507
508    def test_order_by(self):
509        """A basic test that our query caching is taking order clauses
510        into account."""
511        from testapp.models import Genre
512        connection.queries = []
513        first = list(Genre.objects.filter(title__startswith='A').order_by('slug'))
514        second = list(Genre.objects.filter(title__startswith='A').order_by('-slug'))
515        # test that we've indeed done two queries and that the orders
516        # of the results are reversed
517        self.failUnless((first[0], first[1] == second[1], second[0]))
518        self.failUnless(len(connection.queries) == 2)
519
520    def test_signals(self):
521        """Test that the signals we say we're sending are being sent."""
522        from testapp.models import Genre
523        from johnny.signals import qc_hit, qc_miss
524        connection.queries = []
525        misses = []
526        hits = []
527        def qc_hit_listener(sender, **kwargs):
528            hits.append(kwargs['key'])
529        def qc_miss_listener(*args, **kwargs):
530            misses.append(kwargs['key'])
531        qc_hit.connect(qc_hit_listener)
532        qc_miss.connect(qc_miss_listener)
533        first = list(Genre.objects.filter(title__startswith='A').order_by('slug'))
534        second = list(Genre.objects.filter(title__startswith='A').order_by('slug'))
535        self.failUnless(len(misses) == len(hits) == 1)
536
537    def test_in_values_list(self):
538        from testapp.models import Publisher, Book
539        from johnny.cache import get_tables_for_query
540        pubs = Publisher.objects.all()
541        books = Book.objects.filter(publisher__in=pubs.values_list("id", flat=True))
542        tables = list(sorted(get_tables_for_query(books.query)))
543        self.assertEqual(["testapp_book", "testapp_publisher"], tables)
544
545
546class MultiModelTest(QueryCacheBase):
547    fixtures = base.johnny_fixtures
548
549    def test_foreign_keys(self):
550        """Test that simple joining (and deferred loading) functions as we'd
551        expect when involving multiple tables.  In particular, a query that
552        joins 2 tables should invalidate when either table is invalidated."""
553        from testapp.models import Genre, Book, Publisher, Person
554        connection.queries = []
555        books = list(Book.objects.select_related('publisher'))
556        books = list(Book.objects.select_related('publisher'))
557        str(books[0].genre)
558        # this should all have done one query..
559        self.failUnless(len(connection.queries) == 1)
560        books = list(Book.objects.select_related('publisher'))
561        # invalidate the genre key, which shouldn't impact the query
562        Genre(title='Science Fiction', slug='scifi').save()
563        after_save = len(connection.queries)
564        books = list(Book.objects.select_related('publisher'))
565        self.failUnless(len(connection.queries) == after_save)
566        # now invalidate publisher, which _should_
567        p = Publisher(title='McGraw Hill', slug='mcgraw-hill')
568        p.save()
569        after_save = len(connection.queries)
570        books = list(Book.objects.select_related('publisher'))
571        self.failUnless(len(connection.queries) == after_save + 1)
572        # the query should be cached again...
573        books = list(Book.objects.select_related('publisher'))
574        # this time, create a book and the query should again be uncached..
575        Book(title='Anna Karenina', slug='anna-karenina', publisher=p).save()
576        after_save = len(connection.queries)
577        books = list(Book.objects.select_related('publisher'))
578        self.failUnless(len(connection.queries) == after_save + 1)
579
580    def test_invalidate(self):
581        """Test for the module-level invalidation function."""
582        from Queue import Queue as queue
583        from testapp.models import Book, Genre, Publisher
584        from johnny.cache import invalidate
585        q = base.message_queue()
586        b = Book.objects.get(id=1)
587        invalidate(Book)
588        b = Book.objects.get(id=1)
589        first, second = q.get_nowait(), q.get_nowait()
590        self.failUnless(first == second == False)
591        g = Genre.objects.get(id=1)
592        p = Publisher.objects.get(id=1)
593        invalidate('testapp_genre', Publisher)
594        g = Genre.objects.get(id=1)
595        p = Publisher.objects.get(id=1)
596        fg,fp,sg,sp = [q.get() for i in range(4)]
597        self.failUnless(fg == fp == sg == sp == False)
598
599    def test_many_to_many(self):
600        from testapp.models import Book, Person
601        b = Book.objects.get(pk=1)
602        p1 = Person.objects.get(pk=1)
603        p2 = Person.objects.get(pk=2)
604        b.authors.add(p1)
605        connection.queries = []
606
607        list(b.authors.all())
608
609        #many to many should be invalidated
610        self.failUnless(len(connection.queries) == 1)
611        b.authors.remove(p1)
612        b = Book.objects.get(pk=1)
613        list(b.authors.all())
614        #can't determine the queries here, 1.1 and 1.2 uses them differently
615
616        connection.queries = []
617        #many to many should be invalidated, 
618        #person is not invalidated since we just want
619        #the many to many table to be
620        p1 = Person.objects.get(pk=1)
621        self.failUnless(len(connection.queries) == 0)
622
623        p1.books.add(b)
624        connection.queries = []
625
626        #many to many should be invalidated,
627        #this is the first query
628        list(p1.books.all())
629        b = Book.objects.get(pk=1)
630        self.failUnless(len(connection.queries) == 1)
631
632        #query should be cached
633        self.failUnless(len(list(p1.books.all())) == 1)
634        self.failUnless(len(connection.queries) == 1)
635
636        #testing clear
637        b.authors.clear()
638        self.failUnless(b.authors.all().count() == 0)
639        self.failUnless(p1.books.all().count() == 0)
640        b.authors.add(p1)
641        self.failUnless(b.authors.all().count() == 1)
642        queries = len(connection.queries)
643
644        #should be cached
645        b.authors.all().count()
646        self.failUnless(len(connection.queries) == queries)
647        self.failUnless(p1.books.all().count() == 1)
648        p1.books.clear()
649        self.failUnless(b.authors.all().count() == 0)
650
651    def test_subselect_support(self):
652        """Test that subselects are handled properly."""
653        from django import db
654        db.reset_queries()
655        from testapp.models import Book, Person, PersonType
656        author_types = PersonType.objects.filter(title='Author')
657        author_people = Person.objects.filter(person_types__in=author_types)
658        written_books = Book.objects.filter(authors__in=author_people)
659        q = base.message_queue()
660        self.failUnless(len(db.connection.queries) == 0)
661        count = written_books.count()
662        self.failUnless(q.get() == False)
663        # execute the query again, this time it's cached
664        self.failUnless(written_books.count() == count)
665        self.failUnless(q.get() == True)
666        # change the person type of 'Author' to something else
667        pt = PersonType.objects.get(title='Author')
668        pt.title = 'NonAuthor'
669        pt.save()
670        self.failUnless(PersonType.objects.filter(title='Author').count() == 0)
671        q.clear()
672        db.reset_queries()
673        # now execute the same query;  the result should be diff and it should be
674        # a cache miss
675        new_count = written_books.count()
676        self.failUnless(new_count != count)
677        self.failUnless(q.get() == False)
678        PersonType.objects.filter(title='NonAuthor').order_by('-title')[:5]
679
680    def test_foreign_key_delete_cascade(self):
681        """From #32, test that if you have 'Foo' and 'Bar', with bar.foo => Foo,
682        and you delete foo, bar.foo is also deleted, which means you have to
683        invalidate Bar when deletions are made in Foo (but not changes)."""
684
685
686class TransactionSupportTest(TransactionQueryCacheBase):
687    fixtures = base.johnny_fixtures
688
689    def _run_threaded(self, query, queue):
690        """Runs a query (as a string) from testapp in another thread and
691        puts (hit?, result) on the provided queue."""
692        from threading import Thread
693        def _inner(_query):
694            from testapp.models import Genre, Book, Publisher, Person
695            from johnny.signals import qc_hit, qc_miss
696            msg = []
697            def hit(*args, **kwargs):
698                msg.append(True)
699            def miss(*args, **kwargs):
700                msg.append(False)
701            qc_hit.connect(hit)
702            qc_miss.connect(miss)
703            obj = eval(_query)
704            msg.append(obj)
705            queue.put(msg)
706            if connections is not None:
707                #this is to fix a race condition with the
708                #thread to ensure that we close it before 
709                #the next test runs
710                connections['default'].close()
711        t = Thread(target=_inner, args=(query,))
712        t.start()
713        t.join()
714
715    def tearDown(self):
716        from django.db import transaction
717        if transaction.is_managed():
718            if transaction.is_dirty():
719                transaction.rollback()
720            transaction.managed(False)
721            transaction.leave_transaction_management()
722
723    def test_transaction_commit(self):
724        """Test transaction support in Johnny."""
725        from Queue import Queue as queue
726        from django.db import transaction
727        from testapp.models import Genre, Publisher
728        from johnny import cache
729
730        if django.VERSION[:2] < (1, 3):
731            if settings.DATABASE_ENGINE == 'sqlite3':
732                print "\n  Skipping test requiring multiple threads."
733                return
734        else:
735            if settings.DATABASES.get('default', {}).get('ENGINE', '').endswith('sqlite3'):
736                print "\n  Skipping test requiring multiple threads."
737                return
738
739
740        self.failUnless(transaction.is_managed() == False)
741        self.failUnless(transaction.is_dirty() == False)
742        connection.queries = []
743        cache.local.clear()
744        q = queue()
745        other = lambda x: self._run_threaded(x, q)
746        # load some data
747        start = Genre.objects.get(id=1)
748        other('Genre.objects.get(id=1)')
749        hit, ostart = q.get()
750        # these should be the same and should have hit cache
751        self.failUnless(hit)
752        self.failUnless(ostart == start)
753        # enter manual transaction management
754        transaction.enter_transaction_management()
755        transaction.managed()
756        start.title = 'Jackie Chan Novels'
757        # local invalidation, this key should hit the localstore!
758        nowlen = len(cache.local)
759        start.save()
760        self.failUnless(nowlen != len(cache.local))
761        # perform a read OUTSIDE this transaction... it should still see the
762        # old gen key, and should still find the "old" data
763        other('Genre.objects.get(id=1)')
764        hit, ostart = q.get()
765        self.failUnless(hit)
766        self.failUnless(ostart.title != start.title)
767        transaction.commit()
768        # now that we commit, we push the localstore keys out;  this should be
769        # a cache miss, because we never read it inside the previous transaction
770        other('Genre.objects.get(id=1)')
771        hit, ostart = q.get()
772        self.failUnless(not hit)
773        self.failUnless(ostart.title == start.title)
774        transaction.managed(False)
775        transaction.leave_transaction_management()
776
777    def test_transaction_rollback(self):
778        """Tests johnny's handling of transaction rollbacks.
779
780        Similar to the commit, this sets up a write to a db in a transaction,
781        reads from it (to force a cache write of sometime), then rolls back."""
782
783        from Queue import Queue as queue
784        from django.db import transaction
785        from testapp.models import Genre, Publisher
786        from johnny import cache
787        if django.VERSION[:2] < (1, 3):
788            if settings.DATABASE_ENGINE == 'sqlite3':
789                print "\n  Skipping test requiring multiple threads."
790                return
791        else:
792            if settings.DATABASES.get('default', {}).get('ENGINE', '').endswith('sqlite3'):
793                print "\n  Skipping test requiring multiple threads."
794                return
795
796        self.failUnless(transaction.is_managed() == False)
797        self.failUnless(transaction.is_dirty() == False)
798        connection.queries = []
799        cache.local.clear()
800        q = queue()
801        other = lambda x: self._run_threaded(x, q)
802
803        # load some data
804        start = Genre.objects.get(id=1)
805        other('Genre.objects.get(id=1)')
806        hit, ostart = q.get()
807        # these should be the same and should have hit cache
808        self.failUnless(hit)
809        self.failUnless(ostart == start)
810        # enter manual transaction management
811        transaction.enter_transaction_management()
812        transaction.managed()
813        start.title = 'Jackie Chan Novels'
814        # local invalidation, this key should hit the localstore!
815        nowlen = len(cache.local)
816        start.save()
817        self.failUnless(nowlen != len(cache.local))
818        # perform a read OUTSIDE this transaction... it should still see the
819        # old gen key, and should still find the "old" data
820        other('Genre.objects.get(id=1)')
821        hit, ostart = q.get()
822        self.failUnless(hit)
823        self.failUnless(ostart.title != start.title)
824        # perform a READ inside the transaction;  this should hit the localstore
825        # but not the outside!
826        nowlen = len(cache.local)
827        start2 = Genre.objects.get(id=1)
828        self.failUnless(start2.title == start.title)
829        self.failUnless(len(cache.local) > nowlen)
830        transaction.rollback()
831        # we rollback, and flush all johnny keys related to this transaction
832        # subsequent gets should STILL hit the cache in the other thread
833        # and indeed, in this thread.
834
835        self.failUnless(transaction.is_dirty() == False)
836        other('Genre.objects.get(id=1)')
837        hit, ostart = q.get()
838        self.failUnless(hit)
839        start = Genre.objects.get(id=1)
840        self.failUnless(ostart.title == start.title)
841        transaction.managed(False)
842        transaction.leave_transaction_management()
843
844    def test_savepoint_rollback(self):
845        """Tests rollbacks of savepoints"""
846        from django.db import transaction
847        from testapp.models import Genre, Publisher
848        from johnny import cache
849        if not connection.features.uses_savepoints:
850            return
851        self.failUnless(transaction.is_managed() == False)
852        self.failUnless(transaction.is_dirty() == False)
853        connection.queries = []
854        cache.local.clear()
855        transaction.enter_transaction_management()
856        transaction.managed()
857        g = Genre.objects.get(pk=1)
858        start_title = g.title
859        g.title = "Adventures in Savepoint World"
860        g.save()
861        g = Genre.objects.get(pk=1)
862        self.failUnless(g.title == "Adventures in Savepoint World")
863        sid = transaction.savepoint()
864        g.title = "In the Void"
865        g.save()
866        g = Genre.objects.get(pk=1)
867        self.failUnless(g.title == "In the Void")
868        transaction.savepoint_rollback(sid)
869        g = Genre.objects.get(pk=1)
870        self.failUnless(g.title == "Adventures in Savepoint World")
871        transaction.rollback()
872        g = Genre.objects.get(pk=1)
873        self.failUnless(g.title == start_title)
874        transaction.managed(False)
875        transaction.leave_transaction_management()
876
877    def test_savepoint_commit(self):
878        """Tests a transaction commit (release)
879        The release actually pushes the savepoint back into the dirty stack,
880        but at the point it was saved in the transaction"""
881        from django.db import transaction
882        from testapp.models import Genre, Publisher
883        from johnny import cache
884        if not connection.features.uses_savepoints:
885            return
886        self.failUnless(transaction.is_managed() == False)
887        self.failUnless(transaction.is_dirty() == False)
888        connection.queries = []
889        cache.local.clear()
890        transaction.enter_transaction_management()
891        transaction.managed()
892        g = Genre.objects.get(pk=1)
893        start_title = g.title
894        g.title = "Adventures in Savepoint World"
895        g.save()
896        g = Genre.objects.get(pk=1)
897        self.failUnless(g.title == "Adventures in Savepoint World")
898        sid = transaction.savepoint()
899        g.title = "In the Void"
900        g.save()
901        connection.queries = []
902        #should be a database hit because of save in savepoint
903        g = Genre.objects.get(pk=1)
904        self.failUnless(len(connection.queries) == 1)
905        self.failUnless(g.title == "In the Void")
906        transaction.savepoint_commit(sid)
907        #should be a cache hit against the dirty store
908        connection.queries = []
909        g = Genre.objects.get(pk=1)
910        self.failUnless(connection.queries == [])
911        self.failUnless(g.title == "In the Void")
912        transaction.commit()
913        #should have been pushed up to cache store
914        g = Genre.objects.get(pk=1)
915        self.failUnless(connection.queries == [])
916        self.failUnless(g.title == "In the Void")
917        transaction.managed(False)
918        transaction.leave_transaction_management()
919
920import johnny
921class TransactionManagerTestCase(base.TransactionJohnnyTestCase):
922
923    def setUp(self):
924        self.middleware = middleware.QueryCacheMiddleware()
925    
926    def tearDown(self):
927        from django.db import transaction
928        if transaction.is_managed():
929            transaction.managed(False)
930
931    def test_savepoint_localstore_flush(self):
932        """
933        This is a very simple test to see if savepoints will actually
934        be committed, i.e. flushed out from localstore into cache.
935        """
936        from django.db import transaction
937        transaction.enter_transaction_management()
938        transaction.managed()
939
940        TABLE_NAME = 'test_table'
941        cache_backend = johnny.cache.get_backend()
942        cache_backend.patch()
943        keyhandler = cache_backend.keyhandler
944        keygen = keyhandler.keygen
945        
946        tm = cache_backend.cache_backend
947        
948        # First, we set one key-val pair generated for our non-existing table.
949        table_key = keygen.gen_table_key(TABLE_NAME)
950        tm.set(table_key, 'val1')
951
952        # Then we create a savepoint.
953        # The key-value pair is moved into 'trans_sids' item of localstore.
954        tm._create_savepoint('savepoint1')
955        
956        # We then commit all the savepoints (i.e. only one in this case)
957        # The items stored in 'trans_sids' should be moved back to the
958        # top-level dictionary of our localstore
959        tm._commit_all_savepoints()
960        # And this checks if it actually happened.
961        self.failUnless(table_key in tm.local)