bzr branch
/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) |