[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