Source code for schrodinger.test.pytest_customizations
"""
Local Schrodinger py.test customizations.
"""
import inspect
import pathlib
import re
import warnings
from unittest import mock
import pytest
from _pytest.fixtures import getfixturemarker
# Register our custom pytest and other test code for assertion rewriting before
# import of modules
for _test_module in pathlib.Path(__file__).parent.iterdir():
    # registering the same file causes a warning
    if _test_module == pathlib.Path(__file__):
        continue
    pytest.register_assert_rewrite(f"schrodinger.test.{_test_module.stem}")
del _test_module
# isort: split
from .pytest import exetest
from .pytest import fixture
from .pytest import reporter
from .pytest import sessionfixture
from .pytest import startup
[docs]def register_fixtures(module):
    """
    Fixtures need to be registered in the pytest_customizations module.
    :param module: module to register pytest.fixture functions
    """
    for name, obj in inspect.getmembers(module):
        if getfixturemarker(obj):
            globals()[name] = obj 
register_fixtures(sessionfixture)
register_fixtures(fixture)
[docs]def pytest_runtest_setup(item):
    sessionfixture.runtest_setup(item) 
[docs]@pytest.hookimpl(trylast=True)
def pytest_runtest_teardown(item, nextitem):
    sessionfixture.runtest_teardown(item, nextitem) 
[docs]def pytest_addoption(parser):
    startup.addoption(parser) 
[docs]def pytest_cmdline_main(config):
    return startup.cmdline_main(config) 
[docs]def pytest_sessionstart(session):
    warnings.filterwarnings("ignore",
                            message=".*some images may be missing",
                            category=RuntimeWarning,
                            module="schrodinger.ui.qt.style") 
# This prints out K and killed when the test is killed, but doesn't include
# it in the summary
[docs]def pytest_report_teststatus(report):
    """Put Killed tests into a separate group from other failures."""
    if report.when == "call":
        try:
            if report.longrepr and "Killed process after timeout" in str(
                    report.longrepr):
                return 'killed', 'K', 'KILLED'
        except TypeError:
            pass 
[docs]def pytest_runtest_makereport(item, call):
    return reporter.runtest_makereport(item, call) 
[docs]def pytest_runtest_logreport(report):
    reporter.runtest_logreport(report) 
[docs]def pytest_collectreport(report):
    reporter.collectreport(report) 
[docs]@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_terminal_summary(terminalreporter):
    reporter.terminal_summary(terminalreporter)
    yield 
[docs]def pytest_sessionfinish(session, exitstatus):
    reporter.sessionfinish(session, exitstatus) 
[docs]def pytest_collect_file(parent, path):
    """
    Pytest function: Should "path" be collected as a test?
    Adds compiled tests.
    """
    if ((path.ext in (".exe", ".pl", ".t") or not path.ext) and
            'test' in path.basename and 'cutest' not in path.basename):
        return exetest.ExecutableFile.from_parent(parent, fspath=path) 
[docs]def pytest_pycollect_makemodule(path, parent):
    """
    For all Python test files, use `ModuleWithPatchCheck` instead of the normal
    `pytest.Module` class.
    """
    return ModuleWithPatchCheck.from_parent(parent, fspath=path) 
[docs]def pytest_itemcollected(item):
    """Don't run most Python tests under memtest."""
    # Some python tests use an allow_memtest fixture to mark that they need
    # memtest. Doctest and some other test types may not allow fixtures.
    if (isinstance(item, exetest.ExecutableTest) or
            'allow_memtest' in getattr(item, 'fixturenames', [])):
        item.add_marker(pytest.mark.memtest) 
[docs]def pytest_collection_modifyitems(items):
    for item in items:
        # Mark all tests with gui in them as require_display
        filename = item.nodeid.split("::")[0]
        if re.search("gui", filename, re.IGNORECASE):
            item.add_marker(pytest.mark.require_display) 
[docs]def pytest_collection_finish(session):
    """
    Work-around for a bad cache of conftest.py
    Removes the cache after all tests have been loaded. At this point, all
    available conftests will also be loaded, so the caching won't be a problem.
    Still leaves us vulnerable to incorrect caching during test discovery,
    though.
    See https://github.com/pytest-dev/pytest/issues/2016
    """
    session._fs2hookproxy = {} 
[docs]def pytest_addhooks(pluginmanager):
    """
    Add a hook to designate an owner of each test.
    """
    from pluggy import HookspecMarker
    hookspec = HookspecMarker("pytest")
    class newhooks:
        @staticmethod
        @hookspec(firstresult=True)
        def pytest_test_owners():
            """
            Specify the owners of tests in this directory.
            :return: tuple of usernames
            """
    pluginmanager.add_hookspecs(newhooks) 
[docs]class ModuleWithPatchCheck(pytest.Module):
    """
    A Module collector that makes sure there are no active patches after module
    import and again after all tests in the module have completed.
    """
    def _getobj(self):
        obj = super()._getobj()
        # Check to see if the import left any active patches.  If it did, report
        # a collection failure, which will prevent any tests from running on
        # developer machines but will still allow other tests to run on buildbot
        # (since buildbot has continue_on_collection_errors set to True).
        patch_targets = self._stopPatches()
        if patch_targets:
            raise self.CollectError(
                f"Active patches found after import of {self.name} for the "
                f"following targets:\n    {', '.join(patch_targets)}\n")
        return obj
[docs]    def teardown(self):
        super().teardown()
        # super().teardown() will trigger teardown_module and tearDownModule, if
        # either is present, so any patches that are still active here are
        # definitely a problem.
        patch_targets = self._stopPatches()
        if patch_targets:
            # this will cause the last test of the module to report an error
            raise RuntimeError(
                f"Active patches found during teardown of {self.name} for the "
                f"following targets:\n    {', '.join(patch_targets)}\n"
                f"Note that the test that failed may not be the test "
                f"responsible for these patches.") 
    def _stopPatches(self):
        """
        Stop any active patches.
        :return: The targets of all patches that were stopped, or None if no
            patches were active.
        :rtype: list[str]
        """
        active_patches = mock._patch._active_patches
        if not active_patches:
            return None
        targets = [f"{p.target.__name__}.{p.attribute}" for p in active_patches]
        mock.patch.stopall()
        return targets