test_determinism.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. """
  2. Test output reproducibility.
  3. """
  4. import os
  5. import sys
  6. import pytest
  7. import matplotlib as mpl
  8. from matplotlib import pyplot as plt
  9. from matplotlib.cbook import get_sample_data
  10. from matplotlib.collections import PathCollection
  11. from matplotlib.image import BboxImage
  12. from matplotlib.offsetbox import AnchoredOffsetbox, AuxTransformBox
  13. from matplotlib.patches import Circle, PathPatch
  14. from matplotlib.path import Path
  15. from matplotlib.testing import subprocess_run_for_testing
  16. from matplotlib.testing._markers import needs_ghostscript, needs_usetex
  17. import matplotlib.testing.compare
  18. from matplotlib.text import TextPath
  19. from matplotlib.transforms import IdentityTransform
  20. def _save_figure(objects='mhip', fmt="pdf", usetex=False):
  21. mpl.use(fmt)
  22. mpl.rcParams.update({'svg.hashsalt': 'asdf', 'text.usetex': usetex})
  23. fig = plt.figure()
  24. if 'm' in objects:
  25. # use different markers...
  26. ax1 = fig.add_subplot(1, 6, 1)
  27. x = range(10)
  28. ax1.plot(x, [1] * 10, marker='D')
  29. ax1.plot(x, [2] * 10, marker='x')
  30. ax1.plot(x, [3] * 10, marker='^')
  31. ax1.plot(x, [4] * 10, marker='H')
  32. ax1.plot(x, [5] * 10, marker='v')
  33. if 'h' in objects:
  34. # also use different hatch patterns
  35. ax2 = fig.add_subplot(1, 6, 2)
  36. bars = (ax2.bar(range(1, 5), range(1, 5)) +
  37. ax2.bar(range(1, 5), [6] * 4, bottom=range(1, 5)))
  38. ax2.set_xticks([1.5, 2.5, 3.5, 4.5])
  39. patterns = ('-', '+', 'x', '\\', '*', 'o', 'O', '.')
  40. for bar, pattern in zip(bars, patterns):
  41. bar.set_hatch(pattern)
  42. if 'i' in objects:
  43. # also use different images
  44. A = [[1, 2, 3], [2, 3, 1], [3, 1, 2]]
  45. fig.add_subplot(1, 6, 3).imshow(A, interpolation='nearest')
  46. A = [[1, 3, 2], [1, 2, 3], [3, 1, 2]]
  47. fig.add_subplot(1, 6, 4).imshow(A, interpolation='bilinear')
  48. A = [[2, 3, 1], [1, 2, 3], [2, 1, 3]]
  49. fig.add_subplot(1, 6, 5).imshow(A, interpolation='bicubic')
  50. if 'p' in objects:
  51. # clipping support class, copied from demo_text_path.py gallery example
  52. class PathClippedImagePatch(PathPatch):
  53. """
  54. The given image is used to draw the face of the patch. Internally,
  55. it uses BboxImage whose clippath set to the path of the patch.
  56. FIXME : The result is currently dpi dependent.
  57. """
  58. def __init__(self, path, bbox_image, **kwargs):
  59. super().__init__(path, **kwargs)
  60. self.bbox_image = BboxImage(
  61. self.get_window_extent, norm=None, origin=None)
  62. self.bbox_image.set_data(bbox_image)
  63. def set_facecolor(self, color):
  64. """Simply ignore facecolor."""
  65. super().set_facecolor("none")
  66. def draw(self, renderer=None):
  67. # the clip path must be updated every draw. any solution? -JJ
  68. self.bbox_image.set_clip_path(self._path, self.get_transform())
  69. self.bbox_image.draw(renderer)
  70. super().draw(renderer)
  71. # add a polar projection
  72. px = fig.add_subplot(projection="polar")
  73. pimg = px.imshow([[2]])
  74. pimg.set_clip_path(Circle((0, 1), radius=0.3333))
  75. # add a text-based clipping path (origin: demo_text_path.py)
  76. (ax1, ax2) = fig.subplots(2)
  77. arr = plt.imread(get_sample_data("grace_hopper.jpg"))
  78. text_path = TextPath((0, 0), "!?", size=150)
  79. p = PathClippedImagePatch(text_path, arr, ec="k")
  80. offsetbox = AuxTransformBox(IdentityTransform())
  81. offsetbox.add_artist(p)
  82. ao = AnchoredOffsetbox(loc='upper left', child=offsetbox, frameon=True,
  83. borderpad=0.2)
  84. ax1.add_artist(ao)
  85. # add a 2x2 grid of path-clipped axes (origin: test_artist.py)
  86. exterior = Path.unit_rectangle().deepcopy()
  87. exterior.vertices *= 4
  88. exterior.vertices -= 2
  89. interior = Path.unit_circle().deepcopy()
  90. interior.vertices = interior.vertices[::-1]
  91. clip_path = Path.make_compound_path(exterior, interior)
  92. star = Path.unit_regular_star(6).deepcopy()
  93. star.vertices *= 2.6
  94. (row1, row2) = fig.subplots(2, 2, sharex=True, sharey=True)
  95. for row in (row1, row2):
  96. ax1, ax2 = row
  97. collection = PathCollection([star], lw=5, edgecolor='blue',
  98. facecolor='red', alpha=0.7, hatch='*')
  99. collection.set_clip_path(clip_path, ax1.transData)
  100. ax1.add_collection(collection)
  101. patch = PathPatch(star, lw=5, edgecolor='blue', facecolor='red',
  102. alpha=0.7, hatch='*')
  103. patch.set_clip_path(clip_path, ax2.transData)
  104. ax2.add_patch(patch)
  105. ax1.set_xlim([-3, 3])
  106. ax1.set_ylim([-3, 3])
  107. x = range(5)
  108. ax = fig.add_subplot(1, 6, 6)
  109. ax.plot(x, x)
  110. ax.set_title('A string $1+2+\\sigma$')
  111. ax.set_xlabel('A string $1+2+\\sigma$')
  112. ax.set_ylabel('A string $1+2+\\sigma$')
  113. stdout = getattr(sys.stdout, 'buffer', sys.stdout)
  114. fig.savefig(stdout, format=fmt)
  115. @pytest.mark.parametrize(
  116. "objects, fmt, usetex", [
  117. ("", "pdf", False),
  118. ("m", "pdf", False),
  119. ("h", "pdf", False),
  120. ("i", "pdf", False),
  121. ("mhip", "pdf", False),
  122. ("mhip", "ps", False),
  123. pytest.param(
  124. "mhip", "ps", True, marks=[needs_usetex, needs_ghostscript]),
  125. ("p", "svg", False),
  126. ("mhip", "svg", False),
  127. pytest.param("mhip", "svg", True, marks=needs_usetex),
  128. ]
  129. )
  130. def test_determinism_check(objects, fmt, usetex):
  131. """
  132. Output three times the same graphs and checks that the outputs are exactly
  133. the same.
  134. Parameters
  135. ----------
  136. objects : str
  137. Objects to be included in the test document: 'm' for markers, 'h' for
  138. hatch patterns, 'i' for images, and 'p' for paths.
  139. fmt : {"pdf", "ps", "svg"}
  140. Output format.
  141. """
  142. plots = [
  143. subprocess_run_for_testing(
  144. [sys.executable, "-R", "-c",
  145. f"from matplotlib.tests.test_determinism import _save_figure;"
  146. f"_save_figure({objects!r}, {fmt!r}, {usetex})"],
  147. env={**os.environ, "SOURCE_DATE_EPOCH": "946684800",
  148. "MPLBACKEND": "Agg"},
  149. text=False, capture_output=True, check=True).stdout
  150. for _ in range(3)
  151. ]
  152. for p in plots[1:]:
  153. if fmt == "ps" and usetex:
  154. if p != plots[0]:
  155. pytest.skip("failed, maybe due to ghostscript timestamps")
  156. else:
  157. assert p == plots[0]
  158. @pytest.mark.parametrize(
  159. "fmt, string", [
  160. ("pdf", b"/CreationDate (D:20000101000000Z)"),
  161. # SOURCE_DATE_EPOCH support is not tested with text.usetex,
  162. # because the produced timestamp comes from ghostscript:
  163. # %%CreationDate: D:20000101000000Z00\'00\', and this could change
  164. # with another ghostscript version.
  165. ("ps", b"%%CreationDate: Sat Jan 01 00:00:00 2000"),
  166. ]
  167. )
  168. def test_determinism_source_date_epoch(fmt, string):
  169. """
  170. Test SOURCE_DATE_EPOCH support. Output a document with the environment
  171. variable SOURCE_DATE_EPOCH set to 2000-01-01 00:00 UTC and check that the
  172. document contains the timestamp that corresponds to this date (given as an
  173. argument).
  174. Parameters
  175. ----------
  176. fmt : {"pdf", "ps", "svg"}
  177. Output format.
  178. string : bytes
  179. Timestamp string for 2000-01-01 00:00 UTC.
  180. """
  181. buf = subprocess_run_for_testing(
  182. [sys.executable, "-R", "-c",
  183. f"from matplotlib.tests.test_determinism import _save_figure; "
  184. f"_save_figure('', {fmt!r})"],
  185. env={**os.environ, "SOURCE_DATE_EPOCH": "946684800",
  186. "MPLBACKEND": "Agg"}, capture_output=True, text=False, check=True).stdout
  187. assert string in buf