build_tests.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. #!/usr/bin/env python
  2. from unittest import mock
  3. import unittest
  4. from mkdocs.structure.pages import Page
  5. from mkdocs.structure.files import File, Files
  6. from mkdocs.structure.nav import get_navigation
  7. from mkdocs.commands import build
  8. from mkdocs.tests.base import load_config, tempdir, PathAssertionMixin
  9. from mkdocs.utils import meta
  10. def build_page(title, path, config, md_src=''):
  11. """ Helper which returns a Page object. """
  12. files = Files([File(path, config['docs_dir'], config['site_dir'], config['use_directory_urls'])])
  13. page = Page(title, list(files)[0], config)
  14. # Fake page.read_source()
  15. page.markdown, page.meta = meta.get_data(md_src)
  16. return page, files
  17. class BuildTests(PathAssertionMixin, unittest.TestCase):
  18. def assert_mock_called_once(self, mock):
  19. """assert that the mock was called only once.
  20. The `mock.assert_called_once()` method was added in PY36.
  21. TODO: Remove this when PY35 support is dropped.
  22. """
  23. try:
  24. mock.assert_called_once()
  25. except AttributeError:
  26. if not mock.call_count == 1:
  27. msg = ("Expected '%s' to have been called once. Called %s times." %
  28. (mock._mock_name or 'mock', self.call_count))
  29. raise AssertionError(msg)
  30. # Test build.get_context
  31. def test_context_base_url_homepage(self):
  32. nav_cfg = [
  33. {'Home': 'index.md'}
  34. ]
  35. cfg = load_config(nav=nav_cfg, use_directory_urls=False)
  36. files = Files([
  37. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  38. ])
  39. nav = get_navigation(files, cfg)
  40. context = build.get_context(nav, files, cfg, nav.pages[0])
  41. self.assertEqual(context['base_url'], '.')
  42. def test_context_base_url_homepage_use_directory_urls(self):
  43. nav_cfg = [
  44. {'Home': 'index.md'}
  45. ]
  46. cfg = load_config(nav=nav_cfg)
  47. files = Files([
  48. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  49. ])
  50. nav = get_navigation(files, cfg)
  51. context = build.get_context(nav, files, cfg, nav.pages[0])
  52. self.assertEqual(context['base_url'], '.')
  53. def test_context_base_url_nested_page(self):
  54. nav_cfg = [
  55. {'Home': 'index.md'},
  56. {'Nested': 'foo/bar.md'}
  57. ]
  58. cfg = load_config(nav=nav_cfg, use_directory_urls=False)
  59. files = Files([
  60. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  61. File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  62. ])
  63. nav = get_navigation(files, cfg)
  64. context = build.get_context(nav, files, cfg, nav.pages[1])
  65. self.assertEqual(context['base_url'], '..')
  66. def test_context_base_url_nested_page_use_directory_urls(self):
  67. nav_cfg = [
  68. {'Home': 'index.md'},
  69. {'Nested': 'foo/bar.md'}
  70. ]
  71. cfg = load_config(nav=nav_cfg)
  72. files = Files([
  73. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  74. File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  75. ])
  76. nav = get_navigation(files, cfg)
  77. context = build.get_context(nav, files, cfg, nav.pages[1])
  78. self.assertEqual(context['base_url'], '../..')
  79. def test_context_base_url_relative_no_page(self):
  80. cfg = load_config(use_directory_urls=False)
  81. context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
  82. self.assertEqual(context['base_url'], '..')
  83. def test_context_base_url_relative_no_page_use_directory_urls(self):
  84. cfg = load_config()
  85. context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
  86. self.assertEqual(context['base_url'], '..')
  87. def test_context_base_url_absolute_no_page(self):
  88. cfg = load_config(use_directory_urls=False)
  89. context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/')
  90. self.assertEqual(context['base_url'], '/')
  91. def test_context_base_url__absolute_no_page_use_directory_urls(self):
  92. cfg = load_config()
  93. context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/')
  94. self.assertEqual(context['base_url'], '/')
  95. def test_context_base_url_absolute_nested_no_page(self):
  96. cfg = load_config(use_directory_urls=False)
  97. context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/')
  98. self.assertEqual(context['base_url'], '/foo/')
  99. def test_context_base_url__absolute_nested_no_page_use_directory_urls(self):
  100. cfg = load_config()
  101. context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='/foo/')
  102. self.assertEqual(context['base_url'], '/foo/')
  103. def test_context_extra_css_js_from_homepage(self):
  104. nav_cfg = [
  105. {'Home': 'index.md'}
  106. ]
  107. cfg = load_config(
  108. nav=nav_cfg,
  109. extra_css=['style.css'],
  110. extra_javascript=['script.js'],
  111. use_directory_urls=False
  112. )
  113. files = Files([
  114. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  115. ])
  116. nav = get_navigation(files, cfg)
  117. context = build.get_context(nav, files, cfg, nav.pages[0])
  118. self.assertEqual(context['extra_css'], ['style.css'])
  119. self.assertEqual(context['extra_javascript'], ['script.js'])
  120. def test_context_extra_css_js_from_nested_page(self):
  121. nav_cfg = [
  122. {'Home': 'index.md'},
  123. {'Nested': 'foo/bar.md'}
  124. ]
  125. cfg = load_config(
  126. nav=nav_cfg,
  127. extra_css=['style.css'],
  128. extra_javascript=['script.js'],
  129. use_directory_urls=False
  130. )
  131. files = Files([
  132. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  133. File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  134. ])
  135. nav = get_navigation(files, cfg)
  136. context = build.get_context(nav, files, cfg, nav.pages[1])
  137. self.assertEqual(context['extra_css'], ['../style.css'])
  138. self.assertEqual(context['extra_javascript'], ['../script.js'])
  139. def test_context_extra_css_js_from_nested_page_use_directory_urls(self):
  140. nav_cfg = [
  141. {'Home': 'index.md'},
  142. {'Nested': 'foo/bar.md'}
  143. ]
  144. cfg = load_config(
  145. nav=nav_cfg,
  146. extra_css=['style.css'],
  147. extra_javascript=['script.js']
  148. )
  149. files = Files([
  150. File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  151. File('foo/bar.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  152. ])
  153. nav = get_navigation(files, cfg)
  154. context = build.get_context(nav, files, cfg, nav.pages[1])
  155. self.assertEqual(context['extra_css'], ['../../style.css'])
  156. self.assertEqual(context['extra_javascript'], ['../../script.js'])
  157. def test_context_extra_css_js_no_page(self):
  158. cfg = load_config(extra_css=['style.css'], extra_javascript=['script.js'])
  159. context = build.get_context(mock.Mock(), mock.Mock(), cfg, base_url='..')
  160. self.assertEqual(context['extra_css'], ['../style.css'])
  161. self.assertEqual(context['extra_javascript'], ['../script.js'])
  162. def test_extra_context(self):
  163. cfg = load_config(extra={'a': 1})
  164. context = build.get_context(mock.Mock(), mock.Mock(), cfg)
  165. self.assertEqual(context['config']['extra']['a'], 1)
  166. # Test build._build_theme_template
  167. @mock.patch('mkdocs.utils.write_file')
  168. @mock.patch('mkdocs.commands.build._build_template', return_value='some content')
  169. def test_build_theme_template(self, mock_build_template, mock_write_file):
  170. cfg = load_config()
  171. env = cfg['theme'].get_env()
  172. build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock())
  173. self.assert_mock_called_once(mock_write_file)
  174. self.assert_mock_called_once(mock_build_template)
  175. @mock.patch('mkdocs.utils.write_file')
  176. @mock.patch('mkdocs.commands.build._build_template', return_value='some content')
  177. @mock.patch('gzip.GzipFile')
  178. def test_build_sitemap_template(self, mock_gzip_gzipfile, mock_build_template, mock_write_file):
  179. cfg = load_config()
  180. env = cfg['theme'].get_env()
  181. build._build_theme_template('sitemap.xml', env, mock.Mock(), cfg, mock.Mock())
  182. self.assert_mock_called_once(mock_write_file)
  183. self.assert_mock_called_once(mock_build_template)
  184. self.assert_mock_called_once(mock_gzip_gzipfile)
  185. @mock.patch('mkdocs.utils.write_file')
  186. @mock.patch('mkdocs.commands.build._build_template', return_value='')
  187. def test_skip_missing_theme_template(self, mock_build_template, mock_write_file):
  188. cfg = load_config()
  189. env = cfg['theme'].get_env()
  190. with self.assertLogs('mkdocs', level='WARN') as cm:
  191. build._build_theme_template('missing.html', env, mock.Mock(), cfg, mock.Mock())
  192. self.assertEqual(
  193. cm.output,
  194. ["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in theme directories."]
  195. )
  196. mock_write_file.assert_not_called()
  197. mock_build_template.assert_not_called()
  198. @mock.patch('mkdocs.utils.write_file')
  199. @mock.patch('mkdocs.commands.build._build_template', return_value='')
  200. def test_skip_theme_template_empty_output(self, mock_build_template, mock_write_file):
  201. cfg = load_config()
  202. env = cfg['theme'].get_env()
  203. with self.assertLogs('mkdocs', level='INFO') as cm:
  204. build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock())
  205. self.assertEqual(
  206. cm.output,
  207. ["INFO:mkdocs.commands.build:Template skipped: 'main.html' generated empty output."]
  208. )
  209. mock_write_file.assert_not_called()
  210. self.assert_mock_called_once(mock_build_template)
  211. # Test build._build_extra_template
  212. @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content'))
  213. def test_build_extra_template(self):
  214. cfg = load_config()
  215. files = Files([
  216. File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  217. ])
  218. build._build_extra_template('foo.html', files, cfg, mock.Mock())
  219. @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content'))
  220. def test_skip_missing_extra_template(self):
  221. cfg = load_config()
  222. files = Files([
  223. File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  224. ])
  225. with self.assertLogs('mkdocs', level='INFO') as cm:
  226. build._build_extra_template('missing.html', files, cfg, mock.Mock())
  227. self.assertEqual(
  228. cm.output,
  229. ["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in docs_dir."]
  230. )
  231. @mock.patch('mkdocs.commands.build.open', side_effect=OSError('Error message.'))
  232. def test_skip_ioerror_extra_template(self, mock_open):
  233. cfg = load_config()
  234. files = Files([
  235. File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  236. ])
  237. with self.assertLogs('mkdocs', level='INFO') as cm:
  238. build._build_extra_template('foo.html', files, cfg, mock.Mock())
  239. self.assertEqual(
  240. cm.output,
  241. ["WARNING:mkdocs.commands.build:Error reading template 'foo.html': Error message."]
  242. )
  243. @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data=''))
  244. def test_skip_extra_template_empty_output(self):
  245. cfg = load_config()
  246. files = Files([
  247. File('foo.html', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']),
  248. ])
  249. with self.assertLogs('mkdocs', level='INFO') as cm:
  250. build._build_extra_template('foo.html', files, cfg, mock.Mock())
  251. self.assertEqual(
  252. cm.output,
  253. ["INFO:mkdocs.commands.build:Template skipped: 'foo.html' generated empty output."]
  254. )
  255. # Test build._populate_page
  256. @tempdir(files={'index.md': 'page content'})
  257. def test_populate_page(self, docs_dir):
  258. cfg = load_config(docs_dir=docs_dir)
  259. file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  260. page = Page('Foo', file, cfg)
  261. build._populate_page(page, cfg, Files([file]))
  262. self.assertEqual(page.content, '<p>page content</p>')
  263. @tempdir(files={'testing.html': '<p>page content</p>'})
  264. def test_populate_page_dirty_modified(self, site_dir):
  265. cfg = load_config(site_dir=site_dir)
  266. file = File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  267. page = Page('Foo', file, cfg)
  268. build._populate_page(page, cfg, Files([file]), dirty=True)
  269. self.assertTrue(page.markdown.startswith('# Welcome to MkDocs'))
  270. self.assertTrue(page.content.startswith('<h1 id="welcome-to-mkdocs">Welcome to MkDocs</h1>'))
  271. @tempdir(files={'index.md': 'page content'})
  272. @tempdir(files={'index.html': '<p>page content</p>'})
  273. def test_populate_page_dirty_not_modified(self, site_dir, docs_dir):
  274. cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
  275. file = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  276. page = Page('Foo', file, cfg)
  277. build._populate_page(page, cfg, Files([file]), dirty=True)
  278. # Content is empty as file read was skipped
  279. self.assertEqual(page.markdown, None)
  280. self.assertEqual(page.content, None)
  281. @tempdir(files={'index.md': 'new page content'})
  282. @mock.patch('mkdocs.structure.pages.open', side_effect=OSError('Error message.'))
  283. def test_populate_page_read_error(self, docs_dir, mock_open):
  284. cfg = load_config(docs_dir=docs_dir)
  285. file = File('missing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])
  286. page = Page('Foo', file, cfg)
  287. with self.assertLogs('mkdocs', level='ERROR') as cm:
  288. self.assertRaises(OSError, build._populate_page, page, cfg, Files([file]))
  289. self.assertEqual(
  290. cm.output, [
  291. 'ERROR:mkdocs.structure.pages:File not found: missing.md',
  292. "ERROR:mkdocs.commands.build:Error reading page 'missing.md': Error message."
  293. ]
  294. )
  295. self.assert_mock_called_once(mock_open)
  296. # Test build._build_page
  297. @tempdir()
  298. def test_build_page(self, site_dir):
  299. cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
  300. files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
  301. nav = get_navigation(files, cfg)
  302. page = files.documentation_pages()[0].page
  303. # Fake populate page
  304. page.title = 'Title'
  305. page.markdown = 'page content'
  306. page.content = '<p>page content</p>'
  307. build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
  308. self.assertPathIsFile(site_dir, 'index.html')
  309. # TODO: fix this. It seems that jinja2 chokes on the mock object. Not sure how to resolve.
  310. # @tempdir()
  311. # @mock.patch('jinja2.environment.Template')
  312. # def test_build_page_empty(self, site_dir, mock_template):
  313. # mock_template.render = mock.Mock(return_value='')
  314. # cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
  315. # files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
  316. # nav = get_navigation(files, cfg)
  317. # page = files.documentation_pages()[0].page
  318. # # Fake populate page
  319. # page.title = ''
  320. # page.markdown = ''
  321. # page.content = ''
  322. # with self.assertLogs('mkdocs', level='INFO') as cm:
  323. # build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
  324. # self.assertEqual(
  325. # cm.output,
  326. # ["INFO:mkdocs.commands.build:Page skipped: 'index.md'. Generated empty output."]
  327. # )
  328. # self.assert_mock_called_once(mock_template.render)
  329. # self.assertPathNotFile(site_dir, 'index.html')
  330. @tempdir(files={'index.md': 'page content'})
  331. @tempdir(files={'index.html': '<p>page content</p>'})
  332. @mock.patch('mkdocs.utils.write_file')
  333. def test_build_page_dirty_modified(self, site_dir, docs_dir, mock_write_file):
  334. cfg = load_config(docs_dir=docs_dir, site_dir=site_dir, nav=['index.md'], plugins=[])
  335. files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
  336. nav = get_navigation(files, cfg)
  337. page = files.documentation_pages()[0].page
  338. # Fake populate page
  339. page.title = 'Title'
  340. page.markdown = 'new page content'
  341. page.content = '<p>new page content</p>'
  342. build._build_page(page, cfg, files, nav, cfg['theme'].get_env(), dirty=True)
  343. mock_write_file.assert_not_called()
  344. @tempdir(files={'testing.html': '<p>page content</p>'})
  345. @mock.patch('mkdocs.utils.write_file')
  346. def test_build_page_dirty_not_modified(self, site_dir, mock_write_file):
  347. cfg = load_config(site_dir=site_dir, nav=['testing.md'], plugins=[])
  348. files = Files([File('testing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
  349. nav = get_navigation(files, cfg)
  350. page = files.documentation_pages()[0].page
  351. # Fake populate page
  352. page.title = 'Title'
  353. page.markdown = 'page content'
  354. page.content = '<p>page content</p>'
  355. build._build_page(page, cfg, files, nav, cfg['theme'].get_env(), dirty=True)
  356. self.assert_mock_called_once(mock_write_file)
  357. @tempdir()
  358. def test_build_page_custom_template(self, site_dir):
  359. cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
  360. files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
  361. nav = get_navigation(files, cfg)
  362. page = files.documentation_pages()[0].page
  363. # Fake populate page
  364. page.title = 'Title'
  365. page.meta = {'template': '404.html'}
  366. page.markdown = 'page content'
  367. page.content = '<p>page content</p>'
  368. build._build_page(page, cfg, files, nav, cfg['theme'].get_env())
  369. self.assertPathIsFile(site_dir, 'index.html')
  370. @tempdir()
  371. @mock.patch('mkdocs.utils.write_file', side_effect=OSError('Error message.'))
  372. def test_build_page_error(self, site_dir, mock_write_file):
  373. cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[])
  374. files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])])
  375. nav = get_navigation(files, cfg)
  376. page = files.documentation_pages()[0].page
  377. # Fake populate page
  378. page.title = 'Title'
  379. page.markdown = 'page content'
  380. page.content = '<p>page content</p>'
  381. with self.assertLogs('mkdocs', level='ERROR') as cm:
  382. self.assertRaises(OSError, build._build_page, page, cfg, files, nav, cfg['theme'].get_env())
  383. self.assertEqual(
  384. cm.output,
  385. ["ERROR:mkdocs.commands.build:Error building page 'index.md': Error message."]
  386. )
  387. self.assert_mock_called_once(mock_write_file)
  388. # Test build.build
  389. @tempdir(files={
  390. 'index.md': 'page content',
  391. 'empty.md': '',
  392. 'img.jpg': '',
  393. 'static.html': 'content',
  394. '.hidden': 'content',
  395. '.git/hidden': 'content'
  396. })
  397. @tempdir()
  398. def test_copying_media(self, site_dir, docs_dir):
  399. cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
  400. build.build(cfg)
  401. # Verify that only non-empty md file (coverted to html), static HTML file and image are copied.
  402. self.assertPathIsFile(site_dir, 'index.html')
  403. self.assertPathIsFile(site_dir, 'img.jpg')
  404. self.assertPathIsFile(site_dir, 'static.html')
  405. self.assertPathNotExists(site_dir, 'empty.md')
  406. self.assertPathNotExists(site_dir, '.hidden')
  407. self.assertPathNotExists(site_dir, '.git/hidden')
  408. @tempdir(files={'index.md': 'page content'})
  409. @tempdir()
  410. def test_copy_theme_files(self, site_dir, docs_dir):
  411. cfg = load_config(docs_dir=docs_dir, site_dir=site_dir)
  412. build.build(cfg)
  413. # Verify only theme media are copied, not templates or Python files.
  414. self.assertPathIsFile(site_dir, 'index.html')
  415. self.assertPathIsFile(site_dir, '404.html')
  416. self.assertPathIsDir(site_dir, 'js')
  417. self.assertPathIsDir(site_dir, 'css')
  418. self.assertPathIsDir(site_dir, 'img')
  419. self.assertPathIsDir(site_dir, 'fonts')
  420. self.assertPathNotExists(site_dir, '__init__.py')
  421. self.assertPathNotExists(site_dir, '__init__.pyc')
  422. self.assertPathNotExists(site_dir, 'base.html')
  423. self.assertPathNotExists(site_dir, 'content.html')
  424. self.assertPathNotExists(site_dir, 'main.html')
  425. # Test build.site_directory_contains_stale_files
  426. @tempdir(files=['index.html'])
  427. def test_site_dir_contains_stale_files(self, site_dir):
  428. self.assertTrue(build.site_directory_contains_stale_files(site_dir))
  429. @tempdir()
  430. def test_not_site_dir_contains_stale_files(self, site_dir):
  431. self.assertFalse(build.site_directory_contains_stale_files(site_dir))