[Checkins] SVN: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ Merge changes from the jinty-webtest2 branch: WebTest integration and test refactoring.
Brian Sutherland
jinty at web.de
Sun Jan 30 07:36:37 EST 2011
Log message for revision 120003:
Merge changes from the jinty-webtest2 branch: WebTest integration and test refactoring.
Changed:
U zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/README.txt
U zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/cookies.txt
U zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/fixed-bugs.txt
U zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/__init__.py
U zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/ftesting.zcml
A zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/wsgitestapp.py
U zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/testing.py
U zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/helper.py
U zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/test_doctests.py
A zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/webtest.py
A zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/wsgi.txt
A zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/zope-publisher.txt
-=-
Modified: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/README.txt
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/README.txt 2011-01-30 12:20:40 UTC (rev 120002)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/README.txt 2011-01-30 12:36:36 UTC (rev 120003)
@@ -11,8 +11,8 @@
The ``zope.testbrowser.browser`` module exposes a ``Browser`` class that
simulates a web browser similar to Mozilla Firefox or IE.
- >>> from zope.testbrowser.browser import Browser
- >>> browser = Browser()
+ >>> from zope.testbrowser.browser import Browser as RealBrowser
+ >>> browser = RealBrowser()
This version of the browser object can be used to access any web site just as
you would do using a normal web browser.
@@ -24,8 +24,8 @@
`wsgi_intercept`_ and can be used to do functional testing of WSGI
applications, it can be imported from ``zope.testbrowser.wsgi``:
- >>> from zope.testbrowser.wsgi import Browser
- >>> browser = Browser()
+ >>> from zope.testbrowser.wsgi import Browser as WSGIBrowser
+ >>> browser = WSGIBrowser()
.. _`wsgi_intercept`: http://pypi.python.org/pypi/wsgi_intercept
@@ -50,18 +50,28 @@
Zope 3 Test Browser
~~~~~~~~~~~~~~~~~~~
+WSGI applications can also be tested directly when wrapped by WebTest:
+
+ >>> from zope.testbrowser.webtest import Browser as WSGIBrowser
+ >>> from wsgiref.simple_server import demo_app
+ >>> browser = WSGIBrowser(demo_app, url='http://localhost/')
+ >>> print browser.contents
+ Hello world!
+ ...
+
There is also a special version of the ``Browser`` class used to do functional
testing of Zope 3 applications, it can be imported from
``zope.testbrowser.testing``:
- >>> from zope.testbrowser.testing import Browser
- >>> browser = Browser()
+ >>> from zope.testbrowser.testing import Browser as TestingBrowser
+ >>> browser = TestingBrowser()
Bowser Usage
------------
-All browsers are used the same way. An initial page to load can be passed
-to the ``Browser`` constructor:
+To allow this test to be run against different implementations, we will use a
+Browser object from the test globals. An initial page to load can be passed to
+the ``Browser`` constructor:
>>> browser = Browser('http://localhost/@@/testbrowser/simple.html')
>>> browser.url
@@ -1245,20 +1255,16 @@
method that allows a request body to be supplied. This method is particularly
helpful when testing Ajax methods.
-Let's visit a page that echos it's request:
+Let's visit a page that echos some interesting values from it's request:
>>> browser.open('http://localhost/@@echo.html')
- >>> print browser.contents,
- HTTP_USER_AGENT: Python-urllib/2.4
- HTTP_CONNECTION: close
- HTTP_COOKIE:
- REMOTE_ADDR: 127.0.0.1
+ >>> print browser.contents
HTTP_ACCEPT_LANGUAGE: en-US
- REQUEST_METHOD: GET
+ HTTP_CONNECTION: close
HTTP_HOST: localhost
+ HTTP_USER_AGENT: Python-urllib/2.4
PATH_INFO: /@@echo.html
- SERVER_PROTOCOL: HTTP/1.1
- QUERY_STRING:
+ REQUEST_METHOD: GET
Body: ''
Now, we'll try a post. The post method takes a URL, a data string,
@@ -1266,42 +1272,34 @@
a URL-encoded query string is assumed:
>>> browser.post('http://localhost/@@echo.html', 'x=1&y=2')
- >>> print browser.contents,
+ >>> print browser.contents
CONTENT_LENGTH: 7
- HTTP_USER_AGENT: Python-urllib/2.4
- HTTP_CONNECTION: close
- HTTP_COOKIE:
- REMOTE_ADDR: 127.0.0.1
+ CONTENT_TYPE: application/x-www-form-urlencoded
HTTP_ACCEPT_LANGUAGE: en-US
- y: 2
- REQUEST_METHOD: POST
+ HTTP_CONNECTION: close
HTTP_HOST: localhost
+ HTTP_USER_AGENT: Python-urllib/2.4
PATH_INFO: /@@echo.html
- CONTENT_TYPE: application/x-www-form-urlencoded
- SERVER_PROTOCOL: HTTP/1.1
- QUERY_STRING:
+ REQUEST_METHOD: POST
x: 1
+ y: 2
Body: ''
-
The body is empty because it is consumed to get form data.
We can pass a content-type explicitly:
>>> browser.post('http://localhost/@@echo.html',
... '{"x":1,"y":2}', 'application/x-javascript')
- >>> print browser.contents,
+ >>> print browser.contents
CONTENT_LENGTH: 13
- HTTP_USER_AGENT: Python-urllib/2.4
- HTTP_CONNECTION: close
- HTTP_COOKIE:
- REMOTE_ADDR: 127.0.0.1
+ CONTENT_TYPE: application/x-javascript
HTTP_ACCEPT_LANGUAGE: en-US
- REQUEST_METHOD: POST
+ HTTP_CONNECTION: close
HTTP_HOST: localhost
+ HTTP_USER_AGENT: Python-urllib/2.4
PATH_INFO: /@@echo.html
- CONTENT_TYPE: application/x-javascript
- SERVER_PROTOCOL: HTTP/1.1
+ REQUEST_METHOD: POST
Body: '{"x":1,"y":2}'
Here, the body is left in place because it isn't form data.
Modified: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/cookies.txt
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/cookies.txt 2011-01-30 12:20:40 UTC (rev 120002)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/cookies.txt 2011-01-30 12:36:36 UTC (rev 120003)
@@ -9,7 +9,6 @@
setting, and deleting the cookies that the browser is remembering for the
current url, or for an explicitly provided URL.
- >>> from zope.testbrowser.testing import Browser
>>> browser = Browser()
Initially the browser does not point to a URL, and the cookies cannot be used.
@@ -221,11 +220,14 @@
Max-age is converted to an "expires" value.
+ XXX: When using zope.app.testing.functional, silly billy ends up being
+ silly%20billy, webtest gives us a ' ', which is right? -jinty
+
>>> browser.open(
... 'http://localhost/set_cookie.html?name=max&value=min&'
... 'max-age=3000&&comment=silly+billy')
>>> pprint.pprint(browser.cookies.getinfo('max')) # doctest: +ELLIPSIS
- {'comment': 'silly%20billy',
+ {'comment': 'silly billy',
'commenturl': None,
'domain': 'localhost.local',
'expires': datetime.datetime(..., tzinfo=<UTC>),
@@ -254,7 +256,7 @@
'port': None,
'secure': False,
'value': 'bar'},
- {'comment': 'silly%20billy',
+ {'comment': 'silly billy',
'commenturl': None,
'domain': 'localhost.local',
'expires': datetime.datetime(..., tzinfo=<UTC>),
@@ -312,11 +314,6 @@
>>> sorted(browser.cookies.keys())
['foo', 'max', 'sha', 'va', 'wow']
- >>> import zope.site.folder
- >>> getRootFolder()['inner'] = zope.site.folder.Folder()
- >>> getRootFolder()['inner']['path'] = zope.site.folder.Folder()
- >>> import transaction
- >>> transaction.commit()
>>> browser.open('http://localhost/inner/get_cookie.html')
>>> print browser.contents # has gewgaw
foo: bar
Modified: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/fixed-bugs.txt
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/fixed-bugs.txt 2011-01-30 12:20:40 UTC (rev 120002)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/fixed-bugs.txt 2011-01-30 12:36:36 UTC (rev 120003)
@@ -24,12 +24,10 @@
The tests below failed before the change was put in place.
- >>> from zope.testbrowser.testing import Browser
>>> browser = Browser()
>>> browser.addHeader('Cookie', 'test')
>>> browser.open(u'http://localhost/@@/testbrowser/simple.html')
- >>> from zope.testbrowser.testing import Browser
>>> browser = Browser()
>>> browser.addHeader(u'Cookie', 'test')
>>> browser.open('http://localhost/@@/testbrowser/simple.html')
Modified: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/__init__.py
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/__init__.py 2011-01-30 12:20:40 UTC (rev 120002)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/__init__.py 2011-01-30 12:36:36 UTC (rev 120003)
@@ -18,36 +18,32 @@
self.context = context
self.request = request
+_interesting_environ = ('CONTENT_LENGTH',
+ 'CONTENT_TYPE',
+ 'HTTP_ACCEPT_LANGUAGE',
+ 'HTTP_CONNECTION',
+ 'HTTP_HOST',
+ 'HTTP_USER_AGENT',
+ 'PATH_INFO',
+ 'REQUEST_METHOD')
+
class Echo(View):
- """Simply echo the contents of the request"""
+ """Simply echo the interesting parts of the request"""
def __call__(self):
- return ('\n'.join('%s: %s' % x for x in self.request.items()) +
- '\nBody: %r' % self.request.bodyStream.read())
+ items = []
+ for k in _interesting_environ:
+ v = self.request.get(k, None)
+ if v is None:
+ continue
+ items.append('%s: %s' % (k, v))
+ items.extend('%s: %s' % x for x in sorted(self.request.form.items()))
+ items.append('Body: %r' % self.request.bodyStream.read())
+ return '\n'.join(items)
-class GetCookie(View):
- """Gets cookie value"""
- def __call__(self):
- return '\n'.join(
- ('%s: %s' % (k, v)) for k, v in sorted(
- self.request.cookies.items()))
+class EchoOne(View):
+ """Echo one variable from the request"""
-class SetCookie(View):
- """Sets cookie value"""
-
def __call__(self):
- self.request.response.setCookie(
- **dict((str(k), str(v)) for k, v in self.request.form.items()))
-
-
-class SetStatus(View):
- """Sets HTTP status"""
-
- def __call__(self):
- status = self.request.get('status')
- if status:
- self.request.response.setStatus(int(status))
- return 'Just set a status of %s' % status
- else:
- return 'Everything fine'
+ return repr(self.request.get(self.request.form['var']))
Modified: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/ftesting.zcml
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/ftesting.zcml 2011-01-30 12:20:40 UTC (rev 120002)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/ftesting.zcml 2011-01-30 12:36:36 UTC (rev 120003)
@@ -8,15 +8,12 @@
<!-- This file is used for functional testing setup -->
<include package="zope.browserpage" file="meta.zcml" />
- <include package="zope.browserresource" file="meta.zcml" />
<include package="zope.component" file="meta.zcml" />
<include package="zope.security" file="meta.zcml" />
<include package="zope.app.publication" file="meta.zcml" />
- <include package="zope.browserresource" />
<include package="zope.container" />
<include package="zope.principalregistry" />
- <include package="zope.ptresource" />
<include package="zope.publisher" />
<include package="zope.security" />
<include package="zope.site" />
@@ -36,28 +33,10 @@
/>
<browser:page
- name="set_cookie.html"
+ name="echo_one.html"
for="*"
- class=".ftests.SetCookie"
+ class=".ftests.EchoOne"
permission="zope.Public"
/>
- <browser:page
- name="get_cookie.html"
- for="*"
- class=".ftests.GetCookie"
- permission="zope.Public"
- />
-
- <browser:page
- name="set_status.html"
- for="*"
- class=".ftests.SetStatus"
- permission="zope.Public"
- />
-
- <browser:resourceDirectory
- name="testbrowser"
- directory="ftests" />
-
</configure>
Copied: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/wsgitestapp.py (from rev 120000, zope.testbrowser/branches/jinty-webtest2/src/zope/testbrowser/ftests/wsgitestapp.py)
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/wsgitestapp.py (rev 0)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/ftests/wsgitestapp.py 2011-01-30 12:36:36 UTC (rev 120003)
@@ -0,0 +1,144 @@
+##############################################################################
+#
+# Copyright (c) 2010 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""A minimal WSGI application used as a test fixture."""
+
+import os
+import mimetypes
+from datetime import datetime
+
+from webob import Request, Response
+from zope.pagetemplate.pagetemplatefile import PageTemplateFile
+
+from zope.testbrowser import ftests
+
+class NotFound(Exception):
+
+ def __init__(self, ob, name):
+ self.ob = ob
+ self.name = name
+
+ def __str__(self):
+ return 'Object: %s, name: %r' % (self.ob, self.name)
+
+
+class ZopeRequestAdapter(object):
+ """Adapt a webob request into enough of a zope.publisher request for the tests to pass"""
+
+ def __init__(self, request, response=None):
+ self._request = request
+ self._response = response
+
+ @property
+ def form(self):
+ return self._request.params
+
+ def __getitem__(self, name):
+ return self._request.params[name]
+
+_HERE = os.path.dirname(__file__)
+
+class MyPageTemplateFile(PageTemplateFile):
+
+ def pt_getContext(self, args, *extra_args, **kw):
+ request = args[0]
+ namespace = super(MyPageTemplateFile, self).pt_getContext(args, *extra_args, **kw)
+ namespace['request'] = request
+ return namespace
+
+class WSGITestApplication(object):
+
+ def __call__(self, environ, start_response):
+ req = Request(environ)
+ handler = {'/set_status.html': set_status,
+ '/@@echo.html': echo,
+ '/echo_one.html': echo_one,
+ '/set_cookie.html': set_cookie,
+ '/get_cookie.html': get_cookie,
+ '/inner/set_cookie.html': set_cookie,
+ '/inner/get_cookie.html': get_cookie,
+ '/inner/path/set_cookie.html': set_cookie,
+ '/inner/path/get_cookie.html': get_cookie,
+ }.get(req.path_info)
+ if handler is None and req.path_info.startswith('/@@/testbrowser/'):
+ handler = handle_resource
+ if handler is None:
+ handler = handle_notfound
+ try:
+ resp = handler(req)
+ except Exception, exc:
+ if not environ.get('wsgi.handleErrors', True):
+ raise
+ resp = Response()
+ resp.status = {NotFound: 404}.get(type(exc), 500)
+ resp.headers.add('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)')
+ return resp(environ, start_response)
+
+def handle_notfound(req):
+ raise NotFound('<WSGI application>', unicode(req.path_info[1:]))
+
+def handle_resource(req):
+ filename = req.path_info.split('/')[-1]
+ type, _ = mimetypes.guess_type(filename)
+ path = os.path.join(_HERE, filename)
+ if type == 'text/html':
+ pt = MyPageTemplateFile(path)
+ zreq = ZopeRequestAdapter(req)
+ contents = pt(zreq)
+ else:
+ contents = open(path, 'r').read()
+ return Response(contents, content_type=type)
+
+def get_cookie(req):
+ cookies = ['%s: %s' % i for i in sorted(req.cookies.items())]
+ return Response('\n'.join(cookies))
+
+def set_cookie(req):
+ cookie_parms = {'path': None}
+ cookie_parms.update(dict((str(k), str(v)) for k, v in req.params.items()))
+ name = cookie_parms.pop('name')
+ value = cookie_parms.pop('value')
+ if 'max-age' in cookie_parms:
+ cookie_parms['max_age'] = int(cookie_parms.pop('max-age'))
+ if 'expires' in cookie_parms:
+ cookie_parms['expires'] = datetime.strptime(cookie_parms.pop('expires'), '%a, %d %b %Y %H:%M:%S GMT')
+ resp = Response()
+ resp.set_cookie(name, value, **cookie_parms)
+ return resp
+
+def echo(req):
+ items = []
+ for k in ftests._interesting_environ:
+ v = req.environ.get(k, None)
+ if v is None:
+ continue
+ items.append('%s: %s' % (k, v))
+ items.extend('%s: %s' % x for x in sorted(req.params.items()))
+ if req.method == 'POST' and req.content_type == 'application/x-www-form-urlencoded':
+ body = ''
+ else:
+ body = req.body
+ items.append('Body: %r' % body)
+ return Response('\n'.join(items))
+
+def echo_one(req):
+ resp = repr(req.environ.get(req.params['var']))
+ return Response(resp)
+
+def set_status(req):
+ status = req.params.get('status')
+ if status:
+ resp = Response('Just set a status of %s' % status)
+ resp.status = int(status)
+ return resp
+ return Response('Everything fine')
Modified: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/testing.py
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/testing.py 2011-01-30 12:20:40 UTC (rev 120002)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/testing.py 2011-01-30 12:36:36 UTC (rev 120003)
@@ -20,7 +20,88 @@
import sys
import zope.testbrowser.browser
+#
+# Base classes sometimes useful to implement browsers
+#
+class Response(object):
+ """``mechanize`` compatible response object."""
+
+ def __init__(self, content, headers, status, reason):
+ self.content = content
+ self.status = status
+ self.reason = reason
+ self.msg = httplib.HTTPMessage(cStringIO.StringIO(headers), 0)
+ self.content_as_file = cStringIO.StringIO(self.content)
+
+ def read(self, amt=None):
+ return self.content_as_file.read(amt)
+
+ def close(self):
+ """To overcome changes in mechanize and socket in python2.5"""
+ pass
+
+class HTTPHandler(mechanize.HTTPHandler):
+
+ def _connect(self, *args, **kw):
+ raise NotImplementedError("implement")
+
+ def http_request(self, req):
+ # look at data and set content type
+ if req.has_data():
+ data = req.get_data()
+ if isinstance(data, dict):
+ req.add_data(data['body'])
+ req.add_unredirected_header('Content-type',
+ data['content-type'])
+ return mechanize.HTTPHandler.do_request_(self, req)
+
+ https_request = http_request
+
+ def http_open(self, req):
+ """Open an HTTP connection having a ``mechanize`` request."""
+ # Here we connect to the publisher.
+ if sys.version_info > (2, 6) and not hasattr(req, 'timeout'):
+ # Workaround mechanize incompatibility with Python
+ # 2.6. See: LP #280334
+ req.timeout = socket._GLOBAL_DEFAULT_TIMEOUT
+ return self.do_open(self._connect, req)
+
+ https_open = http_open
+
+class MechanizeBrowser(mechanize.Browser):
+ """Special ``mechanize`` browser using the Zope Publisher HTTP handler."""
+
+ default_schemes = ['http']
+ default_others = ['_http_error', '_http_default_error']
+ default_features = ['_redirect', '_cookies', '_referer', '_refresh',
+ '_equiv', '_basicauth', '_digestauth']
+
+
+ def __init__(self, *args, **kws):
+ inherited_handlers = ['_unknown', '_http_error',
+ '_http_default_error', '_basicauth',
+ '_digestauth', '_redirect', '_cookies', '_referer',
+ '_refresh', '_equiv', '_gzip']
+
+ self.handler_classes = {"http": self._http_handler}
+ for name in inherited_handlers:
+ self.handler_classes[name] = mechanize.Browser.handler_classes[name]
+
+ kws['request_class'] = kws.get('request_class',
+ mechanize._request.Request)
+
+ mechanize.Browser.__init__(self, *args, **kws)
+
+ def _http_handler(self, *args, **kw):
+ return NotImplementedError("Try return a sub-class of PublisherHTTPHandler here")
+
+#
+# Zope Publisher Browser implementation
+#
+
+PublisherResponse = Response # BBB
+
class PublisherConnection(object):
"""A ``mechanize`` compatible connection object."""
@@ -91,78 +172,23 @@
headers.insert(0, ('Status', real_response.getStatusString()))
headers = '\r\n'.join('%s: %s' % h for h in headers)
content = real_response.consumeBody()
- return PublisherResponse(content, headers, status, reason)
+ return Response(content, headers, status, reason)
-class PublisherResponse(object):
- """``mechanize`` compatible response object."""
-
- def __init__(self, content, headers, status, reason):
- self.content = content
- self.status = status
- self.reason = reason
- self.msg = httplib.HTTPMessage(cStringIO.StringIO(headers), 0)
- self.content_as_file = cStringIO.StringIO(self.content)
-
- def read(self, amt=None):
- return self.content_as_file.read(amt)
-
- def close(self):
- """To overcome changes in mechanize and socket in python2.5"""
- pass
-
-
-class PublisherHTTPHandler(mechanize.HTTPHandler):
+class PublisherHTTPHandler(HTTPHandler):
"""Special HTTP handler to use the Zope Publisher."""
- def http_request(self, req):
- # look at data and set content type
- if req.has_data():
- data = req.get_data()
- if isinstance(data, dict):
- req.add_data(data['body'])
- req.add_unredirected_header('Content-type',
- data['content-type'])
- return mechanize.HTTPHandler.do_request_(self, req)
+ def _connect(self, *args, **kw):
+ return PublisherConnection(*args, **kw)
- https_request = http_request
- def http_open(self, req):
- """Open an HTTP connection having a ``mechanize`` request."""
- # Here we connect to the publisher.
- if sys.version_info > (2, 6) and not hasattr(req, 'timeout'):
- # Workaround mechanize incompatibility with Python
- # 2.6. See: LP #280334
- req.timeout = socket._GLOBAL_DEFAULT_TIMEOUT
- return self.do_open(PublisherConnection, req)
-
- https_open = http_open
-
-
-class PublisherMechanizeBrowser(mechanize.Browser):
+class PublisherMechanizeBrowser(MechanizeBrowser):
"""Special ``mechanize`` browser using the Zope Publisher HTTP handler."""
- default_schemes = ['http']
- default_others = ['_http_error', '_http_default_error']
- default_features = ['_redirect', '_cookies', '_referer', '_refresh',
- '_equiv', '_basicauth', '_digestauth']
+ def _http_handler(self, *args, **kw):
+ return PublisherHTTPHandler(*args, **kw)
- def __init__(self, *args, **kws):
- inherited_handlers = ['_unknown', '_http_error',
- '_http_default_error', '_basicauth',
- '_digestauth', '_redirect', '_cookies', '_referer',
- '_refresh', '_equiv', '_gzip']
- self.handler_classes = {"http": PublisherHTTPHandler}
- for name in inherited_handlers:
- self.handler_classes[name] = mechanize.Browser.handler_classes[name]
-
- kws['request_class'] = kws.get('request_class',
- mechanize._request.Request)
-
- mechanize.Browser.__init__(self, *args, **kws)
-
-
class Browser(zope.testbrowser.browser.Browser):
"""A Zope `testbrowser` Browser that uses the Zope Publisher."""
Modified: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/helper.py
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/helper.py 2011-01-30 12:20:40 UTC (rev 120002)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/helper.py 2011-01-30 12:36:36 UTC (rev 120003)
@@ -37,4 +37,8 @@
(re.compile(r'Host: localhost'), 'Connection: close'),
(re.compile(r'Content-Type: '), 'Content-type: '),
(re.compile(r'Content-Disposition: '), 'Content-disposition: '),
+ (re.compile(r'; charset=UTF-8'), ';charset=utf-8'),
+ # webtest seems to expire cookies one second before the date set in set_cookie
+ (re.compile(r"'expires': datetime.datetime\(2029, 12, 31, 23, 59, 59, tzinfo=<UTC>\),"), "'expires': datetime.datetime(2030, 1, 1, 0, 0, tzinfo=<UTC>),"),
+ (re.compile(r"Object: <WSGI application>,"), "Object: <zope.site.folder.Folder object at ...>,"),
])
Modified: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/test_doctests.py
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/test_doctests.py 2011-01-30 12:20:40 UTC (rev 120002)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/tests/test_doctests.py 2011-01-30 12:36:36 UTC (rev 120003)
@@ -16,26 +16,40 @@
import unittest
import zope.app.testing.functional
+import zope.testbrowser.ftests.wsgitestapp
+import zope.testbrowser.webtest
+
TestBrowserLayer = zope.app.testing.functional.ZCMLLayer(
pkg_resources.resource_filename(
'zope.testbrowser', 'ftests/ftesting.zcml'),
__name__, 'TestBrowserLayer', allow_teardown=True)
+def make_browser(*args, **kw):
+ app = zope.testbrowser.ftests.wsgitestapp.WSGITestApplication()
+ return zope.testbrowser.webtest.Browser(app, *args, **kw)
def test_suite():
flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
- suite = zope.app.testing.functional.FunctionalDocFileSuite(
+
+ zope_publisher = zope.app.testing.functional.FunctionalDocFileSuite('zope-publisher.txt',
+ optionflags=flags,
+ package='zope.testbrowser',
+ checker=zope.testbrowser.tests.helper.checker)
+ zope_publisher.layer = TestBrowserLayer
+
+ suite = doctest.DocFileSuite(
'README.txt',
'cookies.txt',
+ 'wsgi.txt',
'fixed-bugs.txt',
optionflags=flags,
+ globs=dict(Browser=make_browser),
checker=zope.testbrowser.tests.helper.checker,
package='zope.testbrowser')
- suite.layer = TestBrowserLayer
wire = doctest.DocFileSuite('over_the_wire.txt', optionflags=flags,
package='zope.testbrowser')
wire.level = 2
- return unittest.TestSuite((suite, wire))
+ return unittest.TestSuite((zope_publisher, suite, wire))
Copied: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/webtest.py (from rev 120000, zope.testbrowser/branches/jinty-webtest2/src/zope/testbrowser/webtest.py)
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/webtest.py (rev 0)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/webtest.py 2011-01-30 12:36:36 UTC (rev 120003)
@@ -0,0 +1,147 @@
+##############################################################################
+#
+# Copyright (c) 2010 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""WSGI-specific testing code
+"""
+
+from __future__ import absolute_import
+
+import cStringIO
+import Cookie
+import httplib
+import socket
+import sys
+
+import mechanize
+from webtest import TestApp
+
+import zope.testbrowser.browser
+import zope.testbrowser.testing
+
+class WSGIConnection(object):
+ """A ``mechanize`` compatible connection object."""
+
+ def __init__(self, test_app, host, timeout=None):
+ self._test_app = TestApp(test_app)
+ self.host = host
+
+ def set_debuglevel(self, level):
+ pass
+
+ def _quote(self, url):
+ # XXX: is this necessary with WebTest? Was cargeo-culted from the
+ # Zope Publisher Connection
+ return url.replace(' ', '%20')
+
+ def request(self, method, url, body=None, headers=None):
+ """Send a request to the publisher.
+
+ The response will be stored in ``self.response``.
+ """
+ if body is None:
+ body = ''
+
+ if url == '':
+ url = '/'
+
+ url = self._quote(url)
+
+ # Extract the handle_error option header
+ if sys.version_info >= (2,5):
+ handle_errors_key = 'X-Zope-Handle-Errors'
+ else:
+ handle_errors_key = 'X-zope-handle-errors'
+ handle_errors_header = headers.get(handle_errors_key, True)
+ if handle_errors_key in headers:
+ del headers[handle_errors_key]
+
+ # Translate string to boolean.
+ handle_errors = {'False': False}.get(handle_errors_header, True)
+ extra_environ = {}
+ if not handle_errors:
+ # There doesn't seem to be a "Right Way" to do this
+ extra_environ['wsgi.handleErrors'] = False # zope.app.wsgi does this
+ extra_environ['paste.throw_errors'] = True # the paste way of doing this
+
+ scheme_key = 'X-Zope-Scheme'
+ extra_environ['wsgi.url_scheme'] = headers.get(scheme_key, 'http')
+ if scheme_key in headers:
+ del headers[scheme_key]
+
+ app = self._test_app
+
+ # clear our app cookies so that our testbrowser cookie headers don't
+ # get stomped
+ app.cookies.clear()
+
+ # pass the request to webtest
+ if method == 'GET':
+ assert not body, body
+ response = app.get(url, headers=headers, expect_errors=True, extra_environ=extra_environ)
+ elif method == 'POST':
+ response = app.post(url, body, headers=headers, expect_errors=True, extra_environ=extra_environ)
+ else:
+ raise Exception('Couldnt handle method %s' % method)
+
+ self.response = response
+
+ def getresponse(self):
+ """Return a ``mechanize`` compatible response.
+
+ The goal of ths method is to convert the WebTest's reseponse to
+ a ``mechanize`` compatible response, which is also understood by
+ mechanize.
+ """
+ response = self.response
+ status = int(response.status[:3])
+ reason = response.status[4:]
+
+ headers = response.headers.items()
+ headers.sort()
+ headers.insert(0, ('Status', response.status))
+ headers = '\r\n'.join('%s: %s' % h for h in headers)
+ content = response.body
+ return zope.testbrowser.testing.Response(content, headers, status, reason)
+
+
+class WSGIHTTPHandler(zope.testbrowser.testing.HTTPHandler):
+
+ def __init__(self, test_app, *args, **kw):
+ self._test_app = test_app
+ zope.testbrowser.testing.HTTPHandler.__init__(self, *args, **kw)
+
+ def _connect(self, *args, **kw):
+ return WSGIConnection(self._test_app, *args, **kw)
+
+ def https_request(self, req):
+ req.add_unredirected_header('X-Zope-Scheme', 'https')
+ return self.http_request(req)
+
+
+class WSGIMechanizeBrowser(zope.testbrowser.testing.MechanizeBrowser):
+ """Special ``mechanize`` browser using the WSGI HTTP handler."""
+
+ def __init__(self, test_app, *args, **kw):
+ self._test_app = test_app
+ zope.testbrowser.testing.MechanizeBrowser.__init__(self, *args, **kw)
+
+ def _http_handler(self, *args, **kw):
+ return WSGIHTTPHandler(self._test_app, *args, **kw)
+
+
+class Browser(zope.testbrowser.browser.Browser):
+ """A WSGI `testbrowser` Browser that uses a WebTest wrapped WSGI app."""
+
+ def __init__(self, test_app, url=None):
+ mech_browser = WSGIMechanizeBrowser(test_app)
+ super(Browser, self).__init__(url=url, mech_browser=mech_browser)
Copied: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/wsgi.txt (from rev 119614, zope.testbrowser/branches/jinty-webtest/src/zope/testbrowser/wsgi.txt)
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/wsgi.txt (rev 0)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/wsgi.txt 2011-01-30 12:36:36 UTC (rev 120003)
@@ -0,0 +1,20 @@
+Detailed tests for WSGI Browser
+===============================
+
+ >>> browser = Browser()
+
+HTTPS support
+-------------
+
+Depending on the scheme of the request the variable wsgi.url_scheme will be set
+correctly on the request:
+
+ >>> browser.open('http://localhost/echo_one.html?var=wsgi.url_scheme')
+ >>> print browser.contents
+ 'http'
+
+ >>> browser.open('https://localhost/echo_one.html?var=wsgi.url_scheme')
+ >>> print browser.contents
+ 'https'
+
+see http://www.python.org/dev/peps/pep-3333/ for details.
Copied: zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/zope-publisher.txt (from rev 120000, zope.testbrowser/branches/jinty-webtest2/src/zope/testbrowser/zope-publisher.txt)
===================================================================
--- zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/zope-publisher.txt (rev 0)
+++ zope.testbrowser/branches/jinty-webtest3/src/zope/testbrowser/zope-publisher.txt 2011-01-30 12:36:36 UTC (rev 120003)
@@ -0,0 +1,81 @@
+Zope Publisher Browser Tests
+============================
+
+These tests specifically test the implementation of Browser which uses the Zope
+Publisher via zope.app.testing as the application to test.
+
+ >>> from zope.testbrowser.testing import Browser
+ >>> browser = Browser()
+
+Error Handling
+--------------
+
+handleErrors works as advertised:
+
+ >>> browser.handleErrors
+ True
+ >>> browser.open('http://localhost/invalid')
+ Traceback (most recent call last):
+ ...
+ HTTPError: HTTP Error 404: Not Found
+
+So when we tell the publisher not to handle the errors,
+
+ >>> browser.handleErrors = False
+ >>> browser.open('http://localhost/invalid')
+ Traceback (most recent call last):
+ ...
+ NotFound: Object: <zope.site.folder.Folder object at ...>,
+ name: u'invalid'
+
+Spaces in URLs
+--------------
+
+Spaces in URLs don't cause blowups:
+
+ >>> browser.open('http://localhost/space here')
+ Traceback (most recent call last):
+ ...
+ NotFound: Object: <zope.site.folder.Folder object at ...>,
+ name: u'space here'
+
+Headers
+-------
+
+Sending arbitrary headers works:
+
+ >>> browser.addHeader('Accept-Language', 'en-US')
+ >>> browser.open('http://localhost/echo_one.html?var=HTTP_ACCEPT_LANGUAGE')
+ >>> print browser.contents
+ 'en-US'
+
+POST
+----
+
+HTTP posts are correctly sent:
+
+ >>> browser.post('http://localhost/echo.html', 'x=1&y=2')
+ >>> print browser.contents
+ CONTENT_LENGTH: 7
+ CONTENT_TYPE: application/x-www-form-urlencoded
+ HTTP_ACCEPT_LANGUAGE: en-US
+ HTTP_CONNECTION: close
+ HTTP_HOST: localhost
+ HTTP_USER_AGENT: Python-urllib/2.4
+ PATH_INFO: /echo.html
+ REQUEST_METHOD: POST
+ x: 1
+ y: 2
+ Body: ''
+
+Returned headers
+----------------
+
+Server headers are correctly returned:
+
+ >>> print browser.headers
+ Status: 200 OK
+ Content-Length: 123
+ Content-Type: text/plain;charset=utf-8
+ X-Content-Type-Warning: guessed from content
+ X-Powered-By: Zope (www.zope.org), Python (www.python.org)
More information about the checkins
mailing list