Merge lp:~gary/launchpad/bug872089 into lp:launchpad

Proposed by Gary Poster
Status: Merged
Approved by: Gary Poster
Approved revision: no longer in the source branch.
Merged at revision: 14149
Proposed branch: lp:~gary/launchpad/bug872089
Merge into: lp:launchpad
Diff against target: 702 lines (+385/-97)
8 files modified
lib/canonical/launchpad/scripts/runlaunchpad.py (+21/-7)
lib/lp/app/javascript/server_fixture.js (+23/-1)
lib/lp/testing/tests/test_standard_yuixhr_test_template.js (+3/-15)
lib/lp/testing/tests/test_yuixhr.py (+105/-1)
lib/lp/testing/tests/test_yuixhr_fixture.js (+6/-13)
lib/lp/testing/tests/test_yuixhr_fixture_facet.js (+27/-0)
lib/lp/testing/tests/test_yuixhr_fixture_facet.py (+17/-0)
lib/lp/testing/yuixhr.py (+183/-60)
To merge this branch: bzr merge lp:~gary/launchpad/bug872089
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+79197@code.launchpad.net

Commit message

[r=gmb][bug=872089][no-qa] bugfixes and improvements to the yuixhr test infrastructure

Description of the change

In the course of working on bug 724609, I have encountered some bugs, annoyances, and wishlist items when writing the new yuixhr tests. This branch addresses many of them.

 - There was a bunch of boiler plate that had to happen at the end of the JS file to get the test to run, and render the log in an attractive and usable way, and stash the results in a place that the html5browser testrunner can find them. I had that as cut-and-paste code, but I saw how the non-xhr yui tests have a convenience hookup, and thought that would be nice. I made a simple function that does the work. This shortens the tests by 10 or 15 lines, and makes it possible to change this code in the future in one place.

 - When working on yuixhr tests, you are working on Python fixtures and Javascript tests simultaneously. To make working with these tests interactively simpler, I made several changes.

   * The test JS has explicit headers to not cache.

   * The Python fixtures can optionally be reloaded. This is only allowed in interactive testing, and only if explicitly requested ("?reload=1"), and only when you load the HTML for the test. I have been burnt by reload in the past and vowed never to use it again, but the reload that I'm doing here I think is among the safer uses for it that I can imagine, and it is really convenient (ah, the seduction...). As long as the fixtures don't have (important) global state in the module, it should work fine. We clear out our own global state. The HTML has explicit headers not to cache so that this trigger will be honored, but because we are using a query string for the control the cache headers are really superfluous.

   * I added instructions and warnings on the test page so users can know how to work interactively.

 - Sometimes you want to have tests run within a particular Launchpad subdomain, or facet. This might happen if you want a test to interact with a page, for instance. You can now specify that the test should be run in a facet. This is honored in the automatic tests and in top-level +yuitest described above.

 - The installed config object was not correctly set when running tests. Notably, we were using the "testing" config as a basis rather than "testing-appserver". This makes a difference for the facet feature described above.

 - When running "make run-testapp", the developer had no help as to what to do next. I added simple instructions in the console output to go to a top-level +yuitest page. I created the corresponding page, which lists all the yui xhr tests it could find, and gives a link to them. It tries to honor the facet requested in the test_suite, and warns the user if it can't figure out the facet for one reason or another.

 - There was a bug that calling serverfixture.teardown if you had not run any setups would break. That's a problem because for some tests you have no setup, but you still want to run the test within a suite that has a standard serverfixture.teardown. Fixed.

 - I broke up the yuixhr render method into smaller methods, one per action. It seems nicer to me. My dispatch is a bit terse, but has plenty of Python precedent.

Lint has the following issues. I don't see that resolving the line length for the two Google URLs is valuable, so I don't intend to change that. The __traceback_info__ is intentional: it is for extra information in tracebacks, as rendered by zope.exception and lp.services.stacktrace.

./lib/lp/testing/yuixhr.py
     175: Line exceeds 78 characters.
     224: Line exceeds 78 characters.
     373: local variable '__traceback_info__' is assigned to but never used
     394: local variable '__traceback_info__' is assigned to but never used
     175: E501 line too long (100 characters)
     224: E501 line too long (100 characters)

That's it. Thanks.

Gary

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/scripts/runlaunchpad.py'
2--- lib/canonical/launchpad/scripts/runlaunchpad.py 2011-10-04 14:42:14 +0000
3+++ lib/canonical/launchpad/scripts/runlaunchpad.py 2011-10-13 03:04:49 +0000
4@@ -344,6 +344,7 @@
5
6
7 def start_testapp(argv=list(sys.argv)):
8+ from canonical.config.fixture import ConfigUseFixture
9 from canonical.testing.layers import (
10 BaseLayer,
11 DatabaseLayer,
12@@ -359,11 +360,13 @@
13 '%r does not start with "testrunner-appserver"' %
14 config.instance_name)
15 interactive_tests = 'INTERACTIVE_TESTS' in os.environ
16+ teardowns = []
17
18 def setup():
19 # This code needs to be run after other zcml setup happens in
20 # runlaunchpad, so it is passed in as a callable.
21 BaseLayer.setUp()
22+ teardowns.append(BaseLayer.tearDown)
23 if interactive_tests:
24 # The test suite runs its own RabbitMQ. We only need this
25 # for interactive tests. We set it up here rather than by
26@@ -371,31 +374,42 @@
27 # the appserver config does not normally need/have
28 # RabbitMQ config set.
29 RabbitMQLayer.setUp()
30+ teardowns.append(RabbitMQLayer.tearDown)
31 # We set up the database here even for the test suite because we want
32 # to be able to control the database here in the subprocess. It is
33 # possible to do that when setting the database up in the parent
34 # process, but it is messier. This is simple.
35 installFakeConnect()
36+ teardowns.append(uninstallFakeConnect)
37 DatabaseLayer.setUp()
38+ teardowns.append(DatabaseLayer.tearDown)
39 # The Librarian needs access to the database, so setting it up here
40 # where we are setting up the database makes the most sense.
41 LibrarianLayer.setUp()
42+ teardowns.append(LibrarianLayer.tearDown)
43+ # Switch to the appserver config.
44+ fixture = ConfigUseFixture(BaseLayer.appserver_config_name)
45+ fixture.setUp()
46+ teardowns.append(fixture.cleanUp)
47 # Interactive tests always need this. We let functional tests use
48 # a local one too because of simplicity.
49 LayerProcessController.startSMTPServer()
50+ teardowns.append(LayerProcessController.stopSMTPServer)
51+ if interactive_tests:
52+ root_url = config.appserver_root_url()
53+ print '*' * 70
54+ print 'In a few seconds, go to ' + root_url + '/+yuitest'
55+ print '*' * 70
56 try:
57 start_launchpad(argv, setup)
58 finally:
59- LayerProcessController.stopSMTPServer()
60- LibrarianLayer.tearDown()
61- DatabaseLayer.tearDown()
62- uninstallFakeConnect()
63- if interactive_tests:
64+ teardowns.reverse()
65+ for teardown in teardowns:
66 try:
67- RabbitMQLayer.tearDown()
68+ teardown()
69 except NotImplementedError:
70+ # We are in a separate process anyway. Bah.
71 pass
72- BaseLayer.tearDown()
73
74
75 def start_launchpad(argv=list(sys.argv), setup=None):
76
77=== modified file 'lib/lp/app/javascript/server_fixture.js'
78--- lib/lp/app/javascript/server_fixture.js 2011-09-21 00:15:46 +0000
79+++ lib/lp/app/javascript/server_fixture.js 2011-10-13 03:04:49 +0000
80@@ -44,6 +44,10 @@
81
82 module.teardown = function(testcase) {
83 var fixtures = testcase._lp_fixture_setups;
84+ if (Y.Lang.isUndefined(fixtures)) {
85+ // Nothing to be done.
86+ return;
87+ }
88 var data = Y.QueryString.stringify(
89 {action: 'teardown',
90 fixtures: fixtures.join(','),
91@@ -62,4 +66,22 @@
92 delete testcase._lp_fixture_data;
93 };
94
95- }, "0.1", {"requires": ["io", "json", "querystring"]});
96+module.run = function(suite) {
97+ var handle_complete = function(data) {
98+ window.status = '::::' + Y.JSON.stringify(data);
99+ };
100+ Y.Test.Runner.on('complete', handle_complete);
101+ Y.Test.Runner.add(suite);
102+
103+ var console = new Y.Console({newestOnTop: false});
104+
105+ Y.on('domready', function() {
106+ console.render('#log');
107+ Y.Test.Runner.run();
108+ });
109+};
110+
111+ },
112+ "0.1",
113+ {"requires": [
114+ "io", "json", "querystring", "test", "console", "lp.client"]});
115
116=== modified file 'lib/lp/testing/tests/test_standard_yuixhr_test_template.js'
117--- lib/lp/testing/tests/test_standard_yuixhr_test_template.js 2011-09-20 22:33:07 +0000
118+++ lib/lp/testing/tests/test_standard_yuixhr_test_template.js 2011-10-13 03:04:49 +0000
119@@ -2,7 +2,7 @@
120 base: '/+icing/yui/',
121 filter: 'raw', combine: false, fetchCSS: false
122 // TODO: Add other modules you want to test into the "use" list.
123-}).use('test', 'console', 'json', 'cookie', 'lp.testing.serverfixture',
124+}).use('test', 'lp.testing.serverfixture',
125 function(Y) {
126
127 // This is one-half of an example yuixhr test. The other half of a
128@@ -47,18 +47,6 @@
129 }
130 }));
131
132-// The remaining lines are necessary boilerplate. Include them.
133-
134-var handle_complete = function(data) {
135- window.status = '::::' + Y.JSON.stringify(data);
136- };
137-Y.Test.Runner.on('complete', handle_complete);
138-Y.Test.Runner.add(suite);
139-
140-var console = new Y.Console({newestOnTop: false});
141-
142-Y.on('domready', function() {
143- console.render('#log');
144- Y.Test.Runner.run();
145-});
146+// The last line is necessary. Include it.
147+serverfixture.run(suite);
148 });
149
150=== modified file 'lib/lp/testing/tests/test_yuixhr.py'
151--- lib/lp/testing/tests/test_yuixhr.py 2011-09-21 00:15:46 +0000
152+++ lib/lp/testing/tests/test_yuixhr.py 2011-10-13 03:04:49 +0000
153@@ -29,11 +29,13 @@
154 from canonical.testing.layers import LaunchpadFunctionalLayer
155
156 from lp.registry.interfaces.product import IProductSet
157+from lp.services.osutils import override_environ
158 from lp.testing import (
159 TestCase,
160 login,
161 ANONYMOUS,
162 )
163+from lp.testing.matchers import Contains
164 from lp.testing.views import create_view
165 from lp.testing.yuixhr import setup
166 from lp.testing.tests import test_yuixhr_fixture
167@@ -101,7 +103,7 @@
168 view = create_traversed_view(
169 path_info='/+yuitest/lp/testing/tests/test_yuixhr_fixture')
170 view.initialize()
171- content = view.page()
172+ content = view.renderHTML()
173 self.assertTrue(content.startswith('<!DOCTYPE HTML'))
174 self.assertTextMatchesExpressionIgnoreWhitespace(
175 re.escape(
176@@ -126,6 +128,9 @@
177 self.assertEqual(
178 'text/javascript',
179 view.request.response.getHeader('Content-Type'))
180+ self.assertEqual(
181+ 'no-cache',
182+ view.request.response.getHeader('Cache-Control'))
183
184 def test_javascript_must_have_a_py_fixture(self):
185 js_dir = tempfile.mkdtemp()
186@@ -157,6 +162,9 @@
187 self.assertEqual(
188 'text/html',
189 view.request.response.getHeader('Content-Type'))
190+ self.assertEqual(
191+ 'no-cache',
192+ view.request.response.getHeader('Cache-Control'))
193
194 def test_get_fixtures(self):
195 view = create_traversed_view(
196@@ -389,3 +397,99 @@
197 del called[:]
198 original_fixture.teardown(None, dict())
199 self.assertEquals(['original'], called)
200+
201+ def test_python_fixture_does_not_reload_by_default(self):
202+ # Even though the dangers of Python's "reload" are subtle and
203+ # real, using it can be very nice, particularly with
204+ # Launchpad's slow start-up time. By default, though, it is
205+ # not used. We will show this by scribbling on one of the
206+ # fixtures and showing that the scribble is still there when
207+ # we load the page.
208+ test_yuixhr_fixture._fixtures_['baseline'].scribble = 'hello'
209+ self.addCleanup(
210+ delattr, test_yuixhr_fixture._fixtures_['baseline'], 'scribble')
211+ view = create_traversed_view(
212+ path_info='/+yuitest/lp/testing/tests/'
213+ 'test_yuixhr_fixture')
214+ view.initialize()
215+ view.render()
216+ self.assertEquals(
217+ 'hello', test_yuixhr_fixture._fixtures_['baseline'].scribble)
218+
219+ def test_python_fixture_does_not_reload_without_environ_var(self):
220+ # As a bit of extra paranoia, we only allow a reload if
221+ # 'INTERACTIVE_TESTS' is in the environ. make run-testapp
222+ # sets this environmental variable. However, if we don't set
223+ # the environment, even if we request a reload it will not
224+ # happen.
225+ test_yuixhr_fixture._fixtures_['baseline'].scribble = 'hello'
226+ self.addCleanup(
227+ delattr, test_yuixhr_fixture._fixtures_['baseline'], 'scribble')
228+ view = create_traversed_view(
229+ path_info='/+yuitest/lp/testing/tests/'
230+ 'test_yuixhr_fixture', form=dict(reload='1'))
231+ view.initialize()
232+ view.render()
233+ self.assertEquals(
234+ 'hello', test_yuixhr_fixture._fixtures_['baseline'].scribble)
235+
236+ def test_python_fixture_can_reload(self):
237+ # Now we will turn reloading fully on, with the environmental
238+ # variable and the query string..
239+ test_yuixhr_fixture._fixtures_['baseline'].scribble = 'hello'
240+ with override_environ(INTERACTIVE_TESTS='1'):
241+ view = create_traversed_view(
242+ path_info='/+yuitest/lp/testing/tests/'
243+ 'test_yuixhr_fixture', form=dict(reload='1'))
244+ # reloading only happens at render time, so the scribble is
245+ # still there for now.
246+ view.initialize()
247+ self.assertEquals(
248+ 'hello', test_yuixhr_fixture._fixtures_['baseline'].scribble)
249+ # After a render of the html view, the module is reloaded.
250+ view.render()
251+ self.assertEquals(
252+ None,
253+ getattr(test_yuixhr_fixture._fixtures_['baseline'],
254+ 'scribble',
255+ None))
256+
257+ def test_python_fixture_resets_fixtures(self):
258+ # When we reload, we also clear out _fixtures_. This means
259+ # that if you rename or delete something, it won't be hanging
260+ # around confusing you into thinking everything is fine after
261+ # the reload.
262+ test_yuixhr_fixture._fixtures_['extra_scribble'] = 42
263+ with override_environ(INTERACTIVE_TESTS='1'):
264+ view = create_traversed_view(
265+ path_info='/+yuitest/lp/testing/tests/'
266+ 'test_yuixhr_fixture', form=dict(reload='1'))
267+ view.initialize()
268+ # After a render of the html view, the module is reloaded.
269+ view.render()
270+ self.assertEquals(
271+ None,
272+ test_yuixhr_fixture._fixtures_.get('extra_scribble'))
273+
274+ def test_python_fixture_reload_in_html(self):
275+ # The reload is specifically when we load HTML pages only.
276+ test_yuixhr_fixture._fixtures_['extra_scribble'] = 42
277+ with override_environ(INTERACTIVE_TESTS='1'):
278+ view = create_traversed_view(
279+ path_info='/+yuitest/lp/testing/tests/'
280+ 'test_yuixhr_fixture', form=dict(reload='1'))
281+ view.initialize()
282+ # After a render of the html view, the module is reloaded.
283+ view.renderHTML()
284+ self.assertEquals(
285+ None,
286+ test_yuixhr_fixture._fixtures_.get('extra_scribble'))
287+
288+ def test_index_page(self):
289+ view = create_traversed_view(path_info='/+yuitest')
290+ view.initialize()
291+ output = view.render()
292+ self.assertThat(
293+ output,
294+ Contains(
295+ 'href="/+yuitest/lp/testing/tests/test_yuixhr_fixture'))
296
297=== modified file 'lib/lp/testing/tests/test_yuixhr_fixture.js'
298--- lib/lp/testing/tests/test_yuixhr_fixture.js 2011-09-21 00:15:46 +0000
299+++ lib/lp/testing/tests/test_yuixhr_fixture.js 2011-10-13 03:04:49 +0000
300@@ -1,7 +1,7 @@
301 YUI({
302 base: '/+icing/yui/',
303 filter: 'raw', combine: false, fetchCSS: false
304-}).use('test', 'console', 'json', 'cookie', 'lp.testing.serverfixture',
305+}).use('test', 'json', 'cookie', 'lp.testing.serverfixture',
306 function(Y) {
307
308 var suite = new Y.Test.Suite("lp.testing.yuixhr Tests");
309@@ -157,19 +157,12 @@
310 // tests, so we are not logged in practically--the user is gone--but
311 // also our session cookie is gone.
312 Y.Assert.isFalse(Y.Cookie.exists('launchpad_tests'));
313+ },
314+
315+ test_no_setup_can_still_teardown: function() {
316+ module.teardown(this);
317 }
318 }));
319
320-var handle_complete = function(data) {
321- window.status = '::::' + Y.JSON.stringify(data);
322- };
323-Y.Test.Runner.on('complete', handle_complete);
324-Y.Test.Runner.add(suite);
325-
326-var console = new Y.Console({newestOnTop: false});
327-
328-Y.on('domready', function() {
329- console.render('#log');
330- Y.Test.Runner.run();
331-});
332+module.run(suite);
333 });
334
335=== added file 'lib/lp/testing/tests/test_yuixhr_fixture_facet.js'
336--- lib/lp/testing/tests/test_yuixhr_fixture_facet.js 1970-01-01 00:00:00 +0000
337+++ lib/lp/testing/tests/test_yuixhr_fixture_facet.js 2011-10-13 03:04:49 +0000
338@@ -0,0 +1,27 @@
339+YUI({
340+ base: '/+icing/yui/',
341+ filter: 'raw', combine: false, fetchCSS: false
342+}).use('test', 'lp.testing.serverfixture',
343+ function(Y) {
344+
345+var suite = new Y.Test.Suite("lp.testing.yuixhr facet Tests");
346+var serverfixture = Y.lp.testing.serverfixture;
347+
348+
349+/**
350+ * Test how the yuixhr server fixture handles specified facets.
351+ */
352+suite.add(new Y.Test.Case({
353+ name: 'Serverfixture facet tests',
354+
355+ tearDown: function() {
356+ serverfixture.teardown(this);
357+ },
358+
359+ test_facet_was_honored: function() {
360+ Y.Assert.areEqual('bugs.launchpad.dev', Y.config.doc.location.hostname);
361+ }
362+}));
363+
364+serverfixture.run(suite);
365+});
366
367=== added file 'lib/lp/testing/tests/test_yuixhr_fixture_facet.py'
368--- lib/lp/testing/tests/test_yuixhr_fixture_facet.py 1970-01-01 00:00:00 +0000
369+++ lib/lp/testing/tests/test_yuixhr_fixture_facet.py 2011-10-13 03:04:49 +0000
370@@ -0,0 +1,17 @@
371+# Copyright 2011 Canonical Ltd. This software is licensed under the
372+# GNU Affero General Public License version 3 (see the file LICENSE).
373+
374+"""Test the ability to specify a facet for the yuixhr tests.
375+"""
376+
377+__metaclass__ = type
378+__all__ = []
379+
380+from lp.testing.yuixhr import make_suite
381+
382+
383+def test_suite():
384+ # You can specify a facet, as found in the vhost.* names in
385+ # [root]/configs/testrunner-appserver/launchpad-lazr.conf . This
386+ # can be convenient for code that must be run within a given subdomain.
387+ return make_suite(__name__, 'bugs')
388
389=== modified file 'lib/lp/testing/yuixhr.py'
390--- lib/lp/testing/yuixhr.py 2011-09-21 00:24:13 +0000
391+++ lib/lp/testing/yuixhr.py 2011-10-13 03:04:49 +0000
392@@ -11,6 +11,7 @@
393 'YUITestFixtureControllerView',
394 ]
395
396+from fnmatch import fnmatchcase
397 import os
398 import simplejson
399 import sys
400@@ -158,6 +159,7 @@
401 HTML = 'HTML'
402 SETUP = 'SETUP'
403 TEARDOWN = 'TEARDOWN'
404+ INDEX = 'INDEX'
405
406 page_template = dedent("""\
407 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
408@@ -169,6 +171,8 @@
409 src="/+icing/rev%(revno)s/build/launchpad.js"></script>
410 <link rel="stylesheet"
411 href="/+icing/yui/assets/skins/sam/skin.css"/>
412+ <link type="text/css" rel="stylesheet" media="screen, print"
413+ href="https://fonts.googleapis.com/css?family=Ubuntu:400,400italic,700,700italic" />
414 <link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>
415 <style>
416 /* Taken and customized from testlogger.css */
417@@ -193,6 +197,47 @@
418 </head>
419 <body class="yui3-skin-sam">
420 <div id="log"></div>
421+ <p>Want to re-run your test?</p>
422+ <ul>
423+ <li><a href="?">Reload test JS</a></li>
424+ <li><a href="?reload=1">Reload test JS and the associated
425+ Python fixtures</a></li>
426+ </ul>
427+ <p>Don't forget to run <code>make jsbuild</code> and then do a
428+ hard reload of this page if you change a file that is built
429+ into launchpad.js!</p>
430+ <p>If you change Python code other than the fixtures, you must
431+ restart the server. Sorry.</p>
432+ </body>
433+ </html>
434+ """)
435+
436+ index_template = dedent("""\
437+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
438+ "http://www.w3.org/TR/html4/strict.dtd">
439+ <html>
440+ <head>
441+ <title>YUI XHR Tests</title>
442+ <script type="text/javascript"
443+ src="/+icing/rev%(revno)s/build/launchpad.js"></script>
444+ <link type="text/css" rel="stylesheet" media="screen, print"
445+ href="https://fonts.googleapis.com/css?family=Ubuntu:400,400italic,700,700italic" />
446+ <link rel="stylesheet"
447+ href="/+icing/yui/assets/skins/sam/skin.css"/>
448+ <link rel="stylesheet" href="/+icing/rev%(revno)s/combo.css"/>
449+ <style>
450+ ul {
451+ text-align: left;
452+ }
453+ body, ul, h1 {
454+ margin: 0.3em;
455+ padding: 0.3em;
456+ }
457+ </style>
458+ </head>
459+ <body class="yui3-skin-sam">
460+ <h1>YUI XHR Tests</h1>
461+ <ul>%(tests)s</ul>
462 </body>
463 </html>
464 """)
465@@ -208,6 +253,9 @@
466 return os.path.join(*self.names)
467
468 def initialize(self):
469+ if not self.names:
470+ self.action = self.INDEX
471+ return
472 path, ext = os.path.splitext(self.traversed_path)
473 full_path = os.path.join(config.root, 'lib', path)
474 if not os.path.exists(full_path + '.py'):
475@@ -236,72 +284,145 @@
476 assert os.path.sep not in name, (
477 'traversed name contains os.path.sep: %s' % name)
478 assert name != '..', 'traversing to ..'
479- self.names.append(name)
480+ if name:
481+ self.names.append(name)
482 return self
483
484 def browserDefault(self, request):
485 return self, ()
486
487- def page(self):
488+ @property
489+ def module_name(self):
490+ return '.'.join(self.names)
491+
492+ def get_fixtures(self):
493+ module = __import__(
494+ self.module_name, globals(), locals(), ['_fixtures_'], 0)
495+ return module._fixtures_
496+
497+ def renderINDEX(self):
498+ root = os.path.join(config.root, 'lib')
499+ test_lines = []
500+ for path in find_tests(root):
501+ test_path = '/+yuitest/' + '/'.join(path)
502+ module_name = '.'.join(path)
503+ try:
504+ module = __import__(
505+ module_name, globals(), locals(), ['test_suite'], 0)
506+ except ImportError:
507+ warning = 'cannot import Python fixture file'
508+ else:
509+ try:
510+ suite_factory = module.test_suite
511+ except AttributeError:
512+ warning = 'cannot find test_suite'
513+ else:
514+ try:
515+ suite = suite_factory()
516+ except EXPLOSIVE_ERRORS:
517+ raise
518+ except:
519+ warning = 'test_suite raises errors'
520+ else:
521+ case = None
522+ for case in suite:
523+ if isinstance(case, YUIAppServerTestCase):
524+ root_url = config.appserver_root_url(
525+ case.facet)
526+ if root_url != 'None':
527+ test_path = root_url + test_path
528+ warning = ''
529+ break
530+ else:
531+ warning = (
532+ 'test suite is not instance of '
533+ 'YUIAppServerTestCase')
534+ link = '<a href="%s">%s</a>' % (test_path, test_path)
535+ if warning:
536+ warning = ' <span class="warning">%s</span>' % warning
537+ test_lines.append('<li>%s%s</li>' % (link, warning))
538+ return self.index_template % {
539+ 'revno': revno,
540+ 'tests': '\n'.join(test_lines)}
541+
542+ def renderJAVASCRIPT(self):
543+ self.request.response.setHeader('Content-Type', 'text/javascript')
544+ self.request.response.setHeader('Cache-Control', 'no-cache')
545+ return open(
546+ os.path.join(config.root, 'lib', self.traversed_path))
547+
548+ def renderHTML(self):
549+ self.request.response.setHeader('Content-Type', 'text/html')
550+ self.request.response.setHeader('Cache-Control', 'no-cache')
551+ if ('INTERACTIVE_TESTS' in os.environ and
552+ 'reload' in self.request.form):
553+ # We should try to reload the module.
554+ module = sys.modules.get(self.module_name)
555+ if module is not None:
556+ del module._fixtures_
557+ reload(module)
558 return self.page_template % dict(
559 test_module='/+yuitest/%s.js' % self.traversed_path,
560 revno=revno)
561
562- def get_fixtures(self):
563- module_name = '.'.join(self.names)
564- test_module = __import__(
565- module_name, globals(), locals(), ['_fixtures_'], 0)
566- return test_module._fixtures_
567+ def renderSETUP(self):
568+ data = {}
569+ fixtures = self.get_fixtures()
570+ try:
571+ for fixture_name in self.fixtures:
572+ __traceback_info__ = (fixture_name, data)
573+ fixtures[fixture_name](self.request, data)
574+ except EXPLOSIVE_ERRORS:
575+ raise
576+ except:
577+ self.request.response.setStatus(500)
578+ result = ''.join(format_exception(*sys.exc_info()))
579+ else:
580+ self.request.response.setHeader(
581+ 'Content-Type', 'application/json')
582+ # We use the ProxyFactory so that the restful
583+ # redaction code is always used.
584+ result = simplejson.dumps(
585+ ProxyFactory(data), cls=ResourceJSONEncoder)
586+ return result
587+
588+ def renderTEARDOWN(self):
589+ data = simplejson.loads(self.request.form['data'])
590+ fixtures = self.get_fixtures()
591+ try:
592+ for fixture_name in reversed(self.fixtures):
593+ __traceback_info__ = (fixture_name, data)
594+ fixtures[fixture_name].teardown(self.request, data)
595+ except EXPLOSIVE_ERRORS:
596+ raise
597+ except:
598+ self.request.response.setStatus(500)
599+ result = ''.join(format_exception(*sys.exc_info()))
600+ else:
601+ # Remove the session cookie, in case we have one.
602+ self.request.response.expireCookie(
603+ getUtility(IClientIdManager).namespace)
604+ # Blow up the database once we are out of this transaction
605+ # by passing a result that will do so when it is iterated
606+ # through in asyncore.
607+ self.request.response.setHeader('Content-Length', 1)
608+ result = CloseDbResult()
609+ return result
610
611 def render(self):
612- if self.action == self.JAVASCRIPT:
613- self.request.response.setHeader('Content-Type', 'text/javascript')
614- result = open(
615- os.path.join(config.root, 'lib', self.traversed_path))
616- elif self.action == self.HTML:
617- self.request.response.setHeader('Content-Type', 'text/html')
618- result = self.page()
619- elif self.action == self.SETUP:
620- data = {}
621- fixtures = self.get_fixtures()
622- try:
623- for fixture_name in self.fixtures:
624- __traceback_info__ = (fixture_name, data)
625- fixtures[fixture_name](self.request, data)
626- except EXPLOSIVE_ERRORS:
627- raise
628- except:
629- self.request.response.setStatus(500)
630- result = ''.join(format_exception(*sys.exc_info()))
631- else:
632- self.request.response.setHeader(
633- 'Content-Type', 'application/json')
634- # We use the ProxyFactory so that the restful
635- # redaction code is always used.
636- result = simplejson.dumps(
637- ProxyFactory(data), cls=ResourceJSONEncoder)
638- elif self.action == self.TEARDOWN:
639- data = simplejson.loads(self.request.form['data'])
640- fixtures = self.get_fixtures()
641- try:
642- for fixture_name in reversed(self.fixtures):
643- __traceback_info__ = (fixture_name, data)
644- fixtures[fixture_name].teardown(self.request, data)
645- except EXPLOSIVE_ERRORS:
646- raise
647- except:
648- self.request.response.setStatus(500)
649- result = ''.join(format_exception(*sys.exc_info()))
650- else:
651- # Remove the session cookie, in case we have one.
652- self.request.response.expireCookie(
653- getUtility(IClientIdManager).namespace)
654- # Blow up the database once we are out of this transaction
655- # by passing a result that will do so when it is iterated
656- # through in asyncore.
657- self.request.response.setHeader('Content-Length', 1)
658- result = CloseDbResult()
659- return result
660+ return getattr(self, 'render' + self.action)()
661+
662+
663+def find_tests(root):
664+ for dirpath, dirnames, filenames in os.walk(root):
665+ dirpath = os.path.relpath(dirpath, root)
666+ for filename in filenames:
667+ if fnmatchcase(filename, 'test_*.js'):
668+ name, ext = os.path.splitext(filename)
669+ if name + '.py' in filenames:
670+ names = dirpath.split(os.path.sep)
671+ names.append(name)
672+ yield names
673
674
675 # This class cannot be imported directly into a test suite because
676@@ -313,19 +434,21 @@
677 layer = YUIAppServerLayer
678 _testMethodName = 'runTest'
679
680- def __init__(self, module_name=None):
681+ def __init__(self, module_name, facet='mainsite'):
682 self.module_name = module_name
683+ self.facet = facet
684 # This needs to be done early so the "id" is set correctly.
685 self.test_path = self.module_name.replace('.', '/')
686 super(YUIAppServerTestCase, self).__init__()
687
688 def setUp(self):
689- root_url = LayerProcessController.appserver_root_url()
690- self.html_uri = '%s+yuitest/%s' % (root_url, self.test_path)
691+ config = LayerProcessController.appserver_config
692+ root_url = config.appserver_root_url(self.facet)
693+ self.html_uri = '%s/+yuitest/%s' % (root_url, self.test_path)
694 super(YUIAppServerTestCase, self).setUp()
695
696 runTest = AbstractYUITestCase.checkResults
697
698
699-def make_suite(module_name):
700- return unittest.TestSuite([YUIAppServerTestCase(module_name)])
701+def make_suite(module_name, facet='mainsite'):
702+ return unittest.TestSuite([YUIAppServerTestCase(module_name, facet)])