OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Unit tests for model.py.""" | |
7 | |
8 import json | |
9 import logging | |
10 import os | |
11 import sys | |
12 import unittest | |
13 | |
14 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) | |
15 sys.path.insert(0, os.path.join(ROOT_DIR, '..')) | |
16 from model import PersistentMixIn, TYPE_FLAG, immutable | |
17 | |
18 | |
19 # Used a marker to determine that the check must ignore the value. | |
20 IGNORE = object() | |
21 | |
22 | |
23 def _members(instance): | |
24 return sorted(i for i in dir(instance) if not i.startswith('_')) | |
25 | |
26 | |
27 class Empty(PersistentMixIn): | |
28 pass | |
29 | |
30 | |
31 class Basic(PersistentMixIn): | |
32 a = int | |
33 b = float | |
34 | |
35 def test_me(self): | |
36 return self.a + 1 | |
37 | |
38 | |
39 class Inner(PersistentMixIn): | |
40 c = Basic | |
41 d = unicode | |
42 | |
43 | |
44 class Subclass(Inner): | |
45 e = list | |
46 | |
47 | |
48 class MultiValue(PersistentMixIn): | |
49 f = (None, bool) | |
50 g = (unicode, float) | |
51 | |
52 | |
53 class WithInit(PersistentMixIn): | |
54 h = unicode | |
55 def __init__(self, **kwargs): | |
56 # The values are overriden when loaded. | |
57 kwargs.setdefault('h', u'baz') | |
58 super(WithInit, self).__init__(**kwargs) | |
59 # i is not serialized. | |
60 self.i = 3 | |
61 | |
62 | |
63 class NotType(PersistentMixIn): | |
64 j = set | |
65 # k is not a type so it's not serialized. | |
66 k = 23 | |
67 | |
68 | |
69 class TypeOrDict(PersistentMixIn): | |
70 # Accepts a Basic or a dict. | |
71 l = (Basic, dict) | |
72 | |
73 | |
74 class StrDisallowed(PersistentMixIn): | |
75 m = str | |
76 | |
77 | |
78 def marshall(data): | |
79 """JSON encodes then decodes to make sure the data has passed through JSON | |
80 type reduction. | |
81 """ | |
82 return json.loads(json.dumps(data)) | |
83 | |
84 | |
85 class Base(unittest.TestCase): | |
86 def _check(self, actual, expected_type, **kwargs): | |
87 kwargs['as_dict'] = IGNORE | |
88 kwargs['from_dict'] = IGNORE | |
89 self.assertEqual(expected_type, type(actual)) | |
90 self.assertEqual(sorted(kwargs), _members(actual)) | |
91 for member in sorted(kwargs): | |
92 expected = kwargs[member] | |
93 if expected == IGNORE: | |
94 continue | |
95 self.assertEqual(expected, getattr(actual, member)) | |
96 | |
97 | |
98 class Serialize(Base): | |
99 def testEmpty(self): | |
100 expected = { | |
101 TYPE_FLAG: 'Empty', | |
102 } | |
103 self.assertEqual(expected, Empty().as_dict()) | |
104 | |
105 def testBasic(self): | |
106 data = Basic(b=23.2) | |
107 expected = { | |
108 'a': 0, | |
109 'b': 23.2, | |
110 TYPE_FLAG: 'Basic', | |
111 } | |
112 self.assertEqual(expected, data.as_dict()) | |
113 | |
114 def testBasicFailConstruct(self): | |
115 # TODO(maruel): should int be auto-upgraded to float when requested? | |
116 self.assertRaises(TypeError, Basic, b=23) | |
117 | |
118 def testBasicFailAsDict(self): | |
119 # TODO(maruel): should int be auto-upgraded to float when requested? | |
120 data = Basic() | |
121 data.b = 23 | |
122 self.assertRaises(TypeError, data.as_dict) | |
123 | |
124 def testInner(self): | |
125 data = Inner(c=Basic(a=21, b=23.2), d=u'foo') | |
126 expected = { | |
127 'c': { | |
128 'a': 21, | |
129 'b': 23.2, | |
130 TYPE_FLAG: 'Basic', | |
131 }, | |
132 TYPE_FLAG: 'Inner', | |
133 'd': 'foo', | |
134 } | |
135 self.assertEqual(expected, data.as_dict()) | |
136 | |
137 def testSubclass(self): | |
138 data = Subclass(c=Basic(a=23), e=[Basic(), {'random': 'stuff', 'x': True}]) | |
139 expected = { | |
140 'c': { | |
141 'a': 23, | |
142 'b': 0., | |
143 TYPE_FLAG: 'Basic', | |
144 }, | |
145 'e': [ | |
146 { | |
147 'a': 0, | |
148 'b': 0., | |
149 TYPE_FLAG: 'Basic', | |
150 }, | |
151 { | |
152 'random': 'stuff', | |
153 'x': True, | |
154 }, | |
155 ], | |
156 'd': '', | |
157 TYPE_FLAG: 'Subclass', | |
158 } | |
159 self.assertEqual(expected, data.as_dict()) | |
160 | |
161 def testMultiValue_default(self): | |
162 data = MultiValue() | |
163 expected = { | |
164 'f': None, | |
165 'g': '', | |
166 TYPE_FLAG: 'MultiValue', | |
167 } | |
168 self.assertEqual(expected, data.as_dict()) | |
169 | |
170 def testMultiValue_first(self): | |
171 data = MultiValue(f=None, g=u'foo') | |
172 expected = { | |
173 'f': None, | |
174 'g': 'foo', | |
175 TYPE_FLAG: 'MultiValue', | |
176 } | |
177 self.assertEqual(expected, data.as_dict()) | |
178 | |
179 def testMultiValue_second(self): | |
180 data = MultiValue(f=False, g=3.1) | |
181 expected = { | |
182 'f': False, | |
183 'g': 3.1, | |
184 TYPE_FLAG: 'MultiValue', | |
185 } | |
186 self.assertEqual(expected, data.as_dict()) | |
187 | |
188 def testWithInit(self): | |
189 data = WithInit() | |
190 self._check(data, WithInit, h='baz', i=3) | |
191 expected = { | |
192 'h': 'baz', | |
193 TYPE_FLAG: 'WithInit', | |
194 } | |
195 self.assertEqual(expected, data.as_dict()) | |
196 | |
197 def testNotType(self): | |
198 data = NotType() | |
199 self._check(data, NotType, j=set(), k=23) | |
200 expected = { | |
201 'j': [], | |
202 TYPE_FLAG: 'NotType', | |
203 } | |
204 self.assertEqual(expected, data.as_dict()) | |
205 | |
206 def testTypeOrDict_Basic(self): | |
207 data = TypeOrDict() | |
208 self._check(data, TypeOrDict, l=IGNORE) | |
209 self._check(data.l, Basic, a=0, b=0., test_me=IGNORE) | |
210 expected = { | |
211 'l': { | |
212 'a': 0, | |
213 'b': 0.0, | |
214 TYPE_FLAG: 'Basic', | |
215 }, | |
216 TYPE_FLAG: 'TypeOrDict', | |
217 } | |
218 self.assertEqual(expected, data.as_dict()) | |
219 | |
220 def testTypeOrDict_dict(self): | |
221 data = TypeOrDict(l={'foo': u'bar'}) | |
222 self._check(data, TypeOrDict, l={'foo': u'bar'}) | |
223 expected = { | |
224 'l': { | |
225 'foo': 'bar', | |
226 }, | |
227 TYPE_FLAG: 'TypeOrDict', | |
228 } | |
229 self.assertEqual(expected, data.as_dict()) | |
230 | |
231 def testStrDisallowed(self): | |
232 self.assertRaises(TypeError, StrDisallowed) | |
233 | |
234 | |
235 class Deserialize(Base): | |
236 def testNotFound(self): | |
237 data = { TYPE_FLAG: 'DoesNotExists' } | |
238 self.assertRaises(KeyError, PersistentMixIn.from_dict, marshall(data)) | |
239 | |
240 def testEmpty(self): | |
241 data = { } | |
242 self.assertRaises(KeyError, PersistentMixIn.from_dict, marshall(data)) | |
243 | |
244 def testBasic(self): | |
245 data = { | |
246 'a': 22, | |
247 'b': 23.2, | |
248 TYPE_FLAG: 'Basic', | |
249 } | |
250 actual = PersistentMixIn.from_dict(marshall(data)) | |
251 self._check(actual, Basic, a=22, b=23.2, test_me=IGNORE) | |
252 | |
253 def testBasic_WrongType(self): | |
254 data = { | |
255 'a': None, | |
256 TYPE_FLAG: 'Basic', | |
257 } | |
258 self.assertRaises(TypeError, PersistentMixIn.from_dict, marshall(data)) | |
259 | |
260 def testInner(self): | |
261 data = { | |
262 'c': { | |
263 'a': 42, | |
264 'b': .1, | |
265 TYPE_FLAG: 'Basic', | |
266 }, | |
267 TYPE_FLAG: 'Inner', | |
268 'd': 'foo2', | |
269 } | |
270 actual = PersistentMixIn.from_dict(marshall(data)) | |
271 self._check(actual, Inner, c=IGNORE, d='foo2') | |
272 self._check(actual.c, Basic, a=42, b=.1, test_me=IGNORE) | |
273 | |
274 def testSubclass(self): | |
275 data = { | |
276 'd': 'bar', | |
277 'e': [ | |
278 { | |
279 'a': 1, | |
280 'b': 2., | |
281 TYPE_FLAG: 'Basic', | |
282 }, | |
283 { | |
284 'random': 'stuff', | |
285 'x': True, | |
286 }, | |
287 ], | |
288 TYPE_FLAG: 'Subclass', | |
289 } | |
290 actual = PersistentMixIn.from_dict(marshall(data)) | |
291 self._check(actual, Subclass, c=IGNORE, d='bar', e=IGNORE) | |
292 self._check(actual.c, Basic, a=0, b=0., test_me=IGNORE) | |
293 self.assertEqual(list, type(actual.e)) | |
294 self.assertEqual(2, len(actual.e)) | |
295 self._check(actual.e[0], Basic, a=1, b=2., test_me=IGNORE) | |
296 self.assertEqual({'random': 'stuff', 'x': True}, actual.e[1]) | |
297 | |
298 def testMemberFunction(self): | |
299 # Make sure the member functions are accessible. | |
300 data = { | |
301 TYPE_FLAG: 'Basic', | |
302 'ignored': 'really', | |
303 } | |
304 actual = PersistentMixIn.from_dict(marshall(data)) | |
305 self._check(actual, Basic, a=0, b=0., test_me=IGNORE) | |
306 self.assertEqual(1, actual.test_me()) | |
307 | |
308 def testMultiValue_default(self): | |
309 data = { | |
310 TYPE_FLAG: 'MultiValue', | |
311 } | |
312 actual = PersistentMixIn.from_dict(marshall(data)) | |
313 self._check(actual, MultiValue, f=None, g='') | |
314 | |
315 def testMultiValue_first(self): | |
316 data = { | |
317 'f': None, | |
318 'g': 'foo', | |
319 TYPE_FLAG: 'MultiValue', | |
320 } | |
321 actual = PersistentMixIn.from_dict(marshall(data)) | |
322 self._check(actual, MultiValue, f=None, g='foo') | |
323 | |
324 def testMultiValue_second(self): | |
325 data = { | |
326 'f': False, | |
327 'g': 3.1, | |
328 TYPE_FLAG: 'MultiValue', | |
329 } | |
330 actual = PersistentMixIn.from_dict(marshall(data)) | |
331 self._check(actual, MultiValue, f=False, g=3.1) | |
332 | |
333 def testWithInit_default(self): | |
334 data = { | |
335 TYPE_FLAG: 'WithInit', | |
336 } | |
337 actual = PersistentMixIn.from_dict(marshall(data)) | |
338 self._check(actual, WithInit, h='', i=3) | |
339 | |
340 def testWithInit_values(self): | |
341 data = { | |
342 'h': 'foo', | |
343 'i': 4, | |
344 TYPE_FLAG: 'WithInit', | |
345 } | |
346 actual = PersistentMixIn.from_dict(marshall(data)) | |
347 self._check(actual, WithInit, h='foo', i=3) | |
348 | |
349 def testNotType(self): | |
350 data = { | |
351 'j': ['a', 2], | |
352 TYPE_FLAG: 'NotType', | |
353 } | |
354 actual = PersistentMixIn.from_dict(marshall(data)) | |
355 self._check(actual, NotType, j=set(['a', 2]), k=23) | |
356 | |
357 def testTypeOrDict_Basic(self): | |
358 data = { | |
359 'l': { | |
360 'a': 3, | |
361 'b': 4.0, | |
362 TYPE_FLAG: 'Basic', | |
363 }, | |
364 TYPE_FLAG: 'TypeOrDict', | |
365 } | |
366 actual = PersistentMixIn.from_dict(marshall(data)) | |
367 self._check(actual, TypeOrDict, l=IGNORE) | |
368 self._check(actual.l, Basic, a=3, b=4., test_me=IGNORE) | |
369 | |
370 def testTypeOrDict_dict(self): | |
371 data = { | |
372 'l': { | |
373 'foo': 'bar', | |
374 }, | |
375 TYPE_FLAG: 'TypeOrDict', | |
376 } | |
377 actual = PersistentMixIn.from_dict(marshall(data)) | |
378 self._check(actual, TypeOrDict, l={'foo': 'bar'}) | |
379 | |
380 def testStrDisallowed(self): | |
381 data = { | |
382 TYPE_FLAG: 'StrDisallowed', | |
383 } | |
384 self.assertRaises(TypeError, PersistentMixIn.from_dict, marshall(data)) | |
385 | |
386 | |
387 class Mutable(object): | |
388 def __init__(self, x): | |
389 self.x = x | |
390 | |
391 @immutable | |
392 def func_call_immutable_deep(self): | |
393 return self.func_call_immutable() | |
394 | |
395 @immutable | |
396 def func_call_immutable(self): | |
397 return self.func_immutable(100) | |
398 | |
399 @immutable | |
400 def func_immutable(self, key): | |
401 return self.x + key | |
402 | |
403 @immutable | |
404 def func_call_mutable(self): | |
405 # Raises. | |
406 return self.func_mutable(10) | |
407 | |
408 def func_mutable(self, new_value): | |
409 self.x = new_value | |
410 return new_value | |
411 | |
412 @immutable | |
413 def func_try_mutate(self): | |
414 # Raises. | |
415 self.x = 18 | |
416 | |
417 @immutable | |
418 def func_call_mutable_deep(self): | |
419 # Raises | |
420 return self.func_call_mutable() | |
421 | |
422 | |
423 class MutableStatic(object): | |
424 @immutable | |
425 def func_call_static(self): | |
426 return self.func_static() | |
427 | |
428 @staticmethod | |
429 def func_static(): | |
430 return 'static' | |
431 | |
432 | |
433 class MutableDel(object): | |
434 def __init__(self, x): | |
435 self.x = x | |
436 | |
437 @immutable | |
438 def func_del(self): | |
439 # Raises | |
440 del self.x | |
441 | |
442 def func_del_and_replace(self): | |
443 x = self.x | |
444 del self.x | |
445 assert getattr(self, 'x', 'bleh') == 'bleh' | |
446 self.x = x | |
447 return self.x | |
448 | |
449 | |
450 class MutableProperty(object): | |
451 def __init__(self, x): | |
452 self.x = x | |
453 | |
454 @property | |
455 def property_y(self): | |
456 return self.x + 1 | |
457 | |
458 @immutable | |
459 def func_call_property(self): | |
460 return self.property_y | |
461 | |
462 | |
463 class Immutable(unittest.TestCase): | |
464 def testImmutable(self): | |
465 obj = Mutable(2) | |
466 # 2 + 23 | |
467 self.assertEqual(25, obj.func_immutable(23)) | |
468 # 2 + 100 | |
469 self.assertEqual(102, obj.func_call_immutable()) | |
470 # 2 + 100 | |
471 self.assertEqual(102, obj.func_call_immutable_deep()) | |
472 self.assertEqual(2, obj.x) | |
473 self.assertEqual(23, obj.func_mutable(23)) | |
474 self.assertEqual(23, obj.x) | |
475 self.assertRaises(TypeError, obj.func_call_mutable) | |
476 self.assertRaises(TypeError, obj.func_try_mutate) | |
477 self.assertRaises(TypeError, obj.func_call_mutable_deep) | |
478 | |
479 def testImmutableStatic(self): | |
480 obj = MutableStatic() | |
481 self.assertEqual('static', obj.func_static()) | |
482 self.assertEqual('static', obj.func_call_static()) | |
483 | |
484 def testImmutableDel(self): | |
485 obj = MutableDel(2) | |
486 self.assertEqual(2, obj.func_del_and_replace()) | |
487 self.assertRaises(TypeError, obj.func_del) | |
488 self.assertEqual(2, obj.x) | |
489 | |
490 def testImmutableProperty(self): | |
491 obj = MutableProperty(2) | |
492 self.assertEqual(2, obj.x) | |
493 self.assertEqual(3, obj.property_y) | |
494 self.assertEqual(3, obj.func_call_property()) | |
495 self.assertEqual(2, obj.x) | |
496 | |
497 | |
498 if __name__ == '__main__': | |
499 logging.basicConfig( | |
500 level=logging.DEBUG if '-v' in sys.argv else logging.WARNING, | |
501 format='%(levelname)5s %(module)15s(%(lineno)3d): %(message)s') | |
502 unittest.main() | |
OLD | NEW |