#!/usr/bin/python
"""better_twill is a small wrapper around twill to set some sane defaults and
monkey-patch some better versions of some of twill's methods.
It also handles twill's absense.
"""
import os
from os.path import abspath,dirname,join
import sys
from pkg_resources import parse_version
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
# On OSX lxml needs to be imported before twill to avoid Resolver issues
# somehow caused by the mac specific 'ic' module
try:
from lxml import etree
except ImportError:
pass
try:
import twill
except ImportError:
twill = None
# When twill tries to connect to a site before the site is up, it raises an
# exception. In 0.9b1, it's urlib2.URLError, but in -latest, it's
# _mechanize_dist._mechanize.BrowserStateError.
try:
from _mechanize_dist._mechanize import BrowserStateError
except ImportError:
from urllib2 import URLError
if twill:
# We want Trac to generate valid html, and therefore want to test against
# the html as generated by Trac. "tidy" tries to clean up broken html, and
# is responsible for one difficult to track down testcase failure (for
# #5497). Therefore we turn it off here.
twill.commands.config('use_tidy', '0')
# setup short names to reduce typing
# This twill browser (and the tc commands that use it) are essentially
# global, and not tied to our test fixture.
tc = twill.commands
b = twill.get_browser()
# Setup XHTML validation for all retrieved pages
try:
from lxml import etree
except ImportError:
print "SKIP: validation of XHTML output in functional tests " \
"(no lxml installed)"
etree = None
if etree and pv(etree.__version__) < pv('2.0.0'):
# 2.0.7 and 2.1.x are known to work.
print "SKIP: validation of XHTML output in functional tests " \
"(lxml < 2.0, api incompatibility)"
etree = None
if etree:
class _Resolver(etree.Resolver):
base_dir = dirname(abspath(__file__))
def resolve(self, system_url, public_id, context):
return self.resolve_filename(join(self.base_dir,
system_url.split("/")[-1]),
context)
_parser = etree.XMLParser(dtd_validation=True)
_parser.resolvers.add(_Resolver())
etree.set_default_parser(_parser)
def _format_error_log(data, log):
msg = []
for entry in log:
context = data.splitlines()[max(0, entry.line - 5):
entry.line + 6]
msg.append("\n# %s\n# URL: %s\n# Line %d, column %d\n\n%s\n"
% (entry.message, entry.filename,
entry.line, entry.column,
"\n".join([each.decode('utf-8') for each in context])))
return "\n".join(msg).encode('ascii', 'xmlcharrefreplace')
def _validate_xhtml(func_name, *args, **kwargs):
page = b.get_html()
if "xhtml1-strict.dtd" not in page:
return
etree.clear_error_log()
try:
doc = etree.parse(StringIO(page), base_url=b.get_url())
except etree.XMLSyntaxError, e:
raise twill.errors.TwillAssertionError(
_format_error_log(page, e.error_log))
b._post_load_hooks.append(_validate_xhtml)
# When we can't find something we expected, or find something we didn't
# expect, it helps the debugging effort to have a copy of the html to
# analyze.
def twill_write_html():
"""Write the current html to a file. Name the file based on the
current testcase.
"""
frame = sys._getframe()
while frame:
if frame.f_code.co_name in ('runTest', 'setUp', 'tearDown'):
testcase = frame.f_locals['self']
testname = testcase.__class__.__name__
tracdir = testcase._testenv.tracdir
break
frame = frame.f_back
else:
# We didn't find a testcase in the stack, so we have no clue what's
# going on.
raise Exception("No testcase was found on the stack. This was "
"really not expected, and I don't know how to handle it.")
filename = os.path.join(tracdir, 'log', "%s.html" % testname)
html_file = open(filename, 'w')
html_file.write(b.get_html())
html_file.close()
return filename
# Twill isn't as helpful with errors as I'd like it to be, so we replace
# the formvalue function. This would be better done as a patch to Twill.
def better_formvalue(form, field, value, fv=tc.formvalue):
try:
fv(form, field, value)
except (twill.errors.TwillAssertionError,
twill.utils.ClientForm.ItemNotFoundError), e:
filename = twill_write_html()
args = e.args + (filename,)
raise twill.errors.TwillAssertionError(*args)
tc.formvalue = better_formvalue
tc.fv = better_formvalue
# Twill's formfile function leaves a filehandle open which prevents the
# file from being deleted on Windows. Since we would just assume use a
# StringIO object in the first place, allow the file-like object to be
# provided directly.
def better_formfile(formname, fieldname, filename, content_type=None,
fp=None):
if not fp:
filename = filename.replace('/', os.path.sep)
temp_fp = open(filename, 'rb')
data = temp_fp.read()
temp_fp.close()
fp = StringIO(data)
form = b.get_form(formname)
control = b.get_form_field(form, fieldname)
if not control.is_of_kind('file'):
raise twill.errors.TwillException('ERROR: field is not a file '
'upload field!')
b.clicked(form, control)
control.add_file(fp, content_type, filename)
tc.formfile = better_formfile
# Twill's tc.find() does not provide any guidance on what we got instead of
# what was expected.
def better_find(what, flags='', tcfind=tc.find):
try:
tcfind(what, flags)
except twill.errors.TwillAssertionError, e:
filename = twill_write_html()
args = e.args + (filename,)
raise twill.errors.TwillAssertionError(*args)
tc.find = better_find
def better_notfind(what, flags='', tcnotfind=tc.notfind):
try:
tcnotfind(what, flags)
except twill.errors.TwillAssertionError, e:
filename = twill_write_html()
args = e.args + (filename,)
raise twill.errors.TwillAssertionError(*args)
tc.notfind = better_notfind
else:
b = tc = None
|