diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..c76d5b6 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,16 @@ +Authors +======= + +This file contains the list of people involved in the development of +mirakuru along its history. + +* Mateusz Lenik +* Tomasz Święcicki +* Tomasz Krzyszczyk +* Grzegorz Śliwiński +* Paweł Wilczyński +* Daniel O'Connell +* Michał Pawłowski +* Grégoire Détrez + +Great thanks to `Mateusz Lenik `_ for original package! diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..640f521 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,153 @@ +CHANGELOG +========= + +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/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..734216b --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,44 @@ +Contribute to mirakuru +====================== + +Thank you for taking time to contribute to mirakuru! + +The following is a set of guidelines for contributing to mirakuru. These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. + +Bug Reports +----------- + +#. Use a clear and descriptive title for the issue - it'll be much easier to identify the problem. +#. Describe the steps to reproduce the problems in as many details as possible. +#. If possible, provide a code snippet to reproduce the issue. + +Feature requests/proposals +-------------------------- + +#. Use a clear and descriptive title for the proposal +#. Provide as detailed description as possible + * Use case is great to have +#. There'll be a bit of discussion for the feature. Don't worry, if it is to be accepted, we'd like to support it, so we need to understand it thoroughly. + + +Pull requests +------------- + +#. Start with a bug report or feature request +#. Use a clear and descriptive title +#. Provide a description - which issue does it refers to, and what part of the issue is being solved +#. Be ready for code review :) + +Commits +------- + +#. Make sure commits are atomic, and each atomic change is being followed by test. +#. If the commit solves part of the issue reported, include *refs #[Issue number]* in a commit message. +#. If the commit solves whole issue reported, please refer to `Closing issues via commit messages `_ for ways to close issues when commits will be merged. + + +Coding style +------------ + +#. All python coding style are being enforced by `Pylama `_ and configured in pylama.ini file. +#. Additional, not always mandatory checks are being performed by `QuantifiedCode `_ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2638a08 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE *.rst *.py +recursive-include src/mirakuru/ *.py +prune tests diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..f202de1 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,304 @@ +Metadata-Version: 2.1 +Name: mirakuru +Version: 1.1.0 +Summary: Process executor for tests. +Home-page: https://github.com/ClearcodeHQ/mirakuru +Author: Clearcode - The A Room +Author-email: thearoom@clearcode.cc +License: LGPL +Description: 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=v1.1.0 + :target: http://mirakuru.readthedocs.io/en/v1.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=v1.1.0 + :target: https://travis-ci.org/ClearcodeHQ/mirakuru + :alt: Tests + + .. image:: https://coveralls.io/repos/ClearcodeHQ/mirakuru/badge.png?branch=v1.1.0 + :target: https://coveralls.io/r/ClearcodeHQ/mirakuru?branch=v1.1.0 + :alt: Coverage Status + + .. image:: https://requires.io/github/ClearcodeHQ/mirakuru/requirements.svg?tag=v1.1.0 + :target: https://requires.io/github/ClearcodeHQ/mirakuru/requirements/?tag=v1.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 six 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. + * 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. + + + CHANGELOG + ========= + + 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`. + +Keywords: process executor tests summon_process +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Testing +Provides-Extra: docs +Provides-Extra: tests diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8ec0fdf --- /dev/null +++ b/README.rst @@ -0,0 +1,121 @@ +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=v1.1.0 + :target: http://mirakuru.readthedocs.io/en/v1.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=v1.1.0 + :target: https://travis-ci.org/ClearcodeHQ/mirakuru + :alt: Tests + +.. image:: https://coveralls.io/repos/ClearcodeHQ/mirakuru/badge.png?branch=v1.1.0 + :target: https://coveralls.io/r/ClearcodeHQ/mirakuru?branch=v1.1.0 + :alt: Coverage Status + +.. image:: https://requires.io/github/ClearcodeHQ/mirakuru/requirements.svg?tag=v1.1.0 + :target: https://requires.io/github/ClearcodeHQ/mirakuru/requirements/?tag=v1.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 six 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. +* 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/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cdd2e72 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,15 @@ +[wheel] +universal = 1 + +[pycodestyle] +max-line-length = 80 +exclude = docs/*,build/*,venv/* + +[pydocstyle] +ignore = D203,D212 +match = '(?!docs|build|venv).*\.py' + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..9e421cd --- /dev/null +++ b/setup.py @@ -0,0 +1,92 @@ +# 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's installable but not importable on pypy3. + 'psutil>=4.0.0', +] + +tests_require = ( + 'pytest==3.8.2', # tests framework used + 'pytest-cov==2.6.0', # coverage reports to verify tests quality + 'mock==2.0.0', # tests mocking tool + 'python-daemon==2.2.0', # 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='1.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', + 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 :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + '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.egg-info/PKG-INFO b/src/mirakuru.egg-info/PKG-INFO new file mode 100644 index 0000000..f202de1 --- /dev/null +++ b/src/mirakuru.egg-info/PKG-INFO @@ -0,0 +1,304 @@ +Metadata-Version: 2.1 +Name: mirakuru +Version: 1.1.0 +Summary: Process executor for tests. +Home-page: https://github.com/ClearcodeHQ/mirakuru +Author: Clearcode - The A Room +Author-email: thearoom@clearcode.cc +License: LGPL +Description: 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=v1.1.0 + :target: http://mirakuru.readthedocs.io/en/v1.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=v1.1.0 + :target: https://travis-ci.org/ClearcodeHQ/mirakuru + :alt: Tests + + .. image:: https://coveralls.io/repos/ClearcodeHQ/mirakuru/badge.png?branch=v1.1.0 + :target: https://coveralls.io/r/ClearcodeHQ/mirakuru?branch=v1.1.0 + :alt: Coverage Status + + .. image:: https://requires.io/github/ClearcodeHQ/mirakuru/requirements.svg?tag=v1.1.0 + :target: https://requires.io/github/ClearcodeHQ/mirakuru/requirements/?tag=v1.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 six 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. + * 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. + + + CHANGELOG + ========= + + 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`. + +Keywords: process executor tests summon_process +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+) +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Testing +Provides-Extra: docs +Provides-Extra: tests diff --git a/src/mirakuru.egg-info/SOURCES.txt b/src/mirakuru.egg-info/SOURCES.txt new file mode 100644 index 0000000..a5d04d3 --- /dev/null +++ b/src/mirakuru.egg-info/SOURCES.txt @@ -0,0 +1,23 @@ +AUTHORS.rst +CHANGES.rst +CONTRIBUTING.rst +LICENSE +MANIFEST.in +README.rst +setup.cfg +setup.py +src/mirakuru/__init__.py +src/mirakuru/base.py +src/mirakuru/base_env.py +src/mirakuru/compat.py +src/mirakuru/exceptions.py +src/mirakuru/http.py +src/mirakuru/output.py +src/mirakuru/pid.py +src/mirakuru/tcp.py +src/mirakuru.egg-info/PKG-INFO +src/mirakuru.egg-info/SOURCES.txt +src/mirakuru.egg-info/dependency_links.txt +src/mirakuru.egg-info/not-zip-safe +src/mirakuru.egg-info/requires.txt +src/mirakuru.egg-info/top_level.txt \ No newline at end of file diff --git a/src/mirakuru.egg-info/dependency_links.txt b/src/mirakuru.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/mirakuru.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/mirakuru.egg-info/not-zip-safe b/src/mirakuru.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/mirakuru.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/src/mirakuru.egg-info/requires.txt b/src/mirakuru.egg-info/requires.txt new file mode 100644 index 0000000..1bf7a40 --- /dev/null +++ b/src/mirakuru.egg-info/requires.txt @@ -0,0 +1,10 @@ +psutil>=4.0.0 + +[docs] +sphinx + +[tests] +pytest==3.8.2 +pytest-cov==2.6.0 +mock==2.0.0 +python-daemon==2.2.0 diff --git a/src/mirakuru.egg-info/top_level.txt b/src/mirakuru.egg-info/top_level.txt new file mode 100644 index 0000000..1cdbe41 --- /dev/null +++ b/src/mirakuru.egg-info/top_level.txt @@ -0,0 +1 @@ +mirakuru diff --git a/src/mirakuru/__init__.py b/src/mirakuru/__init__.py new file mode 100644 index 0000000..ce924e1 --- /dev/null +++ b/src/mirakuru/__init__.py @@ -0,0 +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__ = '1.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 new file mode 100644 index 0000000..2a414a2 --- /dev/null +++ b/src/mirakuru/base.py @@ -0,0 +1,461 @@ +# 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 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] + + +@atexit.register +def cleanup_subprocesses(): + """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(object): + """Simple subprocess executor with start/stop/kill functionality.""" + + def __init__( + self, command, shell=False, timeout=3600, sleep=0.1, + sig_stop=signal.SIGTERM, sig_kill=SIGKILL + ): + """ + Initialize executor. + + :param (str, list) command: command to be run by the subprocess + :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) + + .. 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) + """Command that the executor runs.""" + self.command_parts = command + else: + self.command = command + self.command_parts = shlex.split(command) + + 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._endtime = None + self.process = None + """A :class:`subprocess.Popen` instance once process is started.""" + + self._uuid = '{0}:{1}'.format(os.getpid(), uuid.uuid4()) + + def __enter__(self): + """ + Enter context manager starting the subprocess. + + :returns: itself + :rtype: SimpleExecutor + """ + return self.start() + + def __exit__(self, exc_type, exc_value, traceback): + """Exit context manager stopping the subprocess.""" + self.stop() + + def running(self): + """ + Check if executor is running. + + :returns: True if process is running, False otherwise + :rtype: bool + """ + if self.process is None: + return False + return self.process.poll() is None + + def start(self): + """ + Start defined process. + + After process gets started, timeout countdown begins as well. + + :returns: itself + :rtype: SimpleExecutor + + .. 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``. + """ + if self.process is None: + command = self.command + if not self._shell: + command = self.command_parts + + env = os.environ.copy() + # 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 + popen_kwargs = { + 'shell': self._shell, + 'stdin': subprocess.PIPE, + 'stdout': subprocess.PIPE, + 'universal_newlines': True, + 'env': env, + } + if platform.system() != 'Windows': + popen_kwargs['preexec_fn'] = os.setsid + self.process = subprocess.Popen( + command, + **popen_kwargs + ) + + self._set_timeout() + return self + + def _set_timeout(self): + """Set timeout for possible wait.""" + self._endtime = time.time() + self._timeout + + def _clear_process(self): + """ + Close stdin/stdout of subprocess. + + It is required because of ResourceWarning in Python 3. + """ + if self.process: + if self.process.stdin: + self.process.stdin.close() + if self.process.stdout: + self.process.stdout.close() + + self.process = None + + self._endtime = None + + def _kill_all_kids(self, sig): + """ + 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 list + """ + 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, sig=None): + """ + 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(): + """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): + """ + 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, wait=True, sig=None): + """ + 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.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): + """Return subprocess output.""" + if self.process is not None: + return self.process.stdout + + def wait_for(self, wait_for): + """ + 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): + """ + 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): + """Cleanup subprocesses created during Executor lifetime.""" + try: + if self.process: + self.kill() + except Exception: + 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): + """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)) + ) + + def __str__(self): + """Return readable executor representation.""" + return '<{module}.{executor}: "{command}">'.format( + module=self.__class__.__module__, + executor=self.__class__.__name__, + command=self.command + ) + + +class Executor(SimpleExecutor): + """Base class for executors with a pre- and after-start checks.""" + + def pre_start_check(self): + """ + 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): + """ + 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): + """ + Make sure the process didn't exit with an error and run the checks. + + :rtype: bool + :return: the actual check status + :raise ProcessExitedWithError: when the main process exits with + an error + """ + 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): + """ + 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 new file mode 100644 index 0000000..7757403 --- /dev/null +++ b/src/mirakuru/base_env.py @@ -0,0 +1,109 @@ +# 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 + +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, env_value): + """ + 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, env_value): + """ + 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() + 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) + + for line in ps_xe: + line = str(line) + if env in line: + pids.add(int(PS_XE_PID_MATCH.match(line).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/compat.py b/src/mirakuru/compat.py new file mode 100644 index 0000000..772bb0d --- /dev/null +++ b/src/mirakuru/compat.py @@ -0,0 +1,56 @@ +# 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 Python 2 and 3 compatibility module.""" + +import sys +import signal + + +# pylint: disable=no-name-in-module, import-error +if sys.version_info.major == 2: + from httplib import HTTPConnection, HTTPException, OK + from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler + from urlparse import urlparse + http_server_cmd = ( # pylint: disable=invalid-name + "{python} -m SimpleHTTPServer" + ).format(python=sys.executable) +else: + # In Python 3 httplib is renamed to http.client + from http.client import HTTPConnection, HTTPException, OK + # In Python 3 BaseHTTPServer is renamed to http.server + from http.server import HTTPServer, BaseHTTPRequestHandler + # In Python 3 urlparse is renamed to urllib.parse + from urllib.parse import urlparse + http_server_cmd = ( # pylint: disable=invalid-name + "{python} -m http.server" + ).format(python=sys.executable) + +# Windows does not have SIGKILL, fall back to SIGTERM. +SIGKILL = getattr(signal, 'SIGKILL', signal.SIGTERM) + + +__all__ = ( + 'HTTPConnection', + 'HTTPException', + 'OK', + 'HTTPServer', + 'BaseHTTPRequestHandler', + 'urlparse', + 'http_server_cmd', + 'SIGKILL', +) diff --git a/src/mirakuru/exceptions.py b/src/mirakuru/exceptions.py new file mode 100644 index 0000000..cbfaeba --- /dev/null +++ b/src/mirakuru/exceptions.py @@ -0,0 +1,92 @@ +"""Mirakuru exceptions.""" + + +class ExecutorError(Exception): + """Base exception for executor failures.""" + + def __init__(self, executor): + """ + Exception initialization. + + :param mirakuru.base.Executor 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, timeout): + """ + Exception initialization with an extra ``timeout`` argument. + + :param mirakuru.base.Executor executor: for which exception occurred + :param int timeout: timeout for which exception occurred + """ + super(TimeoutExpired, self).__init__(executor) + self.timeout = timeout + + def __str__(self): + """ + Return Exception's string representation. + + :returns: string representation + :rtype: str + """ + return 'Executor {0} timed out after {1} seconds'.format( + self.executor, self.timeout + ) + + +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): + """ + 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)) + + +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, exit_code): + """ + Exception initialization with an extra ``exit_code`` argument. + + :param mirakuru.base.Executor 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): + """ + 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)) diff --git a/src/mirakuru/http.py b/src/mirakuru/http.py new file mode 100644 index 0000000..4127f3d --- /dev/null +++ b/src/mirakuru/http.py @@ -0,0 +1,88 @@ +# 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 . +"""HTTP enabled process executor.""" + +import re +import socket + +from mirakuru.compat import HTTPConnection, HTTPException +from mirakuru.compat import urlparse + +from mirakuru.tcp import TCPExecutor + + +class HTTPExecutor(TCPExecutor): + """Http enabled process executor.""" + + DEFAULT_PORT = 80 + """Default TCP port for the HTTP protocol.""" + + def __init__(self, command, url, status=r'^2\d\d$', **kwargs): + """ + Initialize HTTPExecutor executor. + + :param (str, list) command: command to be run by the subprocess + :param str url: URL that executor checks to verify + if process has already started. + :param bool shell: same as the `subprocess.Popen` shell definition + :param str|int status: HTTP status code(s) that an endpoint must + return for the executor being considered as running. This argument + is interpreted as a single status code - e.g. '200' or '404' but + also it can be a regular expression - e.g. '4..' or '(200|404)'. + Default: any 2XX HTTP status code. + :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` + + """ + self.url = urlparse(url) + """ + An :func:`urlparse.urlparse` representation of an url. + + It'll be used to check process status on. + """ + + port = self.url.port + if port is None: + port = self.DEFAULT_PORT + + self.status = str(status) + self.status_re = re.compile(str(status)) + + super(HTTPExecutor, self).__init__( + command, host=self.url.hostname, port=port, **kwargs + ) + + def after_start_check(self): + """Check if defined URL returns expected status to a HEAD request.""" + try: + conn = HTTPConnection(self.host, self.port) + + conn.request('HEAD', self.url.path) + status = str(conn.getresponse().status) + + if status == self.status or self.status_re.match(status): + conn.close() + return True + + except (HTTPException, socket.timeout, socket.error): + return False diff --git a/src/mirakuru/output.py b/src/mirakuru/output.py new file mode 100644 index 0000000..a347cf6 --- /dev/null +++ b/src/mirakuru/output.py @@ -0,0 +1,97 @@ +# 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 re +import select + +from mirakuru.base import SimpleExecutor + + +class OutputExecutor(SimpleExecutor): + """Executor that awaits for string output being present in output.""" + + def __init__(self, command, banner, **kwargs): + """ + 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) + self.poll_obj = None + + def start(self): + """ + Start process. + + :returns: itself + :rtype: OutputExecutor + + .. note:: + + Process will be considered started, when defined banner will appear + in process output. + """ + super(OutputExecutor, self).start() + + # get a polling object + self.poll_obj = select.poll() + + # register a file descriptor + # POLLIN because we will wait for data to read + self.poll_obj.register(self.output(), select.POLLIN) + + try: + self.wait_for(self._wait_for_output) + + # unregister the file descriptor and delete the polling object + self.poll_obj.unregister(self.output()) + finally: + del self.poll_obj + return self + + def _wait_for_output(self): + """ + Check if output matches banner. + + .. warning:: + Waiting for I/O completion. It does not work on Windows. Sorry. + """ + # 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 = self.poll_obj.poll(0) + + if poll_result: + line = self.output().readline() + if self._banner.match(line): + return True + + return False diff --git a/src/mirakuru/pid.py b/src/mirakuru/pid.py new file mode 100644 index 0000000..c68370e --- /dev/null +++ b/src/mirakuru/pid.py @@ -0,0 +1,79 @@ +# 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 . +"""Pid executor definition.""" + +import os.path +from mirakuru.base import Executor + + +class PidExecutor(Executor): + """ + File existence checking process executor. + + Used to start processes that create pid files (or any other for that + matter). Starts the given process and waits for the given file to be + created. + """ + + def __init__(self, command, filename, **kwargs): + """ + Initialize the PidExecutor executor. + + If the filename is empty, a ValueError is thrown. + + :param (str, list) command: command to be run by the subprocess + :param str filename: the file which is to exist + :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) + + :raises: ValueError + + """ + super(PidExecutor, self).__init__(command, **kwargs) + if not filename: + raise ValueError("filename must be defined") + self.filename = filename + """the name of the file which the process is to create.""" + + def pre_start_check(self): + """ + Check if the specified file has been created. + + .. note:: + + The process will be considered started when it will have created + the specified file as defined in the initializer. + """ + return os.path.isfile(self.filename) + + def after_start_check(self): + """ + Check if the process has created the specified file. + + .. note:: + + The process will be considered started when it will have created + the specified file as defined in the initializer. + """ + return self.pre_start_check() # we can reuse logic from `pre_start()` diff --git a/src/mirakuru/tcp.py b/src/mirakuru/tcp.py new file mode 100644 index 0000000..c87dabc --- /dev/null +++ b/src/mirakuru/tcp.py @@ -0,0 +1,83 @@ +# 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 . +"""TCP executor definition.""" + +import socket +from mirakuru.base import Executor + + +class TCPExecutor(Executor): + """ + TCP-listening process executor. + + Used to start (and wait to actually be running) processes that can accept + TCP connections. + """ + + def __init__(self, command, host, port, **kwargs): + """ + Initialize TCPExecutor executor. + + :param (str, list) command: command to be run by the subprocess + :param str host: host under which process is accessible + :param int port: port under which process is accessible + :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(TCPExecutor, self).__init__(command, **kwargs) + self.host = host + """Host name, process is listening on.""" + self.port = port + """Port number, process is listening on.""" + + def pre_start_check(self): + """ + Check if process accepts connections. + + .. note:: + + Process will be considered started, when it'll be able to accept + TCP connections as defined in initializer. + """ + try: + sock = socket.socket() + sock.connect((self.host, self.port)) + return True + except (socket.error, socket.timeout): + return False + finally: + # close socket manually for sake of PyPy + sock.close() + + def after_start_check(self): + """ + Check if process accepts connections. + + .. note:: + + Process will be considered started, when it'll be able to accept + TCP connections as defined in initializer. + """ + return self.pre_start_check() # we can reuse logic from `pre_start()`