1
# Copyright 2011 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Fixture code for YUITest + XHR integration testing."""
11
'YUITestFixtureControllerView',
17
from textwrap import dedent
21
from lazr.restful import ResourceJSONEncoder
22
from lazr.restful.utils import get_current_browser_request
23
from zope.component import getUtility
24
from zope.exceptions.exceptionformatter import format_exception
25
from zope.interface import implements
26
from zope.publisher.interfaces import NotFound
27
from zope.publisher.interfaces.http import IResult
28
from zope.security.checker import (
31
from zope.security.management import getInteraction
32
from zope.security.proxy import removeSecurityProxy
33
from zope.session.interfaces import IClientIdManager
35
from canonical.config import config
36
from canonical.launchpad.webapp.interfaces import (
37
IPlacelessAuthUtility,
40
from canonical.launchpad.webapp.login import logInPrincipal
41
from canonical.launchpad.webapp.publisher import LaunchpadView
42
from canonical.testing.layers import (
46
LayerProcessController,
49
from lp.app.versioninfo import revno
50
from lp.testing import AbstractYUITestCase
52
EXPLOSIVE_ERRORS = (SystemExit, MemoryError, KeyboardInterrupt)
55
"""Decorator to mark a function as a fixture available from JavaScript.
57
This makes the function available to call from JS integration tests over
58
XHR. The fixture setup can have one or more cleanups tied to it with
59
``add_cleanup`` decorator/callable and can be composed with another
60
function with the ``extend`` decorator/callable.
62
def __init__(self, function, extends=None):
64
self._function = function
65
self._extends = extends
66
# We can't use locals because we want to affect the function's module,
68
module = sys.modules[function.__module__]
69
fixtures = getattr(module, '_fixtures_', None)
71
fixtures = module._fixtures_ = {}
72
fixtures[function.__name__] = self
74
def __call__(self, request, data):
75
"""Call the originally decorated setup function."""
76
if self._extends is not None:
77
self._extends(request, data)
78
self._function(request, data)
80
def add_cleanup(self, function):
81
"""Add a cleanup function to be executed on teardown, FILO."""
82
self._cleanups.append(function)
85
def teardown(self, request, data):
86
"""Run all registered cleanups. If no cleanups, a no-op."""
87
for f in reversed(self._cleanups):
89
if self._extends is not None:
90
self._extends.teardown(request, data)
92
def extend(self, function):
93
return setup(function, self)
96
def login_as_person(person):
97
"""This is a helper function designed to be used within a fixture.
99
Provide a person, such as one generated by LaunchpadObjectFactory, and
100
the browser will become logged in as this person.
102
Explicit tear-down is unnecessary because the database is reset at the end
103
of every test, and the cookie is discarded.
106
raise AssertionError("Please do not try to login as a team")
107
email = removeSecurityProxy(person.preferredemail).email
108
request = get_current_browser_request()
109
assert request is not None, "We do not have a browser request."
110
authutil = getUtility(IPlacelessAuthUtility)
111
principal = authutil.getPrincipalByLogin(email, want_password=False)
112
launchbag = getUtility(IOpenLaunchBag)
113
launchbag.setLogin(email)
114
logInPrincipal(request, principal, email)
120
# This is machinery, not content. We specify our security checker here
121
# directly for clarity.
122
__Security_checker__ = NamesChecker(['next', '__iter__'])
127
LaunchpadLayer.resetSessionDb()
128
# Yield control to asyncore for a second, just to be a little bit
129
# nice. We could be even nicer by moving this whole teardown/setup
130
# dance to a thread and waiting for it to be done, but there's not a
131
# (known) compelling need for that right now, and doing it this way is
134
DatabaseLayer.testSetUp()
136
# Reset the librarian.
137
LibrarianLayer.testTearDown()
139
# Reset the database.
140
DatabaseLayer.testTearDown()
142
LibrarianLayer.testSetUp()
143
except (SystemExit, KeyboardInterrupt):
146
print "Hm, serious error when trying to clean up the test."
147
traceback.print_exc()
148
# We're done, so we can yield the body.
152
class YUITestFixtureControllerView(LaunchpadView):
153
"""Dynamically loads YUI test along their fixtures run over an app server.
156
JAVASCRIPT = 'JAVASCRIPT'
159
TEARDOWN = 'TEARDOWN'
161
page_template = dedent("""\
162
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
163
"http://www.w3.org/TR/html4/strict.dtd">
167
<script type="text/javascript"
168
src="/+icing/rev%(revno)s/build/launchpad.js"></script>
169
<link rel="stylesheet"
170
href="/+icing/yui/assets/skins/sam/skin.css"/>
171
<link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>
173
/* Taken and customized from testlogger.css */
174
.yui-console-entry-src { display:none; }
176
.yui-console-entry.yui-console-entry-pass .yui-console-entry-cat {
177
background-color: green;
181
.yui-console-entry.yui-console-entry-fail .yui-console-entry-cat {
182
background-color: red;
186
.yui-console-entry.yui-console-entry-ignore .yui-console-entry-cat {
187
background-color: #666;
192
<script type="text/javascript" src="%(test_module)s"></script>
194
<body class="yui3-skin-sam">
200
def __init__(self, context, request):
201
super(YUITestFixtureControllerView, self).__init__(context, request)
207
def traversed_path(self):
208
return os.path.join(*self.names)
210
def initialize(self):
211
path, ext = os.path.splitext(self.traversed_path)
212
full_path = os.path.join(config.root, 'lib', path)
213
if not os.path.exists(full_path + '.py'):
214
raise NotFound(self, full_path + '.py', self.request)
215
if not os.path.exists(full_path + '.js'):
216
raise NotFound(self, full_path + '.js', self.request)
219
self.action = self.JAVASCRIPT
221
if self.request.method == 'GET':
222
self.action = self.HTML
224
self.fixtures = self.request.form['fixtures'].split(',')
225
if self.request.form['action'] == 'setup':
226
self.action = self.SETUP
228
self.action = self.TEARDOWN
230
# The following two zope methods publishTraverse and browserDefault
231
# allow this view class to take control of traversal from this point
232
# onwards. Traversed names just end up in self.names.
233
def publishTraverse(self, request, name):
234
"""Traverse to the given name."""
235
# The two following constraints are enforced by the publisher.
236
assert os.path.sep not in name, (
237
'traversed name contains os.path.sep: %s' % name)
238
assert name != '..', 'traversing to ..'
239
self.names.append(name)
242
def browserDefault(self, request):
246
return self.page_template % dict(
247
test_module='/+yuitest/%s.js' % self.traversed_path,
250
def get_fixtures(self):
251
module_name = '.'.join(self.names)
252
test_module = __import__(
253
module_name, globals(), locals(), ['_fixtures_'], 0)
254
return test_module._fixtures_
257
if self.action == self.JAVASCRIPT:
258
self.request.response.setHeader('Content-Type', 'text/javascript')
259
result = open(os.path.join(config.root, 'lib', self.traversed_path))
260
elif self.action == self.HTML:
261
self.request.response.setHeader('Content-Type', 'text/html')
263
elif self.action == self.SETUP:
265
fixtures = self.get_fixtures()
267
for fixture_name in self.fixtures:
268
__traceback_info__ = (fixture_name, data)
269
fixtures[fixture_name](self.request, data)
270
except EXPLOSIVE_ERRORS:
273
self.request.response.setStatus(500)
274
result = ''.join(format_exception(*sys.exc_info()))
276
self.request.response.setHeader(
277
'Content-Type', 'application/json')
278
# We use the ProxyFactory so that the restful redaction code is
280
result = simplejson.dumps(
281
ProxyFactory(data), cls=ResourceJSONEncoder)
282
elif self.action == self.TEARDOWN:
283
data = simplejson.loads(self.request.form['data'])
284
fixtures = self.get_fixtures()
286
for fixture_name in reversed(self.fixtures):
287
__traceback_info__ = (fixture_name, data)
288
fixtures[fixture_name].teardown(self.request, data)
289
except EXPLOSIVE_ERRORS:
292
self.request.response.setStatus(500)
293
result = ''.join(format_exception(*sys.exc_info()))
295
# Remove the session cookie, in case we have one.
296
self.request.response.expireCookie(
297
getUtility(IClientIdManager).namespace)
298
# Blow up the database once we are out of this transaction
299
# by passing a result that will do so when it is iterated
300
# through in asyncore.
301
self.request.response.setHeader('Content-Length', 1)
302
result = CloseDbResult()
305
# This class cannot be imported directly into a test suite because
306
# then the test loader will sniff and (try to) run it. Use make_suite
307
# instead (or import this module rather than this class).
308
class YUIAppServerTestCase(AbstractYUITestCase):
309
"Instantiate this test case with the Python fixture module name."
311
layer = YUIAppServerLayer
312
_testMethodName = 'runTest'
314
def __init__(self, module_name=None):
315
self.module_name = module_name
316
# This needs to be done early so the "id" is set correctly.
317
self.test_path = self.module_name.replace('.', '/')
318
super(YUIAppServerTestCase, self).__init__()
321
root_url = LayerProcessController.appserver_root_url()
322
self.html_uri = '%s+yuitest/%s' % (root_url, self.test_path)
323
super(YUIAppServerTestCase, self).setUp()
325
runTest = AbstractYUITestCase.checkResults
328
def make_suite(module_name):
329
return unittest.TestSuite([YUIAppServerTestCase(module_name)])