summaryrefslogtreecommitdiffstats
path: root/appengine_django/tests/serialization_test.py
blob: 39b92ea195491d8f77e708b8d4d81be9c4e1e35b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
#!/usr/bin/python2.4
#
# Copyright 2008 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests that the serialization modules are functioning correctly.

In particular, these tests verify that the modifications made to the standard
Django serialization modules function correctly and that the combined datastore
and Django models can be dumped and loaded to all of the provided formats.
"""


import os
import re
import unittest
from StringIO import StringIO

from django.core import serializers

from google.appengine.ext import db
from appengine_django.models import BaseModel


class ModelA(BaseModel):
  description = db.StringProperty()


class ModelB(BaseModel):
  description = db.StringProperty()
  friend = db.Reference(ModelA)


class TestAllFormats(type):

  def __new__(cls, name, bases, attrs):
    """Extends base test functions to be called for every serialisation format.

    Looks for functions matching 'run.*Test', where the wildcard in the middle
    matches the desired test name and ensures that a test case is setup to call
    that function once for every defined serialisation format. The test case
    that is created will be called 'test<format><name>'. Eg, for the function
    'runKeyedObjectTest' functions like 'testJsonKeyedObject' will be created.
    """
    test_formats = serializers.get_serializer_formats()
    test_formats.remove("python")  # Python serializer is only used indirectly.

    for func_name in attrs.keys():
      m = re.match("^run(.*)Test$", func_name)
      if not m:
        continue
      for format in test_formats:
        test_name = "test%s%s" % (format.title(), m.group(1))
        test_func = eval("lambda self: getattr(self, \"%s\")(\"%s\")" %
                         (func_name, format))
        attrs[test_name] = test_func

    return super(TestAllFormats, cls).__new__(cls, name, bases, attrs)


class SerializationTest(unittest.TestCase):
  """Unit tests for the serialization/deserialization functionality.

  Tests that every loaded serialization format can successfully dump and then
  reload objects without the objects changing.
  """
  __metaclass__ = TestAllFormats

  def compareObjects(self, orig, new, format="unknown"):
    """Compares two objects to ensure they are identical.

    Args:
      orig: The original object, must be an instance of db.Model.
      new: The new object, must be an instance of db.Model.
      format: The serialization format being tested, used to make error output
        more helpful.

    Raises:
      The function has no return value, but will raise assertion errors if the
      objects do not match correctly.
    """
    if orig.key().name():
      # Only compare object keys when the key is named. Key IDs are not static
      # and will change between dump/load. If you want stable Keys they need to
      # be named!
      self.assertEqual(orig.key(), new.key(),
                       "keys not equal after %s serialization: %s != %s" %
                       (format, repr(orig.key()), repr(new.key())))

    for key in orig.properties().keys():
      oval = getattr(orig, key)
      nval = getattr(new, key)
      if isinstance(orig.properties()[key], db.Reference):
        # Need to compare object keys not the objects themselves.
        oval = oval.key()
        nval = nval.key()
      self.assertEqual(oval, nval, "%s attribute differs after %s "
                       "serialization: %s != %s" % (key, format, oval, nval))

  def doSerialisationTest(self, format, obj, rel_attr=None, obj_ref=None):
    """Runs a serialization test on an object for the specified format.

    Args:
      format: The name of the Django serialization class to use.
      obj: The object to {,de}serialize, must be an instance of db.Model.
      rel_attr: Name of the attribute of obj references another model.
      obj_ref: The expected object reference, must be an instance of db.Model.

    Raises:
      The function has no return value but raises assertion errors if the
      object cannot be successfully serialized and then deserialized back to an
      identical object. If rel_attr and obj_ref are specified the deserialized
      object must also retain the references from the original object.
    """
    serialised = serializers.serialize(format, [obj])
    # Try and get the object back from the serialized string.
    result = list(serializers.deserialize(format, StringIO(serialised)))
    self.assertEqual(1, len(result),
                     "%s serialization should create 1 object" % format)
    result[0].save()  # Must save back into the database to get a Key.
    self.compareObjects(obj, result[0].object, format)
    if rel_attr and obj_ref:
      rel = getattr(result[0].object, rel_attr)
      if callable(rel):
        rel = rel()
      self.compareObjects(rel, obj_ref, format)

  def doLookupDeserialisationReferenceTest(self, lookup_dict, format):
    """Tests the Key reference is loaded OK for a format.

    Args:
      lookup_dict: A dictionary indexed by format containing serialized strings
        of the objects to load.
      format: The format to extract from the dict and deserialize.

    Raises:
      This function has no return value but raises assertion errors if the
      string cannot be deserialized correctly or the resulting object does not
      reference the object correctly.
    """
    if format not in lookup_dict:
      # Check not valid for this format.
      return
    obj = ModelA(description="test object", key_name="test")
    obj.put()
    s = lookup_dict[format]
    result = list(serializers.deserialize(format, StringIO(s)))
    self.assertEqual(1, len(result), "expected 1 object from %s" % format)
    result[0].save()
    self.compareObjects(obj, result[0].object.friend, format)

  def doModelKeyDeserialisationReferenceTest(self, lookup_dict, format):
    """Tests a model with a key can be loaded OK for a format.

    Args:
      lookup_dict: A dictionary indexed by format containing serialized strings
        of the objects to load.
      format: The format to extract from the dict and deserialize.

    Returns:
      This function has no return value but raises assertion errors if the
      string cannot be deserialized correctly or the resulting object is not an
      instance of ModelA with a key named 'test'.
    """
    if format not in lookup_dict:
      # Check not valid for this format.
      return
    s = lookup_dict[format]
    result = list(serializers.deserialize(format, StringIO(s)))
    self.assertEqual(1, len(result), "expected 1 object from %s" % format)
    result[0].save()
    self.assert_(isinstance(result[0].object, ModelA))
    self.assertEqual("test", result[0].object.key().name())

  # Lookup dicts for the above (doLookupDeserialisationReferenceTest) function.
  SERIALIZED_WITH_KEY_AS_LIST = {
      "json": """[{"pk": "agR0ZXN0chMLEgZNb2RlbEIiB21vZGVsYmkM", """
              """"model": "tests.modelb", "fields": {"description": "test", """
              """"friend": ["ModelA", "test"] }}]""",
      "yaml": """- fields: {description: !!python/unicode 'test', friend: """
              """ [ModelA, test]}\n  model: tests.modelb\n  pk: """
              """ agR0ZXN0chMLEgZNb2RlbEEiB21vZGVsYWkM\n"""
  }
  SERIALIZED_WITH_KEY_REPR = {
      "json": """[{"pk": "agR0ZXN0chMLEgZNb2RlbEIiB21vZGVsYmkM", """
              """"model": "tests.modelb", "fields": {"description": "test", """
              """"friend": "datastore_types.Key.from_path("""
              """'ModelA', 'test')" }}]""",
      "yaml": """- fields: {description: !!python/unicode 'test', friend: """
              """\'datastore_types.Key.from_path("ModelA", "test")\'}\n  """
              """model: tests.modelb\n  pk: """
              """ agR0ZXN0chMLEgZNb2RlbEEiB21vZGVsYWkM\n"""
  }

  # Lookup dict for the doModelKeyDeserialisationReferenceTest function.
  MK_SERIALIZED_WITH_LIST = {
      "json": """[{"pk": ["ModelA", "test"], "model": "tests.modela", """
              """"fields": {}}]""",
      "yaml": """-\n fields: {description: null}\n model: tests.modela\n """
              """pk: [ModelA, test]\n"""
  }
  MK_SERIALIZED_WITH_KEY_REPR = {
      "json": """[{"pk": "datastore_types.Key.from_path('ModelA', 'test')", """
              """"model": "tests.modela", "fields": {}}]""",
      "yaml": """-\n fields: {description: null}\n model: tests.modela\n """
              """pk: \'datastore_types.Key.from_path("ModelA", "test")\'\n"""
  }
  MK_SERIALIZED_WITH_KEY_AS_TEXT = {
      "json": """[{"pk": "test", "model": "tests.modela", "fields": {}}]""",
      "yaml": """-\n fields: {description: null}\n model: tests.modela\n """
              """pk: test\n"""
  }

  # Lookup dict for the function.
  SERIALIZED_WITH_NON_EXISTANT_PARENT = {
      "json": """[{"pk": "ahhnb29nbGUtYXBwLWVuZ2luZS1kamFuZ29yIgsSBk1vZG"""
              """VsQiIGcGFyZW50DAsSBk1vZGVsQSIEdGVzdAw", """
              """"model": "tests.modela", "fields": """
              """{"description": null}}]""",
      "yaml": """- fields: {description: null}\n  """
              """model: tests.modela\n  """
              """pk: ahhnb29nbGUtYXBwLWVuZ2luZS1kamFuZ29yIgsSBk1"""
              """vZGVsQiIGcGFyZW50DAsSBk1vZGVsQSIEdGVzdAw\n""",
      "xml":  """<?xml version="1.0" encoding="utf-8"?>\n"""
              """<django-objects version="1.0">\n"""
              """<entity kind="tests.modela" key="ahhnb29nbGUtYXBwL"""
              """WVuZ2luZS1kamFuZ29yIgsSBk1vZGVsQiIGcGFyZW50DA"""
              """sSBk1vZGVsQSIEdGVzdAw">\n  """
              """<key>tag:google-app-engine-django.gmail.com,"""
              """2008-05-13:ModelA[ahhnb29nbGUtYXBwLWVuZ2luZS1kam"""
              """FuZ29yIgsSBk1vZGVsQiIGcGFyZW50DAsSBk1vZGVsQSIEdGVzdAw"""
              """]</key>\n  <property name="description" """
              """type="null"></property>\n</entity>\n</django-objects>"""
  }

  # The following functions are all expanded by the metaclass to be run once
  # for every registered Django serialization module.

  def runKeyedObjectTest(self, format):
    """Test serialization of a basic object with a named key."""
    obj = ModelA(description="test object", key_name="test")
    obj.put()
    self.doSerialisationTest(format, obj)

  def runObjectWithIdTest(self, format):
    """Test serialization of a basic object with a numeric ID key."""
    obj = ModelA(description="test object")
    obj.put()
    self.doSerialisationTest(format, obj)

  def runObjectWithReferenceTest(self, format):
    """Test serialization of an object that references another object."""
    obj = ModelA(description="test object", key_name="test")
    obj.put()
    obj2 = ModelB(description="friend object", friend=obj)
    obj2.put()
    self.doSerialisationTest(format, obj2, "friend", obj)

  def runObjectWithParentTest(self, format):
    """Test serialization of an object that has a parent object reference."""
    obj = ModelA(description="parent object", key_name="parent")
    obj.put()
    obj2 = ModelA(description="child object", key_name="child", parent=obj)
    obj2.put()
    self.doSerialisationTest(format, obj2, "parent", obj)

  def runObjectWithNonExistantParentTest(self, format):
    """Test deserialization of an object referencing a non-existant parent."""
    self.doModelKeyDeserialisationReferenceTest(
        self.SERIALIZED_WITH_NON_EXISTANT_PARENT, format)

  def runCreateKeyReferenceFromListTest(self, format):
    """Tests that a reference specified as a list in json/yaml can be loaded OK."""
    self.doLookupDeserialisationReferenceTest(self.SERIALIZED_WITH_KEY_AS_LIST,
                                              format)

  def runCreateKeyReferenceFromReprTest(self, format):
    """Tests that a reference specified as repr(Key) in can loaded OK."""
    self.doLookupDeserialisationReferenceTest(self.SERIALIZED_WITH_KEY_REPR,
                                              format)

  def runCreateModelKeyFromListTest(self, format):
    """Tests that a model key specified as a list can be loaded OK."""
    self.doModelKeyDeserialisationReferenceTest(self.MK_SERIALIZED_WITH_LIST,
                                                format)

  def runCreateModelKeyFromReprTest(self, format):
    """Tests that a model key specified as a repr(Key) can be loaded OK."""
    self.doModelKeyDeserialisationReferenceTest(
        self.MK_SERIALIZED_WITH_KEY_REPR, format)

  def runCreateModelKeyFromTextTest(self, format):
    """Tests that a reference specified as a plain key_name loads OK."""
    self.doModelKeyDeserialisationReferenceTest(
        self.MK_SERIALIZED_WITH_KEY_AS_TEXT, format)


if __name__ == '__main__':
  unittest.main()