RAHHH
This commit is contained in:
@@ -0,0 +1,767 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the flattening portion of L{twisted.web.template}, implemented in
|
||||
L{twisted.web._flatten}.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from collections import OrderedDict
|
||||
from textwrap import dedent
|
||||
from types import FunctionType
|
||||
from typing import Callable, Dict, List, NoReturn, Optional, Tuple, cast
|
||||
from xml.etree.ElementTree import XML
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from hamcrest import assert_that, equal_to
|
||||
|
||||
from twisted.internet.defer import (
|
||||
CancelledError,
|
||||
Deferred,
|
||||
gatherResults,
|
||||
passthru,
|
||||
succeed,
|
||||
)
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.test.testutils import XMLAssertionMixin
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from twisted.web._flatten import BUFFER_SIZE
|
||||
from twisted.web.error import FlattenerError, UnfilledSlot, UnsupportedType
|
||||
from twisted.web.iweb import IRenderable, IRequest, ITemplateLoader
|
||||
from twisted.web.template import (
|
||||
CDATA,
|
||||
CharRef,
|
||||
Comment,
|
||||
Element,
|
||||
Flattenable,
|
||||
Tag,
|
||||
TagLoader,
|
||||
flatten,
|
||||
flattenString,
|
||||
renderer,
|
||||
slot,
|
||||
tags,
|
||||
)
|
||||
from twisted.web.test._util import FlattenTestCase
|
||||
|
||||
IS_PYTHON_313 = sys.version_info[:2] >= (3, 13)
|
||||
|
||||
|
||||
class SerializationTests(FlattenTestCase, XMLAssertionMixin):
|
||||
"""
|
||||
Tests for flattening various things.
|
||||
"""
|
||||
|
||||
def test_nestedTags(self) -> None:
|
||||
"""
|
||||
Test that nested tags flatten correctly.
|
||||
"""
|
||||
self.assertFlattensImmediately(
|
||||
tags.html(tags.body("42"), hi="there"),
|
||||
b'<html hi="there"><body>42</body></html>',
|
||||
)
|
||||
|
||||
def test_serializeString(self) -> None:
|
||||
"""
|
||||
Test that strings will be flattened and escaped correctly.
|
||||
"""
|
||||
self.assertFlattensImmediately("one", b"one"),
|
||||
self.assertFlattensImmediately("<abc&&>123", b"<abc&&>123"),
|
||||
|
||||
def test_serializeSelfClosingTags(self) -> None:
|
||||
"""
|
||||
The serialized form of a self-closing tag is C{'<tagName />'}.
|
||||
"""
|
||||
self.assertFlattensImmediately(tags.img(), b"<img />")
|
||||
|
||||
def test_serializeAttribute(self) -> None:
|
||||
"""
|
||||
The serialized form of attribute I{a} with value I{b} is C{'a="b"'}.
|
||||
"""
|
||||
self.assertFlattensImmediately(tags.img(src="foo"), b'<img src="foo" />')
|
||||
|
||||
def test_serializedMultipleAttributes(self) -> None:
|
||||
"""
|
||||
Multiple attributes are separated by a single space in their serialized
|
||||
form.
|
||||
"""
|
||||
tag = tags.img()
|
||||
tag.attributes = OrderedDict([("src", "foo"), ("name", "bar")])
|
||||
self.assertFlattensImmediately(tag, b'<img src="foo" name="bar" />')
|
||||
|
||||
def checkAttributeSanitization(
|
||||
self,
|
||||
wrapData: Callable[[str], Flattenable],
|
||||
wrapTag: Callable[[Tag], Flattenable],
|
||||
) -> None:
|
||||
"""
|
||||
Common implementation of L{test_serializedAttributeWithSanitization}
|
||||
and L{test_serializedDeferredAttributeWithSanitization},
|
||||
L{test_serializedAttributeWithTransparentTag}.
|
||||
|
||||
@param wrapData: A 1-argument callable that wraps around the
|
||||
attribute's value so other tests can customize it.
|
||||
|
||||
@param wrapTag: A 1-argument callable that wraps around the outer tag
|
||||
so other tests can customize it.
|
||||
"""
|
||||
self.assertFlattensImmediately(
|
||||
wrapTag(tags.img(src=wrapData('<>&"'))),
|
||||
b'<img src="<>&"" />',
|
||||
)
|
||||
|
||||
def test_serializedAttributeWithSanitization(self) -> None:
|
||||
"""
|
||||
Attribute values containing C{"<"}, C{">"}, C{"&"}, or C{'"'} have
|
||||
C{"<"}, C{">"}, C{"&"}, or C{"""} substituted for those
|
||||
bytes in the serialized output.
|
||||
"""
|
||||
self.checkAttributeSanitization(passthru, passthru)
|
||||
|
||||
def test_serializedDeferredAttributeWithSanitization(self) -> None:
|
||||
"""
|
||||
Like L{test_serializedAttributeWithSanitization}, but when the contents
|
||||
of the attribute are in a L{Deferred
|
||||
<twisted.internet.defer.Deferred>}.
|
||||
"""
|
||||
self.checkAttributeSanitization(succeed, passthru)
|
||||
|
||||
def test_serializedAttributeWithSlotWithSanitization(self) -> None:
|
||||
"""
|
||||
Like L{test_serializedAttributeWithSanitization} but with a slot.
|
||||
"""
|
||||
toss = []
|
||||
|
||||
def insertSlot(value: str) -> Flattenable:
|
||||
toss.append(value)
|
||||
return slot("stuff")
|
||||
|
||||
def fillSlot(tag: Tag) -> Tag:
|
||||
return tag.fillSlots(stuff=toss.pop())
|
||||
|
||||
self.checkAttributeSanitization(insertSlot, fillSlot)
|
||||
|
||||
def test_serializedAttributeWithTransparentTag(self) -> None:
|
||||
"""
|
||||
Attribute values which are supplied via the value of a C{t:transparent}
|
||||
tag have the same substitution rules to them as values supplied
|
||||
directly.
|
||||
"""
|
||||
self.checkAttributeSanitization(tags.transparent, passthru)
|
||||
|
||||
def test_serializedAttributeWithTransparentTagWithRenderer(self) -> None:
|
||||
"""
|
||||
Like L{test_serializedAttributeWithTransparentTag}, but when the
|
||||
attribute is rendered by a renderer on an element.
|
||||
"""
|
||||
|
||||
class WithRenderer(Element):
|
||||
def __init__(self, value: str, loader: Optional[ITemplateLoader]) -> None:
|
||||
self.value = value
|
||||
super().__init__(loader)
|
||||
|
||||
@renderer
|
||||
def stuff(self, request: Optional[IRequest], tag: Tag) -> Flattenable:
|
||||
return self.value
|
||||
|
||||
toss = []
|
||||
|
||||
def insertRenderer(value: str) -> Flattenable:
|
||||
toss.append(value)
|
||||
return tags.transparent(render="stuff")
|
||||
|
||||
def render(tag: Tag) -> Flattenable:
|
||||
return WithRenderer(toss.pop(), TagLoader(tag))
|
||||
|
||||
self.checkAttributeSanitization(insertRenderer, render)
|
||||
|
||||
def test_serializedAttributeWithRenderable(self) -> None:
|
||||
"""
|
||||
Like L{test_serializedAttributeWithTransparentTag}, but when the
|
||||
attribute is a provider of L{IRenderable} rather than a transparent
|
||||
tag.
|
||||
"""
|
||||
|
||||
@implementer(IRenderable)
|
||||
class Arbitrary:
|
||||
def __init__(self, value: Flattenable) -> None:
|
||||
self.value = value
|
||||
|
||||
def render(self, request: Optional[IRequest]) -> Flattenable:
|
||||
return self.value
|
||||
|
||||
def lookupRenderMethod(
|
||||
self, name: str
|
||||
) -> Callable[[Optional[IRequest], Tag], Flattenable]:
|
||||
raise NotImplementedError("Unexpected call")
|
||||
|
||||
self.checkAttributeSanitization(Arbitrary, passthru)
|
||||
|
||||
def checkTagAttributeSerialization(
|
||||
self, wrapTag: Callable[[Tag], Flattenable]
|
||||
) -> None:
|
||||
"""
|
||||
Common implementation of L{test_serializedAttributeWithTag} and
|
||||
L{test_serializedAttributeWithDeferredTag}.
|
||||
|
||||
@param wrapTag: A 1-argument callable that wraps around the attribute's
|
||||
value so other tests can customize it.
|
||||
@type wrapTag: callable taking L{Tag} and returning something
|
||||
flattenable
|
||||
"""
|
||||
innerTag = tags.a('<>&"')
|
||||
outerTag = tags.img(src=wrapTag(innerTag))
|
||||
outer = self.assertFlattensImmediately(
|
||||
outerTag,
|
||||
b'<img src="<a>&lt;&gt;&amp;"</a>" />',
|
||||
)
|
||||
inner = self.assertFlattensImmediately(innerTag, b'<a><>&"</a>')
|
||||
|
||||
# Since the above quoting is somewhat tricky, validate it by making sure
|
||||
# that the main use-case for tag-within-attribute is supported here: if
|
||||
# we serialize a tag, it is quoted *such that it can be parsed out again
|
||||
# as a tag*.
|
||||
self.assertXMLEqual(XML(outer).attrib["src"], inner)
|
||||
|
||||
def test_serializedAttributeWithTag(self) -> None:
|
||||
"""
|
||||
L{Tag} objects which are serialized within the context of an attribute
|
||||
are serialized such that the text content of the attribute may be
|
||||
parsed to retrieve the tag.
|
||||
"""
|
||||
self.checkTagAttributeSerialization(passthru)
|
||||
|
||||
def test_serializedAttributeWithDeferredTag(self) -> None:
|
||||
"""
|
||||
Like L{test_serializedAttributeWithTag}, but when the L{Tag} is in a
|
||||
L{Deferred <twisted.internet.defer.Deferred>}.
|
||||
"""
|
||||
self.checkTagAttributeSerialization(succeed)
|
||||
|
||||
def test_serializedAttributeWithTagWithAttribute(self) -> None:
|
||||
"""
|
||||
Similar to L{test_serializedAttributeWithTag}, but for the additional
|
||||
complexity where the tag which is the attribute value itself has an
|
||||
attribute value which contains bytes which require substitution.
|
||||
"""
|
||||
flattened = self.assertFlattensImmediately(
|
||||
tags.img(src=tags.a(href='<>&"')),
|
||||
b'<img src="<a href='
|
||||
b""&lt;&gt;&amp;&quot;">"
|
||||
b'</a>" />',
|
||||
)
|
||||
|
||||
# As in checkTagAttributeSerialization, belt-and-suspenders:
|
||||
self.assertXMLEqual(
|
||||
XML(flattened).attrib["src"], b'<a href="<>&""></a>'
|
||||
)
|
||||
|
||||
def test_serializeComment(self) -> None:
|
||||
"""
|
||||
Test that comments are correctly flattened and escaped.
|
||||
"""
|
||||
self.assertFlattensImmediately(Comment("foo bar"), b"<!--foo bar-->")
|
||||
|
||||
def test_commentEscaping(self) -> Deferred[List[bytes]]:
|
||||
"""
|
||||
The data in a L{Comment} is escaped and mangled in the flattened output
|
||||
so that the result can be safely included in an HTML document.
|
||||
|
||||
Test that C{>} is escaped when the sequence C{-->} is encountered
|
||||
within a comment, and that comments do not end with C{-}.
|
||||
"""
|
||||
|
||||
def verifyComment(c: bytes) -> None:
|
||||
self.assertTrue(
|
||||
c.startswith(b"<!--"),
|
||||
f"{c!r} does not start with the comment prefix",
|
||||
)
|
||||
self.assertTrue(
|
||||
c.endswith(b"-->"),
|
||||
f"{c!r} does not end with the comment suffix",
|
||||
)
|
||||
# If it is shorter than 7, then the prefix and suffix overlap
|
||||
# illegally.
|
||||
self.assertTrue(len(c) >= 7, f"{c!r} is too short to be a legal comment")
|
||||
content = c[4:-3]
|
||||
if b"foo" in content:
|
||||
self.assertIn(b">", content)
|
||||
else:
|
||||
self.assertNotIn(b">", content)
|
||||
if content:
|
||||
self.assertNotEqual(content[-1], b"-")
|
||||
|
||||
results = []
|
||||
for c in [
|
||||
"",
|
||||
"foo > bar",
|
||||
"abracadabra-",
|
||||
"not-->magic",
|
||||
]:
|
||||
d = flattenString(None, Comment(c))
|
||||
d.addCallback(verifyComment)
|
||||
results.append(d)
|
||||
return gatherResults(results)
|
||||
|
||||
def test_serializeCDATA(self) -> None:
|
||||
"""
|
||||
Test that CDATA is correctly flattened and escaped.
|
||||
"""
|
||||
self.assertFlattensImmediately(CDATA("foo bar"), b"<![CDATA[foo bar]]>"),
|
||||
self.assertFlattensImmediately(
|
||||
CDATA("foo ]]> bar"), b"<![CDATA[foo ]]]]><![CDATA[> bar]]>"
|
||||
)
|
||||
|
||||
def test_serializeUnicode(self) -> None:
|
||||
"""
|
||||
Test that unicode is encoded correctly in the appropriate places, and
|
||||
raises an error when it occurs in inappropriate place.
|
||||
"""
|
||||
snowman = "\N{SNOWMAN}"
|
||||
self.assertFlattensImmediately(snowman, b"\xe2\x98\x83")
|
||||
self.assertFlattensImmediately(tags.p(snowman), b"<p>\xe2\x98\x83</p>")
|
||||
self.assertFlattensImmediately(Comment(snowman), b"<!--\xe2\x98\x83-->")
|
||||
self.assertFlattensImmediately(CDATA(snowman), b"<![CDATA[\xe2\x98\x83]]>")
|
||||
self.assertFlatteningRaises(Tag(snowman), UnicodeEncodeError)
|
||||
self.assertFlatteningRaises(
|
||||
Tag("p", attributes={snowman: ""}), UnicodeEncodeError
|
||||
)
|
||||
|
||||
def test_serializeCharRef(self) -> None:
|
||||
"""
|
||||
A character reference is flattened to a string using the I{&#NNNN;}
|
||||
syntax.
|
||||
"""
|
||||
ref = CharRef(ord("\N{SNOWMAN}"))
|
||||
self.assertFlattensImmediately(ref, b"☃")
|
||||
|
||||
def test_serializeDeferred(self) -> None:
|
||||
"""
|
||||
Test that a deferred is substituted with the current value in the
|
||||
callback chain when flattened.
|
||||
"""
|
||||
self.assertFlattensImmediately(succeed("two"), b"two")
|
||||
|
||||
def test_serializeSameDeferredTwice(self) -> None:
|
||||
"""
|
||||
Test that the same deferred can be flattened twice.
|
||||
"""
|
||||
d = succeed("three")
|
||||
self.assertFlattensImmediately(d, b"three")
|
||||
self.assertFlattensImmediately(d, b"three")
|
||||
|
||||
def test_serializeCoroutine(self) -> None:
|
||||
"""
|
||||
Test that a coroutine returning a value is substituted with the that
|
||||
value when flattened.
|
||||
"""
|
||||
from textwrap import dedent
|
||||
|
||||
namespace: Dict[str, FunctionType] = {}
|
||||
exec(
|
||||
dedent(
|
||||
"""
|
||||
async def coro(x):
|
||||
return x
|
||||
"""
|
||||
),
|
||||
namespace,
|
||||
)
|
||||
coro = namespace["coro"]
|
||||
|
||||
self.assertFlattensImmediately(coro("four"), b"four")
|
||||
|
||||
def test_serializeCoroutineWithAwait(self) -> None:
|
||||
"""
|
||||
Test that a coroutine returning an awaited deferred value is
|
||||
substituted with that value when flattened.
|
||||
"""
|
||||
from textwrap import dedent
|
||||
|
||||
namespace = dict(succeed=succeed)
|
||||
exec(
|
||||
dedent(
|
||||
"""
|
||||
async def coro(x):
|
||||
return await succeed(x)
|
||||
"""
|
||||
),
|
||||
namespace,
|
||||
)
|
||||
coro = namespace["coro"]
|
||||
|
||||
self.assertFlattensImmediately(coro("four"), b"four")
|
||||
|
||||
def test_serializeIRenderable(self) -> None:
|
||||
"""
|
||||
Test that flattening respects all of the IRenderable interface.
|
||||
"""
|
||||
|
||||
@implementer(IRenderable)
|
||||
class FakeElement:
|
||||
def render(ign, ored: object) -> Tag:
|
||||
return tags.p(
|
||||
"hello, ",
|
||||
tags.transparent(render="test"),
|
||||
" - ",
|
||||
tags.transparent(render="test"),
|
||||
)
|
||||
|
||||
def lookupRenderMethod(
|
||||
ign, name: str
|
||||
) -> Callable[[Optional[IRequest], Tag], Flattenable]:
|
||||
self.assertEqual(name, "test")
|
||||
return lambda ign, node: node("world")
|
||||
|
||||
self.assertFlattensImmediately(FakeElement(), b"<p>hello, world - world</p>")
|
||||
|
||||
def test_serializeMissingRenderFactory(self) -> None:
|
||||
"""
|
||||
Test that flattening a tag with a C{render} attribute when no render
|
||||
factory is available in the context raises an exception.
|
||||
"""
|
||||
|
||||
self.assertFlatteningRaises(tags.transparent(render="test"), ValueError)
|
||||
|
||||
def test_serializeSlots(self) -> None:
|
||||
"""
|
||||
Test that flattening a slot will use the slot value from the tag.
|
||||
"""
|
||||
t1 = tags.p(slot("test"))
|
||||
t2 = t1.clone()
|
||||
t2.fillSlots(test="hello, world")
|
||||
self.assertFlatteningRaises(t1, UnfilledSlot)
|
||||
self.assertFlattensImmediately(t2, b"<p>hello, world</p>")
|
||||
|
||||
def test_serializeDeferredSlots(self) -> None:
|
||||
"""
|
||||
Test that a slot with a deferred as its value will be flattened using
|
||||
the value from the deferred.
|
||||
"""
|
||||
t = tags.p(slot("test"))
|
||||
t.fillSlots(test=succeed(tags.em("four>")))
|
||||
self.assertFlattensImmediately(t, b"<p><em>four></em></p>")
|
||||
|
||||
def test_unknownTypeRaises(self) -> None:
|
||||
"""
|
||||
Test that flattening an unknown type of thing raises an exception.
|
||||
"""
|
||||
self.assertFlatteningRaises(None, UnsupportedType) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class FlattenChunkingTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for the way pieces of the result are chunked together in calls to
|
||||
the write function.
|
||||
"""
|
||||
|
||||
def test_oneSmallChunk(self) -> None:
|
||||
"""
|
||||
If the entire value to be flattened is available synchronously and fits
|
||||
into the buffer it is all passed to a single call to the write
|
||||
function.
|
||||
"""
|
||||
output: List[bytes] = []
|
||||
self.successResultOf(flatten(None, ["1", "2", "3"], output.append))
|
||||
assert_that(output, equal_to([b"123"]))
|
||||
|
||||
def test_someLargeChunks(self) -> None:
|
||||
"""
|
||||
If the entire value to be flattened is available synchronously but does
|
||||
not fit into the buffer then it is chunked into buffer-sized pieces
|
||||
and these are passed to the write function.
|
||||
"""
|
||||
some = ["x"] * BUFFER_SIZE
|
||||
someMore = ["y"] * BUFFER_SIZE
|
||||
evenMore = ["z"] * BUFFER_SIZE
|
||||
|
||||
output: List[bytes] = []
|
||||
self.successResultOf(flatten(None, [some, someMore, evenMore], output.append))
|
||||
assert_that(
|
||||
output,
|
||||
equal_to([b"x" * BUFFER_SIZE, b"y" * BUFFER_SIZE, b"z" * BUFFER_SIZE]),
|
||||
)
|
||||
|
||||
def _chunksSeparatedByAsyncTest(
|
||||
self,
|
||||
start: Callable[
|
||||
[Flattenable], Tuple[Deferred[Flattenable], Callable[[], object]]
|
||||
],
|
||||
) -> None:
|
||||
"""
|
||||
Assert that flattening with a L{Deferred} returned by C{start} results
|
||||
in the expected buffering behavior.
|
||||
|
||||
The L{Deferred} need not have a result by it is returned by C{start}
|
||||
but must have a result after the callable returned along with it is
|
||||
called.
|
||||
|
||||
The expected buffering behavior is that flattened values up to the
|
||||
L{Deferred} are written together and then the result of the
|
||||
L{Deferred} is written together with values following it up to the
|
||||
next L{Deferred}.
|
||||
"""
|
||||
first_wait, first_finish = start("first-")
|
||||
second_wait, second_finish = start("second-")
|
||||
value = [
|
||||
"already-available",
|
||||
"-chunks",
|
||||
first_wait,
|
||||
"chunks-already-",
|
||||
"computed",
|
||||
second_wait,
|
||||
"more-chunks-",
|
||||
"already-available",
|
||||
]
|
||||
output: List[bytes] = []
|
||||
d = flatten(None, value, output.append)
|
||||
first_finish()
|
||||
second_finish()
|
||||
self.successResultOf(d)
|
||||
assert_that(
|
||||
output,
|
||||
equal_to(
|
||||
[
|
||||
b"already-available-chunks",
|
||||
b"first-chunks-already-computed",
|
||||
b"second-more-chunks-already-available",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
def test_chunksSeparatedByFiredDeferred(self) -> None:
|
||||
"""
|
||||
When a fired L{Deferred} is encountered any buffered data is
|
||||
passed to the write function. Then the L{Deferred}'s result is passed
|
||||
to another write along with following synchronous values.
|
||||
|
||||
This exact buffering behavior should be considered an implementation
|
||||
detail and can be replaced by some other better behavior in the future
|
||||
if someone wants.
|
||||
"""
|
||||
|
||||
def sync_start(
|
||||
v: Flattenable,
|
||||
) -> Tuple[Deferred[Flattenable], Callable[[], None]]:
|
||||
return (succeed(v), lambda: None)
|
||||
|
||||
self._chunksSeparatedByAsyncTest(sync_start)
|
||||
|
||||
def test_chunksSeparatedByUnfiredDeferred(self) -> None:
|
||||
"""
|
||||
When an unfired L{Deferred} is encountered any buffered data is
|
||||
passed to the write function. After the result of the L{Deferred} is
|
||||
available it is passed to another write along with following
|
||||
synchronous values.
|
||||
"""
|
||||
|
||||
def async_start(
|
||||
v: Flattenable,
|
||||
) -> Tuple[Deferred[Flattenable], Callable[[], None]]:
|
||||
d: Deferred[Flattenable] = Deferred()
|
||||
return (d, lambda: d.callback(v))
|
||||
|
||||
self._chunksSeparatedByAsyncTest(async_start)
|
||||
|
||||
|
||||
# Use the co_filename mechanism (instead of the __file__ mechanism) because
|
||||
# it is the mechanism traceback formatting uses. The two do not necessarily
|
||||
# agree with each other. This requires a code object compiled in this file.
|
||||
# The easiest way to get a code object is with a new function. I'll use a
|
||||
# lambda to avoid adding anything else to this namespace. The result will
|
||||
# be a string which agrees with the one the traceback module will put into a
|
||||
# traceback for frames associated with functions defined in this file.
|
||||
|
||||
HERE = (lambda: None).__code__.co_filename
|
||||
|
||||
|
||||
class FlattenerErrorTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{FlattenerError}.
|
||||
"""
|
||||
|
||||
def test_renderable(self) -> None:
|
||||
"""
|
||||
If a L{FlattenerError} is created with an L{IRenderable} provider root,
|
||||
the repr of that object is included in the string representation of the
|
||||
exception.
|
||||
"""
|
||||
|
||||
@implementer(IRenderable)
|
||||
class Renderable: # type: ignore[misc]
|
||||
def __repr__(self) -> str:
|
||||
return "renderable repr"
|
||||
|
||||
self.assertEqual(
|
||||
str(FlattenerError(RuntimeError("reason"), [Renderable()], [])),
|
||||
"Exception while flattening:\n"
|
||||
" renderable repr\n"
|
||||
"RuntimeError: reason\n",
|
||||
)
|
||||
|
||||
def test_tag(self) -> None:
|
||||
"""
|
||||
If a L{FlattenerError} is created with a L{Tag} instance with source
|
||||
location information, the source location is included in the string
|
||||
representation of the exception.
|
||||
"""
|
||||
tag = Tag("div", filename="/foo/filename.xhtml", lineNumber=17, columnNumber=12)
|
||||
|
||||
self.assertEqual(
|
||||
str(FlattenerError(RuntimeError("reason"), [tag], [])),
|
||||
"Exception while flattening:\n"
|
||||
' File "/foo/filename.xhtml", line 17, column 12, in "div"\n'
|
||||
"RuntimeError: reason\n",
|
||||
)
|
||||
|
||||
def test_tagWithoutLocation(self) -> None:
|
||||
"""
|
||||
If a L{FlattenerError} is created with a L{Tag} instance without source
|
||||
location information, only the tagName is included in the string
|
||||
representation of the exception.
|
||||
"""
|
||||
self.assertEqual(
|
||||
str(FlattenerError(RuntimeError("reason"), [Tag("span")], [])),
|
||||
"Exception while flattening:\n" " Tag <span>\n" "RuntimeError: reason\n",
|
||||
)
|
||||
|
||||
def test_traceback(self) -> None:
|
||||
"""
|
||||
If a L{FlattenerError} is created with traceback frames, they are
|
||||
included in the string representation of the exception.
|
||||
"""
|
||||
|
||||
# Try to be realistic in creating the data passed in for the traceback
|
||||
# frames.
|
||||
def f() -> None:
|
||||
g()
|
||||
|
||||
def g() -> NoReturn:
|
||||
raise RuntimeError("reason")
|
||||
|
||||
try:
|
||||
f()
|
||||
except RuntimeError as e:
|
||||
# Get the traceback, minus the info for *this* frame
|
||||
tbinfo = traceback.extract_tb(sys.exc_info()[2])[1:]
|
||||
exc = e
|
||||
else:
|
||||
self.fail("f() must raise RuntimeError")
|
||||
|
||||
if IS_PYTHON_313:
|
||||
column_marker = " ~^^\n"
|
||||
else:
|
||||
column_marker = ""
|
||||
|
||||
self.assertEqual(
|
||||
str(FlattenerError(exc, [], tbinfo)),
|
||||
"Exception while flattening:\n"
|
||||
' File "%s", line %d, in f\n'
|
||||
" g()\n"
|
||||
"%s"
|
||||
' File "%s", line %d, in g\n'
|
||||
' raise RuntimeError("reason")\n'
|
||||
"RuntimeError: reason\n"
|
||||
% (
|
||||
HERE,
|
||||
f.__code__.co_firstlineno + 1,
|
||||
column_marker,
|
||||
HERE,
|
||||
g.__code__.co_firstlineno + 1,
|
||||
),
|
||||
)
|
||||
|
||||
def test_asynchronousFlattenError(self) -> None:
|
||||
"""
|
||||
When flattening a renderer which raises an exception asynchronously,
|
||||
the error is reported when it occurs.
|
||||
"""
|
||||
failing: Deferred[object] = Deferred()
|
||||
|
||||
@implementer(IRenderable)
|
||||
class NotActuallyRenderable:
|
||||
"No methods provided; this will fail"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<unrenderable>"
|
||||
|
||||
def lookupRenderMethod( # type: ignore[empty-body]
|
||||
self, name: str
|
||||
) -> Callable[[Optional[IRequest], Tag], Flattenable]:
|
||||
...
|
||||
|
||||
def render(self, request: Optional[IRequest]) -> Flattenable:
|
||||
return failing
|
||||
|
||||
flattening = flattenString(None, [NotActuallyRenderable()])
|
||||
self.assertNoResult(flattening)
|
||||
exc = RuntimeError("example")
|
||||
failing.errback(exc)
|
||||
failure = self.failureResultOf(flattening, FlattenerError)
|
||||
if IS_PYTHON_313:
|
||||
column_marker = ".*\n.*\n.*\nRuntimeError: example\n"
|
||||
else:
|
||||
column_marker = ""
|
||||
self.assertRegex(
|
||||
str(failure.value),
|
||||
re.compile(
|
||||
dedent(
|
||||
"""\
|
||||
Exception while flattening:
|
||||
\\[<unrenderable>\\]
|
||||
<unrenderable>
|
||||
<Deferred at .* current result: <twisted.python.failure.Failure builtins.RuntimeError: example>>
|
||||
File ".*", line \\d*, in _flattenTree
|
||||
element = await element.*
|
||||
"""
|
||||
)
|
||||
+ column_marker,
|
||||
flags=re.MULTILINE,
|
||||
),
|
||||
)
|
||||
self.assertIn("RuntimeError: example", str(failure.value))
|
||||
# The original exception is unmodified and will be logged separately if
|
||||
# unhandled.
|
||||
self.failureResultOf(failing, RuntimeError)
|
||||
|
||||
def test_cancel(self) -> None:
|
||||
"""
|
||||
The flattening of a Deferred can be cancelled.
|
||||
"""
|
||||
cancelCount = 0
|
||||
cancelArg = None
|
||||
|
||||
def checkCancel(cancelled: Deferred[object]) -> None:
|
||||
nonlocal cancelArg, cancelCount
|
||||
cancelArg = cancelled
|
||||
cancelCount += 1
|
||||
|
||||
err = None
|
||||
|
||||
def saveErr(failure: Failure) -> None:
|
||||
nonlocal err
|
||||
err = failure
|
||||
|
||||
d: Deferred[object] = Deferred(checkCancel)
|
||||
flattening = flattenString(None, d)
|
||||
self.assertNoResult(flattening)
|
||||
d.addErrback(saveErr)
|
||||
|
||||
flattening.cancel()
|
||||
|
||||
# Check whether we got an orderly cancellation.
|
||||
# Do this first to get more meaningful reporting if something crashed.
|
||||
failure = self.failureResultOf(flattening, FlattenerError)
|
||||
|
||||
self.assertEqual(cancelCount, 1)
|
||||
self.assertIs(cancelArg, d)
|
||||
|
||||
self.assertIsInstance(err, Failure)
|
||||
self.assertIsInstance(cast(Failure, err).value, CancelledError)
|
||||
|
||||
exc = failure.value.args[0]
|
||||
self.assertIsInstance(exc, CancelledError)
|
||||
Reference in New Issue
Block a user