Add a QUnitTestCase class to run qunit test case.
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Tue, 01 Jun 2010 18:18:26 +0200
changeset 5742 74c19dac29cf
parent 5739 aaf9f5ea1405
child 5743 4a0600664f85
Add a QUnitTestCase class to run qunit test case.
bin/qunit
devtools/qunit.py
devtools/test/data/js_examples/dep_1.js
devtools/test/data/js_examples/deps_2.js
devtools/test/data/js_examples/test_simple_failure.js
devtools/test/data/js_examples/test_simple_success.js
devtools/test/data/js_examples/test_with_dep.js
devtools/test/data/js_examples/test_with_ordered_deps.js
devtools/test/data/js_examples/utils.js
devtools/test/unittest_qunit.py
doc/book/en/devweb/js.rst
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/qunit	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+import sys
+
+from cubicweb.devtools.qunit import make_qunit_html
+
+print make_qunit_html(sys.argv[1], sys.argv[2:])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/qunit.py	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,277 @@
+import os, os.path as osp
+import signal
+from tempfile import mkdtemp, NamedTemporaryFile
+import tempfile
+from Queue import Queue, Empty
+from subprocess import Popen, check_call
+from shutil import rmtree, copy as copyfile
+from uuid import uuid4 
+
+# imported by default to simplify further import statements
+from logilab.common.testlib import unittest_main, with_tempdir, InnerTest
+
+import os
+import cubicweb
+from cubicweb.view import StartupView
+from cubicweb.web.controller import Controller
+from cubicweb.devtools.httptest import CubicWebServerTC
+
+class FirefoxHelper(object):
+
+    profile_name_mask = 'PYTEST_PROFILE_%(uid)s'
+
+    def __init__(self, url=None):
+        self._process = None
+        self._tmp_dir = mkdtemp()
+        self._profile_data = {'uid': uuid4()}
+        self._profile_name = self.profile_name_mask % self._profile_data
+        fnull = open(os.devnull, 'w')
+        check_call(['firefox', '-no-remote', '-CreateProfile',
+                    '%s %s' % (self._profile_name, self._tmp_dir)],
+                              stdout=fnull, stderr=fnull)
+        if url is not None:
+            self.start(url)
+
+
+    def start(self, url):
+        self.stop()
+        fnull = open(os.devnull, 'w')
+        self._process = Popen(['firefox', '-no-remote', '-P', self._profile_name, url],
+                              stdout=fnull, stderr=fnull)
+
+    def stop(self):
+        if self._process is not None:
+            os.kill(self._process.pid, signal.SIGTERM)
+            self._process.wait()
+            self._process = None
+
+    def __del__(self):
+        self.stop()
+        rmtree(self._tmp_dir)
+
+
+class QUnitTestCase(CubicWebServerTC):
+
+    # testfile, (dep_a, dep_b)
+    all_js_tests = ()
+
+    def setUp(self):
+        super(QUnitTestCase, self).setUp()
+        self.test_queue = Queue()
+        class MyQUnitResultController(QUnitResultController):
+            tc = self
+            test_queue = self.test_queue
+        self._qunit_controller = MyQUnitResultController
+        self.vreg.register(MyQUnitResultController)
+
+    def tearDown(self):
+        super(QUnitTestCase, self).tearDown()
+        self.vreg.unregister(self._qunit_controller)
+
+
+    def abspath(self, path):
+        """use self.__module__ to build absolute path if necessary"""
+        if not osp.isabs(path):
+           dirname = osp.dirname(__import__(self.__module__).__file__)
+           return osp.abspath(osp.join(dirname,path))
+        return path
+
+
+
+    def test_javascripts(self):
+        for args in self.all_js_tests:
+            test_file = self.abspath(args[0])
+            if len(args) > 1:
+                depends   = [self.abspath(dep) for dep in args[1]]
+            else:
+                depends = ()
+            if len(args) > 2:
+                data   = [self.abspath(data) for data in args[2]]
+            else:
+                data = ()
+            for js_test in self._test_qunit(test_file, depends, data):
+                yield js_test
+
+    @with_tempdir
+    def _test_qunit(self, test_file, depends=(), data_files=(), timeout=30):
+        assert osp.exists(test_file), test_file
+        for dep in depends:
+            assert osp.exists(dep), dep
+        for data in data_files:
+            assert osp.exists(data), data
+
+
+        # generate html test file
+        html_test_file = NamedTemporaryFile(suffix='.html')
+        html_test_file.write(make_qunit_html(test_file, depends,
+                             server_data=(self.test_host, self.test_port)))
+        html_test_file.flush()
+        # copying data file
+        for data in data_files:
+            copyfile(data, tempfile.tempdir)
+
+        while not self.test_queue.empty():
+            self.test_queue.get(False)
+
+        browser = FirefoxHelper()
+        browser.start(html_test_file.name)
+        test_count = 0
+        error = False
+        def raise_exception(cls, *data):
+            raise cls(*data)
+        while not error:
+            try:
+                result, test_name, msg = self.test_queue.get(timeout=timeout)
+                test_name = '%s (%s)' % (test_name, test_file)
+                self.set_description(test_name)
+                if result is None:
+                    break
+                test_count += 1
+                if result:
+                    yield InnerTest(test_name, lambda : 1)
+                else:
+                    yield InnerTest(test_name, self.fail, msg)
+            except Empty:
+                error = True
+                yield InnerTest(test_file, raise_exception, RuntimeError, "%s did not report execution end. %i test processed so far." % (test_file, test_count))
+
+        browser.stop()
+        if test_count <= 0 and not error:
+            yield InnerTest(test_name, raise_exception, RuntimeError, 'No test yielded by qunit for %s' % test_file)
+
+class QUnitResultController(Controller):
+
+    __regid__ = 'qunit_result'
+
+
+    # Class variables to circumvent the instantiation of a new Controller for each request.
+    _log_stack = [] # store QUnit log messages
+    _current_module_name = '' # store the current QUnit module name
+
+    def publish(self, rset=None):
+        event = self._cw.form['event']
+        getattr(self, 'handle_%s' % event)()
+
+    def handle_module_start(self):
+        self.__class__._current_module_name = self._cw.form.get('name', '')
+
+    def handle_test_done(self):
+        name = '%s // %s' %  (self._current_module_name, self._cw.form.get('name', ''))
+        failures = int(self._cw.form.get('failures', 0))
+        total = int(self._cw.form.get('total', 0))
+
+        self._log_stack.append('%i/%i assertions failed' % (failures, total))
+        msg = '\n'.join(self._log_stack)
+
+        if failures:
+            self.tc.test_queue.put((False, name, msg))
+        else:
+            self.tc.test_queue.put((True, name, msg))
+        self._log_stack[:] = []
+
+    def handle_done(self):
+        self.tc.test_queue.put((None, None, None))
+
+    def handle_log(self):
+        result = self._cw.form['result']
+        message = self._cw.form['message']
+        self._log_stack.append('%s: %s' % (result, message))
+
+
+
+def cw_path(*paths):
+  return file_path(osp.join(cubicweb.CW_SOFTWARE_ROOT, *paths))
+
+def file_path(path):
+    return 'file://' + osp.abspath(path)
+
+def build_js_script( host, port):
+    return """
+    var host = '%s';
+    var port = '%s';
+
+    QUnit.moduleStart = function (name) {
+      jQuery.ajax({
+                  url: 'http://'+host+':'+port+'/qunit_result',
+                 data: {"event": "module_start",
+                        "name": name},
+                 async: false});
+    }
+
+    QUnit.testDone = function (name, failures, total) {
+      jQuery.ajax({
+                  url: 'http://'+host+':'+port+'/qunit_result',
+                 data: {"event": "test_done",
+                        "name": name,
+                        "failures": failures,
+                        "total":total},
+                 async: false});
+    }
+
+    QUnit.done = function (failures, total) {
+      jQuery.ajax({
+                   url: 'http://'+host+':'+port+'/qunit_result',
+                   data: {"event": "done",
+                          "failures": failures,
+                          "total":total},
+                   async: false});
+      window.close();
+    }
+
+    QUnit.log = function (result, message) {
+      jQuery.ajax({
+                   url: 'http://'+host+':'+port+'/qunit_result',
+                   data: {"event": "log",
+                          "result": result,
+                          "message": message},
+                   async: false});
+    }
+    """ % (host, port)
+
+def make_qunit_html(test_file, depends=(), server_data=None):
+    """"""
+    data = {
+            'web_data': cw_path('web', 'data'),
+            'web_test': cw_path('web', 'test', 'jstests'),
+        }
+
+    html = ['''<html>
+  <head>
+    <!-- JS lib used as testing framework -->
+    <link rel="stylesheet" type="text/css" media="all" href="%(web_test)s/qunit.css" />
+    <script src="%(web_data)s/jquery.js" type="text/javascript"></script>
+    <script src="%(web_test)s/qunit.js" type="text/javascript"></script>'''
+    % data]
+    if server_data is not None:
+        host, port = server_data
+        html.append('<!-- result report tools -->')
+        html.append('<script type="text/javascript">')
+        html.append(build_js_script(host, port))
+        html.append('</script>')
+    html.append('<!-- Test script dependencies (tested code for example) -->')
+
+    for dep in depends:
+        html.append('    <script src="%s" type="text/javascript"></script>' % file_path(dep))
+
+    html.append('    <!-- Test script itself -->')
+    html.append('    <script src="%s" type="text/javascript"></script>'% (file_path(test_file),))
+    html.append('''  </head>
+  <body>
+    <div id="main">
+    </div>
+    <h1 id="qunit-header">QUnit example</h1>
+    <h2 id="qunit-banner"></h2>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>''')
+    return u'\n'.join(html)
+
+
+
+
+
+
+
+if __name__ == '__main__':
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/dep_1.js	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,1 @@
+a = 4;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/deps_2.js	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,1 @@
+b = a +2;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/test_simple_failure.js	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,18 @@
+$(document).ready(function() {
+
+  module("air");
+
+  test("test 1", function() {
+      equals(2, 4);
+  });
+
+  test("test 2", function() {
+      equals('', '45');
+      equals('1024', '32');
+  });
+
+  module("able");
+  test("test 3", function() {
+      same(1, 1);
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/test_simple_success.js	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,17 @@
+$(document).ready(function() {
+
+  module("air");
+
+  test("test 1", function() {
+      equals(2, 2);
+  });
+
+  test("test 2", function() {
+      equals('45', '45');
+  });
+
+  module("able");
+  test("test 3", function() {
+      same(1, 1);
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/test_with_dep.js	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,9 @@
+$(document).ready(function() {
+
+  module("air");
+
+  test("test 1", function() {
+      equals(a, 4);
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/test_with_ordered_deps.js	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,9 @@
+$(document).ready(function() {
+
+  module("air");
+
+  test("test 1", function() {
+      equals(b, 6);
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/js_examples/utils.js	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,29 @@
+function datetuple(d) {
+    return [d.getFullYear(), d.getMonth()+1, d.getDate(), 
+	    d.getHours(), d.getMinutes()];
+}
+    
+function pprint(obj) {
+    print('{');
+    for(k in obj) {
+	print('  ' + k + ' = ' + obj[k]);
+    }
+    print('}');
+}
+
+function arrayrepr(array) {
+    return '[' + array.join(', ') + ']';
+}
+    
+function assertArrayEquals(array1, array2) {
+    if (array1.length != array2.length) {
+	throw new crosscheck.AssertionFailure(array1.join(', ') + ' != ' + array2.join(', '));
+    }
+    for (var i=0; i<array1.length; i++) {
+	if (array1[i] != array2[i]) {
+	    
+	    throw new crosscheck.AssertionFailure(arrayrepr(array1) + ' and ' + arrayrepr(array2)
+						 + ' differs at index ' + i);
+	}
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/unittest_qunit.py	Tue Jun 01 18:18:26 2010 +0200
@@ -0,0 +1,31 @@
+from logilab.common.testlib import unittest_main
+from cubicweb.devtools.qunit import make_qunit_html, QUnitTestCase
+
+from os import path as osp
+
+JSTESTDIR = osp.abspath(osp.join(osp.dirname(__file__), 'data', 'js_examples'))
+
+
+def js(name):
+    return osp.join(JSTESTDIR, name)
+
+class QUnitTestCaseTC(QUnitTestCase):
+
+    all_js_tests = (
+                    (js('test_simple_success.js'),),
+                    (js('test_with_dep.js'), (js('dep_1.js'),)),
+                    (js('test_with_ordered_deps.js'), (js('dep_1.js'), js('deps_2.js'),)),
+                   )
+
+
+    def test_simple_failure(self):
+        js_tests = list(self._test_qunit(js('test_simple_failure.js')))
+        self.assertEquals(len(js_tests), 3)
+        test_1, test_2, test_3 = js_tests
+        self.assertRaises(self.failureException, test_1[0], *test_1[1:])
+        self.assertRaises(self.failureException, test_2[0], *test_2[1:])
+        test_3[0](*test_3[1:])
+
+
+if __name__ == '__main__':
+    unittest_main()
--- a/doc/book/en/devweb/js.rst	Fri Jun 11 16:11:23 2010 +0200
+++ b/doc/book/en/devweb/js.rst	Tue Jun 01 18:18:26 2010 +0200
@@ -358,3 +358,39 @@
     :maxdepth: 1
     
     js_api/index
+
+
+Testing javascript
+~~~~~~~~~~~~~~~~~~~~~~
+
+You with the ``cubicweb.qunit.QUnitTestCase`` can include standard Qunit tests
+inside the python unittest run . You simply have to define a new class that
+inherit from ``QUnitTestCase`` and register your javascript test file in the
+``all_js_tests`` lclass attribut. This  ``all_js_tests`` is a sequence a
+3-tuple (<test_file, [<dependencies> ,] [<data_files>]):
+
+The <test_file> should contains the qunit test. <dependencies> defines the list
+of javascript file that must be imported before the test script.  Dependencies
+are included their definition order. <data_files> are additional files copied in the
+test directory. both <dependencies> and <data_files> are optionnal.
+``jquery.js`` is preincluded in for all test.
+
+.. sourcecode:: python
+
+    from cubicweb.qunit import QUnitTestCase
+
+    class MyQUnitTest(QUnitTestCase):
+
+        all_js_tests = (
+            ("relative/path/to/my_simple_testcase.js",)
+            ("relative/path/to/my_qunit_testcase.js",(
+                "rel/path/to/dependency_1.js",
+                "rel/path/to/dependency_2.js",)),
+            ("relative/path/to/my_complexe_qunit_testcase.js",(
+                 "rel/path/to/dependency_1.js",
+                 "rel/path/to/dependency_2.js",
+               ),(
+                 "rel/path/file_dependency.html",
+                 "path/file_dependency.json")
+                ),
+            )