launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #06490
[Merge] lp:~rvb/maas/maas-js-tests into lp:maas
Raphaël Badin has proposed merging lp:~rvb/maas/maas-js-tests into lp:maas.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~rvb/maas/maas-js-tests/+merge/94520
This branch adds the running of our YUI3 unit tests to the test suite (i.e. 'make check' will now run the YUI3 unit tests). It uses SST (http://testutils.org/sst/) for that. In many ways, SST is still rough around the edges but it does the job. The trick was to integrate it properly in our nose-run test suite.
This is not yet satisfying because all the HTML tests are run as one test from nose's pow but the error message given when a test fails is readable nonetheless and it is a good start.
I had to create a temporary HTTP server because SST forces use to have urls starting with 'http://' (http://bit.ly/yg3nN2).
I modified testrunner.js to be able to tell when the tests are finisehd running and also to be able to fetch the detailed results of the test run.
I also had to silent quite a few things to get a readable error message when tests are failing (SilentHTTPRequestHandler, LoggerSilencerMixin).
Finally, I moved "from maasserver import provisioning" to models.py: it requires Django to be properly setup so it must be in the (automatically imported by Django) models.py file to allow us to load modules in src/masserver without having Django setup.
--
https://code.launchpad.net/~rvb/maas/maas-js-tests/+merge/94520
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~rvb/maas/maas-js-tests into lp:maas.
=== modified file 'HACKING.txt'
--- HACKING.txt 2012-02-23 14:00:47 +0000
+++ HACKING.txt 2012-02-24 11:18:26 +0000
@@ -33,9 +33,10 @@
$ sudo apt-get install bzr
-If you intend to run the test suite, you also need libxslt1-dev and libxml2-dev::
+If you intend to run the test suite, you also need libxslt1-dev, libxml2-dev,
+xvfb and firefox::
- $ sudo apt-get install libxslt1-dev libxml2-dev
+ $ sudo apt-get install libxslt1-dev libxml2-dev xvfb firefox
All other development dependencies are pulled automatically from `PyPI`_
when buildout runs.
=== modified file 'buildout.cfg'
--- buildout.cfg 2012-02-23 13:45:05 +0000
+++ buildout.cfg 2012-02-24 11:18:26 +0000
@@ -25,6 +25,7 @@
nose
nose-subunit
python-subunit
+ sst
testresources
testtools
=== modified file 'src/maasserver/__init__.py'
--- src/maasserver/__init__.py 2012-02-08 14:22:23 +0000
+++ src/maasserver/__init__.py 2012-02-24 11:18:26 +0000
@@ -10,9 +10,3 @@
__metaclass__ = type
__all__ = []
-
-from maasserver import provisioning
-
-# This has been imported so that it can register its signal handlers early on,
-# before it misses anything. (Mentioned below to silence lint warnings.)
-provisioning
=== modified file 'src/maasserver/models.py'
--- src/maasserver/models.py 2012-02-23 11:20:39 +0000
+++ src/maasserver/models.py 2012-02-24 11:18:26 +0000
@@ -670,3 +670,8 @@
'Invalid permission check (invalid permission name).')
return obj.owner in (None, user)
+
+
+from maasserver import provisioning
+# We mentioned provisioning here to silence lint warnings.
+provisioning
=== modified file 'src/maasserver/static/js/testing/testrunner.js'
--- src/maasserver/static/js/testing/testrunner.js 2012-02-03 14:39:50 +0000
+++ src/maasserver/static/js/testing/testrunner.js 2012-02-24 11:18:26 +0000
@@ -22,9 +22,23 @@
Y.use(suite_name, "test", function(y) {
var module = y, parts = suite_name.split(".");
while (parts.length > 0) { module = module[parts.shift()]; }
- y.Test.Runner.add(module.suite);
- y.Test.Runner.run();
- });
+ var Runner = y.Test.Runner;
+ Runner.add(module.suite);
+
+ var testsFinished = function(){
+ var results = y.Test.Runner.getResults(y.Test.Format.JSON);
+ // Publish the results in a new node.
+ var result_node = Y.Node.create('<div />')
+ .set('id', 'test_results')
+ .set('text', results);
+ Y.one('body').append(result_node);
+ // Set the suite_node content to 'done'.
+ suite_node.set('text', 'done');
+ };
+ Runner.subscribe(Runner.COMPLETE_EVENT, testsFinished);
+
+ Runner.run();
+ });
}
});
});
=== added file 'src/maasserver/tests/test_js.py'
--- src/maasserver/tests/test_js.py 1970-01-01 00:00:00 +0000
+++ src/maasserver/tests/test_js.py 2012-02-24 11:18:26 +0000
@@ -0,0 +1,167 @@
+# Copyright 2012 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Run YUI3 unit tests with SST (http://testutils.org/sst/)."""
+
+from __future__ import (
+ print_function,
+ unicode_literals,
+ )
+
+__metaclass__ = type
+__all__ = [
+ 'TestYUIUnitTests',
+ ]
+
+import BaseHTTPServer
+import json
+import logging
+import os
+import SimpleHTTPServer
+import SocketServer
+import threading
+
+from fixtures import Fixture
+from pyvirtualdisplay import Display
+from sst.actions import (
+ assert_text,
+ get_element,
+ go_to,
+ set_base_url,
+ start,
+ stop,
+ wait_for,
+ )
+from testtools import TestCase
+
+# Parameters used by SST for testing.
+BROWSER_TYPE = 'Firefox'
+BROWSER_VERSION = ''
+BROWSER_PLATFORM = 'ANY'
+# Base path where the HTML files will be searched.
+BASE_PATH = 'src/maasserver/static/js/tests/'
+# Port used by the temporary http server used for testing.
+TESTING_HTTP_PORT = 18463
+
+
+class LoggerSilencerMixin:
+ """Utility mixin to change the log level of loggers.
+
+ All the loggers with names self.logger_names will be changed to
+ self.level (logging.ERROR by default).
+ """
+ logger_names = []
+ level = logging.ERROR
+
+ def __init__(self):
+ for logger_name in self.logger_names:
+ logging.getLogger(logger_name).setLevel(logging.ERROR)
+
+
+class DisplayFixture(LoggerSilencerMixin, Fixture):
+ """Fixture to create a virtual display with pyvirtualdisplay.Display."""
+
+ logger_names = ['easyprocess', 'pyvirtualdisplay']
+
+ def __init__(self, visible=False, size=(1280, 1024)):
+ super(DisplayFixture, self).__init__()
+ self.visible = visible
+ self.size = size
+
+ def setUp(self):
+ super(DisplayFixture, self).setUp()
+ self.display = Display(
+ visible=self.visible, size=self.size)
+ self.display.start()
+ self.addCleanup(self.display.stop)
+
+
+class ThreadingHTTPServer(SocketServer.ThreadingMixIn,
+ BaseHTTPServer.HTTPServer):
+ pass
+
+
+class SilentHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+ # SimpleHTTPRequestHandler logs to stdout: silence it.
+ log_request = lambda *args, **kwargs: None
+ log_error = lambda *args, **kwargs: None
+
+
+class StaticServerFixture(Fixture):
+ """Setup an HTTP server that will serve static files.
+
+ This is only required because SST forces us to request urls that start
+ with 'http://' (and thus does not allow us to use urls starting with
+ 'file:///').
+ """
+
+ port = TESTING_HTTP_PORT
+
+ def __init__(self):
+ self.server = ThreadingHTTPServer(
+ ('localhost', self.port), SilentHTTPRequestHandler)
+ self.server.daemon = True
+ self.server_thread = threading.Thread(target=self.server.serve_forever)
+
+ def setUp(self):
+ super(StaticServerFixture, self).setUp()
+ self.server_thread.start()
+ self.addCleanup(self.server.shutdown)
+
+
+class SSTFixture(LoggerSilencerMixin, Fixture):
+ """Setup a javascript-enabled testing browser instance with SST."""
+
+ logger_names = ['selenium.webdriver.remote.remote_connection']
+
+ def setUp(self):
+ super(SSTFixture, self).setUp()
+ start(BROWSER_TYPE, BROWSER_VERSION, BROWSER_PLATFORM,
+ session_name=None, javascript_disabled=False,
+ assume_trusted_cert_issuer=False,
+ webdriver_remote=None)
+ self.addCleanup(stop)
+
+
+class TestYUIUnitTests(TestCase):
+
+ def setUp(self):
+ super(TestYUIUnitTests, self).setUp()
+ self.useFixture(DisplayFixture())
+ self.port = self.useFixture(StaticServerFixture()).port
+ self.useFixture(SSTFixture())
+
+ def _get_failed_tests_message(self, results):
+ """Return a readable error message with the list of the failed tests.
+
+ Given a YUI3 results_ json object, return a readable error message.
+
+ .. _results: http://yuilibrary.com/yui/docs/test/
+ """
+ result = []
+ suites = [item for item in results.values() if isinstance(item, dict)]
+ for suite in suites:
+ if suite['failed'] != 0:
+ tests = [item for item in suite.values()
+ if isinstance(item, dict)]
+ for test in tests:
+ if test['result'] == 'fail':
+ result.append('\n%s.%s: %s\n' % (
+ suite['name'], test['name'], test['message']))
+ return ''.join(result)
+
+ def test_YUI3_unit_tests(self):
+ set_base_url('localhost:%d' % self.port)
+ # Find all the HTML files in BASE_PATH.
+ for fname in os.listdir(BASE_PATH):
+ if fname.endswith('.html'):
+ # Load the page and then wait for #suite to contain
+ # 'done'. Read the results in '#test_results'.
+ go_to("%s%s" % (BASE_PATH, fname))
+ wait_for(assert_text, 'suite', 'done')
+ results = json.loads(get_element(id='test_results').text)
+ if results['failed'] != 0:
+ raise AssertionError(
+ '%d test(s) failed.\n%s' % (
+ results['failed'],
+ self._get_failed_tests_message(results)))
=== modified file 'versions.cfg'
--- versions.cfg 2012-02-23 13:45:05 +0000
+++ versions.cfg 2012-02-24 11:18:26 +0000
@@ -24,6 +24,7 @@
pyyaml = 3.10
rabbitfixture = 0.3.2
South =
+sst =
testresources >= 0.2.4-r58
testtools =
twisted =