Source code for gemseo.utils.testing.pytest_conftest
# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""Pytest helpers."""
from __future__ import annotations
import contextlib
import faulthandler
import os
import sys
import tempfile
from pathlib import Path
from typing import Any
from typing import Generator
import matplotlib.pyplot as plt
import matplotlib.testing.decorators
import pytest
from packaging import version
from gemseo.core.base_factory import BaseFactory
def __tmp_wd(tmp_path):
"""Generator to move into a temporary directory forth and back.
Return the path to the temporary directory.
"""
prev_cwd = Path.cwd()
os.chdir(str(tmp_path))
try:
yield tmp_path
finally:
os.chdir(str(prev_cwd))
@pytest.fixture(scope="module")
def module_tmp_wd(tmp_path_factory):
"""Generator to move into a temporary subdirectory forth and back.
Return the path to the temporary directory.
"""
prev_cwd = Path.cwd()
tmp_path = tmp_path_factory.getbasetemp()
os.chdir(str(tmp_path))
try:
yield tmp_path
finally:
os.chdir(str(prev_cwd))
# Fixture to move into a temporary directory.
tmp_wd = pytest.fixture()(__tmp_wd)
[docs]def pytest_sessionstart(session) -> None:
"""Bypass console pollution from fortran code."""
# Prevent fortran code (like lbfgs from scipy) from writing to the console
# by setting its stdout and stderr units to the standard file descriptors.
# As a side effect, the files fort.{0,6} are created with the content of
# the console outputs.
os.environ["GFORTRAN_STDOUT_UNIT"] = "1"
os.environ["GFORTRAN_STDERR_UNIT"] = "2"
[docs]def pytest_sessionfinish(session) -> None:
"""Remove file pollution from fortran code."""
# take care of pytest_sessionstart side effects
for file_ in Path(".").glob("fort.*"):
try:
file_.unlink()
except PermissionError:
# On windows the file may be opened and not released by another component.
pass
@pytest.fixture(autouse=True)
def skip_under_windows(request) -> None:
"""Fixture that add a marker to skip under windows.
Use it like a usual skip marker.
"""
if request.node.get_closest_marker("skip_under_windows"):
if sys.platform.startswith("win"):
pytest.skip("skipped on windows")
@pytest.fixture
def baseline_images(request):
"""Return the baseline_images contents.
Used when the compare_images decorator has indirect set.
"""
return request.param
@pytest.fixture
def pyplot_close_all() -> None:
"""Fixture that prevents figures aggregation with matplotlib pyplot."""
if version.parse(matplotlib.__version__) < version.parse("3.6.0"):
plt.close("all")
@pytest.fixture
def reset_factory():
"""Reset the factory cache."""
BaseFactory.clear_cache()
yield
BaseFactory.clear_cache()
# Backup before we monkey patch.
original_image_directories = matplotlib.testing.decorators._image_directories
if "GEMSEO_KEEP_IMAGE_COMPARISONS" not in os.environ:
# Context manager to change the current working directory to a temporary one.
__ctx_tmp_wd = contextlib.contextmanager(__tmp_wd)
def _image_directories(func):
"""Create the result_images directory in a temporary parent directory."""
with __ctx_tmp_wd(tempfile.mkdtemp()):
baseline_dir, result_dir = original_image_directories(func)
return baseline_dir, result_dir
matplotlib.testing.decorators._image_directories = _image_directories
# Fixtures to deal with the Excel disciplines.
# Check the presence of xlwings, and skip accordingly.
@pytest.fixture(scope="module")
def disable_fault_handler() -> Generator[None, None, None]:
"""Generator to temporarily disable the fault handler.
Return a call to disable the fault handler.
"""
if faulthandler.is_enabled():
try:
faulthandler.disable()
yield
finally:
faulthandler.enable()
@pytest.fixture(scope="module")
def import_or_skip_xlwings() -> Any:
"""Fixture to skip a test when xlwings cannot be imported."""
return pytest.importorskip("xlwings", reason="xlwings is not available")
@pytest.fixture(scope="module")
def is_xlwings_usable(import_or_skip_xlwings, disable_fault_handler) -> bool:
"""Check if xlwings is usable.
Args:
import_or_skip_xlwings: Fixture to import xlwings when available,
otherwise skip the test.
disable_fault_handler: Fixture to temporarily disable the fault handler.
"""
xlwings = import_or_skip_xlwings
try:
# Launch xlwings from a context manager to ensure it closes immediately.
# See https://docs.xlwings.org/en/stable/whatsnew.html#v0-24-3-jul-15-2021
with xlwings.App(visible=False) as app: # noqa: F841
pass
except: # noqa: E722,B001
return False
else:
return True
@pytest.fixture(scope="module")
def skip_if_xlwings_is_not_usable(is_xlwings_usable: bool) -> None:
"""Fixture to skip a test when xlwings is not usable."""
if not is_xlwings_usable:
pytest.skip("This test requires excel.")
@pytest.fixture(scope="module")
def skip_if_xlwings_is_usable(is_xlwings_usable: bool) -> None:
"""Fixture to skip a test when xlwings is usable."""
if is_xlwings_usable:
pytest.skip("This test is only required when excel is not available.")