| 33 | 1 # -*- coding: utf-8 -*- | 
|  | 2 # | 
|  | 3 # test/test_metadata.py | 
|  | 4 # Part of ‘python-daemon’, an implementation of PEP 3143. | 
|  | 5 # | 
|  | 6 # Copyright © 2008–2015 Ben Finney <ben+python@benfinney.id.au> | 
|  | 7 # | 
|  | 8 # This is free software: you may copy, modify, and/or distribute this work | 
|  | 9 # under the terms of the Apache License, version 2.0 as published by the | 
|  | 10 # Apache Software Foundation. | 
|  | 11 # No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. | 
|  | 12 | 
|  | 13 """ Unit test for ‘_metadata’ private module. | 
|  | 14     """ | 
|  | 15 | 
|  | 16 from __future__ import (absolute_import, unicode_literals) | 
|  | 17 | 
|  | 18 import sys | 
|  | 19 import errno | 
|  | 20 import re | 
|  | 21 try: | 
|  | 22     # Python 3 standard library. | 
|  | 23     import urllib.parse as urlparse | 
|  | 24 except ImportError: | 
|  | 25     # Python 2 standard library. | 
|  | 26     import urlparse | 
|  | 27 import functools | 
|  | 28 import collections | 
|  | 29 import json | 
|  | 30 | 
|  | 31 import pkg_resources | 
|  | 32 import mock | 
|  | 33 import testtools.helpers | 
|  | 34 import testtools.matchers | 
|  | 35 import testscenarios | 
|  | 36 | 
|  | 37 from . import scaffold | 
|  | 38 from .scaffold import (basestring, unicode) | 
|  | 39 | 
|  | 40 import daemon._metadata as metadata | 
|  | 41 | 
|  | 42 | 
|  | 43 class HasAttribute(testtools.matchers.Matcher): | 
|  | 44     """ A matcher to assert an object has a named attribute. """ | 
|  | 45 | 
|  | 46     def __init__(self, name): | 
|  | 47         self.attribute_name = name | 
|  | 48 | 
|  | 49     def match(self, instance): | 
|  | 50         """ Assert the object `instance` has an attribute named `name`. """ | 
|  | 51         result = None | 
|  | 52         if not testtools.helpers.safe_hasattr(instance, self.attribute_name): | 
|  | 53             result = AttributeNotFoundMismatch(instance, self.attribute_name) | 
|  | 54         return result | 
|  | 55 | 
|  | 56 | 
|  | 57 class AttributeNotFoundMismatch(testtools.matchers.Mismatch): | 
|  | 58     """ The specified instance does not have the named attribute. """ | 
|  | 59 | 
|  | 60     def __init__(self, instance, name): | 
|  | 61         self.instance = instance | 
|  | 62         self.attribute_name = name | 
|  | 63 | 
|  | 64     def describe(self): | 
|  | 65         """ Emit a text description of this mismatch. """ | 
|  | 66         text = ( | 
|  | 67                 "{instance!r}" | 
|  | 68                 " has no attribute named {name!r}").format( | 
|  | 69                     instance=self.instance, name=self.attribute_name) | 
|  | 70         return text | 
|  | 71 | 
|  | 72 | 
|  | 73 class metadata_value_TestCase(scaffold.TestCaseWithScenarios): | 
|  | 74     """ Test cases for metadata module values. """ | 
|  | 75 | 
|  | 76     expected_str_attributes = set([ | 
|  | 77             'version_installed', | 
|  | 78             'author', | 
|  | 79             'copyright', | 
|  | 80             'license', | 
|  | 81             'url', | 
|  | 82             ]) | 
|  | 83 | 
|  | 84     scenarios = [ | 
|  | 85             (name, {'attribute_name': name}) | 
|  | 86             for name in expected_str_attributes] | 
|  | 87     for (name, params) in scenarios: | 
|  | 88         if name == 'version_installed': | 
|  | 89             # No duck typing, this attribute might be None. | 
|  | 90             params['ducktype_attribute_name'] = NotImplemented | 
|  | 91             continue | 
|  | 92         # Expect an attribute of ‘str’ to test this value. | 
|  | 93         params['ducktype_attribute_name'] = 'isdigit' | 
|  | 94 | 
|  | 95     def test_module_has_attribute(self): | 
|  | 96         """ Metadata should have expected value as a module attribute. """ | 
|  | 97         self.assertThat( | 
|  | 98                 metadata, HasAttribute(self.attribute_name)) | 
|  | 99 | 
|  | 100     def test_module_attribute_has_duck_type(self): | 
|  | 101         """ Metadata value should have expected duck-typing attribute. """ | 
|  | 102         if self.ducktype_attribute_name == NotImplemented: | 
|  | 103             self.skipTest("Can't assert this attribute's type") | 
|  | 104         instance = getattr(metadata, self.attribute_name) | 
|  | 105         self.assertThat( | 
|  | 106                 instance, HasAttribute(self.ducktype_attribute_name)) | 
|  | 107 | 
|  | 108 | 
|  | 109 class parse_person_field_TestCase( | 
|  | 110         testscenarios.WithScenarios, testtools.TestCase): | 
|  | 111     """ Test cases for ‘get_latest_version’ function. """ | 
|  | 112 | 
|  | 113     scenarios = [ | 
|  | 114             ('simple', { | 
|  | 115                 'test_person': "Foo Bar <foo.bar@example.com>", | 
|  | 116                 'expected_result': ("Foo Bar", "foo.bar@example.com"), | 
|  | 117                 }), | 
|  | 118             ('empty', { | 
|  | 119                 'test_person': "", | 
|  | 120                 'expected_result': (None, None), | 
|  | 121                 }), | 
|  | 122             ('none', { | 
|  | 123                 'test_person': None, | 
|  | 124                 'expected_error': TypeError, | 
|  | 125                 }), | 
|  | 126             ('no email', { | 
|  | 127                 'test_person': "Foo Bar", | 
|  | 128                 'expected_result': ("Foo Bar", None), | 
|  | 129                 }), | 
|  | 130             ] | 
|  | 131 | 
|  | 132     def test_returns_expected_result(self): | 
|  | 133         """ Should return expected result. """ | 
|  | 134         if hasattr(self, 'expected_error'): | 
|  | 135             self.assertRaises( | 
|  | 136                     self.expected_error, | 
|  | 137                     metadata.parse_person_field, self.test_person) | 
|  | 138         else: | 
|  | 139             result = metadata.parse_person_field(self.test_person) | 
|  | 140             self.assertEqual(self.expected_result, result) | 
|  | 141 | 
|  | 142 | 
|  | 143 class YearRange_TestCase(scaffold.TestCaseWithScenarios): | 
|  | 144     """ Test cases for ‘YearRange’ class. """ | 
|  | 145 | 
|  | 146     scenarios = [ | 
|  | 147             ('simple', { | 
|  | 148                 'begin_year': 1970, | 
|  | 149                 'end_year': 1979, | 
|  | 150                 'expected_text': "1970–1979", | 
|  | 151                 }), | 
|  | 152             ('same year', { | 
|  | 153                 'begin_year': 1970, | 
|  | 154                 'end_year': 1970, | 
|  | 155                 'expected_text': "1970", | 
|  | 156                 }), | 
|  | 157             ('no end year', { | 
|  | 158                 'begin_year': 1970, | 
|  | 159                 'end_year': None, | 
|  | 160                 'expected_text': "1970", | 
|  | 161                 }), | 
|  | 162             ] | 
|  | 163 | 
|  | 164     def setUp(self): | 
|  | 165         """ Set up test fixtures. """ | 
|  | 166         super(YearRange_TestCase, self).setUp() | 
|  | 167 | 
|  | 168         self.test_instance = metadata.YearRange( | 
|  | 169                 self.begin_year, self.end_year) | 
|  | 170 | 
|  | 171     def test_text_representation_as_expected(self): | 
|  | 172         """ Text representation should be as expected. """ | 
|  | 173         result = unicode(self.test_instance) | 
|  | 174         self.assertEqual(result, self.expected_text) | 
|  | 175 | 
|  | 176 | 
|  | 177 FakeYearRange = collections.namedtuple('FakeYearRange', ['begin', 'end']) | 
|  | 178 | 
|  | 179 @mock.patch.object(metadata, 'YearRange', new=FakeYearRange) | 
|  | 180 class make_year_range_TestCase(scaffold.TestCaseWithScenarios): | 
|  | 181     """ Test cases for ‘make_year_range’ function. """ | 
|  | 182 | 
|  | 183     scenarios = [ | 
|  | 184             ('simple', { | 
|  | 185                 'begin_year': "1970", | 
|  | 186                 'end_date': "1979-01-01", | 
|  | 187                 'expected_range': FakeYearRange(begin=1970, end=1979), | 
|  | 188                 }), | 
|  | 189             ('same year', { | 
|  | 190                 'begin_year': "1970", | 
|  | 191                 'end_date': "1970-01-01", | 
|  | 192                 'expected_range': FakeYearRange(begin=1970, end=1970), | 
|  | 193                 }), | 
|  | 194             ('no end year', { | 
|  | 195                 'begin_year': "1970", | 
|  | 196                 'end_date': None, | 
|  | 197                 'expected_range': FakeYearRange(begin=1970, end=None), | 
|  | 198                 }), | 
|  | 199             ('end date UNKNOWN token', { | 
|  | 200                 'begin_year': "1970", | 
|  | 201                 'end_date': "UNKNOWN", | 
|  | 202                 'expected_range': FakeYearRange(begin=1970, end=None), | 
|  | 203                 }), | 
|  | 204             ('end date FUTURE token', { | 
|  | 205                 'begin_year': "1970", | 
|  | 206                 'end_date': "FUTURE", | 
|  | 207                 'expected_range': FakeYearRange(begin=1970, end=None), | 
|  | 208                 }), | 
|  | 209             ] | 
|  | 210 | 
|  | 211     def test_result_matches_expected_range(self): | 
|  | 212         """ Result should match expected YearRange. """ | 
|  | 213         result = metadata.make_year_range(self.begin_year, self.end_date) | 
|  | 214         self.assertEqual(result, self.expected_range) | 
|  | 215 | 
|  | 216 | 
|  | 217 class metadata_content_TestCase(scaffold.TestCase): | 
|  | 218     """ Test cases for content of metadata. """ | 
|  | 219 | 
|  | 220     def test_copyright_formatted_correctly(self): | 
|  | 221         """ Copyright statement should be formatted correctly. """ | 
|  | 222         regex_pattern = ( | 
|  | 223                 "Copyright © " | 
|  | 224                 "\d{4}" # four-digit year | 
|  | 225                 "(?:–\d{4})?" # optional range dash and ending four-digit year | 
|  | 226                 ) | 
|  | 227         regex_flags = re.UNICODE | 
|  | 228         self.assertThat( | 
|  | 229                 metadata.copyright, | 
|  | 230                 testtools.matchers.MatchesRegex(regex_pattern, regex_flags)) | 
|  | 231 | 
|  | 232     def test_author_formatted_correctly(self): | 
|  | 233         """ Author information should be formatted correctly. """ | 
|  | 234         regex_pattern = ( | 
|  | 235                 ".+ " # name | 
|  | 236                 "<[^>]+>" # email address, in angle brackets | 
|  | 237                 ) | 
|  | 238         regex_flags = re.UNICODE | 
|  | 239         self.assertThat( | 
|  | 240                 metadata.author, | 
|  | 241                 testtools.matchers.MatchesRegex(regex_pattern, regex_flags)) | 
|  | 242 | 
|  | 243     def test_copyright_contains_author(self): | 
|  | 244         """ Copyright information should contain author information. """ | 
|  | 245         self.assertThat( | 
|  | 246                 metadata.copyright, | 
|  | 247                 testtools.matchers.Contains(metadata.author)) | 
|  | 248 | 
|  | 249     def test_url_parses_correctly(self): | 
|  | 250         """ Homepage URL should parse correctly. """ | 
|  | 251         result = urlparse.urlparse(metadata.url) | 
|  | 252         self.assertIsInstance( | 
|  | 253                 result, urlparse.ParseResult, | 
|  | 254                 "URL value {url!r} did not parse correctly".format( | 
|  | 255                     url=metadata.url)) | 
|  | 256 | 
|  | 257 | 
|  | 258 try: | 
|  | 259     FileNotFoundError | 
|  | 260 except NameError: | 
|  | 261     # Python 2 uses IOError. | 
|  | 262     FileNotFoundError = functools.partial(IOError, errno.ENOENT) | 
|  | 263 | 
|  | 264 version_info_filename = "version_info.json" | 
|  | 265 | 
|  | 266 def fake_func_has_metadata(testcase, resource_name): | 
|  | 267     """ Fake the behaviour of ‘pkg_resources.Distribution.has_metadata’. """ | 
|  | 268     if ( | 
|  | 269             resource_name != testcase.expected_resource_name | 
|  | 270             or not hasattr(testcase, 'test_version_info')): | 
|  | 271         return False | 
|  | 272     return True | 
|  | 273 | 
|  | 274 | 
|  | 275 def fake_func_get_metadata(testcase, resource_name): | 
|  | 276     """ Fake the behaviour of ‘pkg_resources.Distribution.get_metadata’. """ | 
|  | 277     if not fake_func_has_metadata(testcase, resource_name): | 
|  | 278         error = FileNotFoundError(resource_name) | 
|  | 279         raise error | 
|  | 280     content = testcase.test_version_info | 
|  | 281     return content | 
|  | 282 | 
|  | 283 | 
|  | 284 def fake_func_get_distribution(testcase, distribution_name): | 
|  | 285     """ Fake the behaviour of ‘pkg_resources.get_distribution’. """ | 
|  | 286     if distribution_name != metadata.distribution_name: | 
|  | 287         raise pkg_resources.DistributionNotFound | 
|  | 288     if hasattr(testcase, 'get_distribution_error'): | 
|  | 289         raise testcase.get_distribution_error | 
|  | 290     mock_distribution = testcase.mock_distribution | 
|  | 291     mock_distribution.has_metadata.side_effect = functools.partial( | 
|  | 292             fake_func_has_metadata, testcase) | 
|  | 293     mock_distribution.get_metadata.side_effect = functools.partial( | 
|  | 294             fake_func_get_metadata, testcase) | 
|  | 295     return mock_distribution | 
|  | 296 | 
|  | 297 | 
|  | 298 @mock.patch.object(metadata, 'distribution_name', new="mock-dist") | 
|  | 299 class get_distribution_version_info_TestCase(scaffold.TestCaseWithScenarios): | 
|  | 300     """ Test cases for ‘get_distribution_version_info’ function. """ | 
|  | 301 | 
|  | 302     default_version_info = { | 
|  | 303             'release_date': "UNKNOWN", | 
|  | 304             'version': "UNKNOWN", | 
|  | 305             'maintainer': "UNKNOWN", | 
|  | 306             } | 
|  | 307 | 
|  | 308     scenarios = [ | 
|  | 309             ('version 0.0', { | 
|  | 310                 'test_version_info': json.dumps({ | 
|  | 311                     'version': "0.0", | 
|  | 312                     }), | 
|  | 313                 'expected_version_info': {'version': "0.0"}, | 
|  | 314                 }), | 
|  | 315             ('version 1.0', { | 
|  | 316                 'test_version_info': json.dumps({ | 
|  | 317                     'version': "1.0", | 
|  | 318                     }), | 
|  | 319                 'expected_version_info': {'version': "1.0"}, | 
|  | 320                 }), | 
|  | 321             ('file lorem_ipsum.json', { | 
|  | 322                 'version_info_filename': "lorem_ipsum.json", | 
|  | 323                 'test_version_info': json.dumps({ | 
|  | 324                     'version': "1.0", | 
|  | 325                     }), | 
|  | 326                 'expected_version_info': {'version': "1.0"}, | 
|  | 327                 }), | 
|  | 328             ('not installed', { | 
|  | 329                 'get_distribution_error': pkg_resources.DistributionNotFound(), | 
|  | 330                 'expected_version_info': default_version_info, | 
|  | 331                 }), | 
|  | 332             ('no version_info', { | 
|  | 333                 'expected_version_info': default_version_info, | 
|  | 334                 }), | 
|  | 335             ] | 
|  | 336 | 
|  | 337     def setUp(self): | 
|  | 338         """ Set up test fixtures. """ | 
|  | 339         super(get_distribution_version_info_TestCase, self).setUp() | 
|  | 340 | 
|  | 341         if hasattr(self, 'expected_resource_name'): | 
|  | 342             self.test_args = {'filename': self.expected_resource_name} | 
|  | 343         else: | 
|  | 344             self.test_args = {} | 
|  | 345             self.expected_resource_name = version_info_filename | 
|  | 346 | 
|  | 347         self.mock_distribution = mock.MagicMock() | 
|  | 348         func_patcher_get_distribution = mock.patch.object( | 
|  | 349                 pkg_resources, 'get_distribution') | 
|  | 350         func_patcher_get_distribution.start() | 
|  | 351         self.addCleanup(func_patcher_get_distribution.stop) | 
|  | 352         pkg_resources.get_distribution.side_effect = functools.partial( | 
|  | 353                 fake_func_get_distribution, self) | 
|  | 354 | 
|  | 355     def test_requests_installed_distribution(self): | 
|  | 356         """ The package distribution should be retrieved. """ | 
|  | 357         expected_distribution_name = metadata.distribution_name | 
|  | 358         version_info = metadata.get_distribution_version_info(**self.test_args) | 
|  | 359         pkg_resources.get_distribution.assert_called_with( | 
|  | 360                 expected_distribution_name) | 
|  | 361 | 
|  | 362     def test_requests_specified_filename(self): | 
|  | 363         """ The specified metadata resource name should be requested. """ | 
|  | 364         if hasattr(self, 'get_distribution_error'): | 
|  | 365             self.skipTest("No access to distribution") | 
|  | 366         version_info = metadata.get_distribution_version_info(**self.test_args) | 
|  | 367         self.mock_distribution.has_metadata.assert_called_with( | 
|  | 368                 self.expected_resource_name) | 
|  | 369 | 
|  | 370     def test_result_matches_expected_items(self): | 
|  | 371         """ The result should match the expected items. """ | 
|  | 372         version_info = metadata.get_distribution_version_info(**self.test_args) | 
|  | 373         self.assertEqual(self.expected_version_info, version_info) | 
|  | 374 | 
|  | 375 | 
|  | 376 # Local variables: | 
|  | 377 # coding: utf-8 | 
|  | 378 # mode: python | 
|  | 379 # End: | 
|  | 380 # vim: fileencoding=utf-8 filetype=python : |