[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/ Added (first cut at) functional HTTP doctests. These are doctests

Jim Fulton jim at zope.com
Sun Aug 22 15:12:28 EDT 2004


Log message for revision 27221:
  Added (first cut at) functional HTTP doctests.  These are doctests
  expressed as http request inputs and expected http response outputs.
  
  To do:
  
  - Support for binary inputs (e.g.file uploads)
  
  - Support for accessing the object system, so that assertions
      can be made about the state of the system.
  


Changed:
  A   Zope3/trunk/src/zope/app/ftests/doctest.txt
  U   Zope3/trunk/src/zope/app/ftests/test_functional.py
  U   Zope3/trunk/src/zope/app/tests/functional.py


-=-
Added: Zope3/trunk/src/zope/app/ftests/doctest.txt
===================================================================
--- Zope3/trunk/src/zope/app/ftests/doctest.txt	2004-08-22 19:12:14 UTC (rev 27220)
+++ Zope3/trunk/src/zope/app/ftests/doctest.txt	2004-08-22 19:12:27 UTC (rev 27221)
@@ -0,0 +1,102 @@
+DocTest Functional Tests
+========================
+
+This file documents and tests doctest-based functional tests and basic
+Zope web-application functionality.
+
+You can create Functional tests as doctests.  Typically, this is done
+by using a script such as zope.app.tests.dochttp.py to convert
+tcpwatch recorded output to a doctest, which is then edited to provide
+explanation and to remove uninyeresting details.  That is how this
+file was created.
+
+Here we'll test some of the most basic types of access.
+
+First, we'll test accessing a protected page without credentials:
+
+  >>> print http(r"""
+  ... GET /@@contents.html HTTP/1.1
+  ... """)
+  HTTP/1.1 401 Unauthorized
+  Content-Length: ...
+  Content-Type: text/html;charset=utf-8
+  Www-Authenticate: basic realm=zope
+  <BLANKLINE>
+  <!DOCTYPE html PUBLIC ...
+
+Here we see that we got:
+
+  - A 404 response,
+  - A Www-Authenticate header, and
+  - An html body with an error message
+
+Note that we used ellipeses to indicate ininteresting details.
+
+Next, we'll access the same page with credentials: 
+
+  >>> print http(r"""
+  ... GET /@@contents.html HTTP/1.1
+  ... Authorization: Basic bWdyOm1ncnB3
+  ... """)
+  HTTP/1.1 200 Ok
+  Content-Length: ...
+  Content-Type: text/html;charset=utf-8
+  <BLANKLINE>
+  <!DOCTYPE html PUBLIC ...
+
+And we get a normal output.
+
+Next we'll try accessing site management. Since we used "/manage", 
+we got redirected:
+
+  >>> print http(r"""
+  ... GET /++etc++site/@@manage HTTP/1.1
+  ... Authorization: Basic bWdyOm1ncnB3
+  ... Referer: http://localhost:8081/
+  ... """)
+  HTTP/1.1 303 See Other
+  Content-Length: 0
+  Content-Type: text/plain;charset=utf-8
+  Location: @@tasks.html
+  <BLANKLINE>
+
+Note that, in this case, we got a 303 response.  A 303 response is the
+prefered response for this sort of redirect with HTTP 1.1.  If we used
+HTTP 1.0, we'd get a 302 response:
+
+  >>> print http(r"""
+  ... GET /++etc++site/@@manage HTTP/1.0
+  ... Authorization: Basic bWdyOm1ncnB3
+  ... Referer: http://localhost:8081/
+  ... """)
+  HTTP/1.0 302 Moved Temporarily
+  Content-Length: 0
+  Content-Type: text/plain;charset=utf-8
+  Location: @@tasks.html
+  <BLANKLINE>
+
+Lets visit the page we were rediected to:
+
+  >>> print http(r"""
+  ... GET /++etc++site/@@tasks.html HTTP/1.1
+  ... Authorization: Basic bWdyOm1ncnB3
+  ... Referer: http://localhost:8081/
+  ... """)
+  HTTP/1.1 200 Ok
+  Content-Length: ...
+  Content-Type: text/html;charset=utf-8
+  <BLANKLINE>
+  <!DOCTYPE html PUBLIC ...
+
+Finally, lets access the default page for the site:
+
+  >>> print http(r"""
+  ... GET / HTTP/1.1
+  ... Authorization: Basic bWdyOm1ncnB3
+  ... """)
+  HTTP/1.1 200 Ok
+  Content-Length: ...
+  Content-Type: text/html;charset=utf-8
+  <BLANKLINE>
+  <!DOCTYPE html PUBLIC ...
+

Modified: Zope3/trunk/src/zope/app/ftests/test_functional.py
===================================================================
--- Zope3/trunk/src/zope/app/ftests/test_functional.py	2004-08-22 19:12:14 UTC (rev 27220)
+++ Zope3/trunk/src/zope/app/ftests/test_functional.py	2004-08-22 19:12:27 UTC (rev 27221)
@@ -19,6 +19,7 @@
 
 import unittest
 from zope.app.tests.functional import SampleFunctionalTest, BrowserTestCase
+from zope.app.tests.functional import FunctionalDocFileSuite
 
 class CookieFunctionalTest(BrowserTestCase):
 
@@ -120,6 +121,7 @@
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(SampleFunctionalTest))
     suite.addTest(unittest.makeSuite(CookieFunctionalTest))
+    suite.addTest(FunctionalDocFileSuite('doctest.txt'))
     return suite
 
 

Modified: Zope3/trunk/src/zope/app/tests/functional.py
===================================================================
--- Zope3/trunk/src/zope/app/tests/functional.py	2004-08-22 19:12:14 UTC (rev 27220)
+++ Zope3/trunk/src/zope/app/tests/functional.py	2004-08-22 19:12:27 UTC (rev 27221)
@@ -18,6 +18,8 @@
 $Id$
 """
 import logging
+import re
+import rfc822
 import sys
 import traceback
 import unittest
@@ -28,23 +30,27 @@
 from transaction import get_transaction
 from ZODB.DB import DB
 from ZODB.DemoStorage import DemoStorage
+import zope.interface
 from zope.publisher.browser import BrowserRequest
 from zope.publisher.http import HTTPRequest
 from zope.publisher.publish import publish
+from zope.publisher.xmlrpc import XMLRPCRequest
 from zope.security.interfaces import Forbidden, Unauthorized
 from zope.security.management import endInteraction
+import zope.server.interfaces
+from zope.testing import doctest
 
 from zope.app.debug import Debugger
+from zope.app.publication.http import HTTPPublication
+from zope.app.publication.browser import BrowserPublication
+from zope.app.publication.xmlrpc import XMLRPCPublication
 from zope.app.publication.zopepublication import ZopePublication
 from zope.app.publication.http import HTTPPublication
 import zope.app.tests.setup
 from zope.app.component.hooks import setSite, getSite
 
+HTTPTaskStub = StringIO
 
-class HTTPTaskStub(StringIO):
-    pass
-
-
 class ResponseWrapper(object):
     """A wrapper that adds several introspective methods to a response."""
 
@@ -73,7 +79,6 @@
     def __getattr__(self, attr):
         return getattr(self._response, attr)
 
-
 class FunctionalTestSetup(object):
     """Keeps shared state across several functional test cases."""
 
@@ -146,6 +151,7 @@
 
     def tearDown(self):
         """Cleans up after a functional test case."""
+
         FunctionalTestSetup().tearDown()
         super(FunctionalTestCase, self).tearDown()
 
@@ -169,6 +175,7 @@
 
     def tearDown(self):
         del self.cookies
+
         self.setSite(None)
         super(BrowserTestCase, self).tearDown()
 
@@ -373,6 +380,119 @@
         publish(request, handle_errors=handle_errors)
         return response
 
+
+class HTTPHeaderOutput:
+
+    zope.interface.implements(zope.server.interfaces.IHeaderOutput)
+
+    def __init__(self, protocol, omit):
+        self.headers = {}
+        self.headersl = []
+        self.protocol = protocol
+        self.omit = omit
+    
+    def setResponseStatus(self, status, reason):
+        self.status, self.reason = status, reason
+
+    def setResponseHeaders(self, mapping):
+        self.headers.update(dict(
+            [('-'.join([s.capitalize() for s in name.split('-')]), v)
+             for name, v in mapping.items()
+             if name.lower() not in self.omit]
+        ))
+
+    def appendResponseHeaders(self, lst):
+        headers = [split_header(header) for header in lst]
+        self.headersl.extend(
+            [('-'.join([s.capitalize() for s in name.split('-')]), v)
+             for name, v in headers
+             if name.lower() not in self.omit]
+        )
+
+    def __str__(self):
+        out = ["%s: %s" % header for header in self.headers.items()]
+        out.extend(["%s: %s" % header for header in self.headersl])
+        out.sort()
+        out.insert(0, "%s %s %s" % (self.protocol, self.status, self.reason))
+        return '\n'.join(out)
+
+class DocResponseWrapper(ResponseWrapper):
+    """Response Wrapper for use in doc tests
+    """
+
+    def __init__(self, response, outstream, path, header_output):
+        ResponseWrapper.__init__(self, response, outstream, path)
+        self.header_output = header_output
+
+    def __str__(self):
+        body = self.getOutput()
+        if body:
+            return "%s\n\n%s" % (self.header_output, body)
+        return "%s\n" % (self.header_output)
+
+def http(request_string):
+    """Execute an HTTP request string via the publisher
+
+    This is used for HTTP doc tests.
+    """
+    # Discard leading white space to make call layout simpler
+    request_string = request_string.lstrip()
+
+    # split off and parse the command line
+    l = request_string.find('\n')
+    command_line = request_string[:l].rstrip()
+    request_string = request_string[l+1:]
+    method, path, protocol = command_line.split()
+    
+
+    instream = StringIO(request_string)
+    environment = {"HTTP_HOST": 'localhost',
+                   "HTTP_REFERER": 'localhost',
+                   "SERVER_PROTOCOL": protocol,
+                   }
+
+    headers = [split_header(header)
+               for header in rfc822.Message(instream).headers]
+    for name, value in headers:
+        name = 'HTTP_' + ('_'.join(name.upper().split('-')))
+        environment[name] = value.rstrip()
+
+    outstream = HTTPTaskStub()
+    old_site = getSite()
+    setSite(None)
+    app = FunctionalTestSetup().getApplication()
+    header_output = HTTPHeaderOutput(
+        protocol, ('x-content-type-warning', 'x-powered-by'))
+
+    if method in ('GET', 'POST', 'HEAD'):
+        if (method == 'POST' and
+            env.get('CONTENT_TYPE', '').startswith('text/xml')
+            ):
+            request_cls = XMLRPCRequest
+            publication_cls = XMLRPCPublication
+        else:
+            request_cls = BrowserRequest
+            publication_cls = BrowserPublication
+    else:
+        request_cls = HTTPRequest
+        publication_cls = HTTPPublication
+    
+    request = app._request(path, instream, outstream,
+                           environment=environment,
+                           request=request_cls, publication=publication_cls)
+    request.response.setHeaderOutput(header_output)
+    response = DocResponseWrapper(request.response, outstream, path,
+                                  header_output)
+    
+    publish(request)
+    setSite(old_site)
+    return response
+
+headerre = re.compile('(\S+): (.+)$')
+def split_header(header):
+    return headerre.match(header).group(1, 2)
+
+
 #
 # Sample functional test case
 #
@@ -402,6 +522,29 @@
     suite.addTest(unittest.makeSuite(SampleFunctionalTest))
     return suite
 
+def FunctionalDocFileSuite(*paths, **kw):
+    globs = kw.setdefault('globs', {})
+    globs['http'] = http
 
+    kw['package'] = doctest._normalize_module(kw.get('package'))
+
+    kwsetUp = kw.get('setUp')
+    def setUp():
+        FunctionalTestSetup().setUp()
+        if kwsetUp is not None:
+            kwsetUp()
+    kw['setUp'] = setUp
+
+    kwtearDown = kw.get('tearDown')
+    def tearDown():
+        FunctionalTestSetup().tearDown()
+        if kwtearDown is not None:
+            kwtearDown()
+    kw['tearDown'] = tearDown
+
+    kw['optionflags'] = doctest.ELLIPSIS | doctest.CONTEXT_DIFF
+
+    return doctest.DocFileSuite(*paths, **kw)
+
 if __name__ == '__main__':
     unittest.main()



More information about the Zope3-Checkins mailing list