Source code for dandi.tests.skip

# ex: set sts=4 ts=4 sw=4 noet:
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
#   See LICENSE file distributed along with the dandi-cli package for the
#   copyright and license terms.
#
#   This file is borrowed from ReproMan, MIT license, Copyright 2016-2020
#   ReproMan developers.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""Define `skipif` and `mark` namespaces for custom pytest skippers.

There are two main ways to skip in pytest:

* decorating a test function, such as::

        @pytest.mark.skip(sys.platform.startswith("win"), reason="on windows")
        def test_func():
            [...]

* skipping inline, such as::

        def test_func():
            if sys.platform.startswith("win"):
                pytest.skip("on Windows")
            [...]

This module provides a mechanism to register a reason and condition as both a
decorator and an inline function:

* Within this module, create a condition function that returns a tuple of the
  form (REASON, COND). REASON is a str that will be shown as the reason for
  the skip, and COND is a boolean indicating if the test should be skipped.

  For example::

    def windows():
        return "on windows", sys.platform.startswith("win")

* Then add the above function to CONDITION_FNS.

Doing that will make the skip condition available in two places:
`mark.skipif_NAME` and `skipif.NAME`. So, for the above example, there would
now be `mark.skipif_windows` and `skipif.windows`.
"""
import abc
import os
import shutil
import subprocess

import pytest

# from reproman.cmd import Runner
# from reproman.support.exceptions import CommandError
# from reproman.support.external_versions import external_versions
from ..utils import on_windows as _on_windows

# Condition functions
#
# To create a new condition, (1) add a condition function and (2) add that
# function to CONDITION_FNS.

# Left commented out for future references
# def no_apt_cache():
#     return ("apt-cache not available", not external_versions["cmd:apt-cache"])
#
#
# def no_aws_dependencies():
#     return "boto3 not installed", not external_versions["boto3"]
#
#
# def no_condor():
#     def is_running():
#         try:
#             Runner().run(["condor_status"])
#         except CommandError as exc:
#             return False
#         return True
#
#     return (
#         "condor not available",
#         not (external_versions["cmd:condor"] and is_running()),
#     )
#
#
# def no_datalad():
#     return ("datalad not available", not external_versions["datalad"])
#
#
# def no_docker_dependencies():
#     missing_deps = []
#     for dep in "docker", "dockerpty":
#         if dep not in external_versions:
#             missing_deps.append(dep)
#     msg = "missing dependencies: {}".format(", ".join(missing_deps))
#     return msg, missing_deps

# The following code is modified from the original ReproMan source:
# ### BEGIN MODIFIED CODE


[docs]def no_docker_commands(): missing_cmds = [] for cmd in ("docker", "docker-compose"): if shutil.which(cmd) is None: missing_cmds.append(cmd) msg = "missing Docker commands: {}".format(", ".join(missing_cmds)) return msg, missing_cmds
[docs]def no_docker_engine(): def is_engine_running(): # from reproman.resource.docker_container import DockerContainer # return DockerContainer.is_engine_running() r = subprocess.run(["docker", "info"], stdout=subprocess.DEVNULL) return r.returncode == 0 msg, missing_deps = no_docker_commands() if missing_deps: return msg, missing_deps return "docker engine not running", not is_engine_running()
[docs]def no_git(): return "Git not installed", shutil.which("git") is None
# ### END MODIFIED CODE
[docs]def no_network(): return ("no network settings", os.environ.get("DANDI_TESTS_NONETWORK"))
# def no_singularity(): # return ("singularity not available", not external_versions["cmd:singularity"])
[docs]def no_ssh(): if _on_windows: reason = "no ssh on windows" else: reason = "no ssh (DANDI_TESTS_SSH unset)" return (reason, _on_windows or not os.environ.get("DANDI_TESTS_SSH"))
# def no_svn(): # return ("subversion not available", not external_versions["cmd:svn"]) #
[docs]def on_windows(): return "on windows", _on_windows
CONDITION_FNS = [ # no_apt_cache, # no_aws_dependencies, # no_condor, # no_datalad, # no_docker_dependencies, no_docker_commands, no_docker_engine, no_git, no_network, # no_singularity, no_ssh, # no_svn, on_windows, ] # Entry points: skipif and mark
[docs]class NamespaceAttributeError(AttributeError): """Namespace-specific AttributeError. Raised by Namespace when it cannot find the specified condition function. Using a derived class allows us to distinguish an unknown condition function from a condition function that raises an AttributeError. """
[docs]class Namespace(metaclass=abc.ABCMeta): """Provide namespace skip conditions in CONDITION_FNS.""" fns = {c.__name__: c for c in CONDITION_FNS}
[docs] @abc.abstractmethod def attr_value(self, condition_func): """Given a condition function, return an attribute value."""
def __getattr__(self, item): try: condfn = self.fns[item] except KeyError: raise NamespaceAttributeError(item) from None return self.attr_value(condfn)
[docs]class SkipIf(Namespace): """Namespace for inline variants of the condition functions. Each condition is available under an attribute with the same name as the condition function name. """
[docs] def attr_value(self, condition_func): def fn(): reason, cond = condition_func() if cond: pytest.skip(reason, allow_module_level=True) return fn
skipif = SkipIf()
[docs]class Mark(Namespace): """Namespace for mark variants of the condition functions. Each condition is available under an attribute "skipif_NAME", where NAME is the condition function name. """
[docs] def attr_value(self, condition_func): reason, cond = condition_func() return pytest.mark.skipif(cond, reason=reason)
def __getattr__(self, item): if item.startswith("skipif_"): try: return super().__getattr__(item[len("skipif_") :]) except NamespaceAttributeError: # Fall back to the original item name so that the attribute # error message doesn't confusingly drop "skipif_". pass return super().__getattr__(item)
mark = Mark()