[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
This commit is contained in:
Xavier Morel 2012-11-16 08:17:12 +01:00
parent 8e85f4e0bd
commit a9645151b6
3 changed files with 373 additions and 65 deletions

View File

@ -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",

View File

@ -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) {

View File

@ -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});
});
});