Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Live server subprocess #47

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 87 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
165 changes: 126 additions & 39 deletions pytest_flask/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example.com, example.net, example.org, and example.edu are second-level domain names reserved for documentation purposes and examples of the use of domain names.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll change it back - I was just trying to get the line to squeeze into 80 chars so my pep8 checker would stop complaining :)


"""
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
Expand All @@ -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 '<LiveServer listening at %s>' % 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):
Expand All @@ -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.

Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ def get_version():

extras_require = {
'docs': read('requirements', 'docs.txt').splitlines(),
'tests': tests_require
'tests': tests_require,
'services': ['pytest-services'],
}


Expand Down
16 changes: 8 additions & 8 deletions tests/test_live_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -16,19 +16,19 @@ 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'))
assert res.code == 200
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):
Expand Down Expand Up @@ -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

Expand Down