1# Copyright (c) Twisted Matrix Laboratories.
2# See LICENSE for details.
3
4"""
5Test cases for L{jelly} object serialization.
6"""
7
8
9import datetime
10import decimal
11
12from twisted.spread import banana, jelly, pb
13from twisted.test.proto_helpers import StringTransport
14from twisted.trial import unittest
15from twisted.trial.unittest import TestCase
16
17
18class TestNode(jelly.Jellyable):
19    """
20    An object to test jellyfying of new style class instances.
21    """
22
23    classAttr = 4
24
25    def __init__(self, parent=None):
26        if parent:
27            self.id = parent.id + 1
28            parent.children.append(self)
29        else:
30            self.id = 1
31        self.parent = parent
32        self.children = []
33
34
35class A:
36    """
37    Dummy class.
38    """
39
40    def amethod(self):
41        """
42        Method to be used in serialization tests.
43        """
44
45
46def afunc(self):
47    """
48    A dummy function to test function serialization.
49    """
50
51
52class B:
53    """
54    Dummy class.
55    """
56
57    def bmethod(self):
58        """
59        Method to be used in serialization tests.
60        """
61
62
63class C:
64    """
65    Dummy class.
66    """
67
68    def cmethod(self):
69        """
70        Method to be used in serialization tests.
71        """
72
73
74class D:
75    """
76    Dummy new-style class.
77    """
78
79
80class E:
81    """
82    Dummy new-style class with slots.
83    """
84
85    __slots__ = ("x", "y")
86
87    def __init__(self, x=None, y=None):
88        self.x = x
89        self.y = y
90
91    def __getstate__(self):
92        return {"x": self.x, "y": self.y}
93
94    def __setstate__(self, state):
95        self.x = state["x"]
96        self.y = state["y"]
97
98
99class SimpleJellyTest:
100    def __init__(self, x, y):
101        self.x = x
102        self.y = y
103
104    def isTheSameAs(self, other):
105        return self.__dict__ == other.__dict__
106
107
108def jellyRoundTrip(testCase, toSerialize):
109    """
110    Verify that the given object round-trips through jelly & banana and comes
111    out equivalent to the input.
112    """
113    jellied = jelly.jelly(toSerialize)
114    encoded = banana.encode(jellied)
115    decoded = banana.decode(encoded)
116    unjellied = jelly.unjelly(decoded)
117    testCase.assertEqual(toSerialize, unjellied)
118
119
120class JellyTests(TestCase):
121    """
122    Testcases for L{jelly} module serialization.
123
124    @cvar decimalData: serialized version of decimal data, to be used in tests.
125    @type decimalData: L{list}
126    """
127
128    decimalData = [
129        b"list",
130        [b"decimal", 995, -2],
131        [b"decimal", 0, 0],
132        [b"decimal", 123456, 0],
133        [b"decimal", -78901, -3],
134    ]
135
136    def _testSecurity(self, inputList, atom):
137        """
138        Helper test method to test security options for a type.
139
140        @param inputList: a sample input for the type.
141        @type inputList: L{list}
142
143        @param atom: atom identifier for the type.
144        @type atom: L{str}
145        """
146        c = jelly.jelly(inputList)
147        taster = jelly.SecurityOptions()
148        taster.allowBasicTypes()
149        # By default, it should succeed
150        jelly.unjelly(c, taster)
151        taster.allowedTypes.pop(atom)
152        # But it should raise an exception when disallowed
153        self.assertRaises(jelly.InsecureJelly, jelly.unjelly, c, taster)
154
155    def test_methodsNotSelfIdentity(self):
156        """
157        If a class change after an instance has been created, L{jelly.unjelly}
158        shoud raise a C{TypeError} when trying to unjelly the instance.
159        """
160        a = A()
161        b = B()
162        c = C()
163        a.bmethod = c.cmethod
164        b.a = a
165        savecmethod = C.cmethod
166        del C.cmethod
167        try:
168            self.assertRaises(TypeError, jelly.unjelly, jelly.jelly(b))
169        finally:
170            C.cmethod = savecmethod
171
172    def test_newStyle(self):
173        """
174        Test that a new style class can be jellied and unjellied with its
175        objects and attribute values preserved.
176        """
177        n = D()
178        n.x = 1
179        n2 = D()
180        n.n2 = n2
181        n.n3 = n2
182        c = jelly.jelly(n)
183        m = jelly.unjelly(c)
184        self.assertIsInstance(m, D)
185        self.assertIs(m.n2, m.n3)
186        self.assertEqual(m.x, 1)
187
188    def test_newStyleWithSlots(self):
189        """
190        A class defined with I{slots} can be jellied and unjellied with the
191        values for its attributes preserved.
192        """
193        n = E()
194        n.x = 1
195        c = jelly.jelly(n)
196        m = jelly.unjelly(c)
197        self.assertIsInstance(m, E)
198        self.assertEqual(n.x, 1)
199
200    def test_typeNewStyle(self):
201        """
202        Test that a new style class type can be jellied and unjellied
203        to the original type.
204        """
205        t = [D]
206        r = jelly.unjelly(jelly.jelly(t))
207        self.assertEqual(t, r)
208
209    def test_typeBuiltin(self):
210        """
211        Test that a builtin type can be jellied and unjellied to the original
212        type.
213        """
214        t = [str]
215        r = jelly.unjelly(jelly.jelly(t))
216        self.assertEqual(t, r)
217
218    def test_dateTime(self):
219        """
220        Jellying L{datetime.timedelta} instances and then unjellying the result
221        should produce objects which represent the values of the original
222        inputs.
223        """
224        dtn = datetime.datetime.now()
225        dtd = datetime.datetime.now() - dtn
226        inputList = [dtn, dtd]
227        c = jelly.jelly(inputList)
228        output = jelly.unjelly(c)
229        self.assertEqual(inputList, output)
230        self.assertIsNot(inputList, output)
231
232    def test_bananaTimeTypes(self):
233        """
234        Jellying L{datetime.time}, L{datetime.timedelta}, L{datetime.datetime},
235        and L{datetime.date} objects should result in jellied objects which can
236        be serialized and unserialized with banana.
237        """
238        sampleDate = datetime.date(2020, 7, 11)
239        sampleTime = datetime.time(1, 16, 5, 344)
240        sampleDateTime = datetime.datetime.combine(sampleDate, sampleTime)
241        sampleTimeDelta = sampleDateTime - datetime.datetime(2020, 7, 3)
242        jellyRoundTrip(self, sampleDate)
243        jellyRoundTrip(self, sampleTime)
244        jellyRoundTrip(self, sampleDateTime)
245        jellyRoundTrip(self, sampleTimeDelta)
246
247    def test_decimal(self):
248        """
249        Jellying L{decimal.Decimal} instances and then unjellying the result
250        should produce objects which represent the values of the original
251        inputs.
252        """
253        inputList = [
254            decimal.Decimal("9.95"),
255            decimal.Decimal(0),
256            decimal.Decimal(123456),
257            decimal.Decimal("-78.901"),
258        ]
259        c = jelly.jelly(inputList)
260        output = jelly.unjelly(c)
261        self.assertEqual(inputList, output)
262        self.assertIsNot(inputList, output)
263
264    def test_decimalUnjelly(self):
265        """
266        Unjellying the s-expressions produced by jelly for L{decimal.Decimal}
267        instances should result in L{decimal.Decimal} instances with the values
268        represented by the s-expressions.
269
270        This test also verifies that L{decimalData} contains valid jellied
271        data.  This is important since L{test_decimalMissing} re-uses
272        L{decimalData} and is expected to be unable to produce
273        L{decimal.Decimal} instances even though the s-expression correctly
274        represents a list of them.
275        """
276        expected = [
277            decimal.Decimal("9.95"),
278            decimal.Decimal(0),
279            decimal.Decimal(123456),
280            decimal.Decimal("-78.901"),
281        ]
282        output = jelly.unjelly(self.decimalData)
283        self.assertEqual(output, expected)
284
285    def test_decimalSecurity(self):
286        """
287        By default, C{decimal} objects should be allowed by
288        L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
289        L{jelly.InsecureJelly} when trying to unjelly it.
290        """
291        inputList = [decimal.Decimal("9.95")]
292        self._testSecurity(inputList, b"decimal")
293
294    def test_set(self):
295        """
296        Jellying C{set} instances and then unjellying the result
297        should produce objects which represent the values of the original
298        inputs.
299        """
300        inputList = [{1, 2, 3}]
301        output = jelly.unjelly(jelly.jelly(inputList))
302        self.assertEqual(inputList, output)
303        self.assertIsNot(inputList, output)
304
305    def test_frozenset(self):
306        """
307        Jellying L{frozenset} instances and then unjellying the result
308        should produce objects which represent the values of the original
309        inputs.
310        """
311        inputList = [frozenset([1, 2, 3])]
312        output = jelly.unjelly(jelly.jelly(inputList))
313        self.assertEqual(inputList, output)
314        self.assertIsNot(inputList, output)
315
316    def test_setSecurity(self):
317        """
318        By default, C{set} objects should be allowed by
319        L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
320        L{jelly.InsecureJelly} when trying to unjelly it.
321        """
322        inputList = [{1, 2, 3}]
323        self._testSecurity(inputList, b"set")
324
325    def test_frozensetSecurity(self):
326        """
327        By default, L{frozenset} objects should be allowed by
328        L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
329        L{jelly.InsecureJelly} when trying to unjelly it.
330        """
331        inputList = [frozenset([1, 2, 3])]
332        self._testSecurity(inputList, b"frozenset")
333
334    def test_simple(self):
335        """
336        Simplest test case.
337        """
338        self.assertTrue(
339            SimpleJellyTest("a", "b").isTheSameAs(SimpleJellyTest("a", "b"))
340        )
341        a = SimpleJellyTest(1, 2)
342        cereal = jelly.jelly(a)
343        b = jelly.unjelly(cereal)
344        self.assertTrue(a.isTheSameAs(b))
345
346    def test_identity(self):
347        """
348        Test to make sure that objects retain identity properly.
349        """
350        x = []
351        y = x
352        x.append(y)
353        x.append(y)
354        self.assertIs(x[0], x[1])
355        self.assertIs(x[0][0], x)
356        s = jelly.jelly(x)
357        z = jelly.unjelly(s)
358        self.assertIs(z[0], z[1])
359        self.assertIs(z[0][0], z)
360
361    def test_str(self):
362        x = "blah"
363        y = jelly.unjelly(jelly.jelly(x))
364        self.assertEqual(x, y)
365        self.assertEqual(type(x), type(y))
366
367    def test_stressReferences(self):
368        reref = []
369        toplevelTuple = ({"list": reref}, reref)
370        reref.append(toplevelTuple)
371        s = jelly.jelly(toplevelTuple)
372        z = jelly.unjelly(s)
373        self.assertIs(z[0]["list"], z[1])
374        self.assertIs(z[0]["list"][0], z)
375
376    def test_moreReferences(self):
377        a = []
378        t = (a,)
379        a.append((t,))
380        s = jelly.jelly(t)
381        z = jelly.unjelly(s)
382        self.assertIs(z[0][0][0], z)
383
384    def test_typeSecurity(self):
385        """
386        Test for type-level security of serialization.
387        """
388        taster = jelly.SecurityOptions()
389        dct = jelly.jelly({})
390        self.assertRaises(jelly.InsecureJelly, jelly.unjelly, dct, taster)
391
392    def test_newStyleClasses(self):
393        uj = jelly.unjelly(D)
394        self.assertIs(D, uj)
395
396    def test_lotsaTypes(self):
397        """
398        Test for all types currently supported in jelly
399        """
400        a = A()
401        jelly.unjelly(jelly.jelly(a))
402        jelly.unjelly(jelly.jelly(a.amethod))
403        items = [
404            afunc,
405            [1, 2, 3],
406            not bool(1),
407            bool(1),
408            "test",
409            20.3,
410            (1, 2, 3),
411            None,
412            A,
413            unittest,
414            {"a": 1},
415            A.amethod,
416        ]
417        for i in items:
418            self.assertEqual(i, jelly.unjelly(jelly.jelly(i)))
419
420    def test_setState(self):
421        global TupleState
422
423        class TupleState:
424            def __init__(self, other):
425                self.other = other
426
427            def __getstate__(self):
428                return (self.other,)
429
430            def __setstate__(self, state):
431                self.other = state[0]
432
433            def __hash__(self):
434                return hash(self.other)
435
436        a = A()
437        t1 = TupleState(a)
438        t2 = TupleState(a)
439        t3 = TupleState((t1, t2))
440        d = {t1: t1, t2: t2, t3: t3, "t3": t3}
441        t3prime = jelly.unjelly(jelly.jelly(d))["t3"]
442        self.assertIs(t3prime.other[0].other, t3prime.other[1].other)
443
444    def test_classSecurity(self):
445        """
446        Test for class-level security of serialization.
447        """
448        taster = jelly.SecurityOptions()
449        taster.allowInstancesOf(A, B)
450        a = A()
451        b = B()
452        c = C()
453        # add a little complexity to the data
454        a.b = b
455        a.c = c
456        # and a backreference
457        a.x = b
458        b.c = c
459        # first, a friendly insecure serialization
460        friendly = jelly.jelly(a, taster)
461        x = jelly.unjelly(friendly, taster)
462        self.assertIsInstance(x.c, jelly.Unpersistable)
463        # now, a malicious one
464        mean = jelly.jelly(a)
465        self.assertRaises(jelly.InsecureJelly, jelly.unjelly, mean, taster)
466        self.assertIs(x.x, x.b, "Identity mismatch")
467        # test class serialization
468        friendly = jelly.jelly(A, taster)
469        x = jelly.unjelly(friendly, taster)
470        self.assertIs(x, A, "A came back: %s" % x)
471
472    def test_unjellyable(self):
473        """
474        Test that if Unjellyable is used to deserialize a jellied object,
475        state comes out right.
476        """
477
478        class JellyableTestClass(jelly.Jellyable):
479            pass
480
481        jelly.setUnjellyableForClass(JellyableTestClass, jelly.Unjellyable)
482        input = JellyableTestClass()
483        input.attribute = "value"
484        output = jelly.unjelly(jelly.jelly(input))
485        self.assertEqual(output.attribute, "value")
486        self.assertIsInstance(output, jelly.Unjellyable)
487
488    def test_persistentStorage(self):
489        perst = [{}, 1]
490
491        def persistentStore(obj, jel, perst=perst):
492            perst[1] = perst[1] + 1
493            perst[0][perst[1]] = obj
494            return str(perst[1])
495
496        def persistentLoad(pidstr, unj, perst=perst):
497            pid = int(pidstr)
498            return perst[0][pid]
499
500        a = SimpleJellyTest(1, 2)
501        b = SimpleJellyTest(3, 4)
502        c = SimpleJellyTest(5, 6)
503
504        a.b = b
505        a.c = c
506        c.b = b
507
508        jel = jelly.jelly(a, persistentStore=persistentStore)
509        x = jelly.unjelly(jel, persistentLoad=persistentLoad)
510
511        self.assertIs(x.b, x.c.b)
512        self.assertTrue(perst[0], "persistentStore was not called.")
513        self.assertIs(x.b, a.b, "Persistent storage identity failure.")
514
515    def test_newStyleClassesAttributes(self):
516        n = TestNode()
517        n1 = TestNode(n)
518        TestNode(n1)
519        TestNode(n)
520        # Jelly it
521        jel = jelly.jelly(n)
522        m = jelly.unjelly(jel)
523        # Check that it has been restored ok
524        self._check_newstyle(n, m)
525
526    def _check_newstyle(self, a, b):
527        self.assertEqual(a.id, b.id)
528        self.assertEqual(a.classAttr, 4)
529        self.assertEqual(b.classAttr, 4)
530        self.assertEqual(len(a.children), len(b.children))
531        for x, y in zip(a.children, b.children):
532            self._check_newstyle(x, y)
533
534    def test_referenceable(self):
535        """
536        A L{pb.Referenceable} instance jellies to a structure which unjellies to
537        a L{pb.RemoteReference}.  The C{RemoteReference} has a I{luid} that
538        matches up with the local object key in the L{pb.Broker} which sent the
539        L{Referenceable}.
540        """
541        ref = pb.Referenceable()
542        jellyBroker = pb.Broker()
543        jellyBroker.makeConnection(StringTransport())
544        j = jelly.jelly(ref, invoker=jellyBroker)
545
546        unjellyBroker = pb.Broker()
547        unjellyBroker.makeConnection(StringTransport())
548
549        uj = jelly.unjelly(j, invoker=unjellyBroker)
550        self.assertIn(uj.luid, jellyBroker.localObjects)
551
552
553class JellyDeprecationTests(TestCase):
554    """
555    Tests for deprecated Jelly things
556    """
557
558    def test_deprecatedInstanceAtom(self):
559        """
560        L{jelly.instance_atom} is deprecated since 15.0.0.
561        """
562        jelly.instance_atom
563        warnings = self.flushWarnings([self.test_deprecatedInstanceAtom])
564        self.assertEqual(len(warnings), 1)
565        self.assertEqual(
566            warnings[0]["message"],
567            "twisted.spread.jelly.instance_atom was deprecated in Twisted "
568            "15.0.0: instance_atom is unused within Twisted.",
569        )
570        self.assertEqual(warnings[0]["category"], DeprecationWarning)
571
572    def test_deprecatedUnjellyingInstanceAtom(self):
573        """
574        Unjellying the instance atom is deprecated with 15.0.0.
575        """
576        jelly.unjelly(
577            ["instance", ["class", "twisted.spread.test.test_jelly.A"], ["dictionary"]]
578        )
579        warnings = self.flushWarnings()
580        self.assertEqual(len(warnings), 1)
581        self.assertEqual(
582            warnings[0]["message"],
583            "Unjelly support for the instance atom is deprecated since "
584            "Twisted 15.0.0.  Upgrade peer for modern instance support.",
585        )
586        self.assertEqual(warnings[0]["category"], DeprecationWarning)
587
588
589class ClassA(pb.Copyable, pb.RemoteCopy):
590    def __init__(self):
591        self.ref = ClassB(self)
592
593
594class ClassB(pb.Copyable, pb.RemoteCopy):
595    def __init__(self, ref):
596        self.ref = ref
597
598
599class CircularReferenceTests(TestCase):
600    """
601    Tests for circular references handling in the jelly/unjelly process.
602    """
603
604    def test_simpleCircle(self):
605        jelly.setUnjellyableForClass(ClassA, ClassA)
606        jelly.setUnjellyableForClass(ClassB, ClassB)
607        a = jelly.unjelly(jelly.jelly(ClassA()))
608        self.assertIs(a.ref.ref, a, "Identity not preserved in circular reference")
609
610    def test_circleWithInvoker(self):
611        class DummyInvokerClass:
612            pass
613
614        dummyInvoker = DummyInvokerClass()
615        dummyInvoker.serializingPerspective = None
616        a0 = ClassA()
617        jelly.setUnjellyableForClass(ClassA, ClassA)
618        jelly.setUnjellyableForClass(ClassB, ClassB)
619        j = jelly.jelly(a0, invoker=dummyInvoker)
620        a1 = jelly.unjelly(j)
621        self.failUnlessIdentical(
622            a1.ref.ref, a1, "Identity not preserved in circular reference"
623        )
624
625    def test_set(self):
626        """
627        Check that a C{set} can contain a circular reference and be serialized
628        and unserialized without losing the reference.
629        """
630        s = set()
631        a = SimpleJellyTest(s, None)
632        s.add(a)
633        res = jelly.unjelly(jelly.jelly(a))
634        self.assertIsInstance(res.x, set)
635        self.assertEqual(list(res.x), [res])
636
637    def test_frozenset(self):
638        """
639        Check that a L{frozenset} can contain a circular reference and be
640        serialized and unserialized without losing the reference.
641        """
642        a = SimpleJellyTest(None, None)
643        s = frozenset([a])
644        a.x = s
645        res = jelly.unjelly(jelly.jelly(a))
646        self.assertIsInstance(res.x, frozenset)
647        self.assertEqual(list(res.x), [res])
648