convert js tests to HttpCase

bzr revid: al@openerp.com-20140428152857-h7z61vdw5crezv2u
This commit is contained in:
Antony Lesuisse 2014-04-28 17:28:57 +02:00
parent 065b3c2176
commit d9eec7345b
19 changed files with 8 additions and 519 deletions

View File

@ -2,4 +2,3 @@
import test_js
import test_menu
import test_serving_base
import test_ui

View File

@ -1,116 +0,0 @@
QUnitSuite is a ``unittest.TestSuite`` able to run QUnit_ test suites
within the normal unittest process, through PhantomJS_.
QUnitSuite is built upon `Ben Alman`_'s work of for the interfacing
between PhantomJS_ and the host/reporting code: the shims and the
PhantomJS_ configuration files are those of grunt_'s ``qunit`` task.
Why
---
You're a Python shop or developer, you have tools and tests built
around unittest (or compatible with unittests) and your testing
pipeline is predicated upon that, you're doing web development of some
sort these days (as so many are) and you'd like to do some testing of
your web stuff.
But you don't really want to redo your whole testing stack just for
that.
QUnitSuite simply grafts QUnit_-based tests, run in PhantomJS_, in
your existing ``unittest``-based architecture.
What
----
QUnitSuite currently provides a single object as part of its API:
``qunitsuite.QUnitSuite(testfile[, timeout])``.
This produces a ``unittest.TestSuite`` suitable for all the usual
stuff (running it, and giving it to an other test suite which will run
it, that is).
``testfile`` is the HTML file bootstrapping your qunit tests, as would
usually be accessed via a browser. It can be either a local
(``file:``) url, or an HTTP one. As long as a regular browser can open
and execute it, PhantomJS_ will manage.
``timeout`` is a check passed to the PhantomJS_ runner: if the runner
produces no information for longer than ``timeout`` milliseconds, the
run will be cancelled and a test error will be generated. This
situation usually means either your ``testfile`` is not a qunit test
file, qunit is not running or qunit's runner was stopped (for an async
test) and never restarted.
The default value is very conservative, most tests should run
correctly with lower timeouts (especially if all tests are
synchronous).
How
---
``unittest``'s autodiscovery protocol does not directly work with test
suites (it looks for test cases). If you want autodiscovery to work
correctly, you will have to use the ``load_tests`` protocol::
# in a testing module
def load_tests(loader, tests, pattern):
tests.addTest(QUnitSuite(qunit_test_path.html))
return tests
outside of that specific case, you can use a ``QUnitSuite`` as a
standard ``TestSuite`` instance, running it, adding it to an other
suite or passing it to a ``TestRunner``
Complaints and Grievances
-------------------------
Speed
~~~~~
Starting up a phantomjs instance and running a suite turns out to have
a rather high overhead, on the order of a second on this machine
(2.4GHz, 8GB RAM and an SSD).
As each ``QUnitSuite`` currently creates its own phantomjs instance,
it's probably a good idea to create bigger suites (put many modules &
tests in the same QUnit html file, which doesn't preclude splitting
them across multiple js files).
Hacks
~~~~~
QUnitSuite contains a pretty big hack which may or may not cause
problem depending on your exact setup: in case of case failure or
error, ``unittest.TestResult`` formats the error traceback provided
alongside the test object. This goes through Python's
traceback-formatting code and there are no hooks there.
One could expect to use a custom ``TestResult``, but for test suites
the ``TestResult`` instance must be provided by the caller, so there
is no direct hook onto it.
This leaves three options:
* Create a custom ``TestResult`` class and require that it be the one
provided to the test suite. This requires altered work flows,
customization of the test runner and (as far as I know) isn't
available through Python 2.7's autodiscovery. It's the cleanest
option but completely fails on practicality.
* Create a custom ``TestResult`` which directly alters the original
result's ``errors`` and ``failures`` attributes as they're part of
the testrunner API. This would work but may put custom results in a
strange state and break e.g. unittest2's ``@failfast``.
* Lastly, monkeypatch the undocumented and implementation detail
``_exc_info_to_string`` on the provided ``result``. This is the
route taken, at least for now.
.. _QUnit: http://qunitjs.com/
.. _PhantomJS: http://phantomjs.org/
.. _Ben Alman: http://benalman.com/
.. _grunt: http://gruntjs.com/

View File

@ -1,95 +0,0 @@
/*
* grunt
* http://gruntjs.com/
*
* Copyright (c) 2012 "Cowboy" Ben Alman
* Licensed under the MIT license.
* http://benalman.com/about/license/
*/
/*global phantom:true*/
'use strict';
var fs = require('fs');
// The page .html file to load.
var url = phantom.args[0];
// Extra, optionally overridable stuff.
var options = JSON.parse(phantom.args[1] || {});
// Default options.
if (!options.timeout) { options.timeout = 5000; }
// Keep track of the last time a client message was sent.
var last = new Date();
// Messages are sent to the parent by appending them to the tempfile.
var sendMessage = function(arg) {
var args = Array.isArray(arg) ? arg : [].slice.call(arguments);
last = new Date();
console.log(JSON.stringify(args));
};
// This allows grunt to abort if the PhantomJS version isn't adequate.
sendMessage('private', 'version', phantom.version);
// Abort if the page doesn't send any messages for a while.
setInterval(function() {
if (new Date() - last > options.timeout) {
sendMessage('fail.timeout');
phantom.exit();
}
}, 100);
// Create a new page.
var page = require('webpage').create();
// The client page must send its messages via alert(jsonstring).
page.onAlert = function(args) {
sendMessage(JSON.parse(args));
};
// Keep track if the client-side helper script already has been injected.
var injected;
page.onUrlChanged = function(newUrl) {
injected = false;
sendMessage('onUrlChanged', newUrl);
};
// Relay console logging messages.
page.onConsoleMessage = function(message) {
sendMessage('console', message);
};
// For debugging.
page.onResourceRequested = function(request) {
sendMessage('onResourceRequested', request.url);
};
page.onResourceReceived = function(request) {
if (request.stage === 'end') {
sendMessage('onResourceReceived', request.url);
}
};
// Run when the page has finished loading.
page.onLoadFinished = function(status) {
// The window has loaded.
sendMessage('onLoadFinished', status);
if (status === 'success') {
if (options.inject && !injected) {
// Inject client-side helper script, but only if it has not yet been
// injected.
sendMessage('inject', options.inject);
page.injectJs(options.inject);
}
} else {
// File loading failure.
sendMessage('fail.load', url);
phantom.exit();
}
};
// Actually load url.
page.open(url);

View File

@ -1,22 +0,0 @@
Copyright (c) 2012 "Cowboy" Ben Alman
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,88 +0,0 @@
/*
* grunt
* http://gruntjs.com/
*
* Copyright (c) 2012 "Cowboy" Ben Alman
* Licensed under the MIT license.
* http://benalman.com/about/license/
*/
/*global QUnit:true, alert:true*/
'use strict';
// Don't re-order tests.
QUnit.config.reorder = false;
// Run tests serially, not in parallel.
QUnit.config.autorun = false;
// Send messages to the parent PhantomJS process via alert! Good times!!
function sendMessage() {
var args = [].slice.call(arguments);
alert(JSON.stringify(args));
}
// These methods connect QUnit to PhantomJS.
QUnit.log(function(obj) {
// What is this I dont even
if (obj.message === '[object Object], undefined:undefined') { return; }
// Parse some stuff before sending it.
var actual = QUnit.jsDump.parse(obj.actual);
var expected = QUnit.jsDump.parse(obj.expected);
// Send it.
sendMessage('qunit.log', obj.result, actual, expected, obj.message, obj.source);
});
QUnit.testStart(function(obj) {
sendMessage('qunit.testStart', obj.name);
});
QUnit.testDone(function(obj) {
sendMessage('qunit.testDone', obj.name, obj.failed, obj.passed, obj.total);
});
QUnit.moduleStart(function(obj) {
sendMessage('qunit.moduleStart', obj.name);
});
QUnit.moduleDone(function(obj) {
sendMessage('qunit.moduleDone', obj.name, obj.failed, obj.passed, obj.total);
});
QUnit.begin(function() {
sendMessage('qunit.begin');
});
QUnit.done(function(obj) {
sendMessage('qunit.done', obj.failed, obj.passed, obj.total, obj.runtime);
});
// PhantomJS (up to and including 1.7) uses a version of webkit so old
// it does not have Function.prototype.bind:
// http://code.google.com/p/phantomjs/issues/detail?id=522
// Use moz polyfill:
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== "function") {
// closest thing possible to the ECMAScript 5 internal IsCallable function
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}

View File

@ -1,136 +0,0 @@
import json
import subprocess
import unittest
import os
import time
ROOT = os.path.join(os.path.dirname(__file__), 'grunt')
__all__ = ['QUnitSuite']
def _exc_info_to_string(err, test):
return err
class QUnitTest(unittest.TestCase):
def __init__(self, module, name):
self.module = module
self.name = name
self.failed = False
def shortDescription(self):
return None
def __repr__(self):
return '<QUnitTest %s:%s>' % (self.module, self.name)
def __str__(self):
return '%s: %s' % (self.module, self.name)
class QUnitSuite(unittest.TestSuite):
def __init__(self, qunitfile, timeout=5000):
super(QUnitSuite, self).__init__()
self.testfile = qunitfile
self.timeout = timeout
self._module = None
self._test = None
def run(self, result):
try:
subprocess.call(['phantomjs', '-v'],
stdout=open(os.devnull, 'w'),
stderr=subprocess.STDOUT)
except OSError:
test = QUnitTest('phantomjs', 'javascript tests')
result.startTest(test)
result.startTest(test)
result.addSkip(test , "phantomjs command not found")
result.stopTest(test)
return
result._exc_info_to_string = _exc_info_to_string
try:
self._run(result)
finally:
del result._exc_info_to_string
def _run(self, result):
phantom = subprocess.Popen([
'phantomjs',
'--config=%s' % os.path.join(ROOT, 'phantomjs.json'),
os.path.join(ROOT, 'bootstrap.js'), self.testfile,
json.dumps({
'timeout': self.timeout,
'inject': os.path.join(ROOT, 'qunit-phantomjs-bridge.js')
})
], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
try:
while True:
line = phantom.stdout.readline()
if line:
if self.process(line, result):
break
else:
time.sleep(0.1)
finally:
# If the phantomjs process hasn't quit, kill it
if phantom.poll() is None:
phantom.terminate()
def process(self, line, result):
try:
args = json.loads(line)
except ValueError: # phantomjs stderr
if 'CoreText' not in line:
print line
return False
event_name = args[0]
if event_name == 'qunit.done':
return True
elif event_name == 'fail.load':
self.add_error(result, "PhantomJS unable to load %s" % args[1])
return True
elif event_name == 'fail.timeout':
self.add_error(result, "PhantomJS timed out, possibly due to a"
" missing QUnit start() call")
return True
elif event_name == 'qunit.moduleStart':
self._module = args[1].encode('utf-8') if args[1] else ''
elif event_name == 'qunit.moduleStop':
self._test = None
self._module = None
elif event_name == 'qunit.testStart':
self._test = QUnitTest(self._module, args[1].encode('utf-8'))
result.startTest(self._test)
elif event_name == 'qunit.testDone':
if not self._test.failed:
result.addSuccess(self._test)
result.stopTest(self._test)
self._test = None
elif event_name == 'qunit.log':
if args[1]:
return False
self._test.failed = True
result.addFailure(
self._test, self.failure_to_str(*args[2:]))
elif event_name == 'console':
print args[1]
return False
def add_error(self, result, s):
test = QUnitTest('phantomjs', 'startup')
result.startTest(test)
result.addError(test, s)
result.stopTest(test)
def failure_to_str(self, actual, expected, message, source):
if message or actual == expected:
formatted = str(message or '')
else:
formatted = "%s != %s" % (actual, expected)
if source:
formatted += '\n\n' + source
return formatted

View File

@ -1,24 +1,6 @@
import urllib
import urlparse
from openerp import sql_db, tools
from qunitsuite.suite import QUnitSuite
import openerp
class WebSuite(QUnitSuite):
def __init__(self, module):
url = urlparse.urlunsplit([
'http',
'localhost:{port}'.format(port=tools.config['xmlrpc_port']),
'/web/tests',
urllib.urlencode({
'mod': module,
'source': tools.config['db_name'],
'supadmin': tools.config['admin_passwd'],
'password': 'admin',
}),
''
])
super(WebSuite, self).__init__(url, 50000)
class WebSuite(openerp.tests.HttpCase):
def test_01_js(self):
self.phantom_js('/web/tests?mod=web',"","", login='admin')
def load_tests(loader, standard_tests, _):
standard_tests.addTest(WebSuite('web'))
return standard_tests

View File

@ -1,15 +0,0 @@
{
'name': 'Hello',
'category': 'Hidden',
'description':"""
OpenERP Web example module.
===========================
""",
'version': '2.0',
'depends': [],
'js': ['static/*/*.js', 'static/*/js/*.js'],
'css': [],
'auto_install': False,
'web_preload': False,
}

View File

@ -1,16 +0,0 @@
/*---------------------------------------------------------
* OpenERP base_hello (Example module)
*---------------------------------------------------------*/
openerp.web_hello = function(instance) {
instance.web.SearchView = instance.web.SearchView.extend({
init:function() {
this._super.apply(this,arguments);
this.on('search_data', this, function(){console.log('hello');});
}
});
};
// vim:et fdc=0 fdl=0:

View File

@ -1 +1,2 @@
# -*- coding: utf-8 -*-
import tests

View File

@ -7,7 +7,7 @@ OpenERP Web test suite.
""",
'version': '2.0',
'depends': [],
'depends': ['web', 'web_kanban'],
'js': ['static/src/js/*.js'],
'css': ['static/src/css/*.css'],
'auto_install': True,

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
import test_ui

View File

@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
from openerp.addons.web.tests.test_js import WebSuite
def load_tests(loader, standard_tests, _):
standard_tests.addTest(WebSuite('web_tests_demo'))
return standard_tests