/johnny/tests/cache.py

https://bitbucket.org/jmoiron/johnny-cache/ · Python · 961 lines · 827 code · 64 blank · 70 comment · 18 complexity · b6906ecacd8e833339969a76477d1196 MD5 · raw file

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