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.proxy import removeSecurityProxy
32
from zope.session.interfaces import IClientIdManager
34
from canonical.config import config
35
from canonical.launchpad.webapp.interfaces import (
36
IPlacelessAuthUtility,
39
from canonical.launchpad.webapp.login import logInPrincipal
40
from canonical.launchpad.webapp.publisher import LaunchpadView
41
from canonical.testing.layers import (
45
LayerProcessController,
48
from lp.app.versioninfo import revno
49
from lp.testing import AbstractYUITestCase
51
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
129
# little bit nice. We could be even nicer by moving this
130
# whole teardown/setup dance to a thread and waiting for
131
# it to be done, but there's not a (known) compelling need
132
# for that right now, and doing it this way is slightly
135
DatabaseLayer.testSetUp()
137
# Reset the librarian.
138
LibrarianLayer.testTearDown()
140
# Reset the database.
141
DatabaseLayer.testTearDown()
143
LibrarianLayer.testSetUp()
144
except (SystemExit, KeyboardInterrupt):
147
print "Hm, serious error when trying to clean up the test."
148
traceback.print_exc()
149
# We're done, so we can yield the body.
153
class YUITestFixtureControllerView(LaunchpadView):
154
"""Dynamically loads YUI test along their fixtures run over an app server.
157
JAVASCRIPT = 'JAVASCRIPT'
160
TEARDOWN = 'TEARDOWN'
162
page_template = dedent("""\
163
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
164
"http://www.w3.org/TR/html4/strict.dtd">
168
<script type="text/javascript"
169
src="/+icing/rev%(revno)s/build/launchpad.js"></script>
170
<link rel="stylesheet"
171
href="/+icing/yui/assets/skins/sam/skin.css"/>
172
<link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>
174
/* Taken and customized from testlogger.css */
175
.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')
260
os.path.join(config.root, 'lib', self.traversed_path))
261
elif self.action == self.HTML:
262
self.request.response.setHeader('Content-Type', 'text/html')
264
elif self.action == self.SETUP:
266
fixtures = self.get_fixtures()
268
for fixture_name in self.fixtures:
269
__traceback_info__ = (fixture_name, data)
270
fixtures[fixture_name](self.request, data)
271
except EXPLOSIVE_ERRORS:
274
self.request.response.setStatus(500)
275
result = ''.join(format_exception(*sys.exc_info()))
277
self.request.response.setHeader(
278
'Content-Type', 'application/json')
279
# We use the ProxyFactory so that the restful
280
# redaction code is always used.
281
result = simplejson.dumps(
282
ProxyFactory(data), cls=ResourceJSONEncoder)
283
elif self.action == self.TEARDOWN:
284
data = simplejson.loads(self.request.form['data'])
285
fixtures = self.get_fixtures()
287
for fixture_name in reversed(self.fixtures):
288
__traceback_info__ = (fixture_name, data)
289
fixtures[fixture_name].teardown(self.request, data)
290
except EXPLOSIVE_ERRORS:
293
self.request.response.setStatus(500)
294
result = ''.join(format_exception(*sys.exc_info()))
296
# Remove the session cookie, in case we have one.
297
self.request.response.expireCookie(
298
getUtility(IClientIdManager).namespace)
299
# Blow up the database once we are out of this transaction
300
# by passing a result that will do so when it is iterated
301
# through in asyncore.
302
self.request.response.setHeader('Content-Length', 1)
303
result = CloseDbResult()
307
# This class cannot be imported directly into a test suite because
308
# then the test loader will sniff and (try to) run it. Use make_suite
309
# instead (or import this module rather than this class).
310
class YUIAppServerTestCase(AbstractYUITestCase):
311
"Instantiate this test case with the Python fixture module name."
313
layer = YUIAppServerLayer
314
_testMethodName = 'runTest'
316
def __init__(self, module_name=None):
317
self.module_name = module_name
318
# This needs to be done early so the "id" is set correctly.
319
self.test_path = self.module_name.replace('.', '/')
320
super(YUIAppServerTestCase, self).__init__()
323
root_url = LayerProcessController.appserver_root_url()
324
self.html_uri = '%s+yuitest/%s' % (root_url, self.test_path)
325
super(YUIAppServerTestCase, self).setUp()
327
runTest = AbstractYUITestCase.checkResults
330
def make_suite(module_name):
331
return unittest.TestSuite([YUIAppServerTestCase(module_name)])