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