1
# This file is part of Checkbox.
3
# Copyright 2012-2015 Canonical Ltd.
5
# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
7
# Checkbox is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License version 3,
9
# as published by the Free Software Foundation.
11
# Checkbox is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19
"""Tests for the plainbox.impl.pod module."""
21
from doctest import DocTestSuite
22
from unittest import TestCase
24
from plainbox.impl.pod import Field
25
from plainbox.impl.pod import MANDATORY
26
from plainbox.impl.pod import POD
27
from plainbox.impl.pod import UNSET
28
from plainbox.impl.pod import _FieldCollection
29
from plainbox.impl.pod import read_only_assign_filter
30
from plainbox.impl.pod import sequence_type_check_assign_filter
31
from plainbox.impl.pod import type_check_assign_filter
32
from plainbox.impl.pod import type_convert_assign_filter
33
from plainbox.impl.pod import unset_or_sequence_type_check_assign_filter
34
from plainbox.impl.pod import unset_or_type_check_assign_filter
35
from plainbox.vendor import mock
38
def load_tests(loader, tests, ignore):
40
Protocol for loading unit tests.
42
This function ensures that doctests are executed as well.
44
tests.addTests(DocTestSuite('plainbox.impl.pod'))
48
class SingletonTests(TestCase):
50
"""Tests for several singleton objects."""
52
def test_MANDATORY_repr(self):
53
"""MANDATORY.repr() returns "MANDATORY"."""
54
self.assertEqual(repr(MANDATORY), "MANDATORY")
56
def test_UNSET_repr(self):
57
"""UNSET.repr() returns "UNSET"."""
58
self.assertEqual(repr(UNSET), "UNSET")
61
class FieldTests(TestCase):
63
"""Tests for the Field class."""
68
"""Common set-up code."""
69
self.doc = "doc" # not a mock because it gets set to __doc__
70
self.type = mock.Mock(name='type')
71
self.initial = mock.Mock(name='initial')
72
self.initial_fn = mock.Mock(name='initial_fn')
73
self.field = self.FIELD_CLS(
74
self.doc, self.type, self.initial, self.initial_fn)
75
self.instance = mock.Mock(name='instance')
76
self.owner = mock.Mock(name='owner')
78
def test_initializer(self):
79
""".__init__() stored data correctly."""
80
self.assertEqual(self.field.__doc__, self.doc)
81
self.assertEqual(self.field.type, self.type)
82
self.assertEqual(self.field.initial, self.initial)
83
self.assertEqual(self.field.initial_fn, self.initial_fn)
85
def test_gain_name(self):
86
""".gain_name() sets three extra attributes."""
87
self.assertIsNone(self.field.name)
88
self.assertIsNone(self.field.instance_attr)
89
self.assertIsNone(self.field.signal_name)
90
self.field.gain_name("abcd")
91
self.assertEqual(self.field.name, "abcd")
92
self.assertEqual(self.field.instance_attr, "_abcd")
93
self.assertEqual(self.field.signal_name, "on_abcd_changed")
96
""".repr() works as expected."""
97
self.field.gain_name("field")
98
self.assertEqual(repr(self.field), "<Field name:'field'>")
100
def test_is_mandatory(self):
101
""".is_mandatory looks for initial value of MANDATORY."""
102
self.field.initial = None
103
self.assertFalse(self.field.is_mandatory)
104
self.field.initial = MANDATORY
105
self.assertTrue(self.field.is_mandatory)
107
def test_cls_reads(self):
108
""".__get__() returns the field if accessed via a class."""
109
self.assertIs(self.field.__get__(None, self.owner), self.field)
111
def test_obj_reads(self):
112
""".__get__() reads POD data if accessed via an object."""
113
# Reading the field requires the field to know its name
114
self.field.gain_name("field")
116
self.field.__get__(self.instance, self.owner),
117
self.instance._field)
119
def test_obj_writes(self):
120
""".__set__() writes POD data."""
121
# Writing the field requires the field to know its name
122
self.field.gain_name("field")
123
self.field.__set__(self.instance, "data")
124
self.assertEqual(self.instance._field, "data")
126
def test_obj_writes_fires_notification(self):
127
""".__set__() fires change notifications."""
128
# Let's enable notification and set the name so that the field knows
129
# what to do when it gets set. Let's set the instance data to "old" to
130
# track the actual change.
131
self.field.notify = True
132
self.field.gain_name("field")
133
self.instance._field = "old"
134
# Let's set the data to "new" now
135
self.field.__set__(self.instance, "new")
136
# And check that the notification system worked
137
self.instance.on_field_changed.assert_called_with("old", "new")
139
def test_obj_writes_uses_assign_chain(self):
140
""".__set__() uses the assign filter list."""
141
# Let's enable the assign filter composed out of two functions
142
# and set some data using the field.
145
self.field.assign_filter_list = [fn1, fn2]
146
self.field.gain_name("field")
147
self.instance._field = "old"
148
self.field.__set__(self.instance, "new")
149
# The current value in the field should be the return value of fn2()
150
# and both fn1() and fn2() were called with the right arguments.
151
fn1.assert_called_with(self.instance, self.field, "old", "new")
152
fn2.assert_called_with(self.instance, self.field, "old", fn1())
153
self.assertEqual(self.instance._field, fn2())
155
def test_alter_cls_without_notification(self):
156
""".alter_cls() doesn't do anything if notify is False."""
157
cls = mock.Mock(name='cls')
158
del cls.on_field_changed
159
self.field.notify = False
160
self.field.gain_name('field')
161
self.field.alter_cls(cls)
162
self.assertFalse(hasattr(cls, "on_field_changed"))
164
def test_alter_cls_with_notification(self):
165
""".alter_cls() adds a change signal if notify is True."""
166
cls = mock.Mock(name='cls')
167
del cls.on_field_changed
168
cls.__name__ = "Klass"
169
self.field.notify = True
170
self.field.gain_name('field')
171
self.field.alter_cls(cls)
172
self.assertTrue(hasattr(cls, "on_field_changed"))
174
cls.on_field_changed.signal_name, "Klass.on_field_changed")
177
class FieldCollectionTests(TestCase):
179
"""Tests for the _FieldCollection class."""
182
"""Common set-up code."""
188
'do_sth': lambda: True,
191
self.fc = _FieldCollection()
193
def set_field_names(self):
194
"""Set names of the foo and bar fields."""
195
self.foo.gain_name('foo')
196
self.bar.gain_name('bar')
198
def test_add_field_builds_field_list(self):
199
""".add_field() appends new fields to field_list."""
200
# because we're not calling inspect_namespace() which does that
201
self.set_field_names()
202
self.fc.add_field(self.foo, 'cls')
203
self.assertEqual(self.fc.field_list, [self.foo])
204
self.fc.add_field(self.bar, 'cls')
205
self.assertEqual(self.fc.field_list, [self.foo, self.bar])
207
def test_add_field_builds_field_origin_map(self):
208
""".add_field() builds and maintains field_origin_map."""
209
# because we're not calling inspect_namespace() which does that
210
self.set_field_names()
211
self.fc.add_field(self.foo, 'cls')
212
self.assertEqual(self.fc.field_origin_map, {'foo': 'cls'})
213
self.fc.add_field(self.bar, 'cls')
215
self.fc.field_origin_map, {'foo': 'cls', 'bar': 'cls'})
217
def test_add_field_detects_clashes(self):
218
""".add_Field() detects field clashes and raises TypeError."""
220
foo_clash.name = 'foo'
221
# because we're not calling inspect_namespace() which does that
222
self.set_field_names()
223
self.fc.add_field(self.foo, 'cls')
224
with self.assertRaisesRegex(
225
TypeError, 'field other_cls.foo clashes with cls.foo'):
226
self.fc.add_field(foo_clash, 'other_cls')
228
def test_inspect_base_classes_calls_add_field(self):
229
""".inspect_base_classes() calls add_field() on each Field found."""
238
field_list = [mock.Mock('fake_field')]
240
with mock.patch.object(self.fc, 'add_field') as mock_add_field:
241
self.fc.inspect_base_classes((Base1, Base2, Unrelated))
242
mock_add_field.assert_has_calls([
243
((Base1.foo, 'Base1'), {}),
244
((Base1.bar, 'Base1'), {}),
245
((Base2.froz, 'Base2'), {}),
248
def test_inspect_namespace_calls_add_field(self):
249
""".inspect_namespace() calls add_field() on each Field."""
250
with mock.patch.object(self.fc, 'add_field') as mock_add_field:
251
self.fc.inspect_namespace(self.ns, 'cls')
252
calls = [mock.call(self.foo, 'cls'), mock.call(self.bar, 'cls')]
253
mock_add_field.assert_has_calls(calls, any_order=True)
255
def test_inspect_namespace_sets_field_name(self):
256
""".inspect_namespace() sets .name of each field."""
257
self.assertIsNone(self.foo.name)
258
self.assertIsNone(self.bar.name)
259
fc = _FieldCollection()
260
fc.inspect_namespace(self.ns, 'cls')
261
self.assertEqual(self.foo.name, 'foo')
262
self.assertEqual(self.bar.name, 'bar')
264
def test_inspect_namespace_sets_field_instance_attr(self):
265
""".inspect_namespace() sets .instance_attr of each field."""
266
self.assertIsNone(self.foo.instance_attr)
267
self.assertIsNone(self.bar.instance_attr)
268
fc = _FieldCollection()
269
fc.inspect_namespace(self.ns, 'cls')
270
self.assertEqual(self.foo.instance_attr, '_foo')
271
self.assertEqual(self.bar.instance_attr, '_bar')
273
def test_notifier(self):
274
"""@field.change_notifier changes the notify function."""
275
@self.foo.change_notifier
276
def on_foo_changed(pod, old, new):
278
self.assertTrue(self.foo.notify)
279
self.assertEqual(self.foo.notify_fn, on_foo_changed)
282
class PODTests(TestCase):
284
"""Tests for the POD class."""
286
def test_field_list(self):
287
""".field_list is set by PODMeta."""
292
f2 = Field(initial='default')
293
f3 = Field(initial_fn=lambda: m())
295
self.assertEqual(T.field_list, [T.f1, T.f2, T.f3])
297
def test_namedtuple_cls(self):
298
"""Check that .namedtuple_cls is set up by PODMeta."""
303
f2 = Field(initial='default')
304
f3 = Field(initial_fn=lambda: m())
306
self.assertEqual(T.namedtuple_cls.__name__, 'T')
307
self.assertIsInstance(T.namedtuple_cls.f1, property)
308
self.assertIsInstance(T.namedtuple_cls.f2, property)
309
self.assertIsInstance(T.namedtuple_cls.f3, property)
311
def test_initializer_positional_arguments(self):
312
""".__init__() works correctly with positional arguments."""
317
f2 = Field(initial='default')
318
f3 = Field(initial_fn=lambda: m())
320
self.assertEqual(T().f1, None)
321
self.assertEqual(T().f2, "default")
322
self.assertEqual(T().f3, m())
323
self.assertEqual(T(1).f1, 1)
324
self.assertEqual(T(1).f2, 'default')
325
self.assertEqual(T(1).f3, m())
326
self.assertEqual(T(1, 2).f1, 1)
327
self.assertEqual(T(1, 2).f2, 2)
328
self.assertEqual(T(1, 2, 3).f3, 3)
330
def test_initializer_keyword_arguments(self):
331
""".__init__() works correctly with keyword arguments."""
336
f2 = Field(initial='default')
337
f3 = Field(initial_fn=lambda: m())
339
self.assertEqual(T().f1, None)
340
self.assertEqual(T().f2, "default")
341
self.assertEqual(T().f3, m())
342
self.assertEqual(T(f1=1).f1, 1)
343
self.assertEqual(T(f1=1).f2, 'default')
344
self.assertEqual(T(f1=1).f3, m())
345
self.assertEqual(T(f1=1, f2=2).f1, 1)
346
self.assertEqual(T(f1=1, f2=2).f2, 2)
347
self.assertEqual(T(f1=1, f2=2).f3, m())
348
self.assertEqual(T(f1=1, f2=2, f3=3).f1, 1)
349
self.assertEqual(T(f1=1, f2=2, f3=3).f2, 2)
350
self.assertEqual(T(f1=1, f2=2, f3=3).f3, 3)
352
def test_initializer_mandatory_arguments(self):
353
""".__init__() understands MANDATORY fields."""
355
m1 = Field(initial=MANDATORY)
356
m2 = Field(initial=MANDATORY)
358
with self.assertRaisesRegex(
359
TypeError, "mandatory argument missing: m1"):
361
with self.assertRaisesRegex(
362
TypeError, "mandatory argument missing: m1"):
364
with self.assertRaisesRegex(
365
TypeError, "mandatory argument missing: m2"):
367
with self.assertRaisesRegex(
368
TypeError, "mandatory argument missing: m2"):
371
def test_initializer_default_arguments(self):
372
""".__init__() understands initial (default) field values."""
374
f = Field(initial=42)
375
self.assertEqual(T().f, 42)
376
self.assertEqual(T(1).f, 1)
377
self.assertEqual(T(f=1).f, 1)
379
def test_initializer_duplicate_field_value(self):
380
""".__init__() prevents double-initialization."""
383
with self.assertRaisesRegex(
384
TypeError, "field initialized twice: f"):
387
def test_initializer_unknown_field(self):
388
""".__init__() prevents initializing unknown fields."""
391
with self.assertRaisesRegex(TypeError, "too many arguments"):
393
with self.assertRaisesRegex(TypeError, "no such field: f"):
396
def test_smoke(self):
397
"""Check that basic POD behavior works okay."""
403
return 'Mr. {}'.format(self.name)
405
class Employee(Person):
409
Person.field_list, [Person.name, Person.age])
410
joe = Employee('Joe')
412
self.assertEqual(str(joe), 'Mr. Joe')
413
# Reading attributes works
414
self.assertEqual(joe.name, 'Joe')
415
self.assertEqual(joe.age, None)
416
# Setting attributes works
418
self.assertEqual(joe.age, 42)
420
self.assertEqual(joe.salary, 1000)
421
# Comparison to other PODs works
422
self.assertEqual(joe, Employee('Joe', 42, 1000))
423
self.assertLess(joe, Employee('Joe', 45, 1000))
424
# The .as_{tuple,dict}() methods work
425
self.assertEqual(joe.as_tuple(), ('Joe', 42, 1000))
427
joe.as_dict(), {'name': 'Joe', 'age': 42, 'salary': 1000})
428
# The return value of repr is useful
430
repr(joe), "Employee(name='Joe', age=42, salary=1000)")
432
def test_as_dict_filters_out_UNSET(self):
433
""".as_dict() filters out UNSET values."""
439
self.assertEqual(p.as_dict(), {})
441
def test_notifications(self):
442
""".on_{field}_changed() gets fired by field modification."""
444
f = Field(notify=True)
446
field_callback = mock.Mock(name='field_callback')
447
# Create a POD and connect signal listeners
449
pod.on_f_changed.connect(field_callback)
452
# Ensure the modification worked
453
self.assertEqual(pod.f, 1)
454
# Ensure signals fired
455
field_callback.assert_called_with(None, 1)
457
def test_pod_inheritance(self):
458
"""Check that PODs can be subclassed and new fields can be added."""
460
f1 = Field(notify=True)
465
# D doesn't shadow B.f1
466
self.assertIs(B.on_f1_changed, D.on_f1_changed)
467
# B and D has correct field lists
468
self.assertEqual(B.field_list, [B.f1])
469
self.assertEqual(D.field_list, [B.f1, D.f2])
471
def test_pod_ordering(self):
472
"""Check that comparison among single POD class works okay."""
476
B = A # easier to understand subsequent tests
477
self.assertTrue(A(1) == B(1))
478
self.assertTrue(A(1) != B(0))
479
self.assertTrue(A(0) < B(1))
480
self.assertTrue(A(1) > B(0))
481
self.assertTrue(A(1) >= B(1))
482
self.assertTrue(A(1) <= B(1))
484
def test_pod_ordering_tricky1(self):
485
"""Check that comparison among different POD classes works okay."""
492
self.assertTrue(A(1) == B(1))
493
self.assertTrue(A(1) != B(0))
494
self.assertTrue(A(0) < B(1))
495
self.assertTrue(A(1) > B(0))
496
self.assertTrue(A(1) >= B(1))
497
self.assertTrue(A(1) <= B(1))
499
def test_pod_ordering_tricky2(self):
500
"""Check that comparison doesn't care about field names."""
507
self.assertTrue(A(1) == B(1))
508
self.assertTrue(A(1) != B(0))
509
self.assertTrue(A(0) < B(1))
510
self.assertTrue(A(1) > B(0))
511
self.assertTrue(A(1) >= B(1))
512
self.assertTrue(A(1) <= B(1))
514
def test_pod_ordering_other_types(self):
515
"""Check that comparison between POD and not-POD types doesn't work."""
519
self.assertFalse(A(1) == (1,))
520
self.assertFalse(A(1) == [1])
521
self.assertFalse(A(1) == 1)
524
class AssignFilterTests(TestCase):
526
"""Tests for assignment filters."""
528
def test_read_only_assign_filter(self):
529
"""The read_only_assign_filter works as designed."""
530
instance = mock.Mock(name='instance')
531
instance.__class__.__name__ = 'cls'
532
field = mock.Mock(name='field')
536
# The filter passes the initial data (when old is UNSET)
538
read_only_assign_filter(instance, field, UNSET, new), new)
539
# But rejects everything after that
540
with self.assertRaisesRegex(AttributeError, "cls.field is read-only"):
541
read_only_assign_filter(instance, field, old, new)
543
def test_type_convert_assign_filter(self):
544
"""The type_convert_assign_filter works as designed."""
545
instance = mock.Mock(name='instance')
546
old = mock.Mock(name='old')
547
field = mock.Mock(name='field')
549
# The filter converts values
551
type_convert_assign_filter(instance, field, old, '10'), 10)
552
# And can be used for crude type checking
553
msg = "invalid literal for int\\(\\) with base 10: 'hello\\?'"
554
with self.assertRaisesRegex(ValueError, msg):
555
type_convert_assign_filter(instance, field, old, 'hello?')
557
def test_type_check_assign_filter(self):
558
"""The type_check_assign_filter works as designed."""
559
instance = mock.Mock(name='instance')
560
instance.__class__.__name__ = 'cls'
561
old = mock.Mock(name='old')
562
field = mock.Mock(name='field')
565
# The filter type-checks values without any conversion
566
msg = "cls.field requires objects of type int"
567
with self.assertRaisesRegex(TypeError, msg):
568
type_check_assign_filter(instance, field, old, '10')
569
# The filter passes-through correctly-typed values
571
type_check_assign_filter(instance, field, old, 10), 10)
573
def test_sequence_type_check_assign_filter(self):
574
"""The sequence_type_check_assign_filter works as designed."""
575
instance = mock.Mock(name='instance')
576
instance.__class__.__name__ = 'cls'
577
old = mock.Mock(name='old')
578
field = mock.Mock(name='field')
580
# The filter type-checks values without any conversion
581
msg = "cls.field requires all sequence elements of type int"
582
with self.assertRaisesRegex(TypeError, msg):
583
sequence_type_check_assign_filter(int)(
584
instance, field, old, ['10'])
585
# The filter passes-through correctly-typed values
587
sequence_type_check_assign_filter(int)(
588
instance, field, old, [10, 20]),
591
sequence_type_check_assign_filter(int)(
592
instance, field, old, (10, 20,)),
595
def test_unset_or_type_check_assign_filter(self):
596
"""The unset_or_type_check_assign_filter works as designed."""
597
instance = mock.Mock(name='instance')
598
instance.__class__.__name__ = 'cls'
599
old = mock.Mock(name='old')
600
field = mock.Mock(name='field')
603
# The filter type-checks values without any conversion
604
msg = "cls.field requires objects of type int"
605
with self.assertRaisesRegex(TypeError, msg):
606
unset_or_type_check_assign_filter(instance, field, old, '10')
607
# The filter passes-through correctly-typed values
609
unset_or_type_check_assign_filter(instance, field, old, 10), 10)
610
# The filter also passes UNSET values.
612
unset_or_type_check_assign_filter(instance, field, old, UNSET),
615
def test_unset_or_sequence_type_check_assign_filter(self):
616
"""The unset_or_sequence_type_check_assign_filter works as designed."""
617
instance = mock.Mock(name='instance')
618
instance.__class__.__name__ = 'cls'
619
old = mock.Mock(name='old')
620
field = mock.Mock(name='field')
622
# The filter type-checks values without any conversion
623
msg = "cls.field requires all sequence elements of type int"
624
with self.assertRaisesRegex(TypeError, msg):
625
sequence_type_check_assign_filter(int)(
626
instance, field, old, ['10'])
627
# The filter passes-through correctly-typed values
629
unset_or_sequence_type_check_assign_filter(int)(
630
instance, field, old, [10, 20]),
633
unset_or_sequence_type_check_assign_filter(int)(
634
instance, field, old, (10, 20,)),
636
# The filter also passes UNSET values.
638
unset_or_sequence_type_check_assign_filter(int)(
639
instance, field, old, UNSET),