[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