Source code for consolekit.testing

#!/usr/bin/env python3
#
#  testing.py
"""
Test helpers.

.. versionadded:: 0.9.0

.. extras-require:: testing
	:pyproject:
"""
#
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#
#  Result and CliRunner based on https://github.com/pallets/click
#  Copyright 2014 Pallets
#  |  Redistribution and use in source and binary forms, with or without modification,
#  |  are permitted provided that the following conditions are met:
#  |
#  |      * Redistributions of source code must retain the above copyright notice,
#  |        this list of conditions and the following disclaimer.
#  |      * Redistributions in binary form must reproduce the above copyright notice,
#  |        this list of conditions and the following disclaimer in the documentation
#  |        and/or other materials provided with the distribution.
#  |      * Neither the name of the copyright holder nor the names of its contributors
#  |        may be used to endorse or promote products derived from this software without
#  |        specific prior written permission.
#  |
#  |  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  |  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  |  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#  |  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
#  |  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
#  |  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
#  |  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
#  |  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
#  |  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
#  |  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#  |  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

# stdlib
from types import TracebackType
from typing import IO, Any, Iterable, Mapping, Optional, Tuple, Type, Union

# 3rd party
import click.testing
import pytest  # nodep
from coincidence.regressions import check_file_regression  # nodep
from pytest_regressions.file_regression import FileRegressionFixture  # nodep
from typing_extensions import Literal

__all__ = ("CliRunner", "Result", "cli_runner")

_click_major = int(click.__version__.split('.')[0])


[docs]class Result(click.testing.Result): """ Holds the captured result of an invoked CLI script. :param runner: The runner that created the result. :param stdout_bytes: The standard output as bytes. :param stderr_bytes: The standard error as bytes, or :py:obj:`None` if not available. :param exit_code: The command's exit code. :param exception: The exception that occurred, if any. :param exc_info: The traceback, if an exception occurred. """ runner: click.testing.CliRunner exit_code: int exception: Optional[BaseException] exc_info: Optional[Any] stdout_bytes: bytes stderr_bytes: Optional[bytes] return_value: Optional[Tuple[Type[BaseException], BaseException, TracebackType]] def __init__( self, runner: click.testing.CliRunner, stdout_bytes: bytes, stderr_bytes: Optional[bytes], exit_code: int, exception: Optional[BaseException], exc_info: Optional[Tuple[Type[BaseException], BaseException, TracebackType]] = None, ) -> None: if _click_major >= 8: super().__init__( runner=runner, stdout_bytes=stdout_bytes, stderr_bytes=stderr_bytes, exit_code=exit_code, exception=exception, exc_info=exc_info, return_value=None, ) else: super().__init__( # type: ignore[call-arg] runner=runner, stdout_bytes=stdout_bytes, stderr_bytes=stderr_bytes, exit_code=exit_code, exception=exception, exc_info=exc_info, ) @property def output(self) -> str: """ The (standard) output as a string. """ return super().output @property def stdout(self) -> str: """ The standard output as a string. """ return super().stdout @property def stderr(self) -> str: """ The standard error as a string. """ return super().stderr @classmethod def _from_click_result(cls, result: click.testing.Result) -> "Result": return cls( runner=result.runner, stdout_bytes=result.stdout_bytes, stderr_bytes=result.stderr_bytes, exit_code=result.exit_code, exception=result.exception, exc_info=result.exc_info, )
[docs] def check_stdout( self, file_regression: FileRegressionFixture, extension: str = ".txt", **kwargs, ) -> Literal[True]: r""" Perform a regression check on the standard output from the command. :param file_regression: :param extension: The extension of the reference file. :param \*\*kwargs: Additional keyword arguments passed to :meth:`.FileRegressionFixture.check`. """ __tracebackhide__ = True check_file_regression(self.stdout.rstrip(), file_regression, extension=extension, **kwargs) return True
[docs]class CliRunner(click.testing.CliRunner): """ Provides functionality to invoke and test a Click script in an isolated environment. This only works in single-threaded systems without any concurrency as it changes the global interpreter state. :param charset: The character set for the input and output data. :param env: A dictionary with environment variables to override. :param echo_stdin: If :py:obj:`True`, then reading from stdin writes to stdout. This is useful for showing examples in some circumstances. Note that regular prompts will automatically echo the input. :param mix_stderr: If :py:obj:`False`, then stdout and stderr are preserved as independent streams. This is useful for Unix-philosophy apps that have predictable stdout and noisy stderr, such that each may be measured independently. .. autoclasssumm:: CliRunner :autosummary-sections: ;; """ def __init__( self, charset: str = "UTF-8", env: Optional[Mapping[str, str]] = None, *, echo_stdin: bool = False, mix_stderr: bool = True, ) -> None: super().__init__(charset, env, echo_stdin, mix_stderr)
[docs] def invoke( # type: ignore[override] self, cli: click.BaseCommand, args: Optional[Union[str, Iterable[str]]] = None, input: Optional[Union[bytes, str, IO]] = None, # noqa: A002 # pylint: disable=redefined-builtin env: Optional[Mapping[str, str]] = None, *, catch_exceptions: bool = False, color: bool = False, **extra, ) -> Result: r""" Invokes a command in an isolated environment. The arguments are forwarded directly to the command line script, the ``extra`` keyword arguments are passed to the :meth:`~click.Command.main` function of the command. :param cli: The command to invoke. :param args: The arguments to invoke. It may be given as an iterable or a string. When given as string it will be interpreted as a Unix shell command. More details at :func:`shlex.split`. :param input: The input data for ``sys.stdin``. :param env: The environment overrides. :param catch_exceptions: Whether to catch any other exceptions than :exc:`SystemExit`. :param color: whether the output should contain color codes. The application can still override this explicitly. :param \*\*extra: The keyword arguments to pass to :meth:`click.Command.main`. """ if args is not None and not isinstance(args, str): args = list(args) result = super().invoke( cli, args=args, input=input, env=env, catch_exceptions=catch_exceptions, color=color, **extra, ) return Result._from_click_result(result)
[docs]@pytest.fixture() def cli_runner() -> CliRunner: """ Returns a click runner for this test function. """ return CliRunner()