|
| 1 | +# coding: utf-8 |
| 2 | +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
| 3 | +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
| 4 | + |
| 5 | +"""Tests for coverage.templite.""" |
| 6 | + |
| 7 | +import re |
| 8 | +import unittest |
| 9 | + |
| 10 | +from resttest3.reports.templite import Templite, TempliteSyntaxError, TempliteValueError |
| 11 | + |
| 12 | + |
| 13 | +# pylint: disable=possibly-unused-variable |
| 14 | + |
| 15 | +class AnyOldObject(object): |
| 16 | + """Simple testing object. |
| 17 | +
|
| 18 | + Use keyword arguments in the constructor to set attributes on the object. |
| 19 | +
|
| 20 | + """ |
| 21 | + |
| 22 | + def __init__(self, **attrs): |
| 23 | + for n, v in attrs.items(): |
| 24 | + setattr(self, n, v) |
| 25 | + |
| 26 | + |
| 27 | +class TempliteTest(unittest.TestCase): |
| 28 | + """Tests for Templite.""" |
| 29 | + |
| 30 | + run_in_temp_dir = False |
| 31 | + |
| 32 | + def try_render(self, text, ctx=None, result=None): |
| 33 | + """Render `text` through `ctx`, and it had better be `result`. |
| 34 | +
|
| 35 | + Result defaults to None so we can shorten the calls where we expect |
| 36 | + an exception and never get to the result comparison. |
| 37 | +
|
| 38 | + """ |
| 39 | + actual = Templite(text).render(ctx or {}) |
| 40 | + # If result is None, then an exception should have prevented us getting |
| 41 | + # to here. |
| 42 | + assert result is not None |
| 43 | + self.assertEqual(actual, result) |
| 44 | + |
| 45 | + def assertSynErr(self, msg): |
| 46 | + """Assert that a `TempliteSyntaxError` will happen. |
| 47 | +
|
| 48 | + A context manager, and the message should be `msg`. |
| 49 | +
|
| 50 | + """ |
| 51 | + pat = "^" + re.escape(msg) + "$" |
| 52 | + return self.assertRaisesRegex(TempliteSyntaxError, pat) |
| 53 | + |
| 54 | + def test_passthrough(self): |
| 55 | + # Strings without variables are passed through unchanged. |
| 56 | + self.assertEqual(Templite("Hello").render(), "Hello") |
| 57 | + self.assertEqual( |
| 58 | + Templite("Hello, 20% fun time!").render(), |
| 59 | + "Hello, 20% fun time!" |
| 60 | + ) |
| 61 | + |
| 62 | + def test_variables(self): |
| 63 | + # Variables use {{var}} syntax. |
| 64 | + self.try_render("Hello, {{name}}!", {'name': 'Ned'}, "Hello, Ned!") |
| 65 | + |
| 66 | + def test_undefined_variables(self): |
| 67 | + # Using undefined names is an error. |
| 68 | + with self.assertRaisesRegex(Exception, "'name'"): |
| 69 | + self.try_render("Hi, {{name}}!") |
| 70 | + |
| 71 | + def test_pipes(self): |
| 72 | + # Variables can be filtered with pipes. |
| 73 | + data = { |
| 74 | + 'name': 'Ned', |
| 75 | + 'upper': lambda x: x.upper(), |
| 76 | + 'second': lambda x: x[1], |
| 77 | + } |
| 78 | + self.try_render("Hello, {{name|upper}}!", data, "Hello, NED!") |
| 79 | + |
| 80 | + # Pipes can be concatenated. |
| 81 | + self.try_render("Hello, {{name|upper|second}}!", data, "Hello, E!") |
| 82 | + |
| 83 | + def test_reusability(self): |
| 84 | + # A single Templite can be used more than once with different data. |
| 85 | + globs = { |
| 86 | + 'upper': lambda x: x.upper(), |
| 87 | + 'punct': '!', |
| 88 | + } |
| 89 | + |
| 90 | + template = Templite("This is {{name|upper}}{{punct}}", globs) |
| 91 | + self.assertEqual(template.render({'name': 'Ned'}), "This is NED!") |
| 92 | + self.assertEqual(template.render({'name': 'Ben'}), "This is BEN!") |
| 93 | + |
| 94 | + def test_attribute(self): |
| 95 | + # Variables' attributes can be accessed with dots. |
| 96 | + obj = AnyOldObject(a="Ay") |
| 97 | + self.try_render("{{obj.a}}", locals(), "Ay") |
| 98 | + |
| 99 | + obj2 = AnyOldObject(obj=obj, b="Bee") |
| 100 | + self.try_render("{{obj2.obj.a}} {{obj2.b}}", locals(), "Ay Bee") |
| 101 | + |
| 102 | + def test_member_function(self): |
| 103 | + # Variables' member functions can be used, as long as they are nullary. |
| 104 | + class WithMemberFns(AnyOldObject): |
| 105 | + """A class to try out member function access.""" |
| 106 | + |
| 107 | + def ditto(self): |
| 108 | + """Return twice the .txt attribute.""" |
| 109 | + return self.txt + self.txt |
| 110 | + |
| 111 | + obj = WithMemberFns(txt="Once") |
| 112 | + self.try_render("{{obj.ditto}}", locals(), "OnceOnce") |
| 113 | + |
| 114 | + def test_item_access(self): |
| 115 | + # Variables' items can be used. |
| 116 | + d = {'a': 17, 'b': 23} |
| 117 | + self.try_render("{{d.a}} < {{d.b}}", locals(), "17 < 23") |
| 118 | + |
| 119 | + def test_loops(self): |
| 120 | + # Loops work like in Django. |
| 121 | + nums = [1, 2, 3, 4] |
| 122 | + self.try_render( |
| 123 | + "Look: {% for n in nums %}{{n}}, {% endfor %}done.", |
| 124 | + locals(), |
| 125 | + "Look: 1, 2, 3, 4, done." |
| 126 | + ) |
| 127 | + |
| 128 | + # Loop iterables can be filtered. |
| 129 | + def rev(l): |
| 130 | + """Return the reverse of `l`.""" |
| 131 | + l = l[:] |
| 132 | + l.reverse() |
| 133 | + return l |
| 134 | + |
| 135 | + self.try_render( |
| 136 | + "Look: {% for n in nums|rev %}{{n}}, {% endfor %}done.", |
| 137 | + locals(), |
| 138 | + "Look: 4, 3, 2, 1, done." |
| 139 | + ) |
| 140 | + |
| 141 | + def test_empty_loops(self): |
| 142 | + self.try_render( |
| 143 | + "Empty: {% for n in nums %}{{n}}, {% endfor %}done.", |
| 144 | + {'nums': []}, |
| 145 | + "Empty: done." |
| 146 | + ) |
| 147 | + |
| 148 | + def test_multiline_loops(self): |
| 149 | + self.try_render( |
| 150 | + "Look: \n{% for n in nums %}\n{{n}}, \n{% endfor %}done.", |
| 151 | + {'nums': [1, 2, 3]}, |
| 152 | + "Look: \n\n1, \n\n2, \n\n3, \ndone." |
| 153 | + ) |
| 154 | + |
| 155 | + def test_multiple_loops(self): |
| 156 | + self.try_render( |
| 157 | + "{% for n in nums %}{{n}}{% endfor %} and " |
| 158 | + "{% for n in nums %}{{n}}{% endfor %}", |
| 159 | + {'nums': [1, 2, 3]}, |
| 160 | + "123 and 123" |
| 161 | + ) |
| 162 | + |
| 163 | + def test_comments(self): |
| 164 | + # Single-line comments work: |
| 165 | + self.try_render( |
| 166 | + "Hello, {# Name goes here: #}{{name}}!", |
| 167 | + {'name': 'Ned'}, "Hello, Ned!" |
| 168 | + ) |
| 169 | + # and so do multi-line comments: |
| 170 | + self.try_render( |
| 171 | + "Hello, {# Name\ngoes\nhere: #}{{name}}!", |
| 172 | + {'name': 'Ned'}, "Hello, Ned!" |
| 173 | + ) |
| 174 | + |
| 175 | + def test_if(self): |
| 176 | + self.try_render( |
| 177 | + "Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!", |
| 178 | + {'ned': 1, 'ben': 0}, |
| 179 | + "Hi, NED!" |
| 180 | + ) |
| 181 | + self.try_render( |
| 182 | + "Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!", |
| 183 | + {'ned': 0, 'ben': 1}, |
| 184 | + "Hi, BEN!" |
| 185 | + ) |
| 186 | + self.try_render( |
| 187 | + "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!", |
| 188 | + {'ned': 0, 'ben': 0}, |
| 189 | + "Hi, !" |
| 190 | + ) |
| 191 | + self.try_render( |
| 192 | + "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!", |
| 193 | + {'ned': 1, 'ben': 0}, |
| 194 | + "Hi, NED!" |
| 195 | + ) |
| 196 | + self.try_render( |
| 197 | + "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!", |
| 198 | + {'ned': 1, 'ben': 1}, |
| 199 | + "Hi, NEDBEN!" |
| 200 | + ) |
| 201 | + |
| 202 | + def test_complex_if(self): |
| 203 | + class Complex(AnyOldObject): |
| 204 | + """A class to try out complex data access.""" |
| 205 | + |
| 206 | + def getit(self): |
| 207 | + """Return it.""" |
| 208 | + return self.it |
| 209 | + |
| 210 | + obj = Complex(it={'x': "Hello", 'y': 0}) |
| 211 | + self.try_render( |
| 212 | + "@" |
| 213 | + "{% if obj.getit.x %}X{% endif %}" |
| 214 | + "{% if obj.getit.y %}Y{% endif %}" |
| 215 | + "{% if obj.getit.y|str %}S{% endif %}" |
| 216 | + "!", |
| 217 | + {'obj': obj, 'str': str}, |
| 218 | + "@XS!" |
| 219 | + ) |
| 220 | + |
| 221 | + def test_loop_if(self): |
| 222 | + self.try_render( |
| 223 | + "@{% for n in nums %}{% if n %}Z{% endif %}{{n}}{% endfor %}!", |
| 224 | + {'nums': [0, 1, 2]}, |
| 225 | + "@0Z1Z2!" |
| 226 | + ) |
| 227 | + self.try_render( |
| 228 | + "X{%if nums%}@{% for n in nums %}{{n}}{% endfor %}{%endif%}!", |
| 229 | + {'nums': [0, 1, 2]}, |
| 230 | + "X@012!" |
| 231 | + ) |
| 232 | + self.try_render( |
| 233 | + "X{%if nums%}@{% for n in nums %}{{n}}{% endfor %}{%endif%}!", |
| 234 | + {'nums': []}, |
| 235 | + "X!" |
| 236 | + ) |
| 237 | + |
| 238 | + def test_nested_loops(self): |
| 239 | + self.try_render( |
| 240 | + "@" |
| 241 | + "{% for n in nums %}" |
| 242 | + "{% for a in abc %}{{a}}{{n}}{% endfor %}" |
| 243 | + "{% endfor %}" |
| 244 | + "!", |
| 245 | + {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, |
| 246 | + "@a0b0c0a1b1c1a2b2c2!" |
| 247 | + ) |
| 248 | + |
| 249 | + def test_whitespace_handling(self): |
| 250 | + self.try_render( |
| 251 | + "@{% for n in nums %}\n" |
| 252 | + " {% for a in abc %}{{a}}{{n}}{% endfor %}\n" |
| 253 | + "{% endfor %}!\n", |
| 254 | + {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, |
| 255 | + "@\n a0b0c0\n\n a1b1c1\n\n a2b2c2\n!\n" |
| 256 | + ) |
| 257 | + self.try_render( |
| 258 | + "@{% for n in nums -%}\n" |
| 259 | + " {% for a in abc -%}\n" |
| 260 | + " {# this disappears completely -#}\n" |
| 261 | + " {{a-}}\n" |
| 262 | + " {{n -}}\n" |
| 263 | + " {{n -}}\n" |
| 264 | + " {% endfor %}\n" |
| 265 | + "{% endfor %}!\n", |
| 266 | + {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, |
| 267 | + "@a00b00c00\na11b11c11\na22b22c22\n!\n" |
| 268 | + ) |
| 269 | + self.try_render( |
| 270 | + "@{% for n in nums -%}\n" |
| 271 | + " {{n -}}\n" |
| 272 | + " x\n" |
| 273 | + "{% endfor %}!\n", |
| 274 | + {'nums': [0, 1, 2]}, |
| 275 | + "@0x\n1x\n2x\n!\n" |
| 276 | + ) |
| 277 | + self.try_render(" hello ", {}, " hello ") |
| 278 | + |
| 279 | + def test_eat_whitespace(self): |
| 280 | + self.try_render( |
| 281 | + "Hey!\n" |
| 282 | + "{% joined %}\n" |
| 283 | + "@{% for n in nums %}\n" |
| 284 | + " {% for a in abc %}\n" |
| 285 | + " {# this disappears completely #}\n" |
| 286 | + " X\n" |
| 287 | + " Y\n" |
| 288 | + " {{a}}\n" |
| 289 | + " {{n }}\n" |
| 290 | + " {% endfor %}\n" |
| 291 | + "{% endfor %}!\n" |
| 292 | + "{% endjoined %}\n", |
| 293 | + {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, |
| 294 | + "Hey!\n@XYa0XYb0XYc0XYa1XYb1XYc1XYa2XYb2XYc2!\n" |
| 295 | + ) |
| 296 | + |
| 297 | + def test_non_ascii(self): |
| 298 | + self.try_render( |
| 299 | + u"{{where}} ollǝɥ", |
| 300 | + {'where': u'ǝɹǝɥʇ'}, |
| 301 | + u"ǝɹǝɥʇ ollǝɥ" |
| 302 | + ) |
| 303 | + |
| 304 | + def test_exception_during_evaluation(self): |
| 305 | + # TypeError: Couldn't evaluate {{ foo.bar.baz }}: |
| 306 | + regex = "^Couldn't evaluate None.bar$" |
| 307 | + with self.assertRaisesRegex(TempliteValueError, regex): |
| 308 | + self.try_render( |
| 309 | + "Hey {{foo.bar.baz}} there", {'foo': None}, "Hey ??? there" |
| 310 | + ) |
| 311 | + |
| 312 | + def test_bad_names(self): |
| 313 | + with self.assertSynErr("Not a valid name: 'var%&!@'"): |
| 314 | + self.try_render("Wat: {{ var%&!@ }}") |
| 315 | + with self.assertSynErr("Not a valid name: 'filter%&!@'"): |
| 316 | + self.try_render("Wat: {{ foo|filter%&!@ }}") |
| 317 | + with self.assertSynErr("Not a valid name: '@'"): |
| 318 | + self.try_render("Wat: {% for @ in x %}{% endfor %}") |
| 319 | + |
| 320 | + def test_bogus_tag_syntax(self): |
| 321 | + with self.assertSynErr("Don't understand tag: 'bogus'"): |
| 322 | + self.try_render("Huh: {% bogus %}!!{% endbogus %}??") |
| 323 | + |
| 324 | + def test_malformed_if(self): |
| 325 | + with self.assertSynErr("Don't understand if: '{% if %}'"): |
| 326 | + self.try_render("Buh? {% if %}hi!{% endif %}") |
| 327 | + with self.assertSynErr("Don't understand if: '{% if this or that %}'"): |
| 328 | + self.try_render("Buh? {% if this or that %}hi!{% endif %}") |
| 329 | + |
| 330 | + def test_malformed_for(self): |
| 331 | + with self.assertSynErr("Don't understand for: '{% for %}'"): |
| 332 | + self.try_render("Weird: {% for %}loop{% endfor %}") |
| 333 | + with self.assertSynErr("Don't understand for: '{% for x from y %}'"): |
| 334 | + self.try_render("Weird: {% for x from y %}loop{% endfor %}") |
| 335 | + with self.assertSynErr("Don't understand for: '{% for x, y in z %}'"): |
| 336 | + self.try_render("Weird: {% for x, y in z %}loop{% endfor %}") |
| 337 | + |
| 338 | + def test_bad_nesting(self): |
| 339 | + with self.assertSynErr("Unmatched action tag: 'if'"): |
| 340 | + self.try_render("{% if x %}X") |
| 341 | + with self.assertSynErr("Mismatched end tag: 'for'"): |
| 342 | + self.try_render("{% if x %}X{% endfor %}") |
| 343 | + with self.assertSynErr("Too many ends: '{% endif %}'"): |
| 344 | + self.try_render("{% if x %}{% endif %}{% endif %}") |
| 345 | + |
| 346 | + def test_malformed_end(self): |
| 347 | + with self.assertSynErr("Don't understand end: '{% end if %}'"): |
| 348 | + self.try_render("{% if x %}X{% end if %}") |
| 349 | + with self.assertSynErr("Don't understand end: '{% endif now %}'"): |
| 350 | + self.try_render("{% if x %}X{% endif now %}") |
0 commit comments