示例#1
0
 def _create_document(self, filename, getcwd, getctime, getmtime, _):
     app = Holocron({
         'paths': {
             'content': './content',
         }
     })
     app.add_converter(FakeConverter())
     return content.create_document(filename, app)
示例#2
0
class DocumentTestCase(HolocronTestCase):
    """
    A testcase helper that prepares a document instance.
    """

    _getcwd = 'cwd'         # fake current working dir, used by abspath
    _getctime = 662739000   # UTC: 1991/01/01 2:10pm
    _getmtime = 1420121400  # UTC: 2015/01/01 2:10pm

    _conf = Conf({
        'site': {
            'url': 'http://example.com',
        },
        'encoding': {
            'content': 'cont-enc',
            'output': 'out-enc',
        },
        'paths': {
            'content': './content',
            'output': './_output',
        }
    })

    document_class = None       # a document constructor
    document_filename = None    # a document filename, relative to the content

    @mock.patch('holocron.content.os.path.getmtime', return_value=_getmtime)
    @mock.patch('holocron.content.os.path.getctime', return_value=_getctime)
    @mock.patch('holocron.content.os.getcwd', return_value=_getcwd)
    def setUp(self, getcwd, getctime, getmtime):
        """
        Prepares a document instance with a fake config.
        """
        filename = os.path.join(
            self._conf['paths.content'], self.document_filename)

        self.app = Holocron(self._conf)
        self.app.add_converter(FakeConverter())

        self.doc = self.document_class(filename, self.app)
示例#3
0
class DocumentTestCase(HolocronTestCase):
    """
    A testcase helper that prepares a document instance.
    """

    _getcwd = 'cwd'  # fake current working dir, used by abspath
    _getctime = 662739000  # UTC: 1991/01/01 2:10pm
    _getmtime = 1420121400  # UTC: 2015/01/01 2:10pm

    _conf = Conf({
        'site': {
            'url': 'http://example.com',
        },
        'encoding': {
            'content': 'cont-enc',
            'output': 'out-enc',
        },
        'paths': {
            'content': './content',
            'output': './_output',
        }
    })

    document_class = None  # a document constructor
    document_filename = None  # a document filename, relative to the content

    @mock.patch('holocron.content.os.path.getmtime', return_value=_getmtime)
    @mock.patch('holocron.content.os.path.getctime', return_value=_getctime)
    @mock.patch('holocron.content.os.getcwd', return_value=_getcwd)
    def setUp(self, getcwd, getctime, getmtime):
        """
        Prepares a document instance with a fake config.
        """
        filename = os.path.join(self._conf['paths.content'],
                                self.document_filename)

        self.app = Holocron(self._conf)
        self.app.add_converter(FakeConverter())

        self.doc = self.document_class(filename, self.app)
示例#4
0
class TestFeedGenerator(HolocronTestCase):
    """
    Test feed generator.
    """

    def setUp(self):
        self.app = Holocron(conf={
            'site': {
                'title': 'MyTestSite',
                'author': 'Tester',
                'url': 'http://www.mytest.com/',
            },

            'encoding': {
                'output': 'my-enc',
            },

            'paths': {
                'output': 'path/to/output',
            },

            'ext': {
                'enabled': [],
                'feed': {
                    'save_as': 'myfeed.xml',
                    'posts_number': 3,
                },
            },
        })
        self.feed = Feed(self.app)

        self.date_early = datetime(2012, 2, 2)
        self.date_moderate = datetime(2013, 4, 1)
        self.date_late = datetime(2014, 6, 12)

        self.date_early_updated = datetime(2012, 12, 6)
        self.date_moderate_updated = datetime(2013, 12, 6)
        self.date_late_updated = datetime(2014, 12, 6)

        self.post_early = mock.Mock(
            spec=Post,
            published=self.date_early,
            updated_local=self.date_early_updated,
            abs_url='http://www.post_early.com',
            title='MyEarlyPost')

        self.post_moderate = mock.Mock(
            spec=Post,
            published=self.date_moderate,
            updated_local=self.date_moderate_updated,
            abs_url='http://www.post_moderate.com')

        self.post_late = mock.Mock(
            spec=Post,
            published=self.date_late,
            updated_local=self.date_late_updated,
            url='www.post_late.com',
            abs_url='http://www.post_late.com',
            title='MyTestPost')

        self.late_id = '<id>http://www.post_late.com</id>'
        self.moderate_id = '<id>http://www.post_moderate.com</id>'
        self.early_id = '<id>http://www.post_early.com</id>'

        self.page = mock.Mock(spec=Page, url='www.page.com')
        self.static = mock.Mock(spec=Static, url='www.image.com')

        self.open_fn = 'holocron.ext.feed.open'

    @mock.patch('holocron.ext.feed.mkdir', mock.Mock())
    def _get_content(self, documents):
        """
        This helper method mocks the open function and returns the content
        passed as input to write function.
        """
        with mock.patch(self.open_fn, mock.mock_open(), create=True) as mopen:
            self.feed.generate(documents)

            content, = mopen().write.call_args[0]
            return content

    def _xml_to_dict(self, xml):
        """
        Generates and returns python dict from an xml string passed as input.
        """
        parsed = minidom.parseString(xml)
        root = parsed.documentElement

        #: use this to sort DOM Elements from DOM Text containing \n and spaces
        def is_element(n):
            return n.nodeType == n.ELEMENT_NODE

        #: use this to parse <link> which contain attributes instead of values
        def is_attribute(n):
            return len(n.attributes) != 0

        #: use this to distinguish feed common elements (link, title) from post
        def has_child(n):
            return len(n.childNodes) < 2

        urls = [url for url in filter(is_element, root.childNodes)]

        entries = {}
        url_data = {}
        #: use to store dictionnaries with post data
        posts = []

        #: links in feed differ by attribute (rel or alt), use this attribute
        #: as a key to avoid dicts with same key
        key = '{link}.{fmt}'.format

        for url in urls:
            if has_child(url):
                if is_attribute(url):
                    link = key(link=url.nodeName, fmt=url.getAttribute('rel'))
                    entries[link] = url.getAttribute('href')
                else:
                    entries[url.nodeName] = url.firstChild.nodeValue
            else:
                for attr in filter(is_element, url.childNodes):
                    if is_attribute(attr):
                        url_data[attr.nodeName] = attr.getAttribute('href')
                    else:
                        url_data[attr.nodeName] = attr.firstChild.nodeValue

                posts.append(url_data)
                entries[url.nodeName] = posts

        content = {root.nodeName: entries}

        return content

    @mock.patch('holocron.ext.feed.mkdir', mock.Mock())
    def test_feed_filename_and_enc(self):
        """
        Feed function has to save feed xml file to a proper location and with
        proper filename. All settings are fetched from the configuration file.
        """
        with mock.patch(self.open_fn, mock.mock_open(), create=True) as mopen:
            self.feed.generate([])

            self.assertEqual(
                mopen.call_args[0][0], 'path/to/output/myfeed.xml')
            self.assertEqual(
                mopen.call_args[1]['encoding'], 'my-enc')

    def test_feed_encoding_attr(self):
        """
        The feed.xml has to have an XML tag with right encoding.
        """
        output = self._get_content([])

        self.assertIn('encoding="my-enc"', output)

    def test_feed_template(self):
        """
        Test that feed writes correct values to an xml template.
        """
        content = self._get_content([])
        content = self._xml_to_dict(content)

        feed = content['feed']

        self.assertEqual('MyTestSite', feed['title'])
        self.assertEqual('http://www.mytest.com/', feed['id'])
        self.assertEqual('http://www.mytest.com/myfeed.xml', feed['link.self'])
        self.assertEqual('http://www.mytest.com/', feed['link.alternate'])

    def test_feed_empty(self):
        """
        Feed runned on an empty list of documents has to create an xml file, no
        posts should be listed there.
        """
        content = self._get_content([])
        content = self._xml_to_dict(content)

        self.assertNotIn('entry', content['feed'])

    def test_feed_with_posts(self):
        """
        Feed function has to f*****g work.
        """
        # here we require only one post to test its content correctness
        # we test posts in other test suites
        self.feed._conf['posts_number'] = 1

        content = self._get_content([self.post_early, self.post_late])
        content = self._xml_to_dict(content)

        self.assertIn('entry', content['feed'])
        self.assertEqual(len(content['feed']['entry']), 1)

        feed = content['feed']['entry'][0]

        self.assertEqual('http://www.post_late.com', feed['link'])
        self.assertEqual(self.date_late_updated.isoformat(), feed['updated'])

        self.assertEqual(self.date_late.isoformat(), feed['published'])

        self.assertEqual('http://www.post_late.com', feed['id'])
        self.assertEqual('MyTestPost', feed['title'])

    def test_posts_in_front_order(self):
        """
        Tests posts ordering. Feed must display older posts first.
        """
        posts = [self.post_early, self.post_moderate, self.post_late]
        content = self._get_content(posts)

        #: test that the latest post comes first
        self.assertIn(self.late_id, content)

        #: delete information about the latest post
        post_position = content.index(self.late_id) + len(self.late_id)
        content = content[post_position:]

        self.assertIn(self.moderate_id, content)

        #: another strim to delete next post
        post_position = content.index(self.moderate_id) + len(self.moderate_id)
        content = content[post_position:]

        self.assertIn(self.early_id, content)

    def test_posts_in_reverse_order(self):
        """
        Tests posts ordering. Feed must display older posts first.
        """
        posts = [self.post_late, self.post_moderate, self.post_early]
        content = self._get_content(posts)

        #: test that the latest post comes first
        self.assertIn(self.late_id, content)

        #: delete information about the latest post
        post_position = content.index(self.late_id) + len(self.late_id)
        content = content[post_position:]

        self.assertIn(self.moderate_id, content)

        #: another strim to delete next post
        post_position = content.index(self.moderate_id) + len(self.moderate_id)
        content = content[post_position:]

        self.assertIn(self.early_id, content)

    def test_mixed_documents(self):
        """
        Test that feed generator sorts out post documents out of other types.
        """
        documents = [self.page, self.post_late, self.static]
        content = self._get_content(documents)

        self.assertNotIn('www.page.com', content)
        self.assertNotIn('www.image.com', content)
        self.assertIn('www.post_late.com', content)

    @mock.patch('holocron.content.os.mkdir', mock.Mock())
    @mock.patch('holocron.content.os.path.getmtime')
    @mock.patch('holocron.content.os.path.getctime')
    def test_feed_link_in_html_header(self, _, __):
        """
        Test that html pages have the link to feed.
        """
        # since we're interested in rendered page, let's register
        # a fake converter for that purpose
        self.app.add_converter(FakeConverter())

        open_fn = 'holocron.content.open'
        with mock.patch(open_fn, mock.mock_open(read_data=''), create=True):
            page = Page('filename.fake', self.app)

        with mock.patch(open_fn, mock.mock_open(), create=True) as mopen:
            page.build()
            content = mopen().write.call_args[0][0]

        err = 'could not find link to feed in html header'
        self.assertIn(
            '<link rel="alternate" type="application/atom+xml" '
            'href="http://www.mytest.com/myfeed.xml" title="MyTestSite">',
            content, err)
示例#5
0
class TestHolocron(HolocronTestCase):
    def setUp(self):
        self.app = Holocron({
            'ext': {
                'enabled': [],
            },
        })

    def test_user_settings(self):
        """
        Tests creating an instance with custom settings: check for settings
        overriding.
        """
        app = Holocron({
            'sitename': 'Luke Skywalker',
            'paths': {
                'content': 'path/to/content',
            },
        })

        conf = copy.deepcopy(app.default_conf)
        conf['sitename'] = 'Luke Skywalker'
        conf['paths']['content'] = 'path/to/content'

        self.assertEqual(app.conf, conf)

    def test_add_converter(self):
        """
        Tests converter registration process.
        """
        class TestConverter(abc.Converter):
            extensions = ['.tst', '.test']

            def to_html(self, text):
                return {}, text

        # test registration process
        converter = TestConverter()
        self.assertEqual(len(self.app._converters), 0)
        self.app.add_converter(converter)
        self.assertEqual(len(self.app._converters), 2)

        self.assertIn('.tst', self.app._converters)
        self.assertIn('.test', self.app._converters)

        self.assertIs(self.app._converters['.tst'], converter)
        self.assertIs(self.app._converters['.test'], converter)

        # test protection from double registration
        self.app.add_converter(TestConverter())
        self.assertIs(self.app._converters['.tst'], converter)
        self.assertIs(self.app._converters['.test'], converter)

        # test force registration
        new_converter = TestConverter()
        self.app.add_converter(new_converter, _force=True)
        self.assertIs(self.app._converters['.tst'], new_converter)
        self.assertIs(self.app._converters['.test'], new_converter)

    def test_add_generator(self):
        """
        Tests generator registration process.
        """
        class TestGenerator(abc.Generator):
            def generate(self, text):
                pass

        # test registration process
        generator = TestGenerator()
        self.assertEqual(len(self.app._generators), 0)
        self.app.add_generator(generator)
        self.assertEqual(len(self.app._generators), 1)
        self.assertIn(generator, self.app._generators)

        # test double registration is allowed
        new_generator = TestGenerator()
        self.app.add_generator(new_generator)
        self.assertEqual(len(self.app._generators), 2)
        self.assertIn(new_generator, self.app._generators)

    @mock.patch('holocron.app.mkdir')
    @mock.patch('holocron.app.iterfiles')
    def test_run(self, iterfiles, mkdir):
        """
        Tests build process.
        """
        iterfiles.return_value = ['doc_a', 'doc_b', 'doc_c']
        self.app.__class__.document_factory = mock.Mock()
        self.app._copy_theme = mock.Mock()
        self.app._generators = {
            mock.Mock(): mock.Mock(),
            mock.Mock(): mock.Mock(),
        }

        self.app.run()

        # check iterfiles call signature
        iterfiles.assert_called_with(self.app.conf['paths']['content'],
                                     '[!_.]*', True)

        # check mkdir create ourpur dir
        mkdir.assert_called_with(self.app.conf['paths.output'])

        # check that generators was used
        for generator in self.app._generators:
            self.assertEqual(generator.generate.call_count, 1)

        self.app.__class__.document_factory.assert_has_calls([
            # check that document class was used to generate class instances
            mock.call('doc_a', self.app),
            mock.call('doc_b', self.app),
            mock.call('doc_c', self.app),
            # check that document instances were built
            mock.call().build(),
            mock.call().build(),
            mock.call().build(),
        ])
        self.assertEqual(self.app.__class__.document_factory.call_count, 3)

        # check that _copy_theme was called
        self.app._copy_theme.assert_called_once_with()

    @mock.patch('holocron.app.copy_tree')
    def test_copy_base_theme(self, mcopytree):
        """
        Tests that Holocron do copy default theme.
        """
        output = os.path.join(self.app.conf['paths.output'], 'static')
        theme = os.path.join(os.path.dirname(holocron.__file__), 'theme',
                             'static')

        self.app._copy_theme()

        mcopytree.assert_called_with(theme, output)

    @mock.patch('holocron.app.os.path.exists', return_value=True)
    @mock.patch('holocron.app.copy_tree')
    def test_copy_user_themes(self, mcopytree, _):
        """
        Tests that Holocron do copy user theme.
        """
        output = os.path.join(self.app.conf['paths.output'], 'static')
        theme = os.path.join(os.path.dirname(holocron.__file__), 'theme',
                             'static')

        self.app.add_theme('theme1')
        self.app.add_theme('theme2')
        self.app._copy_theme()

        self.assertEqual(mcopytree.call_args_list, [
            mock.call(theme, output),
            mock.call(os.path.join('theme1', 'static'), output),
            mock.call(os.path.join('theme2', 'static'), output),
        ])

    @mock.patch('holocron.app.os.path.exists', side_effect=[True, False])
    @mock.patch('holocron.app.copy_tree')
    def test_copy_user_themes_not_exist(self, mcopytree, _):
        """
        Tests that Holocron doesn't copy static if it's not exist.
        """
        output = os.path.join(self.app.conf['paths.output'], 'static')
        theme = os.path.join(os.path.dirname(holocron.__file__), 'theme',
                             'static')

        self.app.add_theme('theme1')
        self.app._copy_theme()

        self.assertEqual(mcopytree.call_args_list, [
            mock.call(theme, output),
        ])
示例#6
0
class TestHolocron(HolocronTestCase):

    def setUp(self):
        self.app = Holocron({
            'ext': {
                'enabled': [],
            },
        })

    def test_user_settings(self):
        """
        Tests creating an instance with custom settings: check for settings
        overriding.
        """
        app = Holocron({
            'sitename': 'Luke Skywalker',
            'paths': {
                'content': 'path/to/content',
            },
        })

        conf = copy.deepcopy(app.default_conf)
        conf['sitename'] = 'Luke Skywalker'
        conf['paths']['content'] = 'path/to/content'

        self.assertEqual(app.conf, conf)

    def test_add_converter(self):
        """
        Tests converter registration process.
        """
        class TestConverter(abc.Converter):
            extensions = ['.tst', '.test']

            def to_html(self, text):
                return {}, text

        # test registration process
        converter = TestConverter()
        self.assertEqual(len(self.app._converters), 0)
        self.app.add_converter(converter)
        self.assertEqual(len(self.app._converters), 2)

        self.assertIn('.tst', self.app._converters)
        self.assertIn('.test', self.app._converters)

        self.assertIs(self.app._converters['.tst'], converter)
        self.assertIs(self.app._converters['.test'], converter)

        # test protection from double registration
        self.app.add_converter(TestConverter())
        self.assertIs(self.app._converters['.tst'], converter)
        self.assertIs(self.app._converters['.test'], converter)

        # test force registration
        new_converter = TestConverter()
        self.app.add_converter(new_converter, _force=True)
        self.assertIs(self.app._converters['.tst'], new_converter)
        self.assertIs(self.app._converters['.test'], new_converter)

    def test_add_generator(self):
        """
        Tests generator registration process.
        """
        class TestGenerator(abc.Generator):
            def generate(self, text):
                pass

        # test registration process
        generator = TestGenerator()
        self.assertEqual(len(self.app._generators), 0)
        self.app.add_generator(generator)
        self.assertEqual(len(self.app._generators), 1)
        self.assertIn(generator, self.app._generators)

        # test double registration is allowed
        new_generator = TestGenerator()
        self.app.add_generator(new_generator)
        self.assertEqual(len(self.app._generators), 2)
        self.assertIn(new_generator, self.app._generators)

    @mock.patch('holocron.app.mkdir')
    @mock.patch('holocron.app.iterfiles')
    def test_run(self, iterfiles, mkdir):
        """
        Tests build process.
        """
        iterfiles.return_value = ['doc_a', 'doc_b', 'doc_c']
        self.app.__class__.document_factory = mock.Mock()
        self.app._copy_theme = mock.Mock()
        self.app._generators = {
            mock.Mock(): mock.Mock(),
            mock.Mock(): mock.Mock(),
        }

        self.app.run()

        # check iterfiles call signature
        iterfiles.assert_called_with(
            self.app.conf['paths']['content'], '[!_.]*', True)

        # check mkdir create ourpur dir
        mkdir.assert_called_with(self.app.conf['paths.output'])

        # check that generators was used
        for generator in self.app._generators:
            self.assertEqual(generator.generate.call_count, 1)

        self.app.__class__.document_factory.assert_has_calls([
            # check that document class was used to generate class instances
            mock.call('doc_a', self.app),
            mock.call('doc_b', self.app),
            mock.call('doc_c', self.app),
            # check that document instances were built
            mock.call().build(),
            mock.call().build(),
            mock.call().build(),
        ])
        self.assertEqual(self.app.__class__.document_factory.call_count, 3)

        # check that _copy_theme was called
        self.app._copy_theme.assert_called_once_with()

    @mock.patch('holocron.app.copy_tree')
    def test_copy_base_theme(self, mcopytree):
        """
        Tests that Holocron do copy default theme.
        """
        output = os.path.join(self.app.conf['paths.output'], 'static')
        theme = os.path.join(
            os.path.dirname(holocron.__file__), 'theme', 'static')

        self.app._copy_theme()

        mcopytree.assert_called_with(theme, output)

    @mock.patch('holocron.app.os.path.exists', return_value=True)
    @mock.patch('holocron.app.copy_tree')
    def test_copy_user_themes(self, mcopytree, _):
        """
        Tests that Holocron do copy user theme.
        """
        output = os.path.join(self.app.conf['paths.output'], 'static')
        theme = os.path.join(
            os.path.dirname(holocron.__file__), 'theme', 'static')

        self.app.add_theme('theme1')
        self.app.add_theme('theme2')
        self.app._copy_theme()

        self.assertEqual(mcopytree.call_args_list, [
            mock.call(theme, output),
            mock.call(os.path.join('theme1', 'static'), output),
            mock.call(os.path.join('theme2', 'static'), output),
        ])

    @mock.patch('holocron.app.os.path.exists', side_effect=[True, False])
    @mock.patch('holocron.app.copy_tree')
    def test_copy_user_themes_not_exist(self, mcopytree, _):
        """
        Tests that Holocron doesn't copy static if it's not exist.
        """
        output = os.path.join(self.app.conf['paths.output'], 'static')
        theme = os.path.join(
            os.path.dirname(holocron.__file__), 'theme', 'static')

        self.app.add_theme('theme1')
        self.app._copy_theme()

        self.assertEqual(mcopytree.call_args_list, [
            mock.call(theme, output),
        ])
示例#7
0
 def _create_document(self, filename, getcwd, getctime, getmtime, _):
     app = Holocron({'paths': {
         'content': './content',
     }})
     app.add_converter(FakeConverter())
     return content.create_document(filename, app)
示例#8
0
class TestTagsGenerator(HolocronTestCase):
    """
    Test tags generator.
    """
    #: generate html headers with years that is used for grouping in templates
    h_year = '<span class="year">{0}</span>'.format

    def setUp(self):
        self.app = Holocron(conf={
            'sitename': 'MyTestSite',
            'siteurl': 'www.mytest.com',
            'author': 'Tester',

            'encoding': {
                'output': 'my-enc',
            },

            'paths': {
                'output': 'path/to/output',
            },

            'ext': {
                'enabled': [],
                'tags': {
                    'output': 'mypath/tags/{tag}',
                },
            },
        })
        self.tags = Tags(self.app)

        self.date_early = datetime(2012, 2, 2)
        self.date_moderate = datetime(2013, 4, 1)
        self.date_late = datetime(2014, 6, 12)

        self.post_early = mock.Mock(
            spec=Post,
            published=self.date_early,
            tags=['testtag1', 'testtag2'],
            title='MyTestPost',
            url='www.post_early.com')

        self.post_moderate = mock.Mock(
            spec=Post,
            published=self.date_moderate,
            url='www.post_moderate.com',
            tags=['testtag2', 'testtag3'])

        self.post_late = mock.Mock(
            spec=Post,
            published=self.date_late,
            url='www.post_late.com',
            tags=['testtag2'])

        self.post_malformed = mock.Mock(
            spec=Post,
            short_source='test',
            tags='testtag')

        self.page = mock.Mock(spec=Page)
        self.static = mock.Mock(spec=Static)

        self.open_fn = 'holocron.ext.tags.open'

    @mock.patch('holocron.ext.tags.mkdir', mock.Mock())
    def _get_content(self, documents):
        """
        This helper method mocks the open function and return the content
        passed as input to write function.
        """
        with mock.patch(self.open_fn, mock.mock_open(), create=True) as mopen:
            self.tags.generate(documents)

            # extract what was generated and what was passed to f.write()
            content, = mopen().write.call_args[0]
            return content

    @mock.patch('holocron.ext.tags.mkdir')
    def _get_tags_content(self, documents, mock_mkdir):
        with mock.patch(self.open_fn, mock.mock_open(), create=True) as mopen:
            self.tags.generate(documents)

            open_calls = [c[0][0] for c in mopen.call_args_list if c != '']
            write_calls = [c[0][0] for c in mopen().write.call_args_list]
            mkdir_calls = [c[0][0] for c in mock_mkdir.call_args_list]

            # form a tuple that contains corresponding open and write calls
            # and sort it aplhabetically, so the testtag1 comes first
            content = list(zip(open_calls, write_calls, mkdir_calls))
            content.sort()

            return content

    def test_tag_template_building(self):
        """
        Test that tags function writes a post to a tag template.
        """
        posts = [self.post_early, self.post_moderate]

        content = self._get_tags_content(posts)

        # check that open, write and mkdir functions were called three times
        # according to the number of different tags in test documents
        for entry in content:
            self.assertEqual(len(entry), 3)

        # test that output html contains early_post with its unique tag
        self.assertEqual(
            'path/to/output/mypath/tags/testtag1/index.html', content[0][0])

        self.assertIn(self.h_year(2012), content[0][1])
        self.assertIn('<a href="www.post_early.com">', content[0][1])

        self.assertEqual('path/to/output/mypath/tags/testtag1', content[0][2])

        # test that output html contains posts with common tag
        self.assertEqual(
            'path/to/output/mypath/tags/testtag2/index.html', content[1][0])

        self.assertIn(self.h_year(2012), content[1][1])
        self.assertIn(self.h_year(2013), content[1][1])

        self.assertIn('<a href="www.post_early.com">', content[1][1])
        self.assertIn('<a href="www.post_moderate.com">', content[1][1])

        self.assertEqual('path/to/output/mypath/tags/testtag2', content[1][2])

        # test that output html contains moderate_post with its unique tag
        self.assertEqual(
            'path/to/output/mypath/tags/testtag3/index.html', content[2][0])

        self.assertIn(self.h_year(2013), content[2][1])
        self.assertIn('<a href="www.post_moderate.com">', content[2][1])

        self.assertEqual('path/to/output/mypath/tags/testtag3', content[2][2])

    def test_sorting_out_mixed_documents(self):
        """
        Test that Tags sorts out post documents from documents of other types.
        """
        documents = [self.page, self.static, self.post_late]
        content = self._get_content(documents)

        self.assertIn(self.h_year(2014), content)
        self.assertIn('<a href="www.post_late.com">', content)

    def test_posts_patching_with_tag_objects(self):
        """
        Test that Tags patches post's tags attribute.
        """
        posts = [self.post_late]

        self._get_tags_content(posts)

        self.assertEquals(self.post_late.tags[0].name, 'testtag2')
        self.assertEquals(self.post_late.tags[0].url, '/mypath/tags/testtag2/')

    @mock.patch('holocron.content.os.mkdir', mock.Mock())
    @mock.patch('holocron.content.os.path.getmtime')
    @mock.patch('holocron.content.os.path.getctime')
    def test_tags_are_shown_in_post(self, _, __):
        """
        Test that tags are actually get to the output.
        """
        # since we're interested in rendered page, let's register
        # a fake converter for that purpose
        self.app.add_converter(FakeConverter())

        data = textwrap.dedent('''\
            ---
            tags: [tag1, tag2]
            ---

            some text''')

        open_fn = 'holocron.content.open'
        with mock.patch(open_fn, mock.mock_open(read_data=data), create=True):
            post = Post('2015/05/23/filename.fake', self.app)

        self._get_content([post])

        with mock.patch(open_fn, mock.mock_open(), create=True) as mopen:
            post.build()
            content = mopen().write.call_args[0][0]

        err = 'Could not find link for #tag1.'
        self.assertIn('<a href="/mypath/tags/tag1/">#tag1</a>', content, err)

        err = 'Could not find link for #tag2.'
        self.assertIn('<a href="/mypath/tags/tag2/">#tag2</a>', content, err)

    @mock.patch('holocron.ext.tags.mkdir')
    def test_malformed_tags_are_skipped(self, mock_mkdir):
        """
        Test if tags formatting is correct.
        """
        content = self._get_tags_content([self.post_malformed])

        self.assertEqual(content, [])
示例#9
0
class TestFeedGenerator(HolocronTestCase):
    """
    Test feed generator.
    """
    def setUp(self):
        self.app = Holocron(
            conf={
                'site': {
                    'title': 'MyTestSite',
                    'author': 'Tester',
                    'url': 'http://www.mytest.com/',
                },
                'encoding': {
                    'output': 'my-enc',
                },
                'paths': {
                    'output': 'path/to/output',
                },
                'ext': {
                    'enabled': [],
                    'feed': {
                        'save_as': 'myfeed.xml',
                        'posts_number': 3,
                    },
                },
            })
        self.feed = Feed(self.app)

        self.date_early = datetime(2012, 2, 2)
        self.date_moderate = datetime(2013, 4, 1)
        self.date_late = datetime(2014, 6, 12)

        self.date_early_updated = datetime(2012, 12, 6)
        self.date_moderate_updated = datetime(2013, 12, 6)
        self.date_late_updated = datetime(2014, 12, 6)

        self.post_early = mock.Mock(spec=Post,
                                    published=self.date_early,
                                    updated_local=self.date_early_updated,
                                    abs_url='http://www.post_early.com',
                                    title='MyEarlyPost')

        self.post_moderate = mock.Mock(
            spec=Post,
            published=self.date_moderate,
            updated_local=self.date_moderate_updated,
            abs_url='http://www.post_moderate.com')

        self.post_late = mock.Mock(spec=Post,
                                   published=self.date_late,
                                   updated_local=self.date_late_updated,
                                   url='www.post_late.com',
                                   abs_url='http://www.post_late.com',
                                   title='MyTestPost')

        self.late_id = '<id>http://www.post_late.com</id>'
        self.moderate_id = '<id>http://www.post_moderate.com</id>'
        self.early_id = '<id>http://www.post_early.com</id>'

        self.page = mock.Mock(spec=Page, url='www.page.com')
        self.static = mock.Mock(spec=Static, url='www.image.com')

        self.open_fn = 'holocron.ext.feed.open'

    @mock.patch('holocron.ext.feed.mkdir', mock.Mock())
    def _get_content(self, documents):
        """
        This helper method mocks the open function and returns the content
        passed as input to write function.
        """
        with mock.patch(self.open_fn, mock.mock_open(), create=True) as mopen:
            self.feed.generate(documents)

            content, = mopen().write.call_args[0]
            return content

    def _xml_to_dict(self, xml):
        """
        Generates and returns python dict from an xml string passed as input.
        """
        parsed = minidom.parseString(xml)
        root = parsed.documentElement

        #: use this to sort DOM Elements from DOM Text containing \n and spaces
        def is_element(n):
            return n.nodeType == n.ELEMENT_NODE

        #: use this to parse <link> which contain attributes instead of values
        def is_attribute(n):
            return len(n.attributes) != 0

        #: use this to distinguish feed common elements (link, title) from post
        def has_child(n):
            return len(n.childNodes) < 2

        urls = [url for url in filter(is_element, root.childNodes)]

        entries = {}
        url_data = {}
        #: use to store dictionnaries with post data
        posts = []

        #: links in feed differ by attribute (rel or alt), use this attribute
        #: as a key to avoid dicts with same key
        key = '{link}.{fmt}'.format

        for url in urls:
            if has_child(url):
                if is_attribute(url):
                    link = key(link=url.nodeName, fmt=url.getAttribute('rel'))
                    entries[link] = url.getAttribute('href')
                else:
                    entries[url.nodeName] = url.firstChild.nodeValue
            else:
                for attr in filter(is_element, url.childNodes):
                    if is_attribute(attr):
                        url_data[attr.nodeName] = attr.getAttribute('href')
                    else:
                        url_data[attr.nodeName] = attr.firstChild.nodeValue

                posts.append(url_data)
                entries[url.nodeName] = posts

        content = {root.nodeName: entries}

        return content

    @mock.patch('holocron.ext.feed.mkdir', mock.Mock())
    def test_feed_filename_and_enc(self):
        """
        Feed function has to save feed xml file to a proper location and with
        proper filename. All settings are fetched from the configuration file.
        """
        with mock.patch(self.open_fn, mock.mock_open(), create=True) as mopen:
            self.feed.generate([])

            self.assertEqual(mopen.call_args[0][0],
                             'path/to/output/myfeed.xml')
            self.assertEqual(mopen.call_args[1]['encoding'], 'my-enc')

    def test_feed_encoding_attr(self):
        """
        The feed.xml has to have an XML tag with right encoding.
        """
        output = self._get_content([])

        self.assertIn('encoding="my-enc"', output)

    def test_feed_template(self):
        """
        Test that feed writes correct values to an xml template.
        """
        content = self._get_content([])
        content = self._xml_to_dict(content)

        feed = content['feed']

        self.assertEqual('MyTestSite', feed['title'])
        self.assertEqual('http://www.mytest.com/', feed['id'])
        self.assertEqual('http://www.mytest.com/myfeed.xml', feed['link.self'])
        self.assertEqual('http://www.mytest.com/', feed['link.alternate'])

    def test_feed_empty(self):
        """
        Feed runned on an empty list of documents has to create an xml file, no
        posts should be listed there.
        """
        content = self._get_content([])
        content = self._xml_to_dict(content)

        self.assertNotIn('entry', content['feed'])

    def test_feed_with_posts(self):
        """
        Feed function has to f*****g work.
        """
        # here we require only one post to test its content correctness
        # we test posts in other test suites
        self.feed._conf['posts_number'] = 1

        content = self._get_content([self.post_early, self.post_late])
        content = self._xml_to_dict(content)

        self.assertIn('entry', content['feed'])
        self.assertEqual(len(content['feed']['entry']), 1)

        feed = content['feed']['entry'][0]

        self.assertEqual('http://www.post_late.com', feed['link'])
        self.assertEqual(self.date_late_updated.isoformat(), feed['updated'])

        self.assertEqual(self.date_late.isoformat(), feed['published'])

        self.assertEqual('http://www.post_late.com', feed['id'])
        self.assertEqual('MyTestPost', feed['title'])

    def test_posts_in_front_order(self):
        """
        Tests posts ordering. Feed must display older posts first.
        """
        posts = [self.post_early, self.post_moderate, self.post_late]
        content = self._get_content(posts)

        #: test that the latest post comes first
        self.assertIn(self.late_id, content)

        #: delete information about the latest post
        post_position = content.index(self.late_id) + len(self.late_id)
        content = content[post_position:]

        self.assertIn(self.moderate_id, content)

        #: another strim to delete next post
        post_position = content.index(self.moderate_id) + len(self.moderate_id)
        content = content[post_position:]

        self.assertIn(self.early_id, content)

    def test_posts_in_reverse_order(self):
        """
        Tests posts ordering. Feed must display older posts first.
        """
        posts = [self.post_late, self.post_moderate, self.post_early]
        content = self._get_content(posts)

        #: test that the latest post comes first
        self.assertIn(self.late_id, content)

        #: delete information about the latest post
        post_position = content.index(self.late_id) + len(self.late_id)
        content = content[post_position:]

        self.assertIn(self.moderate_id, content)

        #: another strim to delete next post
        post_position = content.index(self.moderate_id) + len(self.moderate_id)
        content = content[post_position:]

        self.assertIn(self.early_id, content)

    def test_mixed_documents(self):
        """
        Test that feed generator sorts out post documents out of other types.
        """
        documents = [self.page, self.post_late, self.static]
        content = self._get_content(documents)

        self.assertNotIn('www.page.com', content)
        self.assertNotIn('www.image.com', content)
        self.assertIn('www.post_late.com', content)

    @mock.patch('holocron.content.os.mkdir', mock.Mock())
    @mock.patch('holocron.content.os.path.getmtime')
    @mock.patch('holocron.content.os.path.getctime')
    def test_feed_link_in_html_header(self, _, __):
        """
        Test that html pages have the link to feed.
        """
        # since we're interested in rendered page, let's register
        # a fake converter for that purpose
        self.app.add_converter(FakeConverter())

        open_fn = 'holocron.content.open'
        with mock.patch(open_fn, mock.mock_open(read_data=''), create=True):
            page = Page('filename.fake', self.app)

        with mock.patch(open_fn, mock.mock_open(), create=True) as mopen:
            page.build()
            content = mopen().write.call_args[0][0]

        err = 'could not find link to feed in html header'
        self.assertIn(
            '<link rel="alternate" type="application/atom+xml" '
            'href="http://www.mytest.com/myfeed.xml" title="MyTestSite">',
            content, err)
示例#10
0
class TestHolocron(HolocronTestCase):
    def setUp(self):
        self.app = Holocron({"ext": {"enabled": []}})

    def test_user_settings(self):
        """
        Tests creating an instance with custom settings: check for settings
        overriding.
        """
        app = Holocron({"sitename": "Luke Skywalker", "paths": {"content": "path/to/content"}})

        conf = copy.deepcopy(app.default_conf)
        conf["sitename"] = "Luke Skywalker"
        conf["paths"]["content"] = "path/to/content"

        self.assertEqual(app.conf, conf)

    def test_add_converter(self):
        """
        Tests converter registration process.
        """

        class TestConverter(abc.Converter):
            extensions = [".tst", ".test"]

            def to_html(self, text):
                return {}, text

        # test registration process
        converter = TestConverter()
        self.assertEqual(len(self.app._converters), 0)
        self.app.add_converter(converter)
        self.assertEqual(len(self.app._converters), 2)

        self.assertIn(".tst", self.app._converters)
        self.assertIn(".test", self.app._converters)

        self.assertIs(self.app._converters[".tst"], converter)
        self.assertIs(self.app._converters[".test"], converter)

        # test protection from double registration
        self.app.add_converter(TestConverter())
        self.assertIs(self.app._converters[".tst"], converter)
        self.assertIs(self.app._converters[".test"], converter)

        # test force registration
        new_converter = TestConverter()
        self.app.add_converter(new_converter, _force=True)
        self.assertIs(self.app._converters[".tst"], new_converter)
        self.assertIs(self.app._converters[".test"], new_converter)

    def test_add_generator(self):
        """
        Tests generator registration process.
        """

        class TestGenerator(abc.Generator):
            def generate(self, text):
                pass

        # test registration process
        generator = TestGenerator()
        self.assertEqual(len(self.app._generators), 0)
        self.app.add_generator(generator)
        self.assertEqual(len(self.app._generators), 1)
        self.assertIn(generator, self.app._generators)

        # test double registration is allowed
        new_generator = TestGenerator()
        self.app.add_generator(new_generator)
        self.assertEqual(len(self.app._generators), 2)
        self.assertIn(new_generator, self.app._generators)

    @mock.patch("holocron.app.mkdir")
    @mock.patch("holocron.app.iterfiles")
    def test_run(self, iterfiles, mkdir):
        """
        Tests build process.
        """
        iterfiles.return_value = ["doc_a", "doc_b", "doc_c"]
        self.app.__class__.document_factory = mock.Mock()
        self.app._copy_theme = mock.Mock()
        self.app._generators = {mock.Mock(): mock.Mock(), mock.Mock(): mock.Mock()}

        self.app.run()

        # check iterfiles call signature
        iterfiles.assert_called_with(self.app.conf["paths"]["content"], "[!_.]*", True)

        # check mkdir create ourpur dir
        mkdir.assert_called_with(self.app.conf["paths.output"])

        # check that generators was used
        for generator in self.app._generators:
            self.assertEqual(generator.generate.call_count, 1)

        self.app.__class__.document_factory.assert_has_calls(
            [
                # check that document class was used to generate class instances
                mock.call("doc_a", self.app),
                mock.call("doc_b", self.app),
                mock.call("doc_c", self.app),
                # check that document instances were built
                mock.call().build(),
                mock.call().build(),
                mock.call().build(),
            ]
        )
        self.assertEqual(self.app.__class__.document_factory.call_count, 3)

        # check that _copy_theme was called
        self.app._copy_theme.assert_called_once_with()

    @mock.patch("holocron.app.copy_tree")
    def test_copy_base_theme(self, mcopytree):
        """
        Tests that Holocron do copy default theme.
        """
        output = os.path.join(self.app.conf["paths.output"], "static")
        theme = os.path.join(os.path.dirname(holocron.__file__), "theme", "static")

        self.app._copy_theme()

        mcopytree.assert_called_with(theme, output)

    @mock.patch("holocron.app.os.path.exists", return_value=True)
    @mock.patch("holocron.app.copy_tree")
    def test_copy_user_themes(self, mcopytree, _):
        """
        Tests that Holocron do copy user theme.
        """
        output = os.path.join(self.app.conf["paths.output"], "static")
        theme = os.path.join(os.path.dirname(holocron.__file__), "theme", "static")

        self.app.add_theme("theme1")
        self.app.add_theme("theme2")
        self.app._copy_theme()

        self.assertEqual(
            mcopytree.call_args_list,
            [
                mock.call(theme, output),
                mock.call(os.path.join("theme1", "static"), output),
                mock.call(os.path.join("theme2", "static"), output),
            ],
        )

    @mock.patch("holocron.app.os.path.exists", side_effect=[True, False])
    @mock.patch("holocron.app.copy_tree")
    def test_copy_user_themes_not_exist(self, mcopytree, _):
        """
        Tests that Holocron doesn't copy static if it's not exist.
        """
        output = os.path.join(self.app.conf["paths.output"], "static")
        theme = os.path.join(os.path.dirname(holocron.__file__), "theme", "static")

        self.app.add_theme("theme1")
        self.app._copy_theme()

        self.assertEqual(mcopytree.call_args_list, [mock.call(theme, output)])