2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
8 from zope.interface import implements
10 from twisted.trial.unittest import TestCase
11 from twisted.internet.defer import succeed, gatherResults
12 from twisted.web._stan import Tag
13 from twisted.web._flatten import flattenString
14 from twisted.web.error import UnfilledSlot, UnsupportedType, FlattenerError
15 from twisted.web.template import tags, Comment, CDATA, CharRef, slot
16 from twisted.web.iweb import IRenderable
17 from twisted.web.test._util import FlattenTestCase
20 class TestSerialization(FlattenTestCase):
22 Tests for flattening various things.
24 def test_nestedTags(self):
26 Test that nested tags flatten correctly.
28 return self.assertFlattensTo(
29 tags.html(tags.body('42'), hi='there'),
30 '<html hi="there"><body>42</body></html>')
33 def test_serializeString(self):
35 Test that strings will be flattened and escaped correctly.
37 return gatherResults([
38 self.assertFlattensTo('one', 'one'),
39 self.assertFlattensTo('<abc&&>123', '<abc&&>123'),
43 def test_serializeSelfClosingTags(self):
45 Test that some tags are normally written out as self-closing tags.
47 return self.assertFlattensTo(tags.img(src='test'), '<img src="test" />')
50 def test_serializeComment(self):
52 Test that comments are correctly flattened and escaped.
54 return self.assertFlattensTo(Comment('foo bar'), '<!--foo bar-->'),
57 def test_commentEscaping(self):
59 The data in a L{Comment} is escaped and mangled in the flattened output
60 so that the result is a legal SGML and XML comment.
62 SGML comment syntax is complicated and hard to use. This rule is more
63 restrictive, and more compatible:
65 Comments start with <!-- and end with --> and never contain -- or >.
67 Also by XML syntax, a comment may not end with '-'.
69 @see: U{http://www.w3.org/TR/REC-xml/#sec-comments}
74 "%r does not start with the comment prefix" % (c,))
77 "%r does not end with the comment suffix" % (c,))
78 # If it is shorter than 7, then the prefix and suffix overlap
82 "%r is too short to be a legal comment" % (c,))
84 self.assertNotIn('--', content)
85 self.assertNotIn('>', content)
87 self.assertNotEqual(content[-1], '-')
98 d = flattenString(None, Comment(c))
99 d.addCallback(verifyComment)
101 return gatherResults(results)
104 def test_serializeCDATA(self):
106 Test that CDATA is correctly flattened and escaped.
108 return gatherResults([
109 self.assertFlattensTo(CDATA('foo bar'), '<![CDATA[foo bar]]>'),
110 self.assertFlattensTo(
111 CDATA('foo ]]> bar'),
112 '<![CDATA[foo ]]]]><![CDATA[> bar]]>'),
116 def test_serializeUnicode(self):
118 Test that unicode is encoded correctly in the appropriate places, and
119 raises an error when it occurs in inappropriate place.
121 snowman = u'\N{SNOWMAN}'
122 return gatherResults([
123 self.assertFlattensTo(snowman, '\xe2\x98\x83'),
124 self.assertFlattensTo(tags.p(snowman), '<p>\xe2\x98\x83</p>'),
125 self.assertFlattensTo(Comment(snowman), '<!--\xe2\x98\x83-->'),
126 self.assertFlattensTo(CDATA(snowman), '<![CDATA[\xe2\x98\x83]]>'),
127 self.assertFlatteningRaises(
128 Tag(snowman), UnicodeEncodeError),
129 self.assertFlatteningRaises(
130 Tag('p', attributes={snowman: ''}), UnicodeEncodeError),
134 def test_serializeCharRef(self):
136 A character reference is flattened to a string using the I{&#NNNN;}
139 ref = CharRef(ord(u"\N{SNOWMAN}"))
140 return self.assertFlattensTo(ref, "☃")
143 def test_serializeDeferred(self):
145 Test that a deferred is substituted with the current value in the
146 callback chain when flattened.
148 return self.assertFlattensTo(succeed('two'), 'two')
151 def test_serializeSameDeferredTwice(self):
153 Test that the same deferred can be flattened twice.
156 return gatherResults([
157 self.assertFlattensTo(d, 'three'),
158 self.assertFlattensTo(d, 'three'),
162 def test_serializeIRenderable(self):
164 Test that flattening respects all of the IRenderable interface.
166 class FakeElement(object):
167 implements(IRenderable)
168 def render(ign,ored):
171 tags.transparent(render='test'), ' - ',
172 tags.transparent(render='test'))
173 def lookupRenderMethod(ign, name):
174 self.assertEqual(name, 'test')
175 return lambda ign, node: node('world')
177 return gatherResults([
178 self.assertFlattensTo(FakeElement(), '<p>hello, world - world</p>'),
182 def test_serializeSlots(self):
184 Test that flattening a slot will use the slot value from the tag.
186 t1 = tags.p(slot('test'))
188 t2.fillSlots(test='hello, world')
189 return gatherResults([
190 self.assertFlatteningRaises(t1, UnfilledSlot),
191 self.assertFlattensTo(t2, '<p>hello, world</p>'),
195 def test_serializeDeferredSlots(self):
197 Test that a slot with a deferred as its value will be flattened using
198 the value from the deferred.
200 t = tags.p(slot('test'))
201 t.fillSlots(test=succeed(tags.em('four>')))
202 return self.assertFlattensTo(t, '<p><em>four></em></p>')
205 def test_unknownTypeRaises(self):
207 Test that flattening an unknown type of thing raises an exception.
209 return self.assertFlatteningRaises(None, UnsupportedType)
212 # Use the co_filename mechanism (instead of the __file__ mechanism) because
213 # it is the mechanism traceback formatting uses. The two do not necessarily
214 # agree with each other. This requires a code object compiled in this file.
215 # The easiest way to get a code object is with a new function. I'll use a
216 # lambda to avoid adding anything else to this namespace. The result will
217 # be a string which agrees with the one the traceback module will put into a
218 # traceback for frames associated with functions defined in this file.
220 HERE = (lambda: None).func_code.co_filename
223 class FlattenerErrorTests(TestCase):
225 Tests for L{FlattenerError}.
228 def test_string(self):
230 If a L{FlattenerError} is created with a string root, up to around 40
231 bytes from that string are included in the string representation of the
235 str(FlattenerError(RuntimeError("reason"), ['abc123xyz'], [])),
236 "Exception while flattening:\n"
238 "RuntimeError: reason\n")
241 RuntimeError("reason"), ['0123456789' * 10], [])),
242 "Exception while flattening:\n"
243 " '01234567890123456789<...>01234567890123456789'\n"
244 "RuntimeError: reason\n")
247 def test_unicode(self):
249 If a L{FlattenerError} is created with a unicode root, up to around 40
250 characters from that string are included in the string representation
255 RuntimeError("reason"), [u'abc\N{SNOWMAN}xyz'], [])),
256 "Exception while flattening:\n"
257 " u'abc\\u2603xyz'\n" # Codepoint for SNOWMAN
258 "RuntimeError: reason\n")
261 RuntimeError("reason"), [u'01234567\N{SNOWMAN}9' * 10],
263 "Exception while flattening:\n"
264 " u'01234567\\u2603901234567\\u26039<...>01234567\\u2603901234567"
266 "RuntimeError: reason\n")
269 def test_renderable(self):
271 If a L{FlattenerError} is created with an L{IRenderable} provider root,
272 the repr of that object is included in the string representation of the
275 class Renderable(object):
276 implements(IRenderable)
279 return "renderable repr"
283 RuntimeError("reason"), [Renderable()], [])),
284 "Exception while flattening:\n"
286 "RuntimeError: reason\n")
291 If a L{FlattenerError} is created with a L{Tag} instance with source
292 location information, the source location is included in the string
293 representation of the exception.
296 'div', filename='/foo/filename.xhtml', lineNumber=17, columnNumber=12)
299 str(FlattenerError(RuntimeError("reason"), [tag], [])),
300 "Exception while flattening:\n"
301 " File \"/foo/filename.xhtml\", line 17, column 12, in \"div\"\n"
302 "RuntimeError: reason\n")
305 def test_tagWithoutLocation(self):
307 If a L{FlattenerError} is created with a L{Tag} instance without source
308 location information, only the tagName is included in the string
309 representation of the exception.
312 str(FlattenerError(RuntimeError("reason"), [Tag('span')], [])),
313 "Exception while flattening:\n"
315 "RuntimeError: reason\n")
318 def test_traceback(self):
320 If a L{FlattenerError} is created with traceback frames, they are
321 included in the string representation of the exception.
323 # Try to be realistic in creating the data passed in for the traceback
328 raise RuntimeError("reason")
332 except RuntimeError, exc:
333 # Get the traceback, minus the info for *this* frame
334 tbinfo = traceback.extract_tb(sys.exc_info()[2])[1:]
336 self.fail("f() must raise RuntimeError")
339 str(FlattenerError(exc, [], tbinfo)),
340 "Exception while flattening:\n"
341 " File \"%s\", line %d, in f\n"
343 " File \"%s\", line %d, in g\n"
344 " raise RuntimeError(\"reason\")\n"
345 "RuntimeError: reason\n" % (
346 HERE, f.func_code.co_firstlineno + 1,
347 HERE, g.func_code.co_firstlineno + 1))