1 """A library of helper functions for the CherryPy test suite."""
5 log
= logging
.getLogger(__name__
)
7 thisdir
= os
.path
.abspath(os
.path
.dirname(__file__
))
8 serverpem
= os
.path
.join(os
.getcwd(), thisdir
, 'test.pem')
16 from cherrypy
._cpcompat
import basestring
, copyitems
, HTTPSConnection
, ntob
17 from cherrypy
.lib
import httputil
18 from cherrypy
.lib
.reprconf
import unrepr
19 from cherrypy
.test
import webtest
25 def get_tst_config(overconf
= {}):
27 if _testconfig
is None:
30 'protocol': "HTTP/1.1",
39 _conf
= testconfig
.config
.get('supervisor', None)
41 for k
, v
in _conf
.items():
42 if isinstance(v
, basestring
):
48 conf
= _testconfig
.copy()
53 class Supervisor(object):
54 """Base class for modeling and controlling servers during testing."""
56 def __init__(self
, **kwargs
):
57 for k
, v
in kwargs
.items():
59 setattr(self
, k
, int(v
))
63 log_to_stderr
= lambda msg
, level
: sys
.stderr
.write(msg
+ os
.linesep
)
65 class LocalSupervisor(Supervisor
):
66 """Base class for modeling/controlling servers which run in the same process.
68 When the server side runs in a different process, start/stop can dump all
69 state between each test module easily. When the server side runs in the
70 same process as the client, however, we have to do a bit more work to ensure
71 config and mounted apps are reset between tests.
77 def __init__(self
, **kwargs
):
78 for k
, v
in kwargs
.items():
81 cherrypy
.server
.httpserver
= self
.httpserver_class
83 engine
= cherrypy
.engine
84 if hasattr(engine
, "signal_handler"):
85 engine
.signal_handler
.subscribe()
86 if hasattr(engine
, "console_control_handler"):
87 engine
.console_control_handler
.subscribe()
88 #engine.subscribe('log', log_to_stderr)
90 def start(self
, modulename
=None):
91 """Load and start the HTTP server."""
93 # Unhook httpserver so cherrypy.server.start() creates a new
94 # one (with config from setup_server, if declared).
95 cherrypy
.server
.httpserver
= None
97 cherrypy
.engine
.start()
102 """Tell the server about any apps which the setup functions mounted."""
106 td
= getattr(self
, 'teardown', None)
110 cherrypy
.engine
.exit()
112 for name
, server
in copyitems(getattr(cherrypy
, 'servers', {})):
114 del cherrypy
.servers
[name
]
117 class NativeServerSupervisor(LocalSupervisor
):
118 """Server supervisor for the builtin HTTP server."""
120 httpserver_class
= "cherrypy._cpnative_server.CPHTTPServer"
125 return "Builtin HTTP Server on %s:%s" % (self
.host
, self
.port
)
128 class LocalWSGISupervisor(LocalSupervisor
):
129 """Server supervisor for the builtin WSGI server."""
131 httpserver_class
= "cherrypy._cpwsgi_server.CPWSGIServer"
136 return "Builtin WSGI Server on %s:%s" % (self
.host
, self
.port
)
139 """Hook a new WSGI app into the origin server."""
140 cherrypy
.server
.httpserver
.wsgi_app
= self
.get_app()
142 def get_app(self
, app
=None):
143 """Obtain a new (decorated) WSGI app to hook into the origin server."""
151 warnings
.warn("Error importing wsgiconq. pyconquer will not run.")
153 app
= wsgiconq
.WSGILogger(app
, c_calls
=True)
157 from wsgiref
import validate
159 warnings
.warn("Error importing wsgiref. The validator will not run.")
161 #wraps the app in the validator
162 app
= validate
.validator(app
)
167 def get_cpmodpy_supervisor(**options
):
168 from cherrypy
.test
import modpy
169 sup
= modpy
.ModPythonSupervisor(**options
)
170 sup
.template
= modpy
.conf_cpmodpy
173 def get_modpygw_supervisor(**options
):
174 from cherrypy
.test
import modpy
175 sup
= modpy
.ModPythonSupervisor(**options
)
176 sup
.template
= modpy
.conf_modpython_gateway
177 sup
.using_wsgi
= True
180 def get_modwsgi_supervisor(**options
):
181 from cherrypy
.test
import modwsgi
182 return modwsgi
.ModWSGISupervisor(**options
)
184 def get_modfcgid_supervisor(**options
):
185 from cherrypy
.test
import modfcgid
186 return modfcgid
.ModFCGISupervisor(**options
)
188 def get_modfastcgi_supervisor(**options
):
189 from cherrypy
.test
import modfastcgi
190 return modfastcgi
.ModFCGISupervisor(**options
)
192 def get_wsgi_u_supervisor(**options
):
193 cherrypy
.server
.wsgi_version
= ('u', 0)
194 return LocalWSGISupervisor(**options
)
197 class CPWebCase(webtest
.WebCase
):
202 available_servers
= {'wsgi': LocalWSGISupervisor
,
203 'wsgi_u': get_wsgi_u_supervisor
,
204 'native': NativeServerSupervisor
,
205 'cpmodpy': get_cpmodpy_supervisor
,
206 'modpygw': get_modpygw_supervisor
,
207 'modwsgi': get_modwsgi_supervisor
,
208 'modfcgid': get_modfcgid_supervisor
,
209 'modfastcgi': get_modfastcgi_supervisor
,
211 default_server
= "wsgi"
213 def _setup_server(cls
, supervisor
, conf
):
214 v
= sys
.version
.split()[0]
215 log
.info("Python version used to run this test script: %s" % v
)
216 log
.info("CherryPy version: %s" % cherrypy
.__version
__)
217 if supervisor
.scheme
== "https":
221 log
.info("HTTP server version: %s%s" % (supervisor
.protocol
, ssl
))
222 log
.info("PID: %s" % os
.getpid())
224 cherrypy
.server
.using_apache
= supervisor
.using_apache
225 cherrypy
.server
.using_wsgi
= supervisor
.using_wsgi
227 if sys
.platform
[:4] == 'java':
228 cherrypy
.config
.update({'server.nodelay': False})
230 if isinstance(conf
, basestring
):
231 parser
= cherrypy
.lib
.reprconf
.Parser()
232 conf
= parser
.dict_from_file(conf
).get('global', {})
235 baseconf
= conf
.copy()
236 baseconf
.update({'server.socket_host': supervisor
.host
,
237 'server.socket_port': supervisor
.port
,
238 'server.protocol_version': supervisor
.protocol
,
239 'environment': "test_suite",
241 if supervisor
.scheme
== "https":
242 #baseconf['server.ssl_module'] = 'builtin'
243 baseconf
['server.ssl_certificate'] = serverpem
244 baseconf
['server.ssl_private_key'] = serverpem
246 # helper must be imported lazily so the coverage tool
247 # can run against module-level statements within cherrypy.
248 # Also, we have to do "from cherrypy.test import helper",
249 # exactly like each test module does, because a relative import
250 # would stick a second instance of webtest in sys.modules,
251 # and we wouldn't be able to globally override the port anymore.
252 if supervisor
.scheme
== "https":
253 webtest
.WebCase
.HTTP_CONN
= HTTPSConnection
255 _setup_server
= classmethod(_setup_server
)
257 def setup_class(cls
):
260 conf
= get_tst_config()
261 supervisor_factory
= cls
.available_servers
.get(conf
.get('server', 'wsgi'))
262 if supervisor_factory
is None:
263 raise RuntimeError('Unknown server in config: %s' % conf
['server'])
264 supervisor
= supervisor_factory(**conf
)
266 #Copied from "run_test_suite"
267 cherrypy
.config
.reset()
268 baseconf
= cls
._setup
_server
(supervisor
, conf
)
269 cherrypy
.config
.update(baseconf
)
272 if hasattr(cls
, 'setup_server'):
273 # Clear the cherrypy tree and clear the wsgi server so that
274 # it can be updated with the new root
275 cherrypy
.tree
= cherrypy
._cptree
.Tree()
276 cherrypy
.server
.httpserver
= None
278 supervisor
.start(cls
.__module
__)
280 cls
.supervisor
= supervisor
281 setup_class
= classmethod(setup_class
)
283 def teardown_class(cls
):
285 if hasattr(cls
, 'setup_server'):
286 cls
.supervisor
.stop()
287 teardown_class
= classmethod(teardown_class
)
290 return self
.script_name
.rstrip("/")
293 if ((self
.scheme
== "http" and self
.PORT
== 80) or
294 (self
.scheme
== "https" and self
.PORT
== 443)):
297 port
= ":%s" % self
.PORT
299 return "%s://%s%s%s" % (self
.scheme
, self
.HOST
, port
,
300 self
.script_name
.rstrip("/"))
305 def getPage(self
, url
, headers
=None, method
="GET", body
=None, protocol
=None):
306 """Open the url. Return status, headers, body."""
308 url
= httputil
.urljoin(self
.script_name
, url
)
309 return webtest
.WebCase
.getPage(self
, url
, headers
, method
, body
, protocol
)
311 def skip(self
, msg
='skipped '):
312 raise nose
.SkipTest(msg
)
314 def assertErrorPage(self
, status
, message
=None, pattern
=''):
315 """Compare the response body with a built in error page.
317 The function will optionally look for the regexp pattern,
318 within the exception embedded in the error page."""
320 # This will never contain a traceback
321 page
= cherrypy
._cperror
.get_error_page(status
, message
=message
)
323 # First, test the response body without checking the traceback.
324 # Stick a match-all group (.*) in to grab the traceback.
327 epage
= epage
.replace(esc('<pre id="traceback"></pre>'),
328 esc('<pre id="traceback">') + '(.*)' + esc('</pre>'))
329 m
= re
.match(ntob(epage
, self
.encoding
), self
.body
, re
.DOTALL
)
331 self
._handlewebError
('Error page does not match; expected:\n' + page
)
334 # Now test the pattern against the traceback
336 # Special-case None to mean that there should be *no* traceback.
338 self
._handlewebError
('Error page contains traceback')
341 not re
.search(ntob(re
.escape(pattern
), self
.encoding
),
343 msg
= 'Error page does not contain %s in traceback'
344 self
._handlewebError
(msg
% repr(pattern
))
348 def assertEqualDates(self
, dt1
, dt2
, seconds
=None):
349 """Assert abs(dt1 - dt2) is within Y seconds."""
351 seconds
= self
.date_tolerance
357 if not diff
< datetime
.timedelta(seconds
=seconds
):
358 raise AssertionError('%r and %r are not within %r seconds.' %
363 """Set up the WebCase classes to match the server's socket settings."""
364 webtest
.WebCase
.PORT
= cherrypy
.server
.socket_port
365 webtest
.WebCase
.HOST
= cherrypy
.server
.socket_host
366 if cherrypy
.server
.ssl_certificate
:
367 CPWebCase
.scheme
= 'https'
369 # --------------------------- Spawning helpers --------------------------- #
372 class CPProcess(object):
374 pid_file
= os
.path
.join(thisdir
, 'test.pid')
375 config_file
= os
.path
.join(thisdir
, 'test.conf')
376 config_template
= """[global]
377 server.socket_host: '%(host)s'
378 server.socket_port: %(port)s
381 log.error_file: r'%(error_log)s'
382 log.access_file: r'%(access_log)s'
386 error_log
= os
.path
.join(thisdir
, 'test.error.log')
387 access_log
= os
.path
.join(thisdir
, 'test.access.log')
389 def __init__(self
, wait
=False, daemonize
=False, ssl
=False, socket_host
=None, socket_port
=None):
391 self
.daemonize
= daemonize
393 self
.host
= socket_host
or cherrypy
.server
.socket_host
394 self
.port
= socket_port
or cherrypy
.server
.socket_port
396 def write_conf(self
, extra
=""):
398 serverpem
= os
.path
.join(thisdir
, 'test.pem')
400 server.ssl_certificate: r'%s'
401 server.ssl_private_key: r'%s'
402 """ % (serverpem
, serverpem
)
406 conf
= self
.config_template
% {
409 'error_log': self
.error_log
,
410 'access_log': self
.access_log
,
414 f
= open(self
.config_file
, 'wb')
415 f
.write(ntob(conf
, 'utf-8'))
418 def start(self
, imports
=None):
419 """Start cherryd in a subprocess."""
420 cherrypy
._cpserver
.wait_for_free_port(self
.host
, self
.port
)
422 args
= [sys
.executable
, os
.path
.join(thisdir
, '..', 'cherryd'),
423 '-c', self
.config_file
, '-p', self
.pid_file
]
425 if not isinstance(imports
, (list, tuple)):
435 env
= os
.environ
.copy()
436 # Make sure we import the cherrypy package in which this module is defined.
437 grandparentdir
= os
.path
.abspath(os
.path
.join(thisdir
, '..', '..'))
438 if env
.get('PYTHONPATH', ''):
439 env
['PYTHONPATH'] = os
.pathsep
.join((grandparentdir
, env
['PYTHONPATH']))
441 env
['PYTHONPATH'] = grandparentdir
443 self
.exit_code
= os
.spawnve(os
.P_WAIT
, sys
.executable
, args
, env
)
445 os
.spawnve(os
.P_NOWAIT
, sys
.executable
, args
, env
)
446 cherrypy
._cpserver
.wait_for_occupied_port(self
.host
, self
.port
)
448 # Give the engine a wee bit more time to finish STARTING
455 return int(open(self
.pid_file
, 'rb').read())
458 """Wait for the process to exit."""
463 except AttributeError:
468 # Assume the subprocess deleted the pidfile on shutdown.
473 x
= sys
.exc_info()[1]
474 if x
.args
!= (10, 'No child processes'):