__init__.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. """
  2. Helper functions for testing.
  3. """
  4. from pathlib import Path
  5. from tempfile import TemporaryDirectory
  6. import locale
  7. import logging
  8. import os
  9. import subprocess
  10. import sys
  11. import matplotlib as mpl
  12. from matplotlib import _api
  13. _log = logging.getLogger(__name__)
  14. def set_font_settings_for_testing():
  15. mpl.rcParams['font.family'] = 'DejaVu Sans'
  16. mpl.rcParams['text.hinting'] = 'none'
  17. mpl.rcParams['text.hinting_factor'] = 8
  18. def set_reproducibility_for_testing():
  19. mpl.rcParams['svg.hashsalt'] = 'matplotlib'
  20. def setup():
  21. # The baseline images are created in this locale, so we should use
  22. # it during all of the tests.
  23. try:
  24. locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
  25. except locale.Error:
  26. try:
  27. locale.setlocale(locale.LC_ALL, 'English_United States.1252')
  28. except locale.Error:
  29. _log.warning(
  30. "Could not set locale to English/United States. "
  31. "Some date-related tests may fail.")
  32. mpl.use('Agg')
  33. with _api.suppress_matplotlib_deprecation_warning():
  34. mpl.rcdefaults() # Start with all defaults
  35. # These settings *must* be hardcoded for running the comparison tests and
  36. # are not necessarily the default values as specified in rcsetup.py.
  37. set_font_settings_for_testing()
  38. set_reproducibility_for_testing()
  39. def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None,
  40. stderr=None, check=False, text=True,
  41. capture_output=False):
  42. """
  43. Create and run a subprocess.
  44. Thin wrapper around `subprocess.run`, intended for testing. Will
  45. mark fork() failures on Cygwin as expected failures: not a
  46. success, but not indicating a problem with the code either.
  47. Parameters
  48. ----------
  49. args : list of str
  50. env : dict[str, str]
  51. timeout : float
  52. stdout, stderr
  53. check : bool
  54. text : bool
  55. Also called ``universal_newlines`` in subprocess. I chose this
  56. name since the main effect is returning bytes (`False`) vs. str
  57. (`True`), though it also tries to normalize newlines across
  58. platforms.
  59. capture_output : bool
  60. Set stdout and stderr to subprocess.PIPE
  61. Returns
  62. -------
  63. proc : subprocess.Popen
  64. See Also
  65. --------
  66. subprocess.run
  67. Raises
  68. ------
  69. pytest.xfail
  70. If platform is Cygwin and subprocess reports a fork() failure.
  71. """
  72. if capture_output:
  73. stdout = stderr = subprocess.PIPE
  74. try:
  75. proc = subprocess.run(
  76. command, env=env,
  77. timeout=timeout, check=check,
  78. stdout=stdout, stderr=stderr,
  79. text=text
  80. )
  81. except BlockingIOError:
  82. if sys.platform == "cygwin":
  83. # Might want to make this more specific
  84. import pytest
  85. pytest.xfail("Fork failure")
  86. raise
  87. return proc
  88. def subprocess_run_helper(func, *args, timeout, extra_env=None):
  89. """
  90. Run a function in a sub-process.
  91. Parameters
  92. ----------
  93. func : function
  94. The function to be run. It must be in a module that is importable.
  95. *args : str
  96. Any additional command line arguments to be passed in
  97. the first argument to ``subprocess.run``.
  98. extra_env : dict[str, str]
  99. Any additional environment variables to be set for the subprocess.
  100. """
  101. target = func.__name__
  102. module = func.__module__
  103. file = func.__code__.co_filename
  104. proc = subprocess_run_for_testing(
  105. [
  106. sys.executable,
  107. "-c",
  108. f"import importlib.util;"
  109. f"_spec = importlib.util.spec_from_file_location({module!r}, {file!r});"
  110. f"_module = importlib.util.module_from_spec(_spec);"
  111. f"_spec.loader.exec_module(_module);"
  112. f"_module.{target}()",
  113. *args
  114. ],
  115. env={**os.environ, "SOURCE_DATE_EPOCH": "0", **(extra_env or {})},
  116. timeout=timeout, check=True,
  117. stdout=subprocess.PIPE,
  118. stderr=subprocess.PIPE,
  119. text=True
  120. )
  121. return proc
  122. def _check_for_pgf(texsystem):
  123. """
  124. Check if a given TeX system + pgf is available
  125. Parameters
  126. ----------
  127. texsystem : str
  128. The executable name to check
  129. """
  130. with TemporaryDirectory() as tmpdir:
  131. tex_path = Path(tmpdir, "test.tex")
  132. tex_path.write_text(r"""
  133. \documentclass{article}
  134. \usepackage{pgf}
  135. \begin{document}
  136. \typeout{pgfversion=\pgfversion}
  137. \makeatletter
  138. \@@end
  139. """, encoding="utf-8")
  140. try:
  141. subprocess.check_call(
  142. [texsystem, "-halt-on-error", str(tex_path)], cwd=tmpdir,
  143. stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  144. except (OSError, subprocess.CalledProcessError):
  145. return False
  146. return True
  147. def _has_tex_package(package):
  148. try:
  149. mpl.dviread.find_tex_file(f"{package}.sty")
  150. return True
  151. except FileNotFoundError:
  152. return False
  153. def ipython_in_subprocess(requested_backend_or_gui_framework, all_expected_backends):
  154. import pytest
  155. IPython = pytest.importorskip("IPython")
  156. if sys.platform == "win32":
  157. pytest.skip("Cannot change backend running IPython in subprocess on Windows")
  158. if (IPython.version_info[:3] == (8, 24, 0) and
  159. requested_backend_or_gui_framework == "osx"):
  160. pytest.skip("Bug using macosx backend in IPython 8.24.0 fixed in 8.24.1")
  161. # This code can be removed when Python 3.12, the latest version supported
  162. # by IPython < 8.24, reaches end-of-life in late 2028.
  163. for min_version, backend in all_expected_backends.items():
  164. if IPython.version_info[:2] >= min_version:
  165. expected_backend = backend
  166. break
  167. code = ("import matplotlib as mpl, matplotlib.pyplot as plt;"
  168. "fig, ax=plt.subplots(); ax.plot([1, 3, 2]); mpl.get_backend()")
  169. proc = subprocess_run_for_testing(
  170. [
  171. "ipython",
  172. "--no-simple-prompt",
  173. f"--matplotlib={requested_backend_or_gui_framework}",
  174. "-c", code,
  175. ],
  176. check=True,
  177. capture_output=True,
  178. )
  179. assert proc.stdout.strip().endswith(f"'{expected_backend}'")
  180. def is_ci_environment():
  181. # Common CI variables
  182. ci_environment_variables = [
  183. 'CI', # Generic CI environment variable
  184. 'CONTINUOUS_INTEGRATION', # Generic CI environment variable
  185. 'TRAVIS', # Travis CI
  186. 'CIRCLECI', # CircleCI
  187. 'JENKINS', # Jenkins
  188. 'GITLAB_CI', # GitLab CI
  189. 'GITHUB_ACTIONS', # GitHub Actions
  190. 'TEAMCITY_VERSION' # TeamCity
  191. # Add other CI environment variables as needed
  192. ]
  193. for env_var in ci_environment_variables:
  194. if os.getenv(env_var):
  195. return True
  196. return False