def _create_document(self, filename, getcwd, getctime, getmtime, _): app = Holocron({ 'paths': { 'content': './content', } }) app.add_converter(FakeConverter()) return content.create_document(filename, app)
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)
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)
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)
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), ])
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), ])
def _create_document(self, filename, getcwd, getctime, getmtime, _): app = Holocron({'paths': { 'content': './content', }}) app.add_converter(FakeConverter()) return content.create_document(filename, app)
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, [])
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)
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)])