diff --git a/.gitignore b/.gitignore index 5edd0cf..72364f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,89 @@ -# Python bytecode / optimized files -*.py[co] -*.egg-info -build -dist -venv +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation -docs/_build +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject diff --git a/Makefile b/Makefile index 39bef37..7b95cfc 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ test: clean: @rm -rf build dist *.egg-info - @find . -name '*.py?' -delete + @find . | grep -E '(__pycache__|\.pyc|\.pyo$$)' | xargs rm -rf docs: diff --git a/pytest_flask/fixtures.py b/pytest_flask/fixtures.py index b8539d7..d2f6b30 100755 --- a/pytest_flask/fixtures.py +++ b/pytest_flask/fixtures.py @@ -6,9 +6,11 @@ import socket try: - from urllib2 import urlopen -except ImportError: from urllib.request import urlopen + from shutil import which +except ImportError: + from urllib2 import urlopen + from distutils.spawn import find_executable as which from flask import _request_ctx_stack @@ -34,35 +36,38 @@ def login(self, email, password): return self.client.post(url_for('login'), data=credentials) def test_login(self): - assert self.login('vital@example.com', 'pass').status_code == 200 + assert self.login('vital@foo.com', 'pass').status_code == 200 """ if request.cls is not None: request.cls.client = client -class LiveServer(object): - """The helper class uses to manage live server. Handles creation and - stopping application in a separate process. +def _find_unused_port(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(('', 0)) + port = s.getsockname()[1] + s.close() + return port + - :param app: The application to run. - :param port: The port to run application. - """ +class LiveServerBase(object): - def __init__(self, app, port): + def __init__(self, app, monkeypatch, port=None): self.app = app - self.port = port + self.port = port or _find_unused_port() self._process = None + self.monkeypatch = monkeypatch def start(self): - """Start application in a separate process.""" - worker = lambda app, port: app.run(port=port, use_reloader=False) - self._process = multiprocessing.Process( - target=worker, - args=(self.app, self.port) - ) - self._process.start() - + # Explicitly set application ``SERVER_NAME`` for test suite + # and restore original value on test teardown. + server_name = self.app.config['SERVER_NAME'] or '127.0.0.1' + self.monkeypatch.setitem( + self.app.config, 'SERVER_NAME', + _rewrite_server_name(server_name, str(self.port))) + + def _wait_for_server(self): # We must wait for the server to start listening with a maximum # timeout of 5 seconds. timeout = 5 @@ -76,15 +81,109 @@ def start(self): def url(self, url=''): """Returns the complete url based on server options.""" - return 'http://localhost:%d%s' % (self.port, url) + return 'http://127.0.0.1:%d%s' % (self.port, url) + + def __repr__(self): + return '<%s listening at %s>' % ( + self.__class__.__name, + self.url(), + ) + + +class LiveServerMultiprocess(LiveServerBase): + """The helper class uses this to manage live server. + Handles creation and stopping application in a separate process. + + :param app: The application to run. + :param port: The port to run application. + """ + + def start(self): + """Start application in a separate process.""" + def worker(app, port): + return app.run(port=port, use_reloader=False) + super(LiveServerMultiprocess, self).start() + + self._process = multiprocessing.Process( + target=worker, + args=(self.app, self.port) + ) + self._process.start() + self._wait_for_server() def stop(self): """Stop application process.""" if self._process: self._process.terminate() - def __repr__(self): - return '' % self.url() +try: + import pytest_services # noqa + + class LiveServerSubprocess(LiveServerBase): + """The helper class uses this to manage live server. + Handles creation and stopping application in a subprocess + using Popen. Use this if you need more explicit separation + between processes. + + :param app: The application to run. + :param port: The port to run application. + """ + def __init__(self, app, monkeypatch, watcher_getter, port=None): + self.app = app + self.port = port or _find_unused_port() + self._process = None + self.monkeypatch = monkeypatch + self.watcher_getter = watcher_getter + + def start(self, **kwargs): + """ + Start application in a separate process. + + To add environment variables to the process, simply do: + live_server_subprocess.start( + watcher_getter_kwargs={'env': {'MYENV': '1'}}) + """ + def worker(app, port): + return app.run(port=port, use_reloader=False) + super(LiveServerSubprocess, self).start() + + self._process = self.watcher_getter( + name='flask', + arguments=['run', '--port', str(self.port)], + checker=lambda: which('flask'), + kwargs=kwargs.get('watcher_getter_kwargs', {})) + self._wait_for_server() + + def stop(self): + """Stop application process.""" + if self._process: + self._process.terminate() + + @pytest.yield_fixture(scope='function') + def live_server_subprocess(request, app, monkeypatch, watcher_getter): + """Run application in a subprocess. Use this if you need more explicit + separation of processes. Uses os.fork(). + Requires flask >= 0.11 and the pytest-services plugin. + + When the ``live_server_subprocess`` fixture is applyed, + the ``url_for`` function works as expected:: + + def test_server_is_up_and_running(live_server_subprocess): + index_url = url_for('index', _external=True) + assert index_url == 'http://127.0.0.1:5000/' + + res = urllib2.urlopen(index_url) + assert res.code == 200 + """ + + server = LiveServerSubprocess(app, monkeypatch=monkeypatch) + if request.config.getvalue('start_live_server'): + server.start() + yield server + server.stop() + +except ImportError: + pass def _rewrite_server_name(server_name, new_port): @@ -95,7 +194,7 @@ def _rewrite_server_name(server_name, new_port): return sep.join((server_name, new_port)) -@pytest.fixture(scope='function') +@pytest.yield_fixture(scope='function') def live_server(request, app, monkeypatch): """Run application in a separate process. @@ -104,30 +203,18 @@ def live_server(request, app, monkeypatch): def test_server_is_up_and_running(live_server): index_url = url_for('index', _external=True) - assert index_url == 'http://localhost:5000/' + assert index_url == 'http://127.0.0.1:5000/' res = urllib2.urlopen(index_url) assert res.code == 200 """ - # Bind to an open port - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(('', 0)) - port = s.getsockname()[1] - s.close() - # Explicitly set application ``SERVER_NAME`` for test suite - # and restore original value on test teardown. - server_name = app.config['SERVER_NAME'] or 'localhost' - monkeypatch.setitem(app.config, 'SERVER_NAME', - _rewrite_server_name(server_name, str(port))) - - server = LiveServer(app, port) + server = LiveServerMultiprocess(app, monkeypatch=monkeypatch) if request.config.getvalue('start_live_server'): server.start() - - request.addfinalizer(server.stop) - return server + yield server + server.stop() @pytest.fixture diff --git a/setup.py b/setup.py index 6e967be..c893845 100755 --- a/setup.py +++ b/setup.py @@ -132,7 +132,8 @@ def get_version(): extras_require = { 'docs': read('requirements', 'docs.txt').splitlines(), - 'tests': tests_require + 'tests': tests_require, + 'services': ['pytest-services'], } diff --git a/tests/test_live_server.py b/tests/test_live_server.py index 1c00928..6387308 100755 --- a/tests/test_live_server.py +++ b/tests/test_live_server.py @@ -2,9 +2,9 @@ # -*- coding: utf-8 -*- import pytest try: - from urllib2 import urlopen -except ImportError: from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen from flask import url_for @@ -16,8 +16,8 @@ def test_server_is_alive(self, live_server): assert live_server._process.is_alive() def test_server_url(self, live_server): - assert live_server.url() == 'http://localhost:%d' % live_server.port - assert live_server.url('/ping') == 'http://localhost:%d/ping' % live_server.port + assert live_server.url() == 'http://127.0.0.1:%d' % live_server.port + assert live_server.url('/ping') == 'http://127.0.0.1:%d/ping' % live_server.port def test_server_listening(self, live_server): res = urlopen(live_server.url('/ping')) @@ -25,10 +25,10 @@ def test_server_listening(self, live_server): assert b'pong' in res.read() def test_url_for(self, live_server): - assert url_for('ping', _external=True) == 'http://localhost:%s/ping' % live_server.port + assert url_for('ping', _external=True) == 'http://127.0.0.1:%s/ping' % live_server.port def test_set_application_server_name(self, live_server): - assert live_server.app.config['SERVER_NAME'] == 'localhost:%d' % live_server.port + assert live_server.app.config['SERVER_NAME'] == '127.0.0.1:%d' % live_server.port @pytest.mark.options(server_name='example.com:5000') def test_rewrite_application_server_name(self, live_server): @@ -62,9 +62,9 @@ def test_add_endpoint_to_live_server(self, appdir): appdir.create_test_module(''' import pytest try: - from urllib2 import urlopen - except ImportError: from urllib.request import urlopen + except ImportError: + from urllib2 import urlopen from flask import url_for