Source code for capybara.server
from contextlib import closing
from threading import Thread
from time import sleep
import capybara
from capybara.compat import URLError, urlopen
from capybara.helpers import Timer
from capybara.utils import (
Counter,
cached_property,
decode_bytes,
encode_string,
find_available_port)
[docs]class Server(object):
"""
Serves a WSGI-compliant app for Capybara to test.
Args:
app (object): The WSGI-compliant app to serve.
port (int, optional): The port on which the server should be available.
Defaults to :data:`capybara.server_port`, or the last port used by the
given app, or a random available port.
host (str, optional): The host on which the server should be available.
Defaults to :data:`capybara.server_host`.
"""
_ports = {}
def __init__(self, app, port=None, host=None):
self.app = app
self.host = (
host or
capybara.server_host)
self.port = (
port or
capybara.server_port or
type(self)._ports.get(self.port_key, None) or
find_available_port())
self.server_thread = None
@property
def error(self):
return self.middleware.error
[docs] def reset_error(self):
self.middleware.error = None
@property
def port_key(self):
return str(id(self.app))
[docs] @cached_property
def middleware(self):
return Middleware(self.app)
[docs] def wait_for_pending_requests(self):
timer = Timer(60)
while self.has_pending_requests:
if timer.expired:
raise RuntimeError("Requests did not finish in 60 seconds")
sleep(0.01)
[docs] def boot(self):
"""
Boots a server for the app, if it isn't already booted.
Returns:
Server: This server.
"""
if not self.responsive:
# Remember the port so we can reuse it if we try to serve this same app again.
type(self)._ports[self.port_key] = self.port
init_func = capybara.servers[capybara.server_name]
init_args = (self.middleware, self.port, self.host)
self.server_thread = Thread(target=init_func, args=init_args)
# Inform Python that it shouldn't wait for this thread to terminate before
# exiting. (It will still be appropriately terminated when the process exits.)
self.server_thread.daemon = True
self.server_thread.start()
# Make sure the server actually starts and becomes responsive.
timer = Timer(60)
while not self.responsive:
if timer.expired:
raise RuntimeError("WSGI application timed out during boot")
self.server_thread.join(0.1)
return self
@property
def responsive(self):
""" bool: Whether the server for this app is up and responsive. """
if self.server_thread and self.server_thread.join(0):
return False
try:
# Try to fetch the endpoint added by the middleware.
identify_url = "http://{0}:{1}/__identify__".format(self.host, self.port)
with closing(urlopen(identify_url)) as response:
body, status_code = response.read(), response.getcode()
if str(status_code)[0] == "2" or str(status_code)[0] == "3":
return decode_bytes(body) == str(id(self.app))
except URLError:
pass
return False
@property
def has_pending_requests(self):
return self.middleware.has_pending_requests
[docs]class Middleware(object):
def __init__(self, app):
self.app = app
self.counter = Counter()
self.error = None
def __call__(self, environ, start_response):
if environ["PATH_INFO"] == "/__identify__":
return self.identify(environ, start_response)
else:
with self.counter:
try:
return self.app(environ, start_response)
except Exception as e:
self.error = e
raise
[docs] def identify(self, environ, start_response):
start_response("200 OK", [("Content-Type", "text/plain")])
return [encode_string(str(id(self.app)))]
@property
def has_pending_requests(self):
return self.counter.value > 0