#!/usr/bin/env python from unittest import mock import unittest from mkdocs.structure.pages import Page from mkdocs.structure.files import File, Files from mkdocs.structure.nav import get_navigation from mkdocs.commands import build from mkdocs.tests.base import load_config, tempdir, PathAssertionMixin from mkdocs.utils import meta def build_page(title, path, config, md_src=''): """ Helper which returns a Page object. """ files = Files([File(path, config['docs_dir'], config['site_dir'], config['use_directory_urls'])]) page = Page(title, list(files)[0], config) # Fake page.read_source() page.markdown, page.meta = meta.get_data(md_src) return page, files class BuildTests(PathAssertionMixin, unittest.TestCase): def assert_mock_called_once(self, mock): """assert that the mock was called only once. The `mock.assert_called_once()` method was added in PY36. TODO: Remove this when PY35 support is dropped. """ try: mock.assert_called_once() except AttributeError: if not mock.call_count == 1: msg = ("Expected '%s' to have been called once. Called %s times." % (mock._mock_name or 'mock', self.call_count)) raise AssertionError(msg) # Test build.get_context def test_context_base_url_homepage(self): nav_cfg = [ {'Home': 'index.md'} ] cfg = load_config(nav=nav_cfg, use_directory_urls=False) files = Files([ File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), ]) nav = get_navigation(files, cfg) context = build.get_context(nav, files, cfg, nav.pages[0]) self.assertEqual(context['base_url'], '.') def test_context_base_url_homepage_use_directory_urls(self): nav_cfg = [ {'Home': 'index.md'} ] cfg = load_config(nav=nav_cfg) files = Files([ File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), ]) nav = get_navigation(files, cfg) context = build.get_context(nav, files, cfg, nav.pages[0]) self.assertEqual(context['base_url'], '.') def test_context_base_url_nested_page(self): nav_cfg = [ {'Home': 'index.md'}, {'Nested': 'foo/bar.md'} ] cfg = load_config(nav=nav_cfg, use_directory_urls=False) files = Files([ File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) ]) nav = get_navigation(files, cfg) context = build.get_context(nav, files, cfg, nav.pages[1]) self.assertEqual(context['base_url'], '..') def test_context_base_url_nested_page_use_directory_urls(self): nav_cfg = [ {'Home': 'index.md'}, {'Nested': 'foo/bar.md'} ] cfg = load_config(nav=nav_cfg) files = Files([ File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) ]) nav = get_navigation(files, cfg) context = build.get_context(nav, files, cfg, nav.pages[1]) self.assertEqual(context['base_url'], '../..') def test_context_base_url_relative_no_page(self): cfg = load_config(use_directory_urls=False) context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..') self.assertEqual(context['base_url'], '..') def test_context_base_url_relative_no_page_use_directory_urls(self): cfg = load_config() context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..') self.assertEqual(context['base_url'], '..') def test_context_base_url_absolute_no_page(self): cfg = load_config(use_directory_urls=False) context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/') self.assertEqual(context['base_url'], '/') def test_context_base_url__absolute_no_page_use_directory_urls(self): cfg = load_config() context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/') self.assertEqual(context['base_url'], '/') def test_context_base_url_absolute_nested_no_page(self): cfg = load_config(use_directory_urls=False) context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/') self.assertEqual(context['base_url'], '/foo/') def test_context_base_url__absolute_nested_no_page_use_directory_urls(self): cfg = load_config() context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/') self.assertEqual(context['base_url'], '/foo/') def test_context_extra_css_js_from_homepage(self): nav_cfg = [ {'Home': 'index.md'} ] cfg = load_config( nav=nav_cfg, extra_css=['style.css'], extra_javascript=['script.js'], use_directory_urls=False ) files = Files([ File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), ]) nav = get_navigation(files, cfg) context = build.get_context(nav, files, cfg, nav.pages[0]) self.assertEqual(context['extra_css'], ['style.css']) self.assertEqual(context['extra_javascript'], ['script.js']) def test_context_extra_css_js_from_nested_page(self): nav_cfg = [ {'Home': 'index.md'}, {'Nested': 'foo/bar.md'} ] cfg = load_config( nav=nav_cfg, extra_css=['style.css'], extra_javascript=['script.js'], use_directory_urls=False ) files = Files([ File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) ]) nav = get_navigation(files, cfg) context = build.get_context(nav, files, cfg, nav.pages[1]) self.assertEqual(context['extra_css'], ['../style.css']) self.assertEqual(context['extra_javascript'], ['../script.js']) def test_context_extra_css_js_from_nested_page_use_directory_urls(self): nav_cfg = [ {'Home': 'index.md'}, {'Nested': 'foo/bar.md'} ] cfg = load_config( nav=nav_cfg, extra_css=['style.css'], extra_javascript=['script.js'] ) files = Files([ File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) ]) nav = get_navigation(files, cfg) context = build.get_context(nav, files, cfg, nav.pages[1]) self.assertEqual(context['extra_css'], ['../../style.css']) self.assertEqual(context['extra_javascript'], ['../../script.js']) def test_context_extra_css_js_no_page(self): cfg = load_config(extra_css=['style.css'], extra_javascript=['script.js']) context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..') self.assertEqual(context['extra_css'], ['../style.css']) self.assertEqual(context['extra_javascript'], ['../script.js']) def test_extra_context(self): cfg = load_config(extra={'a': 1}) context = build.get_context(mock.Mock(), mock.Mock(), cfg) self.assertEqual(context['config']['extra']['a'], 1) # Test build._build_theme_template @mock.patch('mkdocs.utils.write_file') @mock.patch('mkdocs.commands.build._build_template', return_value='some content') def test_build_theme_template(self, mock_build_template, mock_write_file): cfg = load_config() env = cfg['theme'].get_env() build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock()) self.assert_mock_called_once(mock_write_file) self.assert_mock_called_once(mock_build_template) @mock.patch('mkdocs.utils.write_file') @mock.patch('mkdocs.commands.build._build_template', return_value='some content') @mock.patch('gzip.GzipFile') def test_build_sitemap_template(self, mock_gzip_gzipfile, mock_build_template, mock_write_file): cfg = load_config() env = cfg['theme'].get_env() build._build_theme_template('sitemap.xml', env, mock.Mock(), cfg, mock.Mock()) self.assert_mock_called_once(mock_write_file) self.assert_mock_called_once(mock_build_template) self.assert_mock_called_once(mock_gzip_gzipfile) @mock.patch('mkdocs.utils.write_file') @mock.patch('mkdocs.commands.build._build_template', return_value='') def test_skip_missing_theme_template(self, mock_build_template, mock_write_file): cfg = load_config() env = cfg['theme'].get_env() with self.assertLogs('mkdocs', level='WARN') as cm: build._build_theme_template('missing.html', env, mock.Mock(), cfg, mock.Mock()) self.assertEqual( cm.output, ["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in theme directories."] ) mock_write_file.assert_not_called() mock_build_template.assert_not_called() @mock.patch('mkdocs.utils.write_file') @mock.patch('mkdocs.commands.build._build_template', return_value='') def test_skip_theme_template_empty_output(self, mock_build_template, mock_write_file): cfg = load_config() env = cfg['theme'].get_env() with self.assertLogs('mkdocs', level='INFO') as cm: build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock()) self.assertEqual( cm.output, ["INFO:mkdocs.commands.build:Template skipped: 'main.html' generated empty output."] ) mock_write_file.assert_not_called() self.assert_mock_called_once(mock_build_template) # Test build._build_extra_template @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content')) def test_build_extra_template(self): cfg = load_config() files = Files([ File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), ]) build._build_extra_template('foo.html', files, cfg, mock.Mock()) @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content')) def test_skip_missing_extra_template(self): cfg = load_config() files = Files([ File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), ]) with self.assertLogs('mkdocs', level='INFO') as cm: build._build_extra_template('missing.html', files, cfg, mock.Mock()) self.assertEqual( cm.output, ["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in docs_dir."] ) @mock.patch('mkdocs.commands.build.open', side_effect=OSError('Error message.')) def test_skip_ioerror_extra_template(self, mock_open): cfg = load_config() files = Files([ File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), ]) with self.assertLogs('mkdocs', level='INFO') as cm: build._build_extra_template('foo.html', files, cfg, mock.Mock()) self.assertEqual( cm.output, ["WARNING:mkdocs.commands.build:Error reading template 'foo.html': Error message."] ) @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='')) def test_skip_extra_template_empty_output(self): cfg = load_config() files = Files([ File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']), ]) with self.assertLogs('mkdocs', level='INFO') as cm: build._build_extra_template('foo.html', files, cfg, mock.Mock()) self.assertEqual( cm.output, ["INFO:mkdocs.commands.build:Template skipped: 'foo.html' generated empty output."] ) # Test build._populate_page @tempdir(files={'index.md': 'page content'}) def test_populate_page(self, docs_dir): cfg = load_config(docs_dir=docs_dir) file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) page = Page('Foo', file, cfg) build._populate_page(page, cfg, Files([file])) self.assertEqual(page.content, '
page content
') @tempdir(files={'testing.html': 'page content
'}) def test_populate_page_dirty_modified(self, site_dir): cfg = load_config(site_dir=site_dir) file = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) page = Page('Foo', file, cfg) build._populate_page(page, cfg, Files([file]), dirty=True) self.assertTrue(page.markdown.startswith('# Welcome to MkDocs')) self.assertTrue(page.content.startswith('page content
'}) def test_populate_page_dirty_not_modified(self, site_dir, docs_dir): cfg = load_config(docs_dir=docs_dir, site_dir=site_dir) file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) page = Page('Foo', file, cfg) build._populate_page(page, cfg, Files([file]), dirty=True) # Content is empty as file read was skipped self.assertEqual(page.markdown, None) self.assertEqual(page.content, None) @tempdir(files={'index.md': 'new page content'}) @mock.patch('mkdocs.structure.pages.open', side_effect=OSError('Error message.')) def test_populate_page_read_error(self, docs_dir, mock_open): cfg = load_config(docs_dir=docs_dir) file = File('missing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) page = Page('Foo', file, cfg) with self.assertLogs('mkdocs', level='ERROR') as cm: self.assertRaises(OSError, build._populate_page, page, cfg, Files([file])) self.assertEqual( cm.output, [ 'ERROR:mkdocs.structure.pages:File not found: missing.md', "ERROR:mkdocs.commands.build:Error reading page 'missing.md': Error message." ] ) self.assert_mock_called_once(mock_open) # Test build._build_page @tempdir() def test_build_page(self, site_dir): cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[]) files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]) nav = get_navigation(files, cfg) page = files.documentation_pages()[0].page # Fake populate page page.title = 'Title' page.markdown = 'page content' page.content = 'page content
' build._build_page(page, cfg, files, nav, cfg['theme'].get_env()) self.assertPathIsFile(site_dir, 'index.html') # TODO: fix this. It seems that jinja2 chokes on the mock object. Not sure how to resolve. # @tempdir() # @mock.patch('jinja2.environment.Template') # def test_build_page_empty(self, site_dir, mock_template): # mock_template.render = mock.Mock(return_value='') # cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[]) # files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]) # nav = get_navigation(files, cfg) # page = files.documentation_pages()[0].page # # Fake populate page # page.title = '' # page.markdown = '' # page.content = '' # with self.assertLogs('mkdocs', level='INFO') as cm: # build._build_page(page, cfg, files, nav, cfg['theme'].get_env()) # self.assertEqual( # cm.output, # ["INFO:mkdocs.commands.build:Page skipped: 'index.md'. Generated empty output."] # ) # self.assert_mock_called_once(mock_template.render) # self.assertPathNotFile(site_dir, 'index.html') @tempdir(files={'index.md': 'page content'}) @tempdir(files={'index.html': 'page content
'}) @mock.patch('mkdocs.utils.write_file') def test_build_page_dirty_modified(self, site_dir, docs_dir, mock_write_file): cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, nav=['index.md'], plugins=[]) files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]) nav = get_navigation(files, cfg) page = files.documentation_pages()[0].page # Fake populate page page.title = 'Title' page.markdown = 'new page content' page.content = 'new page content
' build._build_page(page, cfg, files, nav, cfg['theme'].get_env(), dirty=True) mock_write_file.assert_not_called() @tempdir(files={'testing.html': 'page content
'}) @mock.patch('mkdocs.utils.write_file') def test_build_page_dirty_not_modified(self, site_dir, mock_write_file): cfg = load_config(site_dir=site_dir, nav=['testing.md'], plugins=[]) files = Files([File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]) nav = get_navigation(files, cfg) page = files.documentation_pages()[0].page # Fake populate page page.title = 'Title' page.markdown = 'page content' page.content = 'page content
' build._build_page(page, cfg, files, nav, cfg['theme'].get_env(), dirty=True) self.assert_mock_called_once(mock_write_file) @tempdir() def test_build_page_custom_template(self, site_dir): cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[]) files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]) nav = get_navigation(files, cfg) page = files.documentation_pages()[0].page # Fake populate page page.title = 'Title' page.meta = {'template': '404.html'} page.markdown = 'page content' page.content = 'page content
' build._build_page(page, cfg, files, nav, cfg['theme'].get_env()) self.assertPathIsFile(site_dir, 'index.html') @tempdir() @mock.patch('mkdocs.utils.write_file', side_effect=OSError('Error message.')) def test_build_page_error(self, site_dir, mock_write_file): cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[]) files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]) nav = get_navigation(files, cfg) page = files.documentation_pages()[0].page # Fake populate page page.title = 'Title' page.markdown = 'page content' page.content = 'page content
' with self.assertLogs('mkdocs', level='ERROR') as cm: self.assertRaises(OSError, build._build_page, page, cfg, files, nav, cfg['theme'].get_env()) self.assertEqual( cm.output, ["ERROR:mkdocs.commands.build:Error building page 'index.md': Error message."] ) self.assert_mock_called_once(mock_write_file) # Test build.build @tempdir(files={ 'index.md': 'page content', 'empty.md': '', 'img.jpg': '', 'static.html': 'content', '.hidden': 'content', '.git/hidden': 'content' }) @tempdir() def test_copying_media(self, site_dir, docs_dir): cfg = load_config(docs_dir=docs_dir, site_dir=site_dir) build.build(cfg) # Verify that only non-empty md file (coverted to html), static HTML file and image are copied. self.assertPathIsFile(site_dir, 'index.html') self.assertPathIsFile(site_dir, 'img.jpg') self.assertPathIsFile(site_dir, 'static.html') self.assertPathNotExists(site_dir, 'empty.md') self.assertPathNotExists(site_dir, '.hidden') self.assertPathNotExists(site_dir, '.git/hidden') @tempdir(files={'index.md': 'page content'}) @tempdir() def test_copy_theme_files(self, site_dir, docs_dir): cfg = load_config(docs_dir=docs_dir, site_dir=site_dir) build.build(cfg) # Verify only theme media are copied, not templates or Python files. self.assertPathIsFile(site_dir, 'index.html') self.assertPathIsFile(site_dir, '404.html') self.assertPathIsDir(site_dir, 'js') self.assertPathIsDir(site_dir, 'css') self.assertPathIsDir(site_dir, 'img') self.assertPathIsDir(site_dir, 'fonts') self.assertPathNotExists(site_dir, '__init__.py') self.assertPathNotExists(site_dir, '__init__.pyc') self.assertPathNotExists(site_dir, 'base.html') self.assertPathNotExists(site_dir, 'content.html') self.assertPathNotExists(site_dir, 'main.html') # Test build.site_directory_contains_stale_files @tempdir(files=['index.html']) def test_site_dir_contains_stale_files(self, site_dir): self.assertTrue(build.site_directory_contains_stale_files(site_dir)) @tempdir() def test_not_site_dir_contains_stale_files(self, site_dir): self.assertFalse(build.site_directory_contains_stale_files(site_dir))