Merge branch 'julian'
[philo.git] / tests.py
index 874f62f..96ac7b6 100644 (file)
--- a/tests.py
+++ b/tests.py
@@ -1,9 +1,110 @@
 from django.test import TestCase
 from django import template
 from django.conf import settings
+from django.db import connection
+from django.template import loader
+from django.template.loaders import cached
 from philo.exceptions import AncestorDoesNotExist
 from philo.models import Node, Page, Template
 from philo.contrib.penfield.models import Blog, BlogView, BlogEntry
+import sys, traceback
+
+
+class TemplateTestCase(TestCase):
+       fixtures = ['test_fixtures.json']
+       
+       def test_templates(self):
+               "Tests to make sure that embed behaves with complex includes and extends"
+               template_tests = self.get_template_tests()
+               
+               # Register our custom template loader. Shamelessly cribbed from django core regressiontests.
+               def test_template_loader(template_name, template_dirs=None):
+                       "A custom template loader that loads the unit-test templates."
+                       try:
+                               return (template_tests[template_name][0] , "test:%s" % template_name)
+                       except KeyError:
+                               raise template.TemplateDoesNotExist, template_name
+               
+               cache_loader = cached.Loader(('test_template_loader',))
+               cache_loader._cached_loaders = (test_template_loader,)
+               
+               old_template_loaders = loader.template_source_loaders
+               loader.template_source_loaders = [cache_loader]
+               
+               # Turn TEMPLATE_DEBUG off, because tests assume that.
+               old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
+               
+               # Set TEMPLATE_STRING_IF_INVALID to a known string.
+               old_invalid = settings.TEMPLATE_STRING_IF_INVALID
+               expected_invalid_str = 'INVALID'
+               
+               failures = []
+               tests = template_tests.items()
+               tests.sort()
+               # Run tests
+               for name, vals in tests:
+                       xx, context, result = vals
+                       try:
+                               test_template = loader.get_template(name)
+                               output = test_template.render(template.Context(context))
+                       except Exception:
+                               exc_type, exc_value, exc_tb = sys.exc_info()
+                               if exc_type != result:
+                                       tb = '\n'.join(traceback.format_exception(exc_type, exc_value, exc_tb))
+                                       failures.append("Template test %s -- FAILED. Got %s, exception: %s\n%s" % (name, exc_type, exc_value, tb))
+                               continue
+                       if output != result:
+                               failures.append("Template test %s -- FAILED. Expected %r, got %r" % (name, result, output))
+               
+               # Cleanup
+               settings.TEMPLATE_DEBUG = old_td
+               settings.TEMPLATE_STRING_IF_INVALID = old_invalid
+               loader.template_source_loaders = old_template_loaders
+               
+               self.assertEqual(failures, [], "Tests failed:\n%s\n%s" % ('-'*70, ("\n%s\n" % ('-'*70)).join(failures)))
+       
+       
+       def get_template_tests(self):
+               # SYNTAX --
+               # 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
+               blog = Blog.objects.all()[0]
+               return {
+                       # EMBED INCLUSION HANDLING
+                       
+                       'embed01': ('{{ embedded.title|safe }}', {'embedded': blog}, blog.title),
+                       'embed02': ('{{ embedded.title|safe }}{{ var1 }}{{ var2 }}', {'embedded': blog}, blog.title),
+                       'embed03': ('{{ embedded.title|safe }} is a lie!', {'embedded': blog}, '%s is a lie!' % blog.title),
+                       
+                       # Simple template structure with embed
+                       'simple01': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog 1 %}Simple{% block one %}{% endblock %}', {'blog': blog}, '%sSimple' % blog.title),
+                       'simple02': ('{% extends "simple01" %}', {}, '%sSimple' % blog.title),
+                       'simple03': ('{% embed penfield.blog with "embed000" %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+                       'simple04': ('{% embed penfield.blog 1 %}', {}, settings.TEMPLATE_STRING_IF_INVALID),
+                       'simple05': ('{% embed penfield.blog with "embed01" %}{% embed blog %}', {'blog': blog}, blog.title),
+                       
+                       # Kwargs
+                       'kwargs01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1="hi" var2=lo %}', {'lo': 'lo'}, '%shilo' % blog.title),
+                       
+                       # Filters/variables
+                       'filters01': ('{% embed penfield.blog with "embed02" %}{% embed penfield.blog 1 var1=hi|first var2=lo|slice:"3" %}', {'hi': ["These", "words"], 'lo': 'lower'}, '%sTheselow' % blog.title),
+                       'filters02': ('{% embed penfield.blog with "embed01" %}{% embed penfield.blog entry %}', {'entry': 1}, blog.title),
+                       
+                       # Blocky structure
+                       'block01': ('{% block one %}Hello{% endblock %}', {}, 'Hello'),
+                       'block02': ('{% extends "simple01" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s" % (blog.title, blog.title)),
+                       'block03': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s is a lie!" % (blog.title, blog.title)),
+                       
+                       # Blocks and includes
+                       'block-include01': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% include "simple01" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%sSimple%s is a lie!" % (blog.title, blog.title, blog.title)),
+                       'block-include02': ('{% extends "simple01" %}{% block one %}{% include "simple04" %}{% embed penfield.blog with "embed03" %}{% include "simple04" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s%s is a lie!%s is a lie!" % (blog.title, blog.title, blog.title, blog.title)),
+                       
+                       # Tests for more complex situations...
+                       'complex01': ('{% block one %}{% endblock %}complex{% block two %}{% endblock %}', {}, 'complex'),
+                       'complex02': ('{% extends "complex01" %}', {}, 'complex'),
+                       'complex03': ('{% extends "complex02" %}{% embed penfield.blog with "embed01" %}', {}, 'complex'),
+                       'complex04': ('{% extends "complex03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, '%scomplex' % blog.title),
+                       'complex05': ('{% extends "complex03" %}{% block one %}{% include "simple04" %}{% endblock %}', {}, '%scomplex' % blog.title),
+               }
 
 
 class NodeURLTestCase(TestCase):
@@ -18,25 +119,25 @@ class NodeURLTestCase(TestCase):
                        command.handle(all_apps=True)
                
                self.templates = [
-                               ("{% node_url %}", "/root/never/"),
-                               ("{% node_url for node2 %}", "/root/blog/"),
-                               ("{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", "<p>root/never/</p>"),
-                               ("{% node_url for nodes|first %}", "/root/never/"),
+                               ("{% node_url %}", "/root/second/"),
+                               ("{% node_url for node2 %}", "/root/second2/"),
+                               ("{% node_url as hello %}<p>{{ hello|slice:'1:' }}</p>", "<p>root/second/</p>"),
+                               ("{% node_url for nodes|first %}", "/root/"),
                                ("{% node_url with entry %}", settings.TEMPLATE_STRING_IF_INVALID),
-                               ("{% node_url with entry for node2 %}", "/root/blog/2010/10/20/first-entry"),
-                               ("{% node_url with tag for node2 %}", "/root/blog/tags/test-tag/"),
-                               ("{% node_url with date for node2 %}", "/root/blog/2010/10/20"),
-                               ("{% node_url entries_by_day year=date|date:'Y' month=date|date:'m' day=date|date:'d' for node2 as goodbye %}<em>{{ goodbye|upper }}</em>", "<em>/ROOT/BLOG/2010/10/20</em>"),
-                               ("{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", "/root/blog/2010/10"),
-                               ("{% node_url entries_by_year year=date|date:'Y' for node2 %}", "/root/blog/2010/"),
+                               ("{% node_url with entry for node2 %}", "/root/second2/2010/10/20/first-entry"),
+                               ("{% node_url with tag for node2 %}", "/root/second2/tags/test-tag/"),
+                               ("{% node_url with date for node2 %}", "/root/second2/2010/10/20"),
+                               ("{% node_url entries_by_day year=date|date:'Y' month=date|date:'m' day=date|date:'d' for node2 as goodbye %}<em>{{ goodbye|upper }}</em>", "<em>/ROOT/SECOND2/2010/10/20</em>"),
+                               ("{% node_url entries_by_month year=date|date:'Y' month=date|date:'m' for node2 %}", "/root/second2/2010/10"),
+                               ("{% node_url entries_by_year year=date|date:'Y' for node2 %}", "/root/second2/2010/"),
                ]
                
                nodes = Node.objects.all()
                blog = Blog.objects.all()[0]
                
                self.context = template.Context({
-                       'node': nodes[0],
-                       'node2': nodes[1],
+                       'node': nodes.get(slug='second'),
+                       'node2': nodes.get(slug='second2'),
                        'nodes': nodes,
                        'entry': BlogEntry.objects.all()[0],
                        'tag': blog.entry_tags.all()[0],
@@ -57,52 +158,71 @@ class TreePathTestCase(TestCase):
                        command = Command()
                        command.handle(all_apps=True)
        
-       def test_has_ancestor(self):
-               root = Node.objects.get(slug='root')
-               third = Node.objects.get(slug='third')
-               r1 = Node.objects.get(slug='recursive1')
-               r2 = Node.objects.get(slug='recursive2')
-               pr1 = Node.objects.get(slug='postrecursive1')
-               
-               # Simple case: straight path
-               self.assertEqual(third.has_ancestor(root), True)
-               self.assertEqual(root.has_ancestor(root), False)
-               self.assertEqual(root.has_ancestor(None), True)
-               self.assertEqual(third.has_ancestor(None), True)
-               self.assertEqual(root.has_ancestor(root, inclusive=True), True)
-               
-               # Recursive case
-               self.assertEqual(r1.has_ancestor(r1), True)
-               self.assertEqual(r1.has_ancestor(r2), True)
-               self.assertEqual(r2.has_ancestor(r1), True)
-               self.assertEqual(r2.has_ancestor(None), False)
-               
-               # Post-recursive case
-               self.assertEqual(pr1.has_ancestor(r1), True)
-               self.assertEqual(pr1.has_ancestor(pr1), False)
-               self.assertEqual(pr1.has_ancestor(pr1, inclusive=True), True)
-               self.assertEqual(pr1.has_ancestor(None), False)
-               self.assertEqual(pr1.has_ancestor(root), False)
+       def assertQueryLimit(self, max, expected_result, *args, **kwargs):
+               # As a rough measure of efficiency, limit the number of queries required for a given operation.
+               settings.DEBUG = True
+               call = kwargs.pop('callable', Node.objects.get_with_path)
+               try:
+                       queries = len(connection.queries)
+                       if isinstance(expected_result, type) and issubclass(expected_result, Exception):
+                               self.assertRaises(expected_result, call, *args, **kwargs)
+                       else:
+                               self.assertEqual(call(*args, **kwargs), expected_result)
+                       queries = len(connection.queries) - queries
+                       if queries > max:
+                               raise AssertionError('"%d" unexpectedly not less than or equal to "%s"' % (queries, max))
+               finally:
+                       settings.DEBUG = False
        
-       def test_get_path(self):
+       def test_get_with_path(self):
                root = Node.objects.get(slug='root')
                third = Node.objects.get(slug='third')
-               r1 = Node.objects.get(slug='recursive1')
-               r2 = Node.objects.get(slug='recursive2')
-               pr1 = Node.objects.get(slug='postrecursive1')
+               second2 = Node.objects.get(slug='second2')
+               fifth = Node.objects.get(slug='fifth')
+               e = Node.DoesNotExist
+               
+               # Empty segments
+               self.assertQueryLimit(0, root, '', root=root)
+               self.assertQueryLimit(0, e, '')
+               self.assertQueryLimit(0, (root, None), '', root=root, absolute_result=False)
+               
+               # Absolute result
+               self.assertQueryLimit(1, third, 'root/second/third')
+               self.assertQueryLimit(1, third, 'second/third', root=root)
+               self.assertQueryLimit(1, third, 'root//////second/third///')
                
-               # Simple case: straight path to None
-               self.assertEqual(root.get_path(), 'root')
-               self.assertEqual(third.get_path(), 'root/never/more/second/third')
+               self.assertQueryLimit(1, e, 'root/secont/third')
+               self.assertQueryLimit(1, e, 'second/third')
                
-               # Recursive case: Looped path to root None
-               self.assertEqual(r1.get_path(), u'\u2026/recursive1/recursive2/recursive3/recursive1')
-               self.assertEqual(pr1.get_path(), u'\u2026/recursive3/recursive1/recursive2/recursive3/postrecursive1')
+               # Non-absolute result (binary search)
+               self.assertQueryLimit(2, (second2, 'sub/path/tail'), 'root/second2/sub/path/tail', absolute_result=False)
+               self.assertQueryLimit(3, (second2, 'sub/'), 'root/second2/sub/', absolute_result=False)
+               self.assertQueryLimit(2, e, 'invalid/path/1/2/3/4/5/6/7/8/9/1/2/3/4/5/6/7/8/9/0', absolute_result=False)
+               self.assertQueryLimit(1, (root, None), 'root', absolute_result=False)
+               self.assertQueryLimit(2, (second2, None), 'root/second2', absolute_result=False)
+               self.assertQueryLimit(3, (third, None), 'root/second/third', absolute_result=False)
                
-               # Simple error case: straight invalid path
-               self.assertRaises(AncestorDoesNotExist, root.get_path, root=third)
-               self.assertRaises(AncestorDoesNotExist, third.get_path, root=pr1)
+               # with root != None
+               self.assertQueryLimit(1, (second2, None), 'second2', root=root, absolute_result=False)
+               self.assertQueryLimit(2, (third, None), 'second/third', root=root, absolute_result=False)
+               
+               # Preserve trailing slash
+               self.assertQueryLimit(2, (second2, 'sub/path/tail/'), 'root/second2/sub/path/tail/', absolute_result=False)
+               
+               # Speed increase for leaf nodes - should this be tested?
+               self.assertQueryLimit(1, (fifth, 'sub/path/tail/len/five'), 'root/second/third/fourth/fifth/sub/path/tail/len/five', absolute_result=False)
+       
+       def test_get_path(self):
+               root = Node.objects.get(slug='root')
+               root2 = Node.objects.get(slug='root')
+               third = Node.objects.get(slug='third')
+               second2 = Node.objects.get(slug='second2')
+               fifth = Node.objects.get(slug='fifth')
+               e = AncestorDoesNotExist
                
-               # Recursive error case
-               self.assertRaises(AncestorDoesNotExist, r1.get_path, root=root)
-               self.assertRaises(AncestorDoesNotExist, pr1.get_path, root=third)
\ No newline at end of file
+               self.assertQueryLimit(0, 'root', callable=root.get_path)
+               self.assertQueryLimit(0, '', root2, callable=root.get_path)
+               self.assertQueryLimit(1, 'root/second/third', callable=third.get_path)
+               self.assertQueryLimit(1, 'second/third', root, callable=third.get_path)
+               self.assertQueryLimit(1, e, third, callable=second2.get_path)
+               self.assertQueryLimit(1, '? - ?', root, ' - ', 'title', callable=third.get_path)