← Back to team overview

launchpad-reviewers team mailing list archive

[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 =