1# Copyright (c) 2004-2007 Divmod.
2# See LICENSE for details.
3
4"""
5Tests for L{nevow.json}.
6"""
7
8from zope.interface import implements
9
10from nevow.inevow import IAthenaTransportable
11from nevow import json, rend, page, loaders, tags, athena, testutil
12
13from twisted.trial import unittest
14
15TEST_OBJECTS = [
16    0,
17    None,
18    True,
19    False,
20    [],
21    [0],
22    [0, 1, 2],
23    [None, 1, 2],
24    [None, u'one', 2],
25    [True, False, u'string', 10],
26    [[1, 2], [3, 4]],
27    [[1.5, 2.5], [3.5, 4.5]],
28    [0, [1, 2], [u'hello'], [u'world'], [True, None, False]],
29    {},
30    {u'foo': u'bar'},
31    {u'foo': None},
32    {u'bar': True},
33    {u'baz': [1, 2, 3]},
34    {u'quux': {u'bar': u'foo'}},
35    ]
36
37TEST_STRINGLIKE_OBJECTS = [
38    u'',
39    u'string',
40    u'string with "embedded" quotes',
41    u"string with 'embedded' single-quotes",
42    u'string with \\"escaped embedded\\" quotes',
43    u"string with \\'escaped embedded\\' single-quotes",
44    u"string with backslashes\\\\",
45    u"string with trailing accented vowels: \xe1\xe9\xed\xf3\xfa\xfd\xff",
46    u"string with trailing control characters: \f\b\n\t\r",
47    u'string with high codepoint characters: \u0111\u2222\u3333\u4444\uffff',
48    u'string with very high codepoint characters: \U00011111\U00022222\U00033333\U00044444\U000fffff',
49    ]
50
51
52class DummyLivePage(object):
53    """
54    Stand-in for L{athena.LivePage} which implements only enough of its
55    behavior so that a L{LiveFragment} or L{LiveElement} can be set as its
56    child and flattened.
57    """
58    localCounter = 0
59
60    def __init__(self):
61        self.page = self
62        self.liveFragmentChildren = []
63        self._jsDepsMemo = {}
64        self._cssDepsMemo = {}
65        self._didConnect = True
66
67
68    def addLocalObject(self, obj):
69        """
70        Return an Athena ID for the given object.  Returns a new value on every
71        call.
72        """
73        self.localCounter += 1
74        return self.localCounter
75
76
77    def _shouldInclude(self, module):
78        """
79        Stub module-system method.  Always declares that the given module
80        should not be included.
81        """
82        return False
83
84
85
86class JavascriptObjectNotationTestCase(unittest.TestCase):
87
88    def testSerialize(self):
89        for struct in TEST_OBJECTS:
90            json.serialize(struct)
91
92    def testRoundtrip(self):
93        for struct in TEST_OBJECTS:
94            bytes = json.serialize(struct)
95            unstruct = json.parse(bytes)
96            self.assertEquals(
97                unstruct, struct,
98                "Failed to roundtrip %r: %r (through %r)" % (
99                    struct, unstruct, bytes))
100
101
102    def test_undefined(self):
103        """
104        C{undefined} is parsed as Python C{None}.
105        """
106        self.assertEquals(None, json.parse(b'undefined'))
107
108
109    def testStringlikeRountrip(self):
110        for struct in TEST_STRINGLIKE_OBJECTS:
111            bytes = json.serialize(struct)
112            unstruct = json.parse(bytes)
113            failMsg = "Failed to roundtrip %r: %r (through %r)" % (
114                    struct, unstruct, bytes)
115            self.assertEquals(unstruct, struct, failMsg)
116            self.assert_(isinstance(unstruct, unicode), failMsg)
117
118
119    def test_lineTerminators(self):
120        """
121        When passed a unicode string containing a line terminator,
122        L{json.serialize} emits an escape sequence representing that character
123        (not a UTF-8 sequence directly representing that the line terminator
124        code point).
125
126        Literal line terminators are allowed in JSON, but some parsers do not
127        handle them properly.
128        """
129        # These are the four line terminators currently in Unicode.
130        self.assertEqual('"\\r"', json.serialize(u"\r"))
131        self.assertEqual('"\\n"', json.serialize(u"\n"))
132        self.assertEqual('"\\u2028"', json.serialize(u"\u2028"))
133        self.assertEqual('"\\u2029"', json.serialize(u"\u2029"))
134
135
136    def testScientificNotation(self):
137        self.assertEquals(json.parse('1e10'), 10**10)
138        self.assertEquals(json.parse('1e0'), 1)
139
140
141    def testHexEscapedCodepoints(self):
142        self.assertEquals(
143            json.parse('"\\xe1\\xe9\\xed\\xf3\\xfa\\xfd"'),
144            u"\xe1\xe9\xed\xf3\xfa\xfd")
145
146    def testEscapedControls(self):
147        self.assertEquals(
148            json.parse('"\\f\\b\\n\\t\\r"'),
149            u"\f\b\n\t\r")
150
151
152    def _rendererTest(self, cls):
153        self.assertEquals(
154            json.serialize(
155                cls(
156                    docFactory=loaders.stan(tags.p['Hello, world.']))),
157            '"<div xmlns=\\"http://www.w3.org/1999/xhtml\\"><p>Hello, world.</p></div>"')
158
159
160    def test_fragmentSerialization(self):
161        """
162        Test that instances of L{nevow.rend.Fragment} serialize as an xhtml
163        string.
164        """
165        return self._rendererTest(rend.Fragment)
166
167
168    def test_elementSerialization(self):
169        """
170        Test that instances of L{nevow.page.Element} serialize as an xhtml
171        string.
172        """
173        return self._rendererTest(page.Element)
174
175
176    def _doubleSerialization(self, cls):
177        fragment = cls(docFactory=loaders.stan(tags.div['Hello']))
178        self.assertEqual(
179            json.serialize(fragment),
180            json.serialize(fragment))
181
182
183    def test_doubleFragmentSerialization(self):
184        """
185        Test that repeatedly calling L{json.serialize} with an instance of
186        L{rend.Fragment} results in the same result each time.
187        """
188        return self._doubleSerialization(rend.Fragment)
189
190
191    def test_doubleElementSerialization(self):
192        """
193        Like L{test_doubleElementSerialization} but for L{page.Element}
194        instances.
195        """
196        return self._doubleSerialization(page.Element)
197
198
199    def _doubleLiveSerialization(self, cls, renderer):
200        livePage = DummyLivePage()
201        liveFragment = cls(
202            docFactory=loaders.stan(
203                [tags.div(render=tags.directive(renderer))['Hello'],
204                 tags.div(render=tags.directive('foo'))]))
205        liveFragment.setFragmentParent(livePage)
206        self.assertEqual(
207            json.serialize(liveFragment),
208            json.serialize(liveFragment))
209
210
211    def test_doubleLiveFragmentSerialization(self):
212        """
213        Like L{test_doubleFragmentSerialization} but for L{athena.LiveFragment}
214        instances.
215        """
216        class AnyLiveFragment(athena.LiveFragment):
217            """
218            Just some L{LiveFragment} subclass, such as an application might
219            define.
220            """
221            def render_foo(self, ctx, data):
222                return ctx.tag
223        self._doubleLiveSerialization(AnyLiveFragment, 'liveFragment')
224
225
226    def test_doubleLiveElementSerialization(self):
227        """
228        Like L{test_doubleFragmentSerialization} but for L{athena.LiveElement}
229        instances.
230        """
231        requests = []
232        class AnyLiveElement(athena.LiveElement):
233            """
234            Just some L{LiveElement} subclass, such as an application might
235            define.
236            """
237            def foo(self, request, tag):
238                requests.append(request)
239                return tag
240            page.renderer(foo)
241        self._doubleLiveSerialization(AnyLiveElement, 'liveElement')
242        self.assertTrue(isinstance(requests[0], testutil.FakeRequest))
243
244
245    def test_unsupportedSerialization(self):
246        """
247        L{json.serialize} should raise a L{TypeError} if it is passed an object
248        which it does not know how to serialize.
249        """
250        class Unsupported(object):
251            def __repr__(self):
252                return 'an unsupported object'
253        exception = self.assertRaises(TypeError, json.serialize, Unsupported())
254        self.assertEqual(
255            str(exception),
256            "Unsupported type <class 'nevow.test.test_json.Unsupported'>: "
257            "an unsupported object")
258
259
260    def test_customSerialization(self):
261        """
262        L{json.serialize} should emit JavaScript calls to the JavaScript object
263        named by L{IAthenaTransportable.jsClass} with the arguments returned by
264        L{IAthenaTransportable.getInitialArguments} when passed an object which
265        can be adapted to L{IAthenaTransportable}.
266        """
267        class Transportable(object):
268            """
269            Completely parameterized L{IAthenaTransportable} implementation so
270            different data can be easily tested.
271            """
272            implements(IAthenaTransportable)
273
274            def __init__(self, jsClass, initialArgs):
275                self.jsClass = jsClass
276                self.getInitialArguments = lambda: initialArgs
277
278        self.assertEqual(
279            json.serialize(Transportable(u"Foo", ())),
280            "(new Foo())")
281        self.assertEqual(
282            json.serialize(Transportable(u"Bar", (None,))),
283            "(new Bar(null))")
284        self.assertEqual(
285            json.serialize(Transportable(u"Baz.Quux", (1, 2))),
286            "(new Baz.Quux(1,2))")
287
288        # The style of the quotes in this assertion is basically irrelevant.
289        # If, for some reason, the serializer changes to use ' instead of ",
290        # there's no reason not to change this test to reflect that. -exarkun
291        self.assertEqual(
292            json.serialize(Transportable(u"Quux", (u"Foo",))),
293            '(new Quux("Foo"))')
294