diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e40448e..90037e2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,20 +1,20 @@ [bumpversion] commit = True tag = True message = "Release {new_version}" -current_version = 2.0.2 +current_version = 2.1.0 [bumpversion:file:setup.py] search = version='{current_version}' replace = version='{new_version}' [bumpversion:file:src/mirakuru/__init__.py] [bumpversion:file:README.rst] [bumpversion:file:CHANGES.rst] search = unreleased ---------- replace = {new_version} ---------- diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 0000000..c77885a --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,8 @@ +version: 1 +update_configs: + - package_manager: "python" + directory: "/" + update_schedule: "daily" + automerged_updates: + - match: + dependency_name: "*" \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 94ecc5f..16e2918 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,57 +1,57 @@ dist: xenial language: python conditions: v1 python: -- 3.5 - 3.6 - 3.7 -- pypy3.5 +- 3.8-dev +- pypy3 # blocklist branches branches: except: - requires-io-master + - /^dependabot.*$/ install: - pip install "setuptools>=21" - pip install "pip>=9" - pip install -r requirements-test.txt - - pip install -e .[tests] coveralls wheel + - pip install coveralls script: - - pytest -vvv --capture=no --showlocals --cov src/mirakuru tests/ + - pytest after_success: - coveralls jobs: include: - stage: linters python: 3.7 install: - pip install -r requirements-lint.txt - - pip install .[tests] coveralls wheel script: - pycodestyle - pydocstyle - pylint mirakuru tests - mypy src tests - rst-lint *.rst after_success: skip - stage: osx language: generic os: osx before_install: - pip3 install virtualenv - virtualenv venv -p python3 - source venv/bin/activate script: - - pytest -vvv --capture=no --showlocals --cov mirakuru tests/ + - pytest - stage: deploy python: 3.7 if: tag IS present script: skip deploy: provider: pypi user: fizyk password: secure: IBVXG0zLKsBkzdeoC33Lxir01jbvDHdjQ81CPC8PbDPCmUozXgf9eqRFV5VOIYQOboTBzQYRq7RB8efeNKSH3nKf73iahwIYf4ezIxRzUaMzoY4GkyrC/0fQhMk1lAjexrRM1f2o7TIAALPUDyB/EaRcPCBEghxscQEeTlAw08c= on: tags: true repo: ClearcodeHQ/mirakuru distributions: sdist bdist_wheel diff --git a/CHANGES.rst b/CHANGES.rst index 4424d4b..2f1dd27 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,178 +1,183 @@ CHANGELOG ========= -2.0.2 +2.1.0 ---------- -- [enhancement] Change typing definition to allow mirakuru run on - python 3.5.0 to 3.5.2 +- [feature] Drop support for python 3.5. Rely on typing syntax and fstrings that + is available since python 3.6 only +- [ehnancement] For output executor on MacOs fallback to `select.select` for OutputExecutor. + Increases compatibility with MacOS where presence of `select.poll` depends + on the compiler used. +- [enhancement] Apply shelx.quote on command parts if command is given as a list + Should result in similar results when running such command with or without shell. 2.0.1 ---------- - [repackage] - mark python 3.5 as required. Should disallow installing on python 2 2.0.0 ---------- - [feature] Add UnixSocketExecutor for executors that communicate with Unix Sockets - [feature] Mirakuru is now fully type hinted - [feature] Drop support for python 2 - [feature] Allow for configuring process outputs to pipe to - [feature] OutputExecutor can now check for banner in stderr - [feature] HTTPEecutor now can check status on different method. Along with properly configured payload and headers. - [feature] Ability to set custom env vars for orchestrated process - [feature] Ability to set custom cwd path for orchestrated process - [enhancement] psutil is no longer required on cygwin 1.1.0 ---------- - [enhancement] Executor's timeout to be set for both executor's start and stop - [enhancement] It's no longer possible to hang indefinitely on the start or stop. Timeout is set to 3600 seconds by default, with values possible between `0` and `sys.maxsize` with the latter still bit longer than `2924712086` centuries. 1.0.0 ---------- - [enhancement] Do not fail if processes child throw EPERM error during clean up phase - [enhancement] Run subprocesses in shell by default on Windows - [ehnancement] Do not pass preexec_fn on windows 0.9.0 ---------- - [enhancement] Fallback to kill through SIGTERM on Windows, since SIGKILL is not available - [enhancement] detect cases where during stop process already exited, and simply clean up afterwards 0.8.3 ---------- - [enhancement] when killing the process ignore OsError with errno `no such process` as the process have already died. - [enhancement] small context manager code cleanup 0.8.2 ---------- - [bugfix] atexit cleanup_subprocesses() function now reimports needed functions 0.8.1 ---------- - [bugfix] Handle IOErrors from psutil (#112) - [bugfix] Pass global vars to atexit cleanup_subprocesses function (#111) 0.8.0 ---------- - [feature] Kill all running mirakuru subprocesses on python exit. - [enhancement] Prefer psutil library (>=4.0.0) over calling 'ps xe' command to find leaked subprocesses. 0.7.0 ---------- - [feature] HTTPExecutor enriched with the 'status' argument. It allows to define which HTTP status code(s) signify that a HTTP server is running. - [feature] Changed executor methods to return itself to allow method chaining. - [feature] Context Manager to return Executor instance, allows creating Executor instance on the fly. - [style] Migrated `%` string formating to `format()`. - [style] Explicitly numbered replacement fields in string. - [docs] Added documentation for timeouts. 0.6.1 ---------- - [refactoring] Moved source to src directory. - [fix, feature] Python 3.5 fixes. - [fix] Docstring changes for updated pep257. 0.6.0 ---------- - [fix] Modify MANIFEST to prune tests folder. - [feature] HTTPExecutor will now set the default 80 if not present in a URL. - [feature] Detect subprocesses exiting erroneously while polling the checks and error early. - [fix] Make test_forgotten_stop pass by preventing the shell from optimizing forking out. 0.5.0 ---------- - [style] Corrected code to conform with W503, D210 and E402 linters errors as reported by pylama `6.3.1`. - [feature] Introduced a hack that kills all subprocesses of executor process. It requires 'ps xe -ww' command being available in OS otherwise logs error. - [refactoring] Classes name convention change. Executor class got renamed into SimpleExecutor and StartCheckExecutor class got renamed into Executor. 0.4.0 ------- - [feature] Ability to set up custom signal for stopping and killing processes managed by executors. - [feature] Replaced explicit parameters with keywords for kwargs handled by basic Executor init method. - [feature] Executor now accepts both list and string as a command. - [fix] Even it's not recommended to import all but `from mirakuru import *` didn't worked. Now it's fixed. - [tests] increased tests coverage. Even test cover 100% of code it doesn't mean they cover 100% of use cases! - [code quality] Increased Pylint code evaluation. 0.3.0 ------- - [feature] Introduced PidExecutor that waits for specified file to be created. - [feature] Provided PyPy compatibility. - [fix] Closing all resources explicitly. 0.2.0 ------- - [fix] Kill all children processes of Executor started with shell=True. - [feature] Executors are now context managers - to start executors for given context. - [feature] Executor.stopped - context manager for stopping executors for given context. - [feature] HTTPExecutor and TCPExecutor before .start() check whether port is already used by other processes and raise AlreadyRunning if detects it. - [refactoring] Moved python version conditional imports into compat.py module. 0.1.4 ------- - [fix] Fixed an issue where setting shell to True would execute only part of the command. 0.1.3 ------- - [fix] Fixed an issue where OutputExecutor would hang, if started process stopped producing output. 0.1.2 ------- - [fix] Removed leftover sleep from TCPExecutor._wait_for_connection. 0.1.1 ------- - [fix] Fixed `MANIFEST.in`. - Updated packaging options. 0.1.0 ------- - Exposed process attribute on Executor. - Exposed port and host on TCPExecutor. - Exposed URL on HTTPExecutor. - Simplified package structure. - Simplified executors operating API. - Updated documentation. - Added docblocks for every function. - Applied license headers. - Stripped orchestrators. - Forked off from `summon_process`. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 378f68d..0000000 --- a/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM python:3.5.2 - -WORKDIR /usr/src/app - -RUN pip install --upgrade pip -COPY . . -RUN pip install --no-cache-dir -r requirements-lint.txt -RUN pip install --no-cache-dir -r requirements-test.txt -RUN pip install --no-cache-dir -e .[tests] - -COPY . . - -CMD [ "pytest" ] diff --git a/README.rst b/README.rst index 36a50db..acf6ae5 100644 --- a/README.rst +++ b/README.rst @@ -1,123 +1,123 @@ mirakuru ======== Mirakuru is a process orchestration tool designed for functional and integration tests. Maybe you want to be able to start a database before you start your program or maybe you just need to set additional services up for your tests. This is where you should consider using **mirakuru** to add superpowers to your program or tests. .. image:: https://img.shields.io/pypi/v/mirakuru.svg :target: https://pypi.python.org/pypi/mirakuru/ :alt: Latest PyPI version -.. image:: https://readthedocs.org/projects/mirakuru/badge/?version=v2.0.2 - :target: http://mirakuru.readthedocs.io/en/v2.0.2/ +.. image:: https://readthedocs.org/projects/mirakuru/badge/?version=v2.1.0 + :target: http://mirakuru.readthedocs.io/en/v2.1.0/ :alt: Documentation Status .. image:: https://img.shields.io/pypi/wheel/mirakuru.svg :target: https://pypi.python.org/pypi/mirakuru/ :alt: Wheel Status .. image:: https://img.shields.io/pypi/pyversions/mirakuru.svg :target: https://pypi.python.org/pypi/mirakuru/ :alt: Supported Python Versions .. image:: https://img.shields.io/pypi/l/mirakuru.svg :target: https://pypi.python.org/pypi/mirakuru/ :alt: License Package status -------------- -.. image:: https://travis-ci.org/ClearcodeHQ/mirakuru.svg?branch=v2.0.2 +.. image:: https://travis-ci.org/ClearcodeHQ/mirakuru.svg?branch=v2.1.0 :target: https://travis-ci.org/ClearcodeHQ/mirakuru :alt: Tests -.. image:: https://coveralls.io/repos/ClearcodeHQ/mirakuru/badge.png?branch=v2.0.2 - :target: https://coveralls.io/r/ClearcodeHQ/mirakuru?branch=v2.0.2 +.. image:: https://coveralls.io/repos/ClearcodeHQ/mirakuru/badge.png?branch=v2.1.0 + :target: https://coveralls.io/r/ClearcodeHQ/mirakuru?branch=v2.1.0 :alt: Coverage Status -.. image:: https://requires.io/github/ClearcodeHQ/mirakuru/requirements.svg?tag=v2.0.2 - :target: https://requires.io/github/ClearcodeHQ/mirakuru/requirements/?tag=v2.0.2 +.. image:: https://requires.io/github/ClearcodeHQ/mirakuru/requirements.svg?tag=v2.1.0 + :target: https://requires.io/github/ClearcodeHQ/mirakuru/requirements/?tag=v2.1.0 :alt: Requirements Status About ----- In a project that relies on multiple processes there might be a need to guard code with tests that verify interprocess communication. So one needs to set up all of required databases, auxiliary and application services to verify their cooperation. Synchronising (or orchestrating) test procedure with tested processes might be a hell. If so, then **mirakuru** is what you need. ``Mirakuru`` starts your process and waits for the clear indication that it's running. Library provides seven executors to fit different cases: * **SimpleExecutor** - starts a process and does not wait for anything. It is useful to stop or kill a process and its subprocesses. Base class for all the rest of executors. * **Executor** - base class for executors verifying if a process has started. * **OutputExecutor** - waits for a specified output to be printed by a process. * **TCPExecutor** - waits for the ability to connect through TCP with a process. * **UnixSocketExecutor** - waits for the ability to connect through Unix socket with a process * **HTTPExecutor** - waits for a successful HEAD request (and TCP before). * **PidExecutor** - waits for a specified .pid file to exist. .. code-block:: python from mirakuru import HTTPExecutor from httplib import HTTPConnection, OK def test_it_works(): # The ``./http_server`` here launches some HTTP server on the 6543 port, # but naturally it is not immediate and takes a non-deterministic time: executor = HTTPExecutor("./http_server", url="http://127.0.0.1:6543/") # Start the server and wait for it to run (blocking): executor.start() # Here the server should be running! conn = HTTPConnection("127.0.0.1", 6543) conn.request("GET", "/") assert conn.getresponse().status is OK executor.stop() A command by which executor spawns a process can be defined by either string or list. .. code-block:: python # command as string TCPExecutor('python -m smtpd -n -c DebuggingServer localhost:1025', host='localhost', port=1025) # command as list TCPExecutor( ['python', '-m', 'smtpd', '-n', '-c', 'DebuggingServer', 'localhost:1025'], host='localhost', port=1025 ) Authors ------- The project was firstly developed by `Mateusz Lenik `_ as the `summon_process `_. Later forked, renamed into **mirakuru** and tended to by The A Room @ `Clearcode `_ and `the other authors `_. License ------- ``mirakuru`` is licensed under LGPL license, version 3. Contributing and reporting bugs ------------------------------- Source code is available at: `ClearcodeHQ/mirakuru `_. Issue tracker is located at `GitHub Issues `_. Projects `PyPI page `_. When contributing, don't forget to add your name to the AUTHORS.rst file. diff --git a/pytest.ini b/pytest.ini index 39baf33..5588693 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,5 @@ [pytest] +addopts = -vvv --capture=no --showlocals --verbose --cov src/mirakuru --cov tests testpaths = tests/ +filterwarnings = error +xfail_strict = True diff --git a/requirements-lint.txt b/requirements-lint.txt index c3ed494..3de3d3a 100644 --- a/requirements-lint.txt +++ b/requirements-lint.txt @@ -1,7 +1,8 @@ # linters pycodestyle==2.5.0 -pydocstyle==3.0.0 +pydocstyle==4.0.1 pylint==2.3.1 pygments restructuredtext-lint==1.3.0 -mypy==0.710 +mypy==0.720 +-r requirements-test.txt \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index 16c4bcb..5d3164c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,8 @@ -# test runs requirements (versions we'll be testing against) - automatically updated by requires.io -coverage==4.5.3 # pytest-cov +# test runs requirements (versions we'll be testing against) - automatically updated psutil==5.6.3 +pytest==5.1.2 # tests framework used +pytest-cov==2.7.1 # coverage reports to verify tests quality +coverage==4.5.4 # pytest-cov +python-daemon==2.2.3 # used in test for easy creation of daemons docutils # needed for python-daemon +-e .[tests] diff --git a/setup.py b/setup.py index b07a79f..a519f18 100644 --- a/setup.py +++ b/setup.py @@ -1,93 +1,91 @@ # Copyright (C) 2014 by Clearcode # and associates (see AUTHORS). # This file is part of mirakuru. # mirakuru is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # mirakuru 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 mirakuru. If not, see . """Mirakuru installation module.""" import os from setuptools import setup, find_packages here = os.path.dirname(__file__) requirements = [ # psutil is used to find processes leaked during termination. # It runs on many platforms but not Cygwin: # . 'psutil>=4.0.0; sys_platform != "cygwin"', ] tests_require = ( - 'pytest==4.6.3', # tests framework used - 'pytest-cov==2.7.1', # coverage reports to verify tests quality - 'mock==3.0.5', # tests mocking tool - 'python-daemon==2.2.3', # used in test for easy creation of daemons + 'pytest', # tests framework used + 'pytest-cov', # coverage reports to verify tests quality + 'python-daemon', # used in test for easy creation of daemons ) extras_require = { 'docs': ['sphinx'], 'tests': tests_require, } def read(fname): """ Read filename. :param str fname: name of a file to read """ return open(os.path.join(here, fname)).read() setup( name='mirakuru', - version='2.0.2', + version='2.1.0', description='Process executor for tests.', long_description=( read('README.rst') + '\n\n' + read('CHANGES.rst') ), keywords='process executor tests summon_process', url='https://github.com/ClearcodeHQ/mirakuru', author='Clearcode - The A Room', author_email='thearoom@clearcode.cc', license='LGPL', - python_requires='>=3.5', + python_requires='>=3.6', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: ' 'GNU Lesser General Public License v3 or later (LGPLv3+)', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Testing', ], package_dir={'': 'src'}, packages=find_packages('src'), install_requires=requirements, tests_require=tests_require, test_suite='tests', include_package_data=True, zip_safe=False, extras_require=extras_require, ) diff --git a/src/mirakuru/__init__.py b/src/mirakuru/__init__.py index 1c9160e..8d32c83 100644 --- a/src/mirakuru/__init__.py +++ b/src/mirakuru/__init__.py @@ -1,53 +1,53 @@ # Copyright (C) 2014 by Clearcode # and associates (see AUTHORS). # This file is part of mirakuru. # mirakuru is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # mirakuru 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 mirakuru. If not, see . """Mirakuru main module.""" import logging from mirakuru.base import Executor, SimpleExecutor from mirakuru.output import OutputExecutor from mirakuru.tcp import TCPExecutor from mirakuru.http import HTTPExecutor from mirakuru.pid import PidExecutor from mirakuru.exceptions import ( ExecutorError, TimeoutExpired, AlreadyRunning, ProcessExitedWithError, ) -__version__ = '2.0.2' +__version__ = '2.1.0' __all__ = ( 'Executor', 'SimpleExecutor', 'OutputExecutor', 'TCPExecutor', 'HTTPExecutor', 'PidExecutor', 'ExecutorError', 'TimeoutExpired', 'AlreadyRunning', 'ProcessExitedWithError', ) # Set default logging handler to avoid "No handler found" warnings. logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/src/mirakuru/base.py b/src/mirakuru/base.py index c3ae54f..4a281e2 100644 --- a/src/mirakuru/base.py +++ b/src/mirakuru/base.py @@ -1,526 +1,520 @@ # Copyright (C) 2014 by Clearcode # and associates (see AUTHORS). # This file is part of mirakuru. # mirakuru is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # mirakuru 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 mirakuru. If not, see . """Executor with the core functionality.""" import atexit from contextlib import contextmanager import logging import os import shlex import signal import subprocess import time import uuid import errno import platform from types import TracebackType from typing import ( Union, IO, Any, List, Tuple, Optional, Dict, TypeVar, Type, Set, Iterator, Callable ) from mirakuru.base_env import processes_with_env from mirakuru.exceptions import ( AlreadyRunning, ProcessExitedWithError, TimeoutExpired, ) from mirakuru.compat import SIGKILL log = logging.getLogger(__name__) # pylint: disable=invalid-name ENV_UUID = 'mirakuru_uuid' """ Name of the environment variable used by mirakuru to mark its subprocesses. """ IGNORED_ERROR_CODES = [errno.ESRCH] if platform.system() == 'Darwin': IGNORED_ERROR_CODES = [errno.ESRCH, errno.EPERM] # Type variables used for self in functions returning self, so it's correctly # typed in derived classes. SimpleExecutorType = TypeVar("SimpleExecutorType", bound="SimpleExecutor") ExecutorType = TypeVar("ExecutorType", bound="Executor") @atexit.register def cleanup_subprocesses() -> None: """On python exit: find possibly running subprocesses and kill them.""" # pylint: disable=redefined-outer-name, reimported # atexit functions tends to loose global imports sometimes so reimport # everything what is needed again here: import os import errno from mirakuru.base_env import processes_with_env from mirakuru.compat import SIGKILL pids = processes_with_env(ENV_UUID, str(os.getpid())) for pid in pids: try: os.kill(pid, SIGKILL) except OSError as err: if err.errno != errno.ESRCH: print("Can not kill the", pid, "leaked process", err) class SimpleExecutor: # pylint:disable=too-many-instance-attributes """Simple subprocess executor with start/stop/kill functionality.""" def __init__( # pylint:disable=too-many-arguments self, command: Union[str, List[str], Tuple[str, ...]], cwd: Optional[str] = None, shell: bool = False, timeout: Union[int, float] = 3600, sleep: float = 0.1, sig_stop: int = signal.SIGTERM, sig_kill: int = SIGKILL, envvars: Optional[Dict[str, str]] = None, stdin: Union[None, int, IO[Any]] = subprocess.PIPE, stdout: Union[None, int, IO[Any]] = subprocess.PIPE, stderr: Union[None, int, IO[Any]] = None ) -> None: """ Initialize executor. :param (str, list) command: command to be run by the subprocess :param str cwd: current working directory to be set for executor :param bool shell: same as the `subprocess.Popen` shell definition. On Windows always set to True. :param int timeout: number of seconds to wait for the process to start or stop. :param float sleep: how often to check for start/stop condition :param int sig_stop: signal used to stop process run by the executor. default is `signal.SIGTERM` :param int sig_kill: signal used to kill process run by the executor. default is `signal.SIGKILL` (`signal.SIGTERM` on Windows) :param dict envvars: Additional environment variables :param int stdin: file descriptor for stdin :param int stdout: file descriptor for stdout :param int stderr: file descriptor for stderr .. note:: **timeout** set for an executor is valid for all the level of waits on the way up. That means that if some more advanced executor establishes the timeout to 10 seconds and it will take 5 seconds for the first check, second check will only have 5 seconds left. Your executor will raise an exception if something goes wrong during this time. The default value of timeout is ``None``, so it is a good practice to set this. """ if isinstance(command, (list, tuple)): - self.command = ' '.join(command) + self.command = ' '.join((shlex.quote(c) for c in command)) """Command that the executor runs.""" self.command_parts = command else: self.command = command self.command_parts = shlex.split(command) self._cwd = cwd self._shell = True if platform.system() != 'Windows': self._shell = shell self._timeout = timeout self._sleep = sleep self._sig_stop = sig_stop self._sig_kill = sig_kill self._envvars = envvars or {} self._stdin = stdin self._stdout = stdout self._stderr = stderr - self._endtime = None # type: Optional[float] - self.process = None # type: Optional[subprocess.Popen] + self._endtime: Optional[float] = None + self.process: Optional[subprocess.Popen] = None """A :class:`subprocess.Popen` instance once process is started.""" - self._uuid = '{0}:{1}'.format(os.getpid(), uuid.uuid4()) + self._uuid = f'{os.getpid()}:{uuid.uuid4()}' def __enter__(self: SimpleExecutorType) -> SimpleExecutorType: """ Enter context manager starting the subprocess. :returns: itself :rtype: SimpleExecutor """ return self.start() def __exit__(self, - exc_type: Type[BaseException], + exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> None: """Exit context manager stopping the subprocess.""" self.stop() def running(self) -> bool: """ Check if executor is running. :returns: SimpleExecutorTyperue if process is running, False otherwise :rtype: bool """ if self.process is None: return False return self.process.poll() is None @property def _popen_kwargs(self) -> Dict[str, Any]: """ Get kwargs for the process instance. .. note:: We want to open ``stdin``, ``stdout`` and ``stderr`` as text streams in universal newlines mode, so we have to set ``universal_newlines`` to ``True``. :return: """ - kwargs = {} # type: Dict[str, Any] + kwargs: Dict[str, Any] = {} if self._stdin: kwargs['stdin'] = self._stdin if self._stdout: kwargs['stdout'] = self._stdout if self._stderr: kwargs['stderr'] = self._stderr kwargs['universal_newlines'] = True kwargs['shell'] = self._shell env = os.environ.copy() env.update(self._envvars) # Trick with marking subprocesses with an environment variable. # # There is no easy way to recognize all subprocesses that were # spawned during lifetime of a certain subprocess so mirakuru does # this hack in order to mark who was the original parent. Even if # some subprocess got daemonized or changed original process group # mirakuru will be able to find it by this environment variable. # # There may be a situation when some subprocess will abandon # original envs from parents and then it won't be later found. env[ENV_UUID] = self._uuid kwargs['env'] = env kwargs['cwd'] = self._cwd if platform.system() != 'Windows': kwargs['preexec_fn'] = os.setsid return kwargs def start(self: SimpleExecutorType) -> SimpleExecutorType: """ Start defined process. After process gets started, timeout countdown begins as well. :returns: itself :rtype: SimpleExecutor """ if self.process is None: - command = \ - self.command # type: Union[str, List[str], Tuple[str, ...]] + command: Union[str, List[str], Tuple[str, ...]] = self.command if not self._shell: command = self.command_parts self.process = subprocess.Popen( command, **self._popen_kwargs ) self._set_timeout() return self def _set_timeout(self) -> None: """Set timeout for possible wait.""" self._endtime = time.time() + self._timeout def _clear_process(self) -> None: """ Close stdin/stdout of subprocess. It is required because of ResourceWarning in Python 3. """ if self.process: self.process.__exit__(None, None, None) self.process = None self._endtime = None def _kill_all_kids(self, sig: int) -> Set[int]: """ Kill all subprocesses (and its subprocesses) that executor started. This function tries to kill all leftovers in process tree that current executor may have left. It uses environment variable to recognise if process have origin in this Executor so it does not give 100 % and some daemons fired by subprocess may still be running. :param int sig: signal used to stop process run by executor. :return: process ids (pids) of killed processes :rtype: set """ pids = processes_with_env(ENV_UUID, self._uuid) for pid in pids: log.debug("Killing process %d ...", pid) try: os.kill(pid, sig) except OSError as err: if err.errno in IGNORED_ERROR_CODES: # the process has died before we tried to kill it. pass else: raise log.debug("Killed process %d.", pid) return pids def stop(self: SimpleExecutorType, sig: int = None) -> SimpleExecutorType: """ Stop process running. Wait 10 seconds for the process to end, then just kill it. :param int sig: signal used to stop process run by executor. None for default. :returns: itself :rtype: SimpleExecutor .. note:: When gathering coverage for the subprocess in tests, you have to allow subprocesses to end gracefully. """ if self.process is None: return self if sig is None: sig = self._sig_stop try: os.killpg(self.process.pid, sig) except OSError as err: if err.errno in IGNORED_ERROR_CODES: pass else: raise def process_stopped() -> bool: """Return True only only when self.process is not running.""" return self.running() is False self._set_timeout() try: self.wait_for(process_stopped) except TimeoutExpired: # at this moment, process got killed, pass self._kill_all_kids(sig) self._clear_process() return self @contextmanager def stopped(self: SimpleExecutorType) -> Iterator[SimpleExecutorType]: """ Stop process for given context and starts it afterwards. Allows for easier writing resistance integration tests whenever one of the service fails. :yields: itself :rtype: SimpleExecutor """ if self.running(): self.stop() yield self self.start() def kill( self: SimpleExecutorType, wait: bool = True, sig: Optional[int] = None) -> SimpleExecutorType: """ Kill the process if running. :param bool wait: set to `True` to wait for the process to end, or False, to simply proceed after sending signal. :param int sig: signal used to kill process run by the executor. None by default. :returns: itself :rtype: SimpleExecutor """ if sig is None: sig = self._sig_kill if self.process and self.running(): os.killpg(self.process.pid, sig) if wait: self.process.wait() self._kill_all_kids(sig) self._clear_process() return self def output(self) -> Optional[IO[Any]]: """Return subprocess output.""" if self.process is not None: return self.process.stdout return None # pragma: no cover def err_output(self) -> Optional[IO[Any]]: """Return subprocess stderr.""" if self.process is not None: return self.process.stderr return None # pragma: no cover def wait_for( self: SimpleExecutorType, wait_for: Callable[[], bool]) -> SimpleExecutorType: """ Wait for callback to return True. Simply returns if wait_for condition has been met, raises TimeoutExpired otherwise and kills the process. :param callback wait_for: callback to call :raises: mirakuru.exceptions.TimeoutExpired :returns: itself :rtype: SimpleExecutor """ while self.check_timeout(): if wait_for(): return self time.sleep(self._sleep) self.kill() raise TimeoutExpired(self, timeout=self._timeout) def check_timeout(self) -> bool: """ Check if timeout has expired. Returns True if there is no timeout set or the timeout has not expired. Kills the process and raises TimeoutExpired exception otherwise. This method should be used in while loops waiting for some data. :return: True if timeout expired, False if not :rtype: bool """ return self._endtime is None or time.time() <= self._endtime def __del__(self) -> None: """Cleanup subprocesses created during Executor lifetime.""" try: if self.process: self.kill() except Exception: # pragma: no cover print("*" * 80) print("Exception while deleting Executor. '" "It is strongly suggested that you use") print("it as a context manager instead.") print("*" * 80) raise def __repr__(self) -> str: """Return unambiguous executor representation.""" command = self.command if len(command) > 10: command = command[:10] + '...' - return '<{module}.{executor}: "{command}" {id}>'.format( - module=self.__class__.__module__, - executor=self.__class__.__name__, - command=command, - id=hex(id(self)) - ) + module = self.__class__.__module__ + executor = self.__class__.__name__ + return f'<{module}.{executor}: "{command}" {hex(id(self))}>' def __str__(self) -> str: """Return readable executor representation.""" - return '<{module}.{executor}: "{command}">'.format( - module=self.__class__.__module__, - executor=self.__class__.__name__, - command=self.command - ) + module = self.__class__.__module__ + executor = self.__class__.__name__ + return f'<{module}.{executor}: "{self.command}" {hex(id(self))}>' class Executor(SimpleExecutor): """Base class for executors with a pre- and after-start checks.""" def pre_start_check(self) -> bool: """ Check process before the start of executor. Should be overridden in order to return True when some other executor (or process) has already started with the same configuration. :rtype: bool """ raise NotImplementedError def start(self: ExecutorType) -> ExecutorType: """ Start executor with additional checks. Checks if previous executor isn't running then start process (executor) and wait until it's started. :returns: itself :rtype: Executor """ if self.pre_start_check(): # Some other executor (or process) is running with same config: raise AlreadyRunning(self) super(Executor, self).start() self.wait_for(self.check_subprocess) return self def check_subprocess(self) -> bool: """ Make sure the process didn't exit with an error and run the checks. :rtype: bool :return: the actual check status or False before starting the process :raise ProcessExitedWithError: when the main process exits with an error """ if self.process is None: # pragma: no cover # No process was started. return False exit_code = self.process.poll() if exit_code is not None and exit_code != 0: # The main process exited with an error. Clean up the children # if any. self._kill_all_kids(self._sig_kill) self._clear_process() raise ProcessExitedWithError(self, exit_code) return self.after_start_check() def after_start_check(self) -> bool: """ Check process after the start of executor. Should be overridden in order to return boolean value if executor can be treated as started. :rtype: bool """ raise NotImplementedError diff --git a/src/mirakuru/base_env.py b/src/mirakuru/base_env.py index 9538972..fe265a3 100644 --- a/src/mirakuru/base_env.py +++ b/src/mirakuru/base_env.py @@ -1,115 +1,115 @@ # Copyright (C) 2016 by Clearcode # and associates (see AUTHORS). # This file is part of mirakuru. # mirakuru is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # mirakuru 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 mirakuru. If not, see . """Module contains functions used for finding process descendants.""" import errno import logging import re import subprocess from typing import Set try: import psutil except ImportError: psutil = None log = logging.getLogger(__name__) # pylint: disable=invalid-name PS_XE_PID_MATCH = re.compile(r'^.*?(\d+).+$') """_sre.SRE_Pattern matching PIDs in result from `$ ps xe -o pid,cmd`.""" def processes_with_env_psutil(env_name: str, env_value: str) -> Set[int]: """ Find PIDs of processes having environment variable matching given one. Internally it uses `psutil` library. :param str env_name: name of environment variable to be found :param str env_value: environment variable value prefix :return: process identifiers (PIDs) of processes that have certain environment variable equal certain value :rtype: set """ pids = set() for proc in psutil.process_iter(): try: pinfo = proc.as_dict(attrs=['pid', 'environ']) except (psutil.NoSuchProcess, IOError): # can't do much if psutil is not able to get this process details pass else: penv = pinfo.get('environ') if penv and env_value in penv.get(env_name, ''): pids.add(pinfo['pid']) return pids def processes_with_env_ps(env_name: str, env_value: str) -> Set[int]: """ Find PIDs of processes having environment variable matching given one. It uses `$ ps xe -o pid,cmd` command so it works only on systems having such command available (Linux, MacOS). If not available function will just log error. :param str env_name: name of environment variable to be found :param str env_value: environment variable value prefix :return: process identifiers (PIDs) of processes that have certain environment variable equal certain value :rtype: set """ - pids = set() # type: Set[int] + pids: Set[int] = set() ps_xe = '' try: cmd = 'ps', 'xe', '-o', 'pid,cmd' ps_xe = subprocess.check_output(cmd).splitlines() except OSError as err: if err.errno == errno.ENOENT: log.error("`$ ps xe -o pid,cmd` command was called but it is not " "available on this operating system. Mirakuru will not " "be able to list the process tree and find if there are " "any leftovers of the Executor.") return pids except subprocess.CalledProcessError: log.error("`$ ps xe -o pid,cmd` command exited with non-zero code.") - env = '{0}={1}'.format(env_name, env_value) + env = f'{env_name}={env_value}' for line in ps_xe: line = str(line) if env in line: match = PS_XE_PID_MATCH.match(line) # This always matches: all lines other than the header (not # containing our environment variable) have a PID required by the # reggex. Still check it for mypy. if match: pids.add(int(match.group(1))) return pids # pylint: disable=invalid-name if psutil: processes_with_env = processes_with_env_psutil else: # In case psutil can't be imported (on pypy3) we try to use '$ ps xe' processes_with_env = processes_with_env_ps diff --git a/src/mirakuru/exceptions.py b/src/mirakuru/exceptions.py index 5637cb5..58d627b 100644 --- a/src/mirakuru/exceptions.py +++ b/src/mirakuru/exceptions.py @@ -1,102 +1,103 @@ """Mirakuru exceptions.""" from typing import Union, TYPE_CHECKING if TYPE_CHECKING: # pragma: no cover from mirakuru.base import SimpleExecutor # pylint:disable=cyclic-import class ExecutorError(Exception): """Base exception for executor failures.""" def __init__(self, executor: "SimpleExecutor") -> None: """ Exception initialization. :param mirakuru.base.SimpleExecutor executor: for which exception occurred """ super(ExecutorError, self).__init__(self) self.executor = executor class TimeoutExpired(ExecutorError): """Is raised when the timeout expires while starting an executor.""" def __init__(self, executor: "SimpleExecutor", timeout: Union[int, float]) -> None: """ Exception initialization with an extra ``timeout`` argument. :param mirakuru.base.SimpleExecutor executor: for which exception occurred :param int timeout: timeout for which exception occurred """ super(TimeoutExpired, self).__init__(executor) self.timeout = timeout def __str__(self) -> str: """ Return Exception's string representation. :returns: string representation :rtype: str """ - return 'Executor {0} timed out after {1} seconds'.format( - self.executor, self.timeout + return ( + f'Executor {self.executor} timed out after {self.timeout} seconds' ) class AlreadyRunning(ExecutorError): """ Is raised when the executor seems to be already running. When some other process (not necessary executor) seems to be started with same configuration we can't bind to same port. """ def __str__(self) -> str: """ Return Exception's string representation. :returns: string representation :rtype: str """ - return ("Executor {exc.executor} seems to be already running. " - "It looks like the previous executor process hasn't been " - "terminated or killed. Also there might be some completely " - "different service listening on {exc.executor.port} port." - .format(exc=self)) + port = getattr(self.executor, 'port') + return (f"Executor {self.executor} seems to be already running. " + f"It looks like the previous executor process hasn't been " + f"terminated or killed." + + ("" if port is None else + f" Also there might be some completely " + f"different service listening on {port} port.")) class ProcessExitedWithError(ExecutorError): """ Raised when the process invoked by the executor returns a non-zero code. We allow the process to exit with zero because we support daemonizing subprocesses. We assume that when double-forking, the parent process will exit with 0 in case of successful daemonization. """ def __init__(self, executor: "SimpleExecutor", exit_code: int) -> None: """ Exception initialization with an extra ``exit_code`` argument. :param mirakuru.base.SimpleExecutor executor: for which exception occurred :param int exit_code: code the subprocess exited with """ super(ProcessExitedWithError, self).__init__(executor) self.exit_code = exit_code def __str__(self) -> str: """ Return Exception's string representation. :returns: string representation :rtype: str """ - return ("The process invoked by the {exc.executor} executor has " - "exited with a non-zero code: {exc.exit_code}." - .format(exc=self)) + return (f"The process invoked by the {self.executor} executor has " + f"exited with a non-zero code: {self.exit_code}.") diff --git a/src/mirakuru/output.py b/src/mirakuru/output.py index 5dba50c..22c4d28 100644 --- a/src/mirakuru/output.py +++ b/src/mirakuru/output.py @@ -1,125 +1,154 @@ # Copyright (C) 2014 by Clearcode # and associates (see AUTHORS). # This file is part of mirakuru. # mirakuru is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # mirakuru 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 mirakuru. If not, see . """Executor that awaits for appearance of a predefined banner in output.""" - +import platform import re import select -from typing import Union, List, Any, TypeVar, Tuple, IO +from typing import Union, List, Any, TypeVar, Tuple, IO, Optional from mirakuru.base import SimpleExecutor +IS_DARWIN = platform.system() == 'Darwin' + + OutputExecutorType = TypeVar("OutputExecutorType", bound="OutputExecutor") class OutputExecutor(SimpleExecutor): """Executor that awaits for string output being present in output.""" def __init__(self, command: Union[str, List[str], Tuple[str, ...]], banner: str, **kwargs: Any) -> None: """ Initialize OutputExecutor executor. :param (str, list) command: command to be run by the subprocess :param str banner: string that has to appear in process output - should compile to regular expression. :param bool shell: same as the `subprocess.Popen` shell definition :param int timeout: number of seconds to wait for the process to start or stop. If None or False, wait indefinitely. :param float sleep: how often to check for start/stop condition :param int sig_stop: signal used to stop process run by the executor. default is `signal.SIGTERM` :param int sig_kill: signal used to kill process run by the executor. default is `signal.SIGKILL` (`signal.SIGTERM` on Windows) """ super(OutputExecutor, self).__init__(command, **kwargs) self._banner = re.compile(banner) if not any((self._stdout, self._stderr)): raise TypeError( 'At least one of stdout or stderr has to be initialized' ) def start(self: OutputExecutorType) -> OutputExecutorType: """ Start process. :returns: itself :rtype: OutputExecutor .. note:: Process will be considered started, when defined banner will appear in process output. """ super(OutputExecutor, self).start() - polls = [] # type: List[Tuple[select.poll, IO[Any]]] - - for output_handle, output_method in ( - (self._stdout, self.output), - (self._stderr, self.err_output) - ): - if output_handle is not None: - # get a polling object - std_poll = select.poll() - - output_file = output_method() - if output_file is None: - raise ValueError( - "The process is started but the output file is None") - # register a file descriptor - # POLLIN because we will wait for data to read - std_poll.register(output_file, select.POLLIN) - polls.append((std_poll, output_file)) - - try: + if not IS_DARWIN: + polls: List[Tuple[select.poll, IO[Any]]] = [] + for output_handle, output_method in ( + (self._stdout, self.output), + (self._stderr, self.err_output) + ): + if output_handle is not None: + # get a polling object + std_poll = select.poll() + + output_file = output_method() + if output_file is None: + raise ValueError( + "The process is started but " + "the output file is None" + ) + # register a file descriptor + # POLLIN because we will wait for data to read + std_poll.register(output_file, select.POLLIN) + polls.append((std_poll, output_file)) + + try: + def await_for_output() -> bool: + return self._wait_for_output(*polls) + + self.wait_for(await_for_output) + + for poll, output in polls: + # unregister the file descriptor + # and delete the polling object + poll.unregister(output) + finally: + for poll_and_output in polls: + del poll_and_output + else: + outputs = [] + for output_handle, output_method in ( + (self._stdout, self.output), + (self._stderr, self.err_output) + ): + if output_handle is not None: + outputs.append(output_method()) + def await_for_output() -> bool: - return self._wait_for_output(*polls) + return self._wait_for_darwin_output(*outputs) self.wait_for(await_for_output) - for poll, output in polls: - # unregister the file descriptor and delete the polling object - poll.unregister(output) - finally: - for poll_and_output in polls: - del poll_and_output return self + def _wait_for_darwin_output(self, *fds: Optional[IO[Any]]) -> bool: + """Select implementation to be used on MacOSX""" + rlist, _, _ = select.select(fds, [], [], 0) + for output in rlist: + line = output.readline() + if self._banner.match(line): + return True + return False + def _wait_for_output(self, *polls: Tuple[select.poll, IO[Any]]) -> bool: """ Check if output matches banner. .. warning:: Waiting for I/O completion. It does not work on Windows. Sorry. """ for poll, output in polls: # Here we should get an empty list or list with a tuple # [(fd, event)]. When we get list with a tuple we can use readline # method on the file descriptor. poll_result = poll.poll(0) if poll_result: line = output.readline() if self._banner.match(line): return True return False diff --git a/tests/__init__.py b/tests/__init__.py index 21ab4a2..086a51d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,29 +1,27 @@ """ Package of tests for mirakuru. Tests are written using py.test framework which dictates patterns that should be followed in test cases. """ import sys from os import path from subprocess import check_output TEST_PATH = path.abspath(path.dirname(__file__)) TEST_SERVER_PATH = path.join(TEST_PATH, "server_for_tests.py") TEST_SOCKET_SERVER_PATH = path.join(TEST_PATH, 'unixsocketserver_for_tests.py') SAMPLE_DAEMON_PATH = path.join(TEST_PATH, "sample_daemon.py") -HTTP_SERVER_CMD = ( - "{python} -m http.server" -).format(python=sys.executable) +HTTP_SERVER_CMD = f"{sys.executable} -m http.server" def ps_aux(): """ Return output of systems `ps aux -w` call. :rtype str """ return str(check_output(('ps', 'aux', '-w'))) diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 83afd3d..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Tests pre-configuration. - -* Filtering ResourceWarning for the Python 3. -* Fixture for raising an error whenever we leave any resource open. -""" -import platform -import os -import sys -from warnings import simplefilter - -import pytest - -IS_TRAVIS = 'TRAVIS' in os.environ -IS_PYPY_35 = ( - platform.python_implementation() == 'PyPy' and sys.version_info < (3, 6) -) - - -simplefilter( - 'default', - ResourceWarning -) - - -@pytest.fixture(autouse=True) -def error_warn(recwarn): - """Raise error whenever any warning gets listed.""" - yield - if recwarn.list and not (IS_PYPY_35 and IS_TRAVIS): - raise recwarn.list[0].message diff --git a/tests/executors/test_executor.py b/tests/executors/test_executor.py index c9bdc98..8500e4b 100644 --- a/tests/executors/test_executor.py +++ b/tests/executors/test_executor.py @@ -1,232 +1,242 @@ # mypy: no-strict-optional """Test basic executor functionality.""" import gc import shlex import signal from subprocess import check_output import uuid +from unittest import mock import pytest -import mock from mirakuru import Executor from mirakuru.base import SimpleExecutor from mirakuru.exceptions import ProcessExitedWithError, TimeoutExpired from tests import SAMPLE_DAEMON_PATH, ps_aux SLEEP_300 = 'sleep 300' @pytest.mark.parametrize('command', (SLEEP_300, SLEEP_300.split())) def test_running_process(command): """Start process and shuts it down.""" - executor = SimpleExecutor(command).start() + executor = SimpleExecutor(command) executor.start() assert executor.running() is True executor.stop() assert executor.running() is False # check proper __str__ and __repr__ rendering: assert 'SimpleExecutor' in repr(executor) assert SLEEP_300 in str(executor) +@pytest.mark.parametrize('command', (SLEEP_300, SLEEP_300.split())) +def test_command(command): + """Check that the command and command parts are equivalent.""" + + executor = SimpleExecutor(command) + assert executor.command == SLEEP_300 + assert executor.command_parts == SLEEP_300.split() + + def test_custom_signal_stop(): """Start process and shuts it down using signal SIGQUIT.""" executor = SimpleExecutor(SLEEP_300, sig_stop=signal.SIGQUIT) executor.start() assert executor.running() is True executor.stop() assert executor.running() is False def test_stop_custom_signal_stop(): """Start process and shuts it down using signal SIGQUIT passed to stop.""" executor = SimpleExecutor(SLEEP_300) executor.start() assert executor.running() is True executor.stop(sig=signal.SIGQUIT) assert executor.running() is False def test_running_context(): """Start process and shuts it down.""" executor = SimpleExecutor(SLEEP_300) with executor: assert executor.running() is True assert executor.running() is False def test_executor_in_context_only(): """Start process and shuts it down only in context.""" with SimpleExecutor(SLEEP_300) as executor: assert executor.running() is True def test_context_stopped(): """Start for context, and shuts it for nested context.""" executor = SimpleExecutor(SLEEP_300) with executor: assert executor.running() is True with executor.stopped(): assert executor.running() is False assert executor.running() is True assert executor.running() is False ECHO_FOOBAR = 'echo "foobar"' @pytest.mark.parametrize('command', (ECHO_FOOBAR, shlex.split(ECHO_FOOBAR))) def test_process_output(command): """Start process, check output and shut it down.""" executor = SimpleExecutor(command) executor.start() assert executor.output().read() == 'foobar\n' executor.stop() @pytest.mark.parametrize('command', (ECHO_FOOBAR, shlex.split(ECHO_FOOBAR))) def test_process_output_shell(command): """Start process, check output and shut it down with shell set to True.""" executor = SimpleExecutor(command, shell=True) executor.start() assert executor.output().read().strip() == 'foobar' executor.stop() def test_start_check_executor(): """Validate Executor base class having NotImplemented methods.""" executor = Executor(SLEEP_300) with pytest.raises(NotImplementedError): executor.pre_start_check() with pytest.raises(NotImplementedError): executor.after_start_check() def test_stopping_not_yet_running_executor(): """ Test if SimpleExecutor can be stopped even it was never running. We must make sure that it's possible to call .stop() and SimpleExecutor will not raise any exception and .start() can be called afterwards. """ executor = SimpleExecutor(SLEEP_300) executor.stop() executor.start() assert executor.running() is True executor.stop() def test_forgotten_stop(): """ Test if SimpleExecutor subprocess is killed after an instance is deleted. Existence can end because of context scope end or by calling 'del'. If someone forgot to stop() or kill() subprocess it should be killed by default on instance cleanup. """ mark = str(uuid.uuid1()) # We cannot simply do `sleep 300 #` in a shell because in that # case bash (default shell on some systems) does `execve` without cloning # itself - that means there will be no process with commandline like: # '/bin/sh -c sleep 300 && true #' - instead that process would # get substituted with 'sleep 300' and the marked commandline would be # overwritten. # Injecting some flow control (`&&`) forces bash to fork properly. - marked_command = 'sleep 300 && true #{0!s}'.format(mark) + marked_command = f'sleep 300 && true #{mark!s}' executor = SimpleExecutor(marked_command, shell=True) executor.start() assert executor.running() is True assert mark in ps_aux(), "The test process should be running." del executor gc.collect() # to force 'del' immediate effect assert mark not in ps_aux(), \ "The test process should not be running at this point." def test_executor_raises_if_process_exits_with_error(): """ Test process exit detection. If the process exits with an error while checks are being polled, executor should raise an exception. """ error_code = 12 failing_executor = Executor( - ['bash', '-c', 'exit {0!s}'.format(error_code)], + ['bash', '-c', f'exit {error_code!s}'], timeout=5 ) failing_executor.pre_start_check = mock.Mock( # type: ignore return_value=False) # After-start check will keep returning False to let the process terminate. failing_executor.after_start_check = mock.Mock( # type: ignore return_value=False) with pytest.raises(ProcessExitedWithError) as exc: failing_executor.start() assert exc.value.exit_code == 12 - error_msg = 'exited with a non-zero code: {0!s}'.format(error_code) + error_msg = f'exited with a non-zero code: {error_code!s}' assert error_msg in str(exc.value) # Pre-start check should have been called - after-start check might or # might not have been called - depending on the timing. assert failing_executor.pre_start_check.called is True # type: ignore def test_executor_ignores_processes_exiting_with_0(): """ Test process exit detection. Subprocess exiting with zero should be tolerated in order to support double-forking applications. """ # We execute a process that will return zero. In order to give the process # enough time to return we keep the polling loop spinning for a second. executor = Executor(['bash', '-c', 'exit 0'], timeout=1.0) executor.pre_start_check = mock.Mock(return_value=False) # type: ignore executor.after_start_check = mock.Mock(return_value=False) # type: ignore with pytest.raises(TimeoutExpired): # We keep the post-checks spinning forever so it eventually times out. executor.start() # Both checks should have been called. assert executor.pre_start_check.called is True # type: ignore assert executor.after_start_check.called is True # type: ignore def test_executor_methods_returning_self(): """Test if SimpleExecutor lets to chain start, stop and kill methods.""" executor = SimpleExecutor(SLEEP_300).start().stop().kill().stop() assert not executor.running() # Check if context manager returns executor to use it in 'as' phrase: with SimpleExecutor(SLEEP_300) as executor: assert executor.running() with SimpleExecutor(SLEEP_300).start().stopped() as executor: assert not executor.running() assert SimpleExecutor(SLEEP_300).start().stop().output def test_mirakuru_cleanup(): """Test if cleanup_subprocesses is fired correctly on python exit.""" - cmd = ''' + cmd = f''' python -c 'from mirakuru import SimpleExecutor; from time import sleep; import gc; gc.disable(); - ex = SimpleExecutor(("python", "{0}")).start(); + ex = SimpleExecutor( + ("python", "{SAMPLE_DAEMON_PATH}")).start(); sleep(1); ' - '''.format(SAMPLE_DAEMON_PATH) + ''' check_output(shlex.split(cmd.replace('\n', ''))) assert SAMPLE_DAEMON_PATH not in ps_aux() diff --git a/tests/executors/test_executor_kill.py b/tests/executors/test_executor_kill.py index e624eec..ce5f42e 100644 --- a/tests/executors/test_executor_kill.py +++ b/tests/executors/test_executor_kill.py @@ -1,117 +1,123 @@ # mypy: no-strict-optional """Tests that check various kill behaviours.""" import signal import time import sys import errno import os -from mock import patch +from unittest.mock import patch +import pytest from mirakuru import SimpleExecutor, HTTPExecutor from mirakuru.compat import SIGKILL from tests import SAMPLE_DAEMON_PATH, ps_aux, TEST_SERVER_PATH SLEEP_300 = 'sleep 300' def test_custom_signal_kill(): """Start process and shuts it down using signal SIGQUIT.""" executor = SimpleExecutor(SLEEP_300, sig_kill=signal.SIGQUIT) executor.start() assert executor.running() is True executor.kill() assert executor.running() is False def test_kill_custom_signal_kill(): """Start process and shuts it down using signal SIGQUIT passed to kill.""" executor = SimpleExecutor(SLEEP_300) executor.start() assert executor.running() is True executor.kill(sig=signal.SIGQUIT) assert executor.running() is False def test_already_closed(): """Check that the executor cleans after itself after it exited earlier.""" with SimpleExecutor('python') as executor: assert executor.running() os.killpg(executor.process.pid, SIGKILL) def process_stopped(): """Return True only only when self.process is not running.""" return executor.running() is False executor.wait_for(process_stopped) assert executor.process assert not executor.process +@pytest.mark.xfail( + condition=sys.version_info >= (3, 8), + reason='python-daemon 2.2.3 fails with ' + '; ' + 'unxfail when a newer version is used') def test_daemons_killing(): """ Test if all subprocesses of SimpleExecutor can be killed. The most problematic subprocesses are daemons or other services that change the process group ID. This test verifies that daemon process is killed after executor's kill(). """ executor = SimpleExecutor(('python', SAMPLE_DAEMON_PATH), shell=True) executor.start() time.sleep(2) assert executor.running() is not True, \ "Executor should not have subprocess running as it started a daemon." assert SAMPLE_DAEMON_PATH in ps_aux() executor.kill() assert SAMPLE_DAEMON_PATH not in ps_aux() def test_stopping_brutally(): """ Test if SimpleExecutor is stopping insubordinate process. Check if the process that doesn't react to SIGTERM signal will be killed by executor with SIGKILL automatically. """ host_port = "127.0.0.1:8000" - cmd = '{0} {1} {2} True'.format(sys.executable, TEST_SERVER_PATH, host_port) - executor = HTTPExecutor(cmd, 'http://{0!s}/'.format(host_port), timeout=20) + cmd = f'{sys.executable} {TEST_SERVER_PATH} {host_port} True' + executor = HTTPExecutor(cmd, f'http://{host_port!s}/', timeout=20) executor.start() assert executor.running() is True stop_at = time.time() + 10 executor.stop() assert executor.running() is False assert stop_at <= time.time(), "Subprocess killed earlier than in 10 secs" def test_stopping_children_of_stopped_process(): """ Check that children exiting between listing and killing are ignored. Given: Executor is running and it's process spawn children, and we requested it's stop, and it's stopped When: At the time of the check for subprocesses they're still active, but before we start killing them, they are already dead. Then: We ignore and skip OsError indicates there's no such process. """ # pylint: disable=protected-access, missing-docstring def raise_os_error(*_, **__): os_error = OSError() os_error.errno = errno.ESRCH raise os_error def processes_with_env_mock(*_, **__): return [1] with patch( 'mirakuru.base.processes_with_env', new=processes_with_env_mock ), patch('os.kill', new=raise_os_error): executor = SimpleExecutor(SLEEP_300) executor._kill_all_kids(executor._sig_stop) diff --git a/tests/executors/test_http_executor.py b/tests/executors/test_http_executor.py index 6dfebf9..06c502f 100644 --- a/tests/executors/test_http_executor.py +++ b/tests/executors/test_http_executor.py @@ -1,233 +1,220 @@ """HTTP Executor tests.""" import sys import socket from functools import partial from http.client import HTTPConnection, OK from typing import Dict, Any +from unittest.mock import patch import pytest -from mock import patch from mirakuru import HTTPExecutor, TCPExecutor from mirakuru import TimeoutExpired, AlreadyRunning from tests import TEST_SERVER_PATH, HTTP_SERVER_CMD HOST = "127.0.0.1" PORT = 7987 -HTTP_NORMAL_CMD = '{0} {1}'.format(HTTP_SERVER_CMD, PORT) -HTTP_SLOW_CMD = '{python} {srv} {host}:{port}' \ - .format(python=sys.executable, srv=TEST_SERVER_PATH, host=HOST, port=PORT) +HTTP_NORMAL_CMD = f'{HTTP_SERVER_CMD} {PORT}' +HTTP_SLOW_CMD = f'{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT}' slow_server_executor = partial( # pylint: disable=invalid-name HTTPExecutor, HTTP_SLOW_CMD, - 'http://{0}:{1}/'.format(HOST, PORT), + f'http://{HOST}:{PORT}/', ) def connect_to_server(): """Connect to http server and assert 200 response.""" conn = HTTPConnection(HOST, PORT) conn.request('GET', '/') assert conn.getresponse().status == OK conn.close() def test_executor_starts_and_waits(): """Test if process awaits for HEAD request to be completed.""" - command = 'bash -c "sleep 3 && {0}"'.format(HTTP_NORMAL_CMD) + command = f'bash -c "sleep 3 && {HTTP_NORMAL_CMD}"' executor = HTTPExecutor( command, - 'http://{0}:{1}/'.format(HOST, PORT), + f'http://{HOST}:{PORT}/', timeout=20 ) executor.start() assert executor.running() is True connect_to_server() executor.stop() # check proper __str__ and __repr__ rendering: assert 'HTTPExecutor' in repr(executor) assert command in str(executor) def test_shell_started_server_stops(): """Test if executor terminates properly executor with shell=True.""" executor = HTTPExecutor( HTTP_NORMAL_CMD, - 'http://{0}:{1}/'.format(HOST, PORT), + f'http://{HOST}:{PORT}/', timeout=20, shell=True ) with pytest.raises(socket.error): connect_to_server() with executor: assert executor.running() is True connect_to_server() assert executor.running() is False with pytest.raises(socket.error): connect_to_server() @pytest.mark.parametrize('method', ( 'HEAD', 'GET', 'POST' )) def test_slow_method_server_starting(method): """ Test whether or not executor awaits for slow starting servers. Simple example. You run Gunicorn and it is working but you have to wait for worker processes. """ - http_method_slow_cmd = '{python} {srv} {host}:{port} False {method}'.format( - python=sys.executable, - srv=TEST_SERVER_PATH, - host=HOST, - port=PORT, - method=method + http_method_slow_cmd = ( + f'{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False {method}' ) with HTTPExecutor( http_method_slow_cmd, - 'http://{0}:{1}/'.format(HOST, PORT), method=method, timeout=30 + f'http://{HOST}:{PORT}/', method=method, timeout=30 ) as executor: assert executor.running() is True connect_to_server() def test_slow_post_payload_server_starting(): """ Test whether or not executor awaits for slow starting servers. Simple example. You run Gunicorn and it is working but you have to wait for worker processes. """ - http_method_slow_cmd = '{python} {srv} {host}:{port} False {method}'.format( - python=sys.executable, - srv=TEST_SERVER_PATH, - host=HOST, - port=PORT, - method='Key' + http_method_slow_cmd = ( + f'{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False Key' ) with HTTPExecutor( http_method_slow_cmd, - 'http://{0}:{1}/'.format(HOST, PORT), + f'http://{HOST}:{PORT}/', method='POST', timeout=30, payload={'key': 'hole'} ) as executor: assert executor.running() is True connect_to_server() @pytest.mark.parametrize('method', ( 'HEAD', 'GET', 'POST' )) def test_slow_method_server_timed_out(method): """Check if timeout properly expires.""" - http_method_slow_cmd = '{python} {srv} {host}:{port} False {method}'.format( - python=sys.executable, - srv=TEST_SERVER_PATH, - host=HOST, - port=PORT, - method=method + http_method_slow_cmd = ( + f'{sys.executable} {TEST_SERVER_PATH} {HOST}:{PORT} False {method}' ) executor = HTTPExecutor( http_method_slow_cmd, - 'http://{0}:{1}/'.format(HOST, PORT), method=method, timeout=1 + f'http://{HOST}:{PORT}/', method=method, timeout=1 ) with pytest.raises(TimeoutExpired) as exc: executor.start() assert executor.running() is False - assert 'timed out after' in str(exc) + assert 'timed out after' in str(exc.value) def test_fail_if_other_running(): """Test raising AlreadyRunning exception when port is blocked.""" executor = HTTPExecutor( - HTTP_NORMAL_CMD, 'http://{0}:{1}/'.format(HOST, PORT), + HTTP_NORMAL_CMD, f'http://{HOST}:{PORT}/', ) executor2 = HTTPExecutor( - HTTP_NORMAL_CMD, 'http://{0}:{1}/'.format(HOST, PORT), + HTTP_NORMAL_CMD, f'http://{HOST}:{PORT}/', ) with executor: assert executor.running() is True with pytest.raises(AlreadyRunning): executor2.start() with pytest.raises(AlreadyRunning) as exc: with executor2: pass - assert 'seems to be already running' in str(exc) + assert 'seems to be already running' in str(exc.value) @patch.object(HTTPExecutor, 'DEFAULT_PORT', PORT) def test_default_port(): """ Test default port for the base TCP check. Check if HTTP executor fills in the default port for the TCP check from the base class if no port is provided in the URL. """ - executor = HTTPExecutor(HTTP_NORMAL_CMD, 'http://{0}/'.format(HOST)) + executor = HTTPExecutor(HTTP_NORMAL_CMD, f'http://{HOST}/') assert executor.url.port is None assert executor.port == PORT assert TCPExecutor.pre_start_check(executor) is False executor.start() assert TCPExecutor.pre_start_check(executor) is True executor.stop() @pytest.mark.parametrize('accepted_status, expected_timeout', ( # default behaviour - only 2XX HTTP status codes are accepted (None, True), # one explicit integer status code (200, True), # one explicit status code as a string ('404', False), # status codes as a regular expression (r'(2|4)\d\d', False), # status codes as a regular expression ('(200|404)', False), )) def test_http_status_codes(accepted_status, expected_timeout): """ Test how 'status' argument influences executor start. :param int|str accepted_status: Executor 'status' value :param bool expected_timeout: if Executor raises TimeoutExpired or not """ - kwargs = { + kwargs: Dict[str, Any] = { 'command': HTTP_NORMAL_CMD, - 'url': 'http://{0}:{1}/badpath'.format(HOST, PORT), + 'url': f'http://{HOST}:{PORT}/badpath', 'timeout': 2 - } # type: Dict[str, Any] + } if accepted_status: kwargs['status'] = accepted_status executor = HTTPExecutor(**kwargs) if not expected_timeout: executor.start() executor.stop() else: with pytest.raises(TimeoutExpired): executor.start() executor.stop() diff --git a/tests/executors/test_pid_executor.py b/tests/executors/test_pid_executor.py index 1dacd3f..e513f76 100644 --- a/tests/executors/test_pid_executor.py +++ b/tests/executors/test_pid_executor.py @@ -1,82 +1,82 @@ """PidExecutor tests.""" import os import pytest from mirakuru import PidExecutor from mirakuru import TimeoutExpired, AlreadyRunning -FILENAME = "pid-test-tmp{0}".format(os.getpid()) -SLEEP = 'bash -c "sleep 1 && touch {0} && sleep 1"'.format(FILENAME) +FILENAME = f"pid-test-tmp{os.getpid()}" +SLEEP = f'bash -c "sleep 1 && touch {FILENAME} && sleep 1"' @pytest.yield_fixture(autouse=True) def run_around_tests(): """ Make sure the **FILENAME** file is not present. This executor actually removes FILENAME as process used to test PidExecutor only creates it. """ try: os.remove(FILENAME) except OSError: pass yield try: os.remove(FILENAME) except OSError: pass def test_start_and_wait(): """Test if the executor will await for the process to create a file.""" - process = 'bash -c "sleep 2 && touch {0} && sleep 10"'.format(FILENAME) + process = f'bash -c "sleep 2 && touch {FILENAME} && sleep 10"' with PidExecutor(process, FILENAME, timeout=5) as executor: assert executor.running() is True # check proper __str__ and __repr__ rendering: assert 'PidExecutor' in repr(executor) assert process in str(executor) @pytest.mark.parametrize('pid_file', (None, "")) def test_empty_filename(pid_file): """Check whether an exception is raised if an empty FILENAME is given.""" with pytest.raises(ValueError): PidExecutor(SLEEP, pid_file) def test_if_file_created(): """Check whether the process really created the given file.""" assert os.path.isfile(FILENAME) is False executor = PidExecutor(SLEEP, FILENAME) with executor: assert os.path.isfile(FILENAME) is True def test_timeout_error(): """Check if timeout properly expires.""" executor = PidExecutor(SLEEP, FILENAME, timeout=1) with pytest.raises(TimeoutExpired): executor.start() assert executor.running() is False def test_fail_if_other_executor_running(): """Test raising AlreadyRunning exception when port is blocked.""" - process = 'bash -c "sleep 2 && touch {0} && sleep 10"'.format(FILENAME) + process = f'bash -c "sleep 2 && touch {FILENAME} && sleep 10"' executor = PidExecutor(process, FILENAME) executor2 = PidExecutor(process, FILENAME) with executor: assert executor.running() is True with pytest.raises(AlreadyRunning): executor2.start() diff --git a/tests/executors/test_tcp_executor.py b/tests/executors/test_tcp_executor.py index 8df6fd0..2dd2920 100644 --- a/tests/executors/test_tcp_executor.py +++ b/tests/executors/test_tcp_executor.py @@ -1,58 +1,58 @@ """ TCPExecutor tests. Some of these tests run ``nc``: when running Debian, make sure the ``netcat-openbsd`` package is used, not ``netcat-traditional``. """ import pytest from mirakuru import TCPExecutor from mirakuru import TimeoutExpired, AlreadyRunning from tests import HTTP_SERVER_CMD PORT = 7986 -HTTP_SERVER = '{0} {1}'.format(HTTP_SERVER_CMD, PORT) +HTTP_SERVER = f'{HTTP_SERVER_CMD} {PORT}' def test_start_and_wait(): """Test if executor await for process to accept connections.""" command = 'bash -c "sleep 2 && nc -l 3000"' executor = TCPExecutor(command, 'localhost', port=3000, timeout=5) executor.start() assert executor.running() is True executor.stop() # check proper __str__ and __repr__ rendering: assert 'TCPExecutor' in repr(executor) assert command in str(executor) def test_it_raises_error_on_timeout(): """Check if TimeoutExpired gets raised correctly.""" command = 'bash -c "sleep 10 && nc -l 3000"' executor = TCPExecutor(command, host='localhost', port=3000, timeout=2) with pytest.raises(TimeoutExpired): executor.start() assert executor.running() is False def test_fail_if_other_executor_running(): """Test raising AlreadyRunning exception.""" executor = TCPExecutor(HTTP_SERVER, host='localhost', port=PORT) executor2 = TCPExecutor(HTTP_SERVER, host='localhost', port=PORT) with executor: assert executor.running() is True with pytest.raises(AlreadyRunning): executor2.start() with pytest.raises(AlreadyRunning): with executor2: pass diff --git a/tests/executors/test_unixsocket_executor.py b/tests/executors/test_unixsocket_executor.py index c92db84..8f317e3 100644 --- a/tests/executors/test_unixsocket_executor.py +++ b/tests/executors/test_unixsocket_executor.py @@ -1,44 +1,38 @@ """ TCPExecutor tests. Some of these tests run ``nc``: when running Debian, make sure the ``netcat-openbsd`` package is used, not ``netcat-traditional``. """ import sys import pytest from mirakuru import TimeoutExpired from mirakuru.unixsocket import UnixSocketExecutor from tests import TEST_SOCKET_SERVER_PATH SOCKET_PATH = '/tmp/mirakuru.sock' -SOCKET_SERVER_CMD = ( - "{python} {srv} {socket_path}" -).format( - python=sys.executable, - srv=TEST_SOCKET_SERVER_PATH, - socket_path=SOCKET_PATH, -) +SOCKET_SERVER_CMD = f"{sys.executable} {TEST_SOCKET_SERVER_PATH} {SOCKET_PATH}" def test_start_and_wait(): """Test if executor await for process to accept connections.""" executor = UnixSocketExecutor( SOCKET_SERVER_CMD + " 2", socket_name=SOCKET_PATH, timeout=5 ) with executor: assert executor.running() is True def test_start_and_timeout(): """Test if executor will properly times out.""" executor = UnixSocketExecutor( SOCKET_SERVER_CMD + " 10", socket_name=SOCKET_PATH, timeout=5 ) with pytest.raises(TimeoutExpired): executor.start() assert executor.running() is False diff --git a/tests/server_for_tests.py b/tests/server_for_tests.py index 3a32f75..3837210 100644 --- a/tests/server_for_tests.py +++ b/tests/server_for_tests.py @@ -1,145 +1,145 @@ """ HTTP server that responses with delays used for tests. Example usage: python tests/slow_server.py [HOST:PORT] - run HTTP Server, HOST and PORT are optional python tests/slow_server.py [HOST:PORT] True - run IMMORTAL server (stopping process only by SIGKILL) """ import ast import sys import os import time from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import parse_qs sys.path.append(os.getcwd()) # noqa # pylint:disable=wrong-import-position from tests.signals import block_signals # pylint:enable=wrong-import-position class SlowServerHandler(BaseHTTPRequestHandler): """Slow server handler.""" timeout = 2 endtime = None def do_GET(self): # pylint:disable=invalid-name """Serve GET request.""" self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(b'Hi. I am very slow.') def do_HEAD(self): # pylint:disable=invalid-name """ Serve HEAD request. but count to wait and return 500 response if wait time not exceeded due to the fact that HTTPServer will hang waiting for response to return otherwise if none response will be returned. """ self.timeout_status() self.end_headers() def timeout_status(self): """Set proper response status based on timeout.""" if self.count_timeout(): self.send_response(200) else: self.send_response(500) def count_timeout(self): # pylint: disable=no-self-use """Count down the timeout time.""" if SlowServerHandler.endtime is None: SlowServerHandler.endtime = time.time() + SlowServerHandler.timeout return time.time() >= SlowServerHandler.endtime class SlowGetServerHandler(SlowServerHandler): """Responds only on GET after a while.""" def do_GET(self): # pylint:disable=invalid-name "Serve GET request." self.timeout_status() self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(b'Hi. I am very slow.') def do_HEAD(self): # pylint:disable=invalid-name "Serve HEAD request." self.send_response(500) self.end_headers() class SlowPostServerHandler(SlowServerHandler): """Responds only on POST after a while.""" def do_POST(self): # pylint:disable=invalid-name "Serve POST request." self.timeout_status() self.end_headers() self.wfile.write(b'Hi. I am very slow.') def do_HEAD(self): # pylint:disable=invalid-name "Serve HEAD request." self.send_response(500) self.end_headers() class SlowPostKeyServerHandler(SlowServerHandler): """Responds only on POST after a while.""" def do_POST(self): # pylint:disable=invalid-name "Serve POST request." content_len = int(self.headers.get('Content-Length')) post_body = self.rfile.read(content_len) form = parse_qs(post_body) if form.get(b'key') == [b'hole']: self.timeout_status() else: self.send_response(500) self.end_headers() self.wfile.write(b'Hi. I am very slow.') def do_HEAD(self): # pylint:disable=invalid-name "Serve HEAD request." self.send_response(500) self.end_headers() HANDLERS = { 'HEAD': SlowServerHandler, 'GET': SlowGetServerHandler, 'POST': SlowPostServerHandler, 'Key': SlowPostKeyServerHandler, } if __name__ == "__main__": HOST, PORT, IMMORTAL, METHOD = "127.0.0.1", "8000", "False", 'HEAD' if len(sys.argv) >= 2: HOST, PORT = sys.argv[1].split(":") if len(sys.argv) >= 3: IMMORTAL = sys.argv[2] if len(sys.argv) == 4: METHOD = sys.argv[3] if ast.literal_eval(IMMORTAL): block_signals() server = HTTPServer( # pylint: disable=invalid-name (HOST, int(PORT)), HANDLERS[METHOD] ) - print("Starting slow server on {0}:{1}...".format(HOST, PORT)) + print(f"Starting slow server on {HOST}:{PORT}...") server.serve_forever() diff --git a/tests/signals.py b/tests/signals.py index ef52e32..4b2e188 100644 --- a/tests/signals.py +++ b/tests/signals.py @@ -1,22 +1,22 @@ """Contains `block_signals` function for tests purposes.""" import signal def block_signals(): """ Catch all of the signals that it is possible. Reject their default behaviour. The process is actually mortal but the only way to kill is to send SIGKILL signal (kill -9). """ def sighandler(signum, _): """Signal handling function.""" - print('Tried to kill with signal {0}.'.format(signum)) + print(f'Tried to kill with signal {signum}.') for sgn in [x for x in dir(signal) if x.startswith("SIG")]: try: signum = getattr(signal, sgn) signal.signal(signum, sighandler) except (ValueError, RuntimeError, OSError): pass diff --git a/tests/unixsocketserver_for_tests.py b/tests/unixsocketserver_for_tests.py index 5d1a5c8..17f3f74 100644 --- a/tests/unixsocketserver_for_tests.py +++ b/tests/unixsocketserver_for_tests.py @@ -1,78 +1,78 @@ # Copyright (c) 2015, Doug Hellmann, All Rights Reserved # # 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. # 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 OWNER 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. """Sample unixsocket server with small modifications.""" import socket import sys import os from time import sleep SOCKET_ADDRESS = './uds_socket' SLEEP = 0 if len(sys.argv) >= 2: SOCKET_ADDRESS = sys.argv[1] if len(sys.argv) >= 3: SLEEP = int(sys.argv[2]) # Make sure the socket does not already exist try: os.unlink(SOCKET_ADDRESS) except OSError: if os.path.exists(SOCKET_ADDRESS): raise # Create a UDS socket SOCK = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) # Bind the socket to the address -print('starting up on {}'.format(SOCKET_ADDRESS)) +print(f'starting up on {SOCKET_ADDRESS}') SOCK.bind(SOCKET_ADDRESS) sleep(SLEEP) # Listen for incoming connections SOCK.listen(1) while True: # Wait for a connection print('waiting for a connection') CONNECTION, CLIENT_ADDRESS = SOCK.accept() try: print('connection from', CLIENT_ADDRESS) # Receive the data in small chunks and retransmit it while True: RECEIVED_DATA = CONNECTION.recv(16) - print('received {!r}'.format(RECEIVED_DATA)) + print(f'received {RECEIVED_DATA!r}') if RECEIVED_DATA: print('sending data back to the client') CONNECTION.sendall(RECEIVED_DATA) else: print('no data from', CLIENT_ADDRESS) break finally: # Clean up the connection CONNECTION.close()