/update-manager/devel/gsoc09

To get this branch, use:
bzr branch https://alioth.debian.org/scm/loggerhead/update-manager/devel/gsoc09
1436 by Stephan Peijnik
Added interface checking infrastructure to unit test helpers.
1
# tests/_helpers.py
2
#
3
#  Copyright (c) 2009 Canonical
4
#                2009 Stephan Peijnik
5
#
6
#  Author: Stephan Peijnik <debian@sp.or.at>
7
#
8
#  This program is free software; you can redistribute it and/or
9
#  modify it under the terms of the GNU General Public License as
10
#  published by the Free Software Foundation; either version 2 of the
11
#  License, or (at your option) any later version.
12
#
13
#  This program is distributed in the hope that it will be useful,
14
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
15
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
#  GNU General Public License for more details.
17
#
18
#  You should have received a copy of the GNU General Public License
19
#  along with this program; if not, write to the Free Software
20
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
21
#  USA.
22
23
""" test helpers """
24
25
class ValidationError(Exception):
26
    """ Common validation error """
27
    pass
28
29
class NotSubclassError(ValidationError):
30
    """ Instance is not a subclass of given interface """
31
    def __init__(self, iface, inst):
32
        msg = '%s is not a subclass of %s, validation cannot be carried out' %\
33
            (inst.__name__, iface.__name__)
34
        ValidationError.__init__(self, msg)
35
36
class ValidationFailed(ValidationError):
37
    def __init__(self, ni_names, sig_names, typ_names):
38
        msg = 'VALIDATION FAILED\n\n'
39
        if len(ni_names):
40
            msg += 'not implemented   : %s\n\n' % ' '.join(ni_names)
41
        if len(sig_names):
42
            msg += 'signature mismatch: %s\n\n' % ' '.join(sig_names)
43
        if len(typ_names):
44
            msg += 'type mismatch     : %s\n\n' % ' '.join(typ_names)
45
        plural = ''
46
        error_count = len(ni_names) + len(sig_names) + len(typ_names)
47
        if error_count > 1:
48
            plural = 'S'
49
        msg += '%d ERROR%s FOUND' % (error_count, plural)
50
        ValidationError.__init__(self, msg)
51
52
class _InternalError(ValidationError):
53
    pass
54
55
class _NotImplementedError(_InternalError):
56
    pass
57
58
class _SignatureMismatch(_InternalError):
59
    pass
60
61
class _FunctionTypeMismatch(_InternalError):
62
    pass
63
64
class InterfaceValidator(object):
65
    """ Validates a given interface against an implementation """
66
    def __init__(self, interface, implementation):
67
        self._iface = interface
68
        self._impl = implementation
69
70
    @staticmethod
71
    def _validate_common(f_code, iface_code):
72
        if not InterfaceValidator._is_implemented(f_code):
73
            raise _NotImplementedError
74
        elif not InterfaceValidator._compare_signatures(f_code, iface_code):
75
            raise _SignatureMismatch
76
77
    @staticmethod
78
    def _compare_signatures(f_code, i_code):
79
        if f_code.co_argcount != i_code.co_argcount:
80
            return False
81
        return True
82
83
    def _validate_method(self, method_name, iface_code):
84
        """ Validates a single method """
85
        meth_code = self._get_method_code(getattr(self._impl, method_name))
86
        if not meth_code:
87
            raise _FunctionTypeMismatch
88
89
        InterfaceValidator._validate_common(meth_code, iface_code)
90
91
    def _validate_static(self, static_name, iface_code):
92
        """ Validates a single static method """
93
        static_code = self._get_static_code(getattr(self._impl, static_name))
94
        if not static_code:
95
            raise _FunctionTypeMismatch
96
        InterfaceValidator._validate_common(static_code, iface_code)
97
98
    @staticmethod
99
    def _is_implemented(f_code):
100
        """ Checks if a given function is implemented or only raises a
101
        NotImplementedError.
102
        """
103
        # Function used for comparing co_code
104
        def comparison_func():
105
            raise NotImplementedError
106
        if f_code.co_code == comparison_func.func_code.co_code:
107
            return False
108
        return True
109
110
    @staticmethod
111
    def _get_method_code(func_obj):
112
        """ Gets the func_code of a method """
113
        if callable(func_obj) and hasattr(func_obj, 'im_func'):
114
            return func_obj.im_func.func_code
115
        return None
116
117
    @staticmethod
118
    def _get_static_code(func_obj):
119
        """ Gets the func_code of a static method """
120
        if callable(func_obj) and not hasattr(func_obj, 'im_func'):
121
            return func_obj.func_code
122
        return None
123
124
    def validate(self):
125
        """ Validation logic """
126
        # Validation can only be carried out if _impl is a subclass/instance
127
        # of _iface.
128
        if not issubclass(self._impl, self._iface):
129
            raise NotSubclassError(self._iface, self._impl)
130
131
        # This function is used for checking if all mandatory methods of an 
132
        # interface have been implemented. Requires interface functions to
133
        # raise a NotImplementedError to work properly.
134
        def notimplemented(self):
135
            raise NotImplementedError
136
137
        # Find method names in interface and validate each one.
138
        not_implemented_names = []
139
        signature_mismatch_names = []
140
        type_mismatch_names = []
141
        for attr_name in dir(self._iface):
142
            attr = getattr(self._iface, attr_name)
143
            
144
            # All attributes starting with _ are considered private and
145
            # can be ignored.
146
            if attr_name[0] != '_' and callable(attr):
147
                # The attribute is callable, so it's likely a method
148
                meth = self._get_method_code(attr)
149
                static = self._get_static_code(attr)
150
                try:
151
                    if meth:
152
                        self._validate_method(attr_name, meth)
153
                    elif static:
154
                        self._validate_static(attr_name, static)
155
156
                except _NotImplementedError:
157
                    not_implemented_names.append(attr_name)
158
                except _SignatureMismatch:
159
                    signature_mismatch_names.append(attr_name)
160
                except _FunctionTypeMismatch:
161
                    type_mismatch_names.append(attr_name)
162
            
163
        if len(not_implemented_names) or len(signature_mismatch_names) or \
164
                len(type_mismatch_names):
165
            raise ValidationFailed(not_implemented_names, 
166
                                   signature_mismatch_names, 
167
                                   type_mismatch_names)
168
        
169
170
171
### UNIT TESTS for helpers
172
173
### mock interfaces and implementations
174
class TestIFace(object):
175
    def method(self, a, b):
176
        raise NotImplementedError
177
178
    @staticmethod
179
    def static(a, b):
180
        raise NotImplementedError
181
182
    def optional_method(self, a, b):
183
        pass
184
185
    @staticmethod
186
    def optional_static(a, b):
187
        pass
188
189
class CorrectImpl(TestIFace):
190
    def method(self, a, b):
191
        pass
192
193
    @staticmethod
194
    def static(a, b):
195
        pass
196
197
class CorrectImplWithOptional(CorrectImpl):
198
    def optional_method(self, a, b):
199
        pass
200
201
    @staticmethod
202
    def optional_static(a, b):
203
        pass
204
205
class CorrectBaseClass(TestIFace):
206
    pass
207
208
class IncorrectType0(TestIFace):
209
    @staticmethod
210
    def method(a, b):
211
        pass
212
    
213
    @staticmethod
214
    def static(a, b):
215
        pass
216
217
class IncorrectType1(TestIFace):
218
    def method(self, a, b):
219
        pass
220
221
    def static(self, a, b):
222
        pass
223
224
class IncorrectSignatureMethod(TestIFace):
225
    def method(self, a):
226
        pass
227
228
    @staticmethod
229
    def static(a, b):
230
        pass
231
232
class IncorrectSignatureStatic(TestIFace):
233
    def method(self, a, b):
234
        pass
235
236
    @staticmethod
237
    def static(a):
238
        pass
239
240
### the actual tests
241
import unittest
242
243
loader = unittest.TestLoader()
244
245
class InterfaceValidatorCase(unittest.TestCase):
246
    def test0_all_correct(self):
247
        try:
248
            InterfaceValidator(TestIFace, CorrectImpl).validate()
249
        except ValidationFailed, v_failed:
250
            self.fail('Validation of correct interface/implementation pair '+\
251
                          'failed:\n%s' % v_failed.message)
252
253
    def test1_all_correct_with_opt(self):
254
        try:
255
            InterfaceValidator(TestIFace, CorrectImplWithOptional).validate()
256
        except ValidationFailed, v_failed:
257
            self.fail('Validation of correct interface/implementation pair '+\
258
                          'failed:\n%s' % v_failed.message)
259
260
    def test2_incorrect_base_class(self):
261
        v = InterfaceValidator(TestIFace, object)
262
        self.assertRaises(NotSubclassError, v.validate)
263
264
    def test3_correct_base_class(self):
265
        v = InterfaceValidator(TestIFace, CorrectBaseClass)
266
        self.assertRaises(ValidationFailed, v.validate)
267
268
    def test4_incorrect_type(self):
269
        v = InterfaceValidator(TestIFace, IncorrectType0)
270
        self.assertRaises(ValidationFailed, v.validate)
271
        v = InterfaceValidator(TestIFace, IncorrectType1)
272
        self.assertRaises(ValidationFailed, v.validate)
273
274
    def test5_incorrect_signature(self):
275
        v = InterfaceValidator(TestIFace, IncorrectSignatureMethod)
276
        self.assertRaises(ValidationFailed, v.validate)
277
        v = InterfaceValidator(TestIFace, IncorrectSignatureStatic)
278
        self.assertRaises(ValidationFailed, v.validate)
279
280
InterfaceValidatorSuite = loader.loadTestsFromTestCase(InterfaceValidatorCase)