prosody/spec/util_promise_spec.lua

731 lines
21 KiB
Lua

local promise = require "util.promise";
describe("util.promise", function ()
--luacheck: ignore 212/resolve 212/reject
describe("new()", function ()
it("returns a promise object", function ()
assert(promise.new());
end);
end);
it("supplies a sensible tostring()", function ()
local s = tostring(promise.new());
assert.truthy(s:find("promise", 1, true));
assert.truthy(s:find("pending", 1, true));
end);
it("notifies immediately for fulfilled promises", function ()
local p = promise.new(function (resolve)
resolve("foo");
end);
local cb = spy.new(function (v)
assert.equal("foo", v);
end);
p:next(cb);
assert.spy(cb).was_called(1);
end);
it("notifies on fulfillment of pending promises", function ()
local r;
local p = promise.new(function (resolve)
r = resolve;
end);
local cb = spy.new(function (v)
assert.equal("foo", v);
end);
p:next(cb);
assert.spy(cb).was_called(0);
r("foo");
assert.spy(cb).was_called(1);
end);
it("ignores resolve/reject of settled promises", function ()
local res, rej;
local p = promise.new(function (resolve, reject)
res, rej = resolve, reject;
end);
local cb = spy.new(function (v)
assert.equal("foo", v);
end);
p:next(cb, cb);
assert.spy(cb).was_called(0);
res("foo");
assert.spy(cb).was_called(1);
rej("bar");
assert.spy(cb).was_called(1);
rej(promise.resolve("bar"));
assert.spy(cb).was_called(1);
res(promise.reject("bar"));
assert.spy(cb).was_called(1);
res(promise.resolve("bar"));
assert.spy(cb).was_called(1);
end);
it("allows chaining :next() calls", function ()
local r;
local result;
local p = promise.new(function (resolve)
r = resolve;
end);
local cb1 = spy.new(function (v)
assert.equal("foo", v);
return "bar";
end);
local cb2 = spy.new(function (v)
assert.equal("bar", v);
result = v;
end);
p:next(cb1):next(cb2);
assert.spy(cb1).was_called(0);
assert.spy(cb2).was_called(0);
r("foo");
assert.spy(cb1).was_called(1);
assert.spy(cb2).was_called(1);
assert.equal("bar", result);
end);
it("supports multiple :next() calls on the same promise", function ()
local r;
local result;
local p = promise.new(function (resolve)
r = resolve;
end);
local cb1 = spy.new(function (v)
assert.equal("foo", v);
result = v;
end);
local cb2 = spy.new(function (v)
assert.equal("foo", v);
result = v;
end);
p:next(cb1);
p:next(cb2);
assert.spy(cb1).was_called(0);
assert.spy(cb2).was_called(0);
r("foo");
assert.spy(cb1).was_called(1);
assert.spy(cb2).was_called(1);
assert.equal("foo", result);
end);
it("automatically rejects on error", function ()
local r;
local p = promise.new(function (resolve)
r = resolve;
error("oh no");
end);
local cb = spy.new(function () end);
local err_cb = spy.new(function (v)
assert.equal("oh no", v);
end);
p:next(cb, err_cb);
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(1);
r("foo");
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(1);
end);
it("supports reject()", function ()
local r, result;
local p = promise.new(function (resolve, reject)
r = reject;
end);
local cb = spy.new(function () end);
local err_cb = spy.new(function (v)
result = v;
assert.equal("oh doh", v);
end);
p:next(cb, err_cb);
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(0);
r("oh doh");
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(1);
assert.equal("oh doh", result);
end);
it("supports chaining of rejected promises", function ()
local r, result;
local p = promise.new(function (resolve, reject)
r = reject;
end);
local cb = spy.new(function () end);
local err_cb = spy.new(function (v)
result = v;
assert.equal("oh doh", v);
return "ok"
end);
local cb2 = spy.new(function (v)
result = v;
end);
local err_cb2 = spy.new(function () end);
p:next(cb, err_cb):next(cb2, err_cb2)
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(0);
assert.spy(cb2).was_called(0);
assert.spy(err_cb2).was_called(0);
r("oh doh");
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(1);
assert.spy(cb2).was_called(1);
assert.spy(err_cb2).was_called(0);
assert.equal("ok", result);
end);
it("propagates errors down the chain, even when some handlers are not provided", function ()
local r, result;
local test_error = {};
local p = promise.new(function (resolve, reject)
r = reject;
end);
local cb = spy.new(function () end);
local err_cb = spy.new(function (e) result = e end);
local p2 = p:next(function () error(test_error) end);
local p3 = p2:next(cb)
p3:catch(err_cb);
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(0);
r("oh doh");
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(1);
assert.spy(err_cb).was_called_with("oh doh");
assert.equal("oh doh", result);
end);
it("propagates values down the chain, even when some handlers are not provided", function ()
local r;
local p = promise.new(function (resolve, reject)
r = resolve;
end);
local cb = spy.new(function () end);
local err_cb = spy.new(function () end);
local p2 = p:next(function (v) return v; end);
local p3 = p2:catch(err_cb)
p3:next(cb);
assert.spy(cb).was_called(0);
assert.spy(err_cb).was_called(0);
r(1337);
assert.spy(cb).was_called(1);
assert.spy(cb).was_called_with(1337);
assert.spy(err_cb).was_called(0);
end);
it("fulfilled promises do not call error handlers and do propagate value", function ()
local p = promise.resolve("foo");
local cb = spy.new(function () end);
local p2 = p:catch(cb);
assert.spy(cb).was_called(0);
local cb2 = spy.new(function () end);
p2:catch(cb2);
assert.spy(cb2).was_called(0);
end);
it("rejected promises do not call fulfilled handlers and do propagate reason", function ()
local p = promise.reject("foo");
local cb = spy.new(function () end);
local p2 = p:next(cb);
assert.spy(cb).was_called(0);
local cb2 = spy.new(function () end);
local cb2_err = spy.new(function () end);
p2:next(cb2, cb2_err);
assert.spy(cb2).was_called(0);
assert.spy(cb2_err).was_called(1);
assert.spy(cb2_err).was_called_with("foo");
end);
describe("allows callbacks to return", function ()
it("pending promises", function ()
local r;
local p = promise.resolve()
local cb = spy.new(function ()
return promise.new(function (resolve)
r = resolve;
end);
end);
local cb2 = spy.new(function () end);
p:next(cb):next(cb2);
assert.spy(cb).was_called(1);
assert.spy(cb2).was_called(0);
r("hello");
assert.spy(cb).was_called(1);
assert.spy(cb2).was_called(1);
assert.spy(cb2).was_called_with("hello");
end);
it("resolved promises", function ()
local p = promise.resolve()
local cb = spy.new(function ()
return promise.resolve("hello");
end);
local cb2 = spy.new(function () end);
p:next(cb):next(cb2);
assert.spy(cb).was_called(1);
assert.spy(cb2).was_called(1);
assert.spy(cb2).was_called_with("hello");
end);
it("rejected promises", function ()
local p = promise.resolve()
local cb = spy.new(function ()
return promise.reject("hello");
end);
local cb2 = spy.new(function ()
return promise.reject("goodbye");
end);
local cb3 = spy.new(function () end);
p:next(cb):catch(cb2):catch(cb3);
assert.spy(cb).was_called(1);
assert.spy(cb2).was_called(1);
assert.spy(cb2).was_called_with("hello");
assert.spy(cb3).was_called(1);
assert.spy(cb3).was_called_with("goodbye");
end);
it("ordinary values", function ()
local p = promise.resolve()
local cb = spy.new(function ()
return "hello"
end);
local cb2 = spy.new(function () end);
p:next(cb):next(cb2);
assert.spy(cb).was_called(1);
assert.spy(cb2).was_called(1);
assert.spy(cb2).was_called_with("hello");
end);
it("nil", function ()
local p = promise.resolve()
local cb = spy.new(function ()
return
end);
local cb2 = spy.new(function () end);
p:next(cb):next(cb2);
assert.spy(cb).was_called(1);
assert.spy(cb2).was_called(1);
assert.spy(cb2).was_called_with(nil);
end);
end);
describe("race()", function ()
it("works with fulfilled promises", function ()
local p1, p2 = promise.resolve("yep"), promise.resolve("nope");
local p = promise.race({ p1, p2 });
local result;
p:next(function (v)
result = v;
end);
assert.equal("yep", result);
end);
it("works with pending promises", function ()
local r1, r2;
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
local p = promise.race({ p1, p2 });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r2("yep");
r1("nope");
assert.spy(cb).was_called(1);
assert.equal("yep", result);
end);
end);
describe("all()", function ()
it("works with fulfilled promises", function ()
local p1, p2 = promise.resolve("yep"), promise.resolve("nope");
local p = promise.all({ p1, p2 });
local result;
p:next(function (v)
result = v;
end);
assert.same({ "yep", "nope" }, result);
end);
it("works with pending promises", function ()
local r1, r2;
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
local p = promise.all({ p1, p2 });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r2("yep");
assert.spy(cb).was_called(0);
r1("nope");
assert.spy(cb).was_called(1);
assert.same({ "nope", "yep" }, result);
end);
it("rejects if any promise rejects", function ()
local r1, r2;
local p1 = promise.new(function (resolve, reject) r1 = reject end);
local p2 = promise.new(function (resolve, reject) r2 = reject end);
local p = promise.all({ p1, p2 });
local result;
local cb = spy.new(function (v)
result = v;
end);
local cb_err = spy.new(function (v)
result = v;
end);
p:next(cb, cb_err);
assert.spy(cb).was_called(0);
assert.spy(cb_err).was_called(0);
r2("fail");
assert.spy(cb).was_called(0);
assert.spy(cb_err).was_called(1);
r1("nope");
assert.spy(cb).was_called(0);
assert.spy(cb_err).was_called(1);
assert.equal("fail", result);
end);
it("works with non-numeric keys", function ()
local r1, r2;
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
local p = promise.all({ [true] = p1, [false] = p2 });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r2("yep");
assert.spy(cb).was_called(0);
r1("nope");
assert.spy(cb).was_called(1);
assert.same({ [true] = "nope", [false] = "yep" }, result);
end);
it("passes through non-promise values", function ()
local r1;
local p1 = promise.new(function (resolve) r1 = resolve end);
local p = promise.all({ [true] = p1, [false] = "yep" });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r1("nope");
assert.spy(cb).was_called(1);
assert.same({ [true] = "nope", [false] = "yep" }, result);
end);
end);
describe("all_settled()", function ()
it("works with fulfilled promises", function ()
local p1, p2 = promise.resolve("yep"), promise.resolve("nope");
local p = promise.all_settled({ p1, p2 });
local result;
p:next(function (v)
result = v;
end);
assert.same({
{ status = "fulfilled", value = "yep" };
{ status = "fulfilled", value = "nope" };
}, result);
end);
it("works with pending promises", function ()
local r1, r2;
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
local p = promise.all_settled({ p1, p2 });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r2("yep");
assert.spy(cb).was_called(0);
r1("nope");
assert.spy(cb).was_called(1);
assert.same({
{ status = "fulfilled", value = "nope" };
{ status = "fulfilled", value = "yep" };
}, result);
end);
it("works when some promises reject", function ()
local r1, r2;
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (_, reject) r2 = reject end);
local p = promise.all_settled({ p1, p2 });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r2("this fails");
assert.spy(cb).was_called(0);
r1("this succeeds");
assert.spy(cb).was_called(1);
assert.same({
{ status = "fulfilled", value = "this succeeds" };
{ status = "rejected", reason = "this fails" };
}, result);
end);
it("works when all promises reject", function ()
local r1, r2;
local p1, p2 = promise.new(function (_, reject) r1 = reject end), promise.new(function (_, reject) r2 = reject end);
local p = promise.all_settled({ p1, p2 });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r2("this fails");
assert.spy(cb).was_called(0);
r1("this fails too");
assert.spy(cb).was_called(1);
assert.same({
{ status = "rejected", reason = "this fails too" };
{ status = "rejected", reason = "this fails" };
}, result);
end);
it("works with non-numeric keys", function ()
local r1, r2;
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
local p = promise.all_settled({ foo = p1, bar = p2 });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r2("yep");
assert.spy(cb).was_called(0);
r1("nope");
assert.spy(cb).was_called(1);
assert.same({
foo = { status = "fulfilled", value = "nope" };
bar = { status = "fulfilled", value = "yep" };
}, result);
end);
it("passes through non-promise values", function ()
local r1;
local p1 = promise.new(function (resolve) r1 = resolve end);
local p = promise.all_settled({ foo = p1, bar = "yep" });
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r1("nope");
assert.spy(cb).was_called(1);
assert.same({
foo = { status = "fulfilled", value = "nope" };
bar = "yep";
}, result);
end);
end);
describe("catch()", function ()
it("works", function ()
local result;
local p = promise.new(function (resolve)
error({ foo = true });
end);
local cb1 = spy.new(function (v)
result = v;
end);
assert.spy(cb1).was_called(0);
p:catch(cb1);
assert.spy(cb1).was_called(1);
assert.same({ foo = true }, result);
end);
end);
describe("join()", function ()
it("works", function ()
local r1, r2;
local res1, res2;
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
local p = promise.join(function (_res1, _res2)
res1, res2 = _res1, _res2;
return promise.resolve("works");
end, p1, p2);
local result;
local cb = spy.new(function (v)
result = v;
end);
p:next(cb);
assert.spy(cb).was_called(0);
r2("yep");
assert.spy(cb).was_called(0);
r1("nope");
assert.spy(cb).was_called(1);
assert.same("works", result);
assert.equals("nope", res1);
assert.equals("yep", res2);
end);
end);
it("promises may be resolved by other promises", function ()
local r1, r2;
local p1, p2 = promise.new(function (resolve) r1 = resolve end), promise.new(function (resolve) r2 = resolve end);
local result;
local cb = spy.new(function (v)
result = v;
end);
p1:next(cb);
assert.spy(cb).was_called(0);
r1(p2);
assert.spy(cb).was_called(0);
r2("yep");
assert.spy(cb).was_called(1);
assert.equal("yep", result);
end);
describe("reject()", function ()
it("returns a rejected promise", function ()
local p = promise.reject("foo");
local cb = spy.new(function () end);
p:catch(cb);
assert.spy(cb).was_called(1);
assert.spy(cb).was_called_with("foo");
end);
it("returns a rejected promise and does not call on_fulfilled", function ()
local p = promise.reject("foo");
local cb = spy.new(function () end);
p:next(cb);
assert.spy(cb).was_called(0);
end);
end);
describe("finally()", function ()
local p, p2, resolve, reject, on_finally;
before_each(function ()
p = promise.new(function (_resolve, _reject)
resolve, reject = _resolve, _reject;
end);
on_finally = spy.new(function () end);
p2 = p:finally(on_finally);
end);
it("runs when a promise is resolved", function ()
assert.spy(on_finally).was_called(0);
resolve("foo");
assert.spy(on_finally).was_called(1);
assert.spy(on_finally).was_not_called_with("foo");
end);
it("runs when a promise is rejected", function ()
assert.spy(on_finally).was_called(0);
reject("foo");
assert.spy(on_finally).was_called(1);
assert.spy(on_finally).was_not_called_with("foo");
end);
it("returns a promise that fulfills with the original value", function ()
local cb2 = spy.new(function () end);
p2:next(cb2);
assert.spy(on_finally).was_called(0);
assert.spy(cb2).was_called(0);
resolve("foo");
assert.spy(on_finally).was_called(1);
assert.spy(cb2).was_called(1);
assert.spy(on_finally).was_not_called_with("foo");
assert.spy(cb2).was_called_with("foo");
end);
it("returns a promise that rejects with the original error", function ()
local on_finally_err = spy.new(function () end);
local on_finally_ok = spy.new(function () end);
p2:catch(on_finally_err);
p2:next(on_finally_ok);
assert.spy(on_finally).was_called(0);
assert.spy(on_finally_err).was_called(0);
reject("foo");
assert.spy(on_finally).was_called(1);
-- Since the original promise was rejected, the finally promise should also be
assert.spy(on_finally_ok).was_called(0);
assert.spy(on_finally_err).was_called(1);
assert.spy(on_finally).was_not_called_with("foo");
assert.spy(on_finally_err).was_called_with("foo");
end);
it("returns a promise that rejects with an uncaught error inside on_finally", function ()
p = promise.new(function (_resolve, _reject)
resolve, reject = _resolve, _reject;
end);
local test_error = {};
on_finally = spy.new(function () error(test_error) end);
p2 = p:finally(on_finally);
local on_finally_err = spy.new(function () end);
p2:catch(on_finally_err);
assert.spy(on_finally).was_called(0);
assert.spy(on_finally_err).was_called(0);
reject("foo");
assert.spy(on_finally).was_called(1);
assert.spy(on_finally_err).was_called(1);
assert.spy(on_finally).was_not_called_with("foo");
assert.spy(on_finally).was_not_called_with(test_error);
assert.spy(on_finally_err).was_called_with(test_error);
end);
end);
describe("try()", function ()
it("works with functions that return a promise", function ()
local resolve;
local p = promise.try(function ()
return promise.new(function (_resolve)
resolve = _resolve;
end);
end);
assert.is_function(resolve);
local on_resolved = spy.new(function () end);
p:next(on_resolved);
assert.spy(on_resolved).was_not_called();
resolve("foo");
assert.spy(on_resolved).was_called_with("foo");
end);
it("works with functions that return a value", function ()
local p = promise.try(function ()
return "foo";
end);
local on_resolved = spy.new(function () end);
p:next(on_resolved);
assert.spy(on_resolved).was_called_with("foo");
end);
it("works with functions that return a promise that rejects", function ()
local reject;
local p = promise.try(function ()
return promise.new(function (_, _reject)
reject = _reject;
end);
end);
assert.is_function(reject);
local on_rejected = spy.new(function () end);
p:catch(on_rejected);
assert.spy(on_rejected).was_not_called();
reject("foo");
assert.spy(on_rejected).was_called_with("foo");
end);
it("works with functions that throw errors", function ()
local test_error = {};
local p = promise.try(function ()
error(test_error);
end);
local on_rejected = spy.new(function () end);
p:catch(on_rejected);
assert.spy(on_rejected).was_called(1);
assert.spy(on_rejected).was_called_with(test_error);
end);
end);
describe("set_nexttick()", function ()
it("works", function ()
local next_tick = spy.new(function (f)
f();
end)
local cb = spy.new(function () end);
promise.set_nexttick(next_tick);
promise.new(function (y, _)
y("okay");
end):next(cb);
assert.spy(next_tick).was.called();
assert.spy(cb).was.called_with("okay");
end);
end)
end);