cubicweb/devtools/httptest.py
changeset 11057 0b59724cb3f2
parent 11015 baf463175505
child 11158 669eac69ea21
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """this module contains base classes and utilities for integration with running
       
    19 http server
       
    20 """
       
    21 from __future__ import print_function
       
    22 
       
    23 __docformat__ = "restructuredtext en"
       
    24 
       
    25 import random
       
    26 import threading
       
    27 import socket
       
    28 
       
    29 from six.moves import range, http_client
       
    30 from six.moves.urllib.parse import urlparse
       
    31 
       
    32 
       
    33 from cubicweb.devtools.testlib import CubicWebTC
       
    34 from cubicweb.devtools import ApptestConfiguration
       
    35 
       
    36 
       
    37 def get_available_port(ports_scan):
       
    38     """return the first available port from the given ports range
       
    39 
       
    40     Try to connect port by looking for refused connection (111) or transport
       
    41     endpoint already connected (106) errors
       
    42 
       
    43     Raise a RuntimeError if no port can be found
       
    44 
       
    45     :type ports_range: list
       
    46     :param ports_range: range of ports to test
       
    47     :rtype: int
       
    48 
       
    49     .. see:: :func:`test.test_support.bind_port`
       
    50     """
       
    51     ports_scan = list(ports_scan)
       
    52     random.shuffle(ports_scan)  # lower the chance of race condition
       
    53     for port in ports_scan:
       
    54         try:
       
    55             s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
       
    56             sock = s.connect(("localhost", port))
       
    57         except socket.error as err:
       
    58             if err.args[0] in (111, 106):
       
    59                 return port
       
    60         finally:
       
    61             s.close()
       
    62     raise RuntimeError('get_available_port([ports_range]) cannot find an available port')
       
    63 
       
    64 
       
    65 class CubicWebServerTC(CubicWebTC):
       
    66     """Class for running a Twisted-based test web server.
       
    67     """
       
    68     ports_range = range(7000, 8000)
       
    69 
       
    70     def start_server(self):
       
    71         from twisted.internet import reactor
       
    72         from cubicweb.etwist.server import run
       
    73         # use a semaphore to avoid starting test while the http server isn't
       
    74         # fully initilialized
       
    75         semaphore = threading.Semaphore(0)
       
    76         def safe_run(*args, **kwargs):
       
    77             try:
       
    78                 run(*args, **kwargs)
       
    79             finally:
       
    80                 semaphore.release()
       
    81 
       
    82         reactor.addSystemEventTrigger('after', 'startup', semaphore.release)
       
    83         t = threading.Thread(target=safe_run, name='cubicweb_test_web_server',
       
    84                 args=(self.config, True), kwargs={'repo': self.repo})
       
    85         self.web_thread = t
       
    86         t.start()
       
    87         semaphore.acquire()
       
    88         if not self.web_thread.isAlive():
       
    89             # XXX race condition with actual thread death
       
    90             raise RuntimeError('Could not start the web server')
       
    91         #pre init utils connection
       
    92         parseurl = urlparse(self.config['base-url'])
       
    93         assert parseurl.port == self.config['port'], (self.config['base-url'], self.config['port'])
       
    94         self._web_test_cnx = http_client.HTTPConnection(parseurl.hostname,
       
    95                                                         parseurl.port)
       
    96         self._ident_cookie = None
       
    97 
       
    98     def stop_server(self, timeout=15):
       
    99         """Stop the webserver, waiting for the thread to return"""
       
   100         from twisted.internet import reactor
       
   101         if self._web_test_cnx is None:
       
   102             self.web_logout()
       
   103             self._web_test_cnx.close()
       
   104         try:
       
   105             reactor.stop()
       
   106             self.web_thread.join(timeout)
       
   107             assert not self.web_thread.isAlive()
       
   108 
       
   109         finally:
       
   110             reactor.__init__()
       
   111 
       
   112     def web_login(self, user=None, passwd=None):
       
   113         """Log the current http session for the provided credential
       
   114 
       
   115         If no user is provided, admin connection are used.
       
   116         """
       
   117         if user is None:
       
   118             user  = self.admlogin
       
   119             passwd = self.admpassword
       
   120         if passwd is None:
       
   121             passwd = user
       
   122         response = self.web_get("login?__login=%s&__password=%s" %
       
   123                                 (user, passwd))
       
   124         assert response.status == http_client.SEE_OTHER, response.status
       
   125         self._ident_cookie = response.getheader('Set-Cookie')
       
   126         assert self._ident_cookie
       
   127         return True
       
   128 
       
   129     def web_logout(self, user='admin', pwd=None):
       
   130         """Log out current http user"""
       
   131         if self._ident_cookie is not None:
       
   132             response = self.web_get('logout')
       
   133         self._ident_cookie = None
       
   134 
       
   135     def web_request(self, path='', method='GET', body=None, headers=None):
       
   136         """Return an http_client.HTTPResponse object for the specified path
       
   137 
       
   138         Use available credential if available.
       
   139         """
       
   140         if headers is None:
       
   141             headers = {}
       
   142         if self._ident_cookie is not None:
       
   143             assert 'Cookie' not in headers
       
   144             headers['Cookie'] = self._ident_cookie
       
   145         self._web_test_cnx.request(method, '/' + path, headers=headers, body=body)
       
   146         response = self._web_test_cnx.getresponse()
       
   147         response.body = response.read() # to chain request
       
   148         response.read = lambda : response.body
       
   149         return response
       
   150 
       
   151     def web_get(self, path='', body=None, headers=None):
       
   152         return self.web_request(path=path, body=body, headers=headers)
       
   153 
       
   154     def setUp(self):
       
   155         super(CubicWebServerTC, self).setUp()
       
   156         port = self.config['port'] or get_available_port(self.ports_range)
       
   157         self.config.global_set_option('port', port) # force rewrite here
       
   158         self.config.global_set_option('base-url', 'http://127.0.0.1:%d/' % port)
       
   159         # call load_configuration again to let the config reset its datadir_url
       
   160         self.config.load_configuration()
       
   161         self.start_server()
       
   162 
       
   163     def tearDown(self):
       
   164         from twisted.internet import error
       
   165         try:
       
   166             self.stop_server()
       
   167         except error.ReactorNotRunning as err:
       
   168             # Server could be launched manually
       
   169             print(err)
       
   170         super(CubicWebServerTC, self).tearDown()