devtools/qunit.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 from __future__ import absolute_import
       
    19 
       
    20 import os, os.path as osp
       
    21 import errno
       
    22 from tempfile import mkdtemp
       
    23 from subprocess import Popen, PIPE, STDOUT
       
    24 
       
    25 from six.moves.queue import Queue, Empty
       
    26 
       
    27 # imported by default to simplify further import statements
       
    28 from logilab.common.testlib import unittest_main, with_tempdir, InnerTest, Tags
       
    29 import webtest.http
       
    30 
       
    31 import cubicweb
       
    32 from cubicweb.view import View
       
    33 from cubicweb.web.controller import Controller
       
    34 from cubicweb.web.views.staticcontrollers import StaticFileController, STATIC_CONTROLLERS
       
    35 from cubicweb.devtools import webtest as cwwebtest
       
    36 
       
    37 
       
    38 class FirefoxHelper(object):
       
    39 
       
    40     def __init__(self, url=None):
       
    41         self._process = None
       
    42         self._profile_dir = mkdtemp(prefix='cwtest-ffxprof-')
       
    43         self.firefox_cmd = ['firefox', '-no-remote']
       
    44         if os.name == 'posix':
       
    45             self.firefox_cmd = [osp.join(osp.dirname(__file__), 'data', 'xvfb-run.sh'),
       
    46                                 '-a', '-s', '-noreset -screen 0 800x600x24'] + self.firefox_cmd
       
    47 
       
    48     def test(self):
       
    49         try:
       
    50             proc = Popen(['firefox', '--help'], stdout=PIPE, stderr=STDOUT)
       
    51             stdout, _ = proc.communicate()
       
    52             return proc.returncode == 0, stdout
       
    53         except OSError as exc:
       
    54             if exc.errno == errno.ENOENT:
       
    55                 msg = '[%s] %s' % (errno.errorcode[exc.errno], exc.strerror)
       
    56                 return False, msg
       
    57             raise
       
    58 
       
    59     def start(self, url):
       
    60         self.stop()
       
    61         cmd = self.firefox_cmd + ['-silent', '--profile', self._profile_dir,
       
    62                                   '-url', url]
       
    63         with open(os.devnull, 'w') as fnull:
       
    64             self._process = Popen(cmd, stdout=fnull, stderr=fnull)
       
    65 
       
    66     def stop(self):
       
    67         if self._process is not None:
       
    68             assert self._process.returncode is None,  self._process.returncode
       
    69             self._process.terminate()
       
    70             self._process.wait()
       
    71             self._process = None
       
    72 
       
    73     def __del__(self):
       
    74         self.stop()
       
    75 
       
    76 
       
    77 class QUnitTestCase(cwwebtest.CubicWebTestTC):
       
    78 
       
    79     tags = cwwebtest.CubicWebTestTC.tags | Tags(('qunit',))
       
    80 
       
    81     # testfile, (dep_a, dep_b)
       
    82     all_js_tests = ()
       
    83 
       
    84     def setUp(self):
       
    85         super(QUnitTestCase, self).setUp()
       
    86         self.test_queue = Queue()
       
    87         class MyQUnitResultController(QUnitResultController):
       
    88             tc = self
       
    89             test_queue = self.test_queue
       
    90         self._qunit_controller = MyQUnitResultController
       
    91         self.webapp.app.appli.vreg.register(MyQUnitResultController)
       
    92         self.webapp.app.appli.vreg.register(QUnitView)
       
    93         self.webapp.app.appli.vreg.register(CWDevtoolsStaticController)
       
    94         self.server = webtest.http.StopableWSGIServer.create(self.webapp.app)
       
    95         self.config.global_set_option('base-url', self.server.application_url)
       
    96 
       
    97     def tearDown(self):
       
    98         self.server.shutdown()
       
    99         self.webapp.app.appli.vreg.unregister(self._qunit_controller)
       
   100         self.webapp.app.appli.vreg.unregister(QUnitView)
       
   101         self.webapp.app.appli.vreg.unregister(CWDevtoolsStaticController)
       
   102         super(QUnitTestCase, self).tearDown()
       
   103 
       
   104     def test_javascripts(self):
       
   105         for args in self.all_js_tests:
       
   106             self.assertIn(len(args), (1, 2))
       
   107             test_file = args[0]
       
   108             if len(args) > 1:
       
   109                 depends = args[1]
       
   110             else:
       
   111                 depends = ()
       
   112             for js_test in self._test_qunit(test_file, depends):
       
   113                 yield js_test
       
   114 
       
   115     @with_tempdir
       
   116     def _test_qunit(self, test_file, depends=(), timeout=10):
       
   117         QUnitView.test_file = test_file
       
   118         QUnitView.depends = depends
       
   119 
       
   120         while not self.test_queue.empty():
       
   121             self.test_queue.get(False)
       
   122 
       
   123         browser = FirefoxHelper()
       
   124         isavailable, reason = browser.test()
       
   125         if not isavailable:
       
   126             self.fail('firefox not available or not working properly (%s)' % reason)
       
   127         browser.start(self.config['base-url'] + "?vid=qunit")
       
   128         test_count = 0
       
   129         error = False
       
   130         def raise_exception(cls, *data):
       
   131             raise cls(*data)
       
   132         while not error:
       
   133             try:
       
   134                 result, test_name, msg = self.test_queue.get(timeout=timeout)
       
   135                 test_name = '%s (%s)' % (test_name, test_file)
       
   136                 self.set_description(test_name)
       
   137                 if result is None:
       
   138                     break
       
   139                 test_count += 1
       
   140                 if result:
       
   141                     yield InnerTest(test_name, lambda : 1)
       
   142                 else:
       
   143                     yield InnerTest(test_name, self.fail, msg)
       
   144             except Empty:
       
   145                 error = True
       
   146                 msg = '%s inactivity timeout (%is). %i test results received'
       
   147                 yield InnerTest(test_file, raise_exception, RuntimeError,
       
   148                                  msg % (test_file, timeout, test_count))
       
   149         browser.stop()
       
   150         if test_count <= 0 and not error:
       
   151             yield InnerTest(test_name, raise_exception, RuntimeError,
       
   152                             'No test yielded by qunit for %s' % test_file)
       
   153 
       
   154 class QUnitResultController(Controller):
       
   155 
       
   156     __regid__ = 'qunit_result'
       
   157 
       
   158 
       
   159     # Class variables to circumvent the instantiation of a new Controller for each request.
       
   160     _log_stack = [] # store QUnit log messages
       
   161     _current_module_name = '' # store the current QUnit module name
       
   162 
       
   163     def publish(self, rset=None):
       
   164         event = self._cw.form['event']
       
   165         getattr(self, 'handle_%s' % event)()
       
   166         return b''
       
   167 
       
   168     def handle_module_start(self):
       
   169         self.__class__._current_module_name = self._cw.form.get('name', '')
       
   170 
       
   171     def handle_test_done(self):
       
   172         name = '%s // %s' %  (self._current_module_name, self._cw.form.get('name', ''))
       
   173         failures = int(self._cw.form.get('failures', 0))
       
   174         total = int(self._cw.form.get('total', 0))
       
   175 
       
   176         self._log_stack.append('%i/%i assertions failed' % (failures, total))
       
   177         msg = '\n'.join(self._log_stack)
       
   178 
       
   179         if failures:
       
   180             self.tc.test_queue.put((False, name, msg))
       
   181         else:
       
   182             self.tc.test_queue.put((True, name, msg))
       
   183         self._log_stack[:] = []
       
   184 
       
   185     def handle_done(self):
       
   186         self.tc.test_queue.put((None, None, None))
       
   187 
       
   188     def handle_log(self):
       
   189         result = self._cw.form['result']
       
   190         message = self._cw.form.get('message', '<no message>')
       
   191         actual = self._cw.form.get('actual')
       
   192         expected = self._cw.form.get('expected')
       
   193         source = self._cw.form.get('source')
       
   194         log = '%s: %s' % (result, message)
       
   195         if result == 'false' and actual is not None and expected is not None:
       
   196             log += ' (got: %s, expected: %s)' % (actual, expected)
       
   197             if source is not None:
       
   198                 log += '\n' + source
       
   199         self._log_stack.append(log)
       
   200 
       
   201 
       
   202 class QUnitView(View):
       
   203     __regid__ = 'qunit'
       
   204 
       
   205     templatable = False
       
   206 
       
   207     depends = None
       
   208     test_file = None
       
   209 
       
   210     def call(self, **kwargs):
       
   211         w = self.w
       
   212         req = self._cw
       
   213         w(u'''<!DOCTYPE html>
       
   214         <html>
       
   215         <head>
       
   216         <meta http-equiv="content-type" content="application/html; charset=UTF-8"/>
       
   217         <!-- JS lib used as testing framework -->
       
   218         <link rel="stylesheet" type="text/css" media="all" href="/devtools/qunit.css" />
       
   219         <script src="/data/jquery.js" type="text/javascript"></script>
       
   220         <script src="/devtools/cwmock.js" type="text/javascript"></script>
       
   221         <script src="/devtools/qunit.js" type="text/javascript"></script>''')
       
   222         w(u'<!-- result report tools -->')
       
   223         w(u'<script type="text/javascript">')
       
   224         w(u"var BASE_URL = '%s';" % req.base_url())
       
   225         w(u'''
       
   226             QUnit.moduleStart(function (details) {
       
   227               jQuery.ajax({
       
   228                           url: BASE_URL + 'qunit_result',
       
   229                          data: {"event": "module_start",
       
   230                                 "name": details.name},
       
   231                          async: false});
       
   232             });
       
   233 
       
   234             QUnit.testDone(function (details) {
       
   235               jQuery.ajax({
       
   236                           url: BASE_URL + 'qunit_result',
       
   237                          data: {"event": "test_done",
       
   238                                 "name": details.name,
       
   239                                 "failures": details.failed,
       
   240                                 "total": details.total},
       
   241                          async: false});
       
   242             });
       
   243 
       
   244             QUnit.done(function (details) {
       
   245               jQuery.ajax({
       
   246                            url: BASE_URL + 'qunit_result',
       
   247                            data: {"event": "done",
       
   248                                   "failures": details.failed,
       
   249                                   "total": details.total},
       
   250                            async: false});
       
   251             });
       
   252 
       
   253             QUnit.log(function (details) {
       
   254               jQuery.ajax({
       
   255                            url: BASE_URL + 'qunit_result',
       
   256                            data: {"event": "log",
       
   257                                   "result": details.result,
       
   258                                   "actual": details.actual,
       
   259                                   "expected": details.expected,
       
   260                                   "source": details.source,
       
   261                                   "message": details.message},
       
   262                            async: false});
       
   263             });''')
       
   264         w(u'</script>')
       
   265         w(u'<!-- Test script dependencies (tested code for example) -->')
       
   266 
       
   267         for dep in self.depends:
       
   268             w(u'    <script src="%s" type="text/javascript"></script>\n' % dep)
       
   269 
       
   270         w(u'    <!-- Test script itself -->')
       
   271         w(u'    <script src="%s" type="text/javascript"></script>' % self.test_file)
       
   272         w(u'''  </head>
       
   273         <body>
       
   274         <div id="qunit-fixture"></div>
       
   275         <div id="qunit"></div>
       
   276         </body>
       
   277         </html>''')
       
   278 
       
   279 
       
   280 class CWDevtoolsStaticController(StaticFileController):
       
   281     __regid__ = 'devtools'
       
   282 
       
   283     def publish(self, rset=None):
       
   284         staticdir = osp.join(osp.dirname(__file__), 'data')
       
   285         relpath = self.relpath[len(self.__regid__) + 1:]
       
   286         return self.static_file(osp.join(staticdir, relpath))
       
   287 
       
   288 
       
   289 STATIC_CONTROLLERS.append(CWDevtoolsStaticController)
       
   290 
       
   291 
       
   292 if __name__ == '__main__':
       
   293     unittest_main()