test_sphinxext.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """Tests for tinypages build using sphinx extensions."""
  2. import filecmp
  3. import os
  4. from pathlib import Path
  5. import shutil
  6. import sys
  7. from matplotlib.testing import subprocess_run_for_testing
  8. import pytest
  9. pytest.importorskip('sphinx', minversion='4.1.3')
  10. def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None):
  11. # Build the pages with warnings turned into errors
  12. extra_args = [] if extra_args is None else extra_args
  13. cmd = [sys.executable, '-msphinx', '-W', '-b', 'html',
  14. '-d', str(doctree_dir), str(source_dir), str(html_dir), *extra_args]
  15. proc = subprocess_run_for_testing(
  16. cmd, capture_output=True, text=True,
  17. env={**os.environ, "MPLBACKEND": ""})
  18. out = proc.stdout
  19. err = proc.stderr
  20. assert proc.returncode == 0, \
  21. f"sphinx build failed with stdout:\n{out}\nstderr:\n{err}\n"
  22. if err:
  23. pytest.fail(f"sphinx build emitted the following warnings:\n{err}")
  24. assert html_dir.is_dir()
  25. def test_tinypages(tmp_path):
  26. shutil.copytree(Path(__file__).parent / 'tinypages', tmp_path,
  27. dirs_exist_ok=True)
  28. html_dir = tmp_path / '_build' / 'html'
  29. img_dir = html_dir / '_images'
  30. doctree_dir = tmp_path / 'doctrees'
  31. # Build the pages with warnings turned into errors
  32. cmd = [sys.executable, '-msphinx', '-W', '-b', 'html',
  33. '-d', str(doctree_dir),
  34. str(Path(__file__).parent / 'tinypages'), str(html_dir)]
  35. # On CI, gcov emits warnings (due to agg headers being included with the
  36. # same name in multiple extension modules -- but we don't care about their
  37. # coverage anyways); hide them using GCOV_ERROR_FILE.
  38. proc = subprocess_run_for_testing(
  39. cmd, capture_output=True, text=True,
  40. env={**os.environ, "MPLBACKEND": "", "GCOV_ERROR_FILE": os.devnull}
  41. )
  42. out = proc.stdout
  43. err = proc.stderr
  44. # Build the pages with warnings turned into errors
  45. build_sphinx_html(tmp_path, doctree_dir, html_dir)
  46. def plot_file(num):
  47. return img_dir / f'some_plots-{num}.png'
  48. def plot_directive_file(num):
  49. # This is always next to the doctree dir.
  50. return doctree_dir.parent / 'plot_directive' / f'some_plots-{num}.png'
  51. range_10, range_6, range_4 = (plot_file(i) for i in range(1, 4))
  52. # Plot 5 is range(6) plot
  53. assert filecmp.cmp(range_6, plot_file(5))
  54. # Plot 7 is range(4) plot
  55. assert filecmp.cmp(range_4, plot_file(7))
  56. # Plot 11 is range(10) plot
  57. assert filecmp.cmp(range_10, plot_file(11))
  58. # Plot 12 uses the old range(10) figure and the new range(6) figure
  59. assert filecmp.cmp(range_10, plot_file('12_00'))
  60. assert filecmp.cmp(range_6, plot_file('12_01'))
  61. # Plot 13 shows close-figs in action
  62. assert filecmp.cmp(range_4, plot_file(13))
  63. # Plot 14 has included source
  64. html_contents = (html_dir / 'some_plots.html').read_text(encoding='utf-8')
  65. assert '# Only a comment' in html_contents
  66. # check plot defined in external file.
  67. assert filecmp.cmp(range_4, img_dir / 'range4.png')
  68. assert filecmp.cmp(range_6, img_dir / 'range6_range6.png')
  69. # check if figure caption made it into html file
  70. assert 'This is the caption for plot 15.' in html_contents
  71. # check if figure caption using :caption: made it into html file (because this plot
  72. # doesn't use srcset, the caption preserves newlines in the output.)
  73. assert 'Plot 17 uses the caption option,\nwith multi-line input.' in html_contents
  74. # check if figure alt text using :alt: made it into html file
  75. assert 'Plot 17 uses the alt option, with multi-line input.' in html_contents
  76. # check if figure caption made it into html file
  77. assert 'This is the caption for plot 18.' in html_contents
  78. # check if the custom classes made it into the html file
  79. assert 'plot-directive my-class my-other-class' in html_contents
  80. # check that the multi-image caption is applied twice
  81. assert html_contents.count('This caption applies to both plots.') == 2
  82. # Plot 21 is range(6) plot via an include directive. But because some of
  83. # the previous plots are repeated, the argument to plot_file() is only 17.
  84. assert filecmp.cmp(range_6, plot_file(17))
  85. # plot 22 is from the range6.py file again, but a different function
  86. assert filecmp.cmp(range_10, img_dir / 'range6_range10.png')
  87. # Modify the included plot
  88. contents = (tmp_path / 'included_plot_21.rst').read_bytes()
  89. contents = contents.replace(b'plt.plot(range(6))', b'plt.plot(range(4))')
  90. (tmp_path / 'included_plot_21.rst').write_bytes(contents)
  91. # Build the pages again and check that the modified file was updated
  92. modification_times = [plot_directive_file(i).stat().st_mtime
  93. for i in (1, 2, 3, 5)]
  94. build_sphinx_html(tmp_path, doctree_dir, html_dir)
  95. assert filecmp.cmp(range_4, plot_file(17))
  96. # Check that the plots in the plot_directive folder weren't changed.
  97. # (plot_directive_file(1) won't be modified, but it will be copied to html/
  98. # upon compilation, so plot_file(1) will be modified)
  99. assert plot_directive_file(1).stat().st_mtime == modification_times[0]
  100. assert plot_directive_file(2).stat().st_mtime == modification_times[1]
  101. assert plot_directive_file(3).stat().st_mtime == modification_times[2]
  102. assert filecmp.cmp(range_10, plot_file(1))
  103. assert filecmp.cmp(range_6, plot_file(2))
  104. assert filecmp.cmp(range_4, plot_file(3))
  105. # Make sure that figures marked with context are re-created (but that the
  106. # contents are the same)
  107. assert plot_directive_file(5).stat().st_mtime > modification_times[3]
  108. assert filecmp.cmp(range_6, plot_file(5))
  109. def test_plot_html_show_source_link(tmp_path):
  110. parent = Path(__file__).parent
  111. shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py')
  112. shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static')
  113. doctree_dir = tmp_path / 'doctrees'
  114. (tmp_path / 'index.rst').write_text("""
  115. .. plot::
  116. plt.plot(range(2))
  117. """)
  118. # Make sure source scripts are created by default
  119. html_dir1 = tmp_path / '_build' / 'html1'
  120. build_sphinx_html(tmp_path, doctree_dir, html_dir1)
  121. assert len(list(html_dir1.glob("**/index-1.py"))) == 1
  122. # Make sure source scripts are NOT created when
  123. # plot_html_show_source_link` is False
  124. html_dir2 = tmp_path / '_build' / 'html2'
  125. build_sphinx_html(tmp_path, doctree_dir, html_dir2,
  126. extra_args=['-D', 'plot_html_show_source_link=0'])
  127. assert len(list(html_dir2.glob("**/index-1.py"))) == 0
  128. @pytest.mark.parametrize('plot_html_show_source_link', [0, 1])
  129. def test_show_source_link_true(tmp_path, plot_html_show_source_link):
  130. # Test that a source link is generated if :show-source-link: is true,
  131. # whether or not plot_html_show_source_link is true.
  132. parent = Path(__file__).parent
  133. shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py')
  134. shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static')
  135. doctree_dir = tmp_path / 'doctrees'
  136. (tmp_path / 'index.rst').write_text("""
  137. .. plot::
  138. :show-source-link: true
  139. plt.plot(range(2))
  140. """)
  141. html_dir = tmp_path / '_build' / 'html'
  142. build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[
  143. '-D', f'plot_html_show_source_link={plot_html_show_source_link}'])
  144. assert len(list(html_dir.glob("**/index-1.py"))) == 1
  145. @pytest.mark.parametrize('plot_html_show_source_link', [0, 1])
  146. def test_show_source_link_false(tmp_path, plot_html_show_source_link):
  147. # Test that a source link is NOT generated if :show-source-link: is false,
  148. # whether or not plot_html_show_source_link is true.
  149. parent = Path(__file__).parent
  150. shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py')
  151. shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static')
  152. doctree_dir = tmp_path / 'doctrees'
  153. (tmp_path / 'index.rst').write_text("""
  154. .. plot::
  155. :show-source-link: false
  156. plt.plot(range(2))
  157. """)
  158. html_dir = tmp_path / '_build' / 'html'
  159. build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[
  160. '-D', f'plot_html_show_source_link={plot_html_show_source_link}'])
  161. assert len(list(html_dir.glob("**/index-1.py"))) == 0
  162. def test_srcset_version(tmp_path):
  163. shutil.copytree(Path(__file__).parent / 'tinypages', tmp_path,
  164. dirs_exist_ok=True)
  165. html_dir = tmp_path / '_build' / 'html'
  166. img_dir = html_dir / '_images'
  167. doctree_dir = tmp_path / 'doctrees'
  168. build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[
  169. '-D', 'plot_srcset=2x'])
  170. def plot_file(num, suff=''):
  171. return img_dir / f'some_plots-{num}{suff}.png'
  172. # check some-plots
  173. for ind in [1, 2, 3, 5, 7, 11, 13, 15, 17]:
  174. assert plot_file(ind).exists()
  175. assert plot_file(ind, suff='.2x').exists()
  176. assert (img_dir / 'nestedpage-index-1.png').exists()
  177. assert (img_dir / 'nestedpage-index-1.2x.png').exists()
  178. assert (img_dir / 'nestedpage-index-2.png').exists()
  179. assert (img_dir / 'nestedpage-index-2.2x.png').exists()
  180. assert (img_dir / 'nestedpage2-index-1.png').exists()
  181. assert (img_dir / 'nestedpage2-index-1.2x.png').exists()
  182. assert (img_dir / 'nestedpage2-index-2.png').exists()
  183. assert (img_dir / 'nestedpage2-index-2.2x.png').exists()
  184. # Check html for srcset
  185. assert ('srcset="_images/some_plots-1.png, _images/some_plots-1.2x.png 2.00x"'
  186. in (html_dir / 'some_plots.html').read_text(encoding='utf-8'))
  187. st = ('srcset="../_images/nestedpage-index-1.png, '
  188. '../_images/nestedpage-index-1.2x.png 2.00x"')
  189. assert st in (html_dir / 'nestedpage/index.html').read_text(encoding='utf-8')
  190. st = ('srcset="../_images/nestedpage2-index-2.png, '
  191. '../_images/nestedpage2-index-2.2x.png 2.00x"')
  192. assert st in (html_dir / 'nestedpage2/index.html').read_text(encoding='utf-8')