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