From a9645151b6f070d23d2f35810aee562c1354aba9 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 16 Nov 2012 08:17:12 +0100 Subject: [PATCH] [IMP] setup & teardown of test runner simplify code and make setup & teardown processes more reliable add testing.Stack tools which stacks promise-returning functions around the actual promise-returning function to execute (the test case itself). testing.Stack returns an object with 3 methods, ``push([setup][, teardown])``, ``unshift([setup][, teardown])`` and ``execute(fn, *args)``. ``push`` and ``unshift`` create a new stack with the provided setup and teardown added respectively at the top and bottom of the stack. ``execute`` will walk the stack from bottom to top executing ``setup`` handlers (and waiting on their result), if all setup handlers execute without failure the ``fn`` callback gets executed (and waited on) then *all* ``teardown`` handlers are executed from top to bottom: | setup | setup | setup | setup | fn | teardown | teardown | teardown V teardown If a ``setup`` handler fails (the promise is rejected), teardowns will start executing *from the previous level* (so the ``teardown`` matching the failed ``setup`` will *not* be run), but all ``teardowns`` below that will be run regardless, even if one fails the next one will still be executed. The stack will either ultimately return a promise rejection using the *first* rejection it got (a rejection may be cascading, so the rejection of a setup may also lead to or be linked to a teardown being rejected later), or will return the resolution from ``fn``. If ``execute`` is passed further arguments, those arguments will in turn be forwarded to ``fn`` as well as all ``setup`` and ``teardown`` handlers. bzr revid: xmo@openerp.com-20121116071712-zuld957icellezum --- addons/web/__openerp__.py | 1 + addons/web/static/src/js/testing.js | 184 +++++++++++++------- addons/web/static/test/testing.js | 253 ++++++++++++++++++++++++++++ 3 files changed, 373 insertions(+), 65 deletions(-) create mode 100644 addons/web/static/test/testing.js diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 7bdfa8b55dd..c8f65d56404 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -69,6 +69,7 @@ This module provides the core of the OpenERP Web Client. "static/src/xml/*.xml", ], 'test': [ + "static/test/testing.js", "static/test/class.js", "static/test/registry.js", "static/test/form.js", diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js index 7686499b939..10fe7ecd778 100644 --- a/addons/web/static/src/js/testing.js +++ b/addons/web/static/src/js/testing.js @@ -85,6 +85,90 @@ openerp.testing = {}; }; }; + var StackProto = { + execute: function (fn) { + var args = [].slice.call(arguments, 1); + // Warning: here be dragons + var i = 0, setups = this.setups, teardowns = this.teardowns; + var d = $.Deferred(); + + var succeeded, failed; + var success = function () { + succeeded = _.toArray(arguments); + return teardown(); + }; + var failure = function () { + // save first failure + if (!failed) { + failed = _.toArray(arguments); + } + // chain onto next teardown + return teardown(); + }; + + var setup = function () { + // if setup to execute + if (i < setups.length) { + var f = setups[i] || testing.noop; + $.when(f.apply(null, args)).then(function () { + ++i; + setup(); + }, failure); + } else { + var actual_call; + try { + actual_call = $.when(fn.apply(null, args)) + } catch (e) { + actual_call = $.Deferred().reject(e); + } + actual_call.pipe(success, failure); + } + }; + var teardown = function () { + // if teardown to execute + if (i > 0) { + var f = teardowns[--i] || testing.noop; + $.when(f.apply(null, args)).then(teardown, failure); + } else { + if (failed) { + d.reject.apply(d, failed); + } else if (succeeded) { + d.resolve.apply(d, succeeded); + } else { + throw new Error("Didn't succeed or fail?"); + } + } + }; + setup(); + + return d; + }, + push: function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: this.setups.concat([setup]), + teardowns: this.teardowns.concat([teardown]) + }); + }, + unshift: function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: [setup].concat(this.setups), + teardowns: [teardown].concat(this.teardowns) + }); + } + }; + /** + * + * @param {Function} [setup] + * @param {Function} [teardown] + * @return {*} + */ + testing.Stack = function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: setup ? [setup] : [], + teardowns: teardown ? [teardown] : [] + }); + }; + var db = window['oe_db_info']; testing.section = function (name, options, body) { if (_.isFunction(options)) { @@ -104,10 +188,6 @@ openerp.testing = {}; callback = options; options = {}; } - _.defaults(options, { - setup: testing.noop, - teardown: testing.noop - }); var module = testing.current_module; var module_index = _.indexOf(testing.dependencies, module); @@ -120,27 +200,15 @@ openerp.testing = {}; // Serialize options for this precise test case // WARNING: typo is from jquery, do not fix! var env = QUnit.config.currentModuleTestEnviroment; - var opts = _.defaults({ - // section setup - // case setup - // test - // case teardown - // section teardown - setup: function () { - var args = [].slice.call(arguments); - return $.when(env._oe.setup.apply(null, args)) - .pipe(function () { - return options.setup.apply(null, args); - }); - }, - teardown: function () { - var args = [].slice.call(arguments); - return $.when(options.teardown.apply(null, args)) - .pipe(function () { - return env._oe.teardown.apply(null, args); - }); - } - }, options, env._oe); + // section setup + // case setup + // test + // case teardown + // section teardown + var case_stack = testing.Stack() + .push(env._oe.setup, env._oe.teardown) + .push(options.setup, options.teardown); + var opts = _.defaults({}, options, env._oe); // FIXME: if this test is ignored, will still query if (opts.rpc === 'rpc' && !db) { QUnit.config.autostart = false; @@ -177,9 +245,6 @@ openerp.testing = {}; } QUnit.test(name, function () { - // module testing environment - var self = this; - var instance; if (!opts.dependencies) { instance = openerp.init(module_deps); @@ -240,10 +305,11 @@ openerp.testing = {}; break; case 'rpc': async = true; - (function (setup, teardown) { + (function () { // Bunch of random base36 characters var dbname = 'test_' + Math.random().toString(36).slice(2); - opts.setup = function (instance, $s) { + // Add db setup/teardown at the start of the stack + case_stack = case_stack.unshift(function (instance) { // FIXME hack: don't want the session to go through shitty loading process of everything instance.session.session_init = testing.noop; instance.session.load_modules = testing.noop; @@ -260,38 +326,27 @@ openerp.testing = {}; } return instance.session.session_authenticate( dbname, 'admin', db.password, true); - }).pipe(function () { - return setup(instance, $s); }); - }; - opts.teardown = function (instance, $s) { - return $.when(teardown(instance, $s)).pipe(function () { - return instance.session.session_logout() - }).pipe(function () { - return instance.session.rpc('/web/database/drop', { + }, function (instance) { + return instance.session.rpc('/web/database/drop', { fields: [ {name: 'drop_pwd', value: db.supadmin}, {name: 'drop_db', value: dbname} ] - }); - }).pipe(function (result) { + }).pipe(function (result) { if (result.error) { return $.Deferred().reject(result.error).promise(); } return result; }); - }; - })(opts.setup, opts.teardown); + }); + })(); } // Always execute tests asynchronously stop(); var timeout; - var teardown = function () { - return opts.teardown(instance, $fixture, mock) - }; - $.when(opts.setup(instance, $fixture, mock)) - .pipe(function () { + case_stack.execute(function () { var result = callback(instance, $fixture, mock); if (!(result && _.isFunction(result.then))) { if (async) { @@ -303,25 +358,24 @@ openerp.testing = {}; + "number of assertions they expect"); } } - var d = $.Deferred(); - $.when(result).then(function () { - d.resolve.apply(d, arguments) - }, function () { - d.reject.apply(d, arguments); + + return $.Deferred(function (d) { + $.when(result).then(function () { + d.resolve.apply(d, arguments) + }, function () { + d.reject.apply(d, arguments); + }); + if (async || (result && result.then)) { + // async test can be either implicit async (rpc) or + // promise-returning + timeout = setTimeout(function () { + QUnit.config.semaphore = 1; + d.reject({message: "Test timed out"}); + }, 2000); + } }); - if (async || (result && result.then)) { - // async test can be either implicit async (rpc) or - // promise-returning - timeout = setTimeout(function () { - QUnit.config.semaphore = 1; - d.reject({message: "Test timed out"}); - }, 2000); - } - return d.pipe(teardown, teardown); - }).always(function () { - if (timeout) { - clearTimeout(timeout); - } + }, instance, $fixture, mock).always(function () { + if (timeout) { clearTimeout(timeout); } start(); }).fail(function (error) { if (options.fail_on_rejection === false) { diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js new file mode 100644 index 00000000000..919143ab533 --- /dev/null +++ b/addons/web/static/test/testing.js @@ -0,0 +1,253 @@ +openerp.testing.section('testing.stack', function (test) { + // I heard you like tests, so I put tests in your testing infrastructure, + // so you can test what you test + var reject = function () { + // utility function, rejects a success + var args = _.toArray(arguments); + return $.Deferred(function (d) { + d.reject.apply(d, ["unexpected success"].concat(args)); + }); + }; + test('direct, value, success', {asserts: 1}, function () { + var s = openerp.testing.Stack(); + return s.execute(function () { + return 42; + }).then(function (val) { + strictEqual(val, 42, "should return the handler value"); + }); + }); + test('direct, deferred, success', {asserts: 1}, function () { + var s = openerp.testing.Stack(); + return s.execute(function () { + return $.when(42); + }).then(function (val) { + strictEqual(val, 42, "should return the handler value") + }); + }); + test('direct, value, error', {asserts: 1}, function () { + var s = openerp.testing.Stack(); + return s.execute(function () { + throw new Error("foo"); + }).pipe(reject, function (f) { + strictEqual(f.message, "foo", "should reject with exception"); + return $.when(); + }); + }); + test('direct, deferred, failure', {asserts: 1}, function () { + var s = openerp.testing.Stack(); + return s.execute(function () { + return $.Deferred(function (d) { + d.reject("failed"); + }); + }).pipe(reject, function (f) { + strictEqual(f, "failed", "should propagate failure"); + return $.when(); + }); + }); + + test('successful setup', {asserts: 2}, function () { + var setup_done = false; + var s = openerp.testing.Stack(); + return s.push(function () { + return $.Deferred(function (d) { + setTimeout(function () { + setup_done = true; + d.resolve(2); + }, 50); + }); + }).execute(function () { + return 42; + }).then(function (val) { + ok(setup_done, "should have executed setup"); + strictEqual(val, 42, "should return executed function value (not setup)"); + }); + }); + test('successful teardown', {asserts: 2}, function () { + var teardown = false; + var s = openerp.testing.Stack(); + return s.push(null, function () { + return $.Deferred(function (d) { + setTimeout(function () { + teardown = true; + d.resolve(2); + }, 50); + }); + }).execute(function () { + return 42; + }).then(function (val) { + ok(teardown, "should have executed teardown"); + strictEqual(val, 42, "should return executed function value (not setup)"); + }); + }); + test('successful setup and teardown', {asserts: 3}, function () { + var setup = false, teardown = false; + var s = openerp.testing.Stack(); + return s.push(function () { + return $.Deferred(function (d) { + setTimeout(function () { + setup = true; + d.resolve(2); + }, 50); + }); + }, function () { + return $.Deferred(function (d) { + setTimeout(function () { + teardown = true; + d.resolve(2); + }, 50); + }); + }).execute(function () { + return 42; + }).then(function (val) { + ok(setup, "should have executed setup"); + ok(teardown, "should have executed teardown"); + strictEqual(val, 42, "should return executed function value (not setup)"); + }); + }); + + test('multiple setups', {asserts: 2}, function () { + var setups = 0; + var s = openerp.testing.Stack(); + return s.push(function () { + setups++; + }).push(function () { + setups++; + }).push(function () { + setups++; + }).push(function () { + setups++; + }).execute(function () { + return 42; + }).then(function (val) { + strictEqual(setups, 4, "should have executed all setups of stack"); + strictEqual(val, 42); + }); + }); + test('multiple teardowns', {asserts: 2}, function () { + var teardowns = 0; + var s = openerp.testing.Stack(); + return s.push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + }).execute(function () { + return 42; + }).then(function (val) { + strictEqual(teardowns, 4, "should have executed all teardowns of stack"); + strictEqual(val, 42); + }); + }); + test('holes in setups', {asserts: 2}, function () { + var setups = []; + var s = openerp.testing.Stack(); + return s.push(function () { + setups.push(0); + }).push().push().push(function () { + setups.push(3); + }).push(function () { + setups.push(4); + }).push().push(function () { + setups.push(6); + }).execute(function () { + return 42; + }).then(function (val) { + deepEqual(setups, [0, 3, 4, 6], + "should have executed setups in correct order"); + strictEqual(val, 42); + }); + }); + test('holes in teardowns', {asserts: 2}, function () { + var teardowns = []; + var s = openerp.testing.Stack(); + return s.push(null, function () { + teardowns.push(0); + }).push().push().push(null, function () { + teardowns.push(3); + }).push(null, function () { + teardowns.push(4); + }).push().push(null, function () { + teardowns.push(6); + }).execute(function () { + return 42; + }).then(function (val) { + deepEqual(teardowns, [6, 4, 3, 0], + "should have executed teardowns in correct order"); + strictEqual(val, 42); + }); + + }); + + test('failed setup', {asserts: 5}, function () { + var setup, teardown, teardown2, code; + return openerp.testing.Stack().push(function () { + setup = true; + }, function () { + teardown = true; + }).push(function () { + return $.Deferred().reject("Fail!"); + }, function () { + teardown2 = true; + }).execute(function () { + code = true; + return 42; + }).pipe(reject, function (m) { + ok(setup, "should have executed first setup function"); + ok(teardown, "should have executed first teardown function"); + ok(!teardown2, "should not have executed second teardown function"); + strictEqual(m, "Fail!", "should return setup failure message"); + ok(!code, "should not have executed callback"); + return $.when(); + }); + }); + test('failed teardown', {asserts: 2}, function () { + var teardowns = 0; + return openerp.testing.Stack().push(null, function () { + teardowns++; + return $.Deferred().reject('Fail 1'); + }).push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + return $.Deferred().reject('Fail 3'); + }).execute(function () { + return 42; + }).pipe(reject, function (m) { + strictEqual(teardowns, 3, + "should have tried executing all teardowns"); + strictEqual(m, "Fail 3", "should return first failure message"); + return $.when(); + }); + }); + test('failed call + teardown', {asserts: 2}, function () { + var teardowns = 0; + return openerp.testing.Stack().push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + return $.Deferred().reject('Fail 2'); + }).execute(function () { + return $.Deferred().reject("code"); + }).pipe(reject, function (m) { + strictEqual(teardowns, 2, + "should have tried executing all teardowns"); + strictEqual(m, "code", "should return first failure message"); + return $.when(); + }); + }); + + test('arguments passing', {asserts: 9}, function () { + var asserter = function (a, b, c) { + strictEqual(a, 1); + strictEqual(b, "foo"); + deepEqual(c, {bar: "baz", qux: 42}); + }; + + return openerp.testing.Stack() + .push(asserter, asserter) + .execute(asserter, 1, "foo", {bar: 'baz', qux: 42}); + }); +});