Home | History | Annotate | Download | only in webtest
      1 # -*- coding: utf-8 -*-
      2 """
      3 This module contains some helpers to deal with the real http
      4 world.
      5 """
      6 
      7 import threading
      8 import logging
      9 import select
     10 import socket
     11 import time
     12 import os
     13 
     14 import six
     15 import webob
     16 from six.moves import http_client
     17 from waitress.server import TcpWSGIServer
     18 
     19 
     20 def get_free_port():
     21     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     22     s.bind(('', 0))
     23     ip, port = s.getsockname()
     24     s.close()
     25     ip = os.environ.get('WEBTEST_SERVER_BIND', '127.0.0.1')
     26     return ip, port
     27 
     28 
     29 def check_server(host, port, path_info='/', timeout=3, retries=30):
     30     """Perform a request until the server reply"""
     31     if retries < 0:
     32         return 0
     33     time.sleep(.3)
     34     for i in range(retries):
     35         try:
     36             conn = http_client.HTTPConnection(host, int(port), timeout=timeout)
     37             conn.request('GET', path_info)
     38             res = conn.getresponse()
     39             return res.status
     40         except (socket.error, http_client.HTTPException):
     41             time.sleep(.3)
     42     return 0
     43 
     44 
     45 class StopableWSGIServer(TcpWSGIServer):
     46     """StopableWSGIServer is a TcpWSGIServer which run in a separated thread.
     47     This allow to use tools like casperjs or selenium.
     48 
     49     Server instance have an ``application_url`` attribute formated with the
     50     server host and port.
     51     """
     52 
     53     was_shutdown = False
     54 
     55     def __init__(self, application, *args, **kwargs):
     56         super(StopableWSGIServer, self).__init__(self.wrapper, *args, **kwargs)
     57         self.runner = None
     58         self.test_app = application
     59         self.application_url = 'http://%s:%s/' % (self.adj.host, self.adj.port)
     60 
     61     def wrapper(self, environ, start_response):
     62         """Wrap the wsgi application to override some path:
     63 
     64         ``/__application__``: allow to ping the server.
     65 
     66         ``/__file__?__file__={path}``: serve the file found at ``path``
     67         """
     68         if '__file__' in environ['PATH_INFO']:
     69             req = webob.Request(environ)
     70             resp = webob.Response()
     71             resp.content_type = 'text/html; charset=UTF-8'
     72             filename = req.params.get('__file__')
     73             if os.path.isfile(filename):
     74                 body = open(filename, 'rb').read()
     75                 body = body.replace(six.b('http://localhost/'),
     76                                     six.b('http://%s/' % req.host))
     77                 resp.body = body
     78             else:
     79                 resp.status = '404 Not Found'
     80             return resp(environ, start_response)
     81         elif '__application__' in environ['PATH_INFO']:
     82             return webob.Response('server started')(environ, start_response)
     83         return self.test_app(environ, start_response)
     84 
     85     def run(self):
     86         """Run the server"""
     87         try:
     88             self.asyncore.loop(.5, map=self._map)
     89         except select.error:  # pragma: no cover
     90             if not self.was_shutdown:
     91                 raise
     92 
     93     def shutdown(self):
     94         """Shutdown the server"""
     95         # avoid showing traceback related to asyncore
     96         self.was_shutdown = True
     97         self.logger.setLevel(logging.FATAL)
     98         while self._map:
     99             triggers = list(self._map.values())
    100             for trigger in triggers:
    101                 trigger.handle_close()
    102         self.maintenance(0)
    103         self.task_dispatcher.shutdown()
    104         return True
    105 
    106     @classmethod
    107     def create(cls, application, **kwargs):
    108         """Start a server to serve ``application``. Return a server
    109         instance."""
    110         host, port = get_free_port()
    111         if 'port' not in kwargs:
    112             kwargs['port'] = port
    113         if 'host' not in kwargs:
    114             kwargs['host'] = host
    115         if 'expose_tracebacks' not in kwargs:
    116             kwargs['expose_tracebacks'] = True
    117         server = cls(application, **kwargs)
    118         server.runner = threading.Thread(target=server.run)
    119         server.runner.daemon = True
    120         server.runner.start()
    121         return server
    122 
    123     def wait(self, retries=30):
    124         """Wait until the server is started"""
    125         running = check_server(self.adj.host, self.adj.port,
    126                                '/__application__', retries=retries)
    127         if running:
    128             return True
    129         try:
    130             self.shutdown()
    131         finally:
    132             return False
    133