lively.lang
Version:
JavaScript utils providing useful abstractions for working with collections, functions, objects.
713 lines (578 loc) • 24.2 kB
JavaScript
/*global beforeEach, afterEach, describe, it, setInterval, clearInterval, setTimeout*/
import { expect } from "mocha-es6";
import * as fun from "../function.js";
describe('fun', function() {
describe('accessing methods -- own and all', function() {
var obj1 = {
foo: 23,
method1: function() { return 23; },
get method2() { return function() { return 42; }; }
}, obj2 = {
bar: 99,
method3: function() { return 66; },
get method4() { return function() { return 44; }; }
}
obj1.__proto__ = obj2;
it("finds own functions", function() {
// note getters are currently ignored:
// expect(fun.own(obj1)).to.eql(['method1', 'method2']);
expect(fun.own(obj1)).to.eql(['method1']);
});
it("finds inherited functions", function() {
// note getters are currently ignored
expect(fun.all(obj1)).to.eql(['method1', 'method3']);
});
});
describe('inspection', function() {
it("can tell its args", function() {
expect(fun.argumentNames(function(arg1, arg2, arg4) { return arg2 + arg4; }))
.to.eql(["arg1", "arg2", "arg4"]);
expect(fun.argumentNames(function() { return 23; })) .to.eql([]);
expect(fun.argumentNames(function(/*...*/) { return 23; })).to.eql([]);
});
it("can tell args of arrow function", function() {
expect(fun.argumentNames((arg1, arg2, arg4) => { return arg2 + arg4; })).to.eql(["arg1", "arg2", "arg4"]);
expect(fun.argumentNames(foo => 23)).to.eql(["foo"]);
expect(fun.argumentNames(() => 23)).to.eql([]);
});
it("can extract a function body a string", function() {
var f = function(arg1, arg2, arg4) { var x = {n: 33}; return x.n + arg2 + arg4; };
expect(fun.extractBody(f)).to.equal('var x = { n: 33 };\nreturn x.n + arg2 + arg4;');
expect(fun.extractBody(function() {})).to.equal("");
expect(fun.extractBody(function() { 123 })).to.equal("123;");
});
});
describe('async', function() {
describe('rate limiting', function() {
beforeEach(function() {
this._queues = fun._queues;
fun._queues = {};
this._debouncedByName = fun._debouncedByName;
fun._debouncedByName = {};
});
afterEach(function() {
fun._queues = this._queues;
fun._debouncedByName = this._debouncedByName;
});
it('debounce function is looked up by name', function(done) {
var called = 0, result;
[1,2,3,4,5,6,7,8,9,10].reduceRight(function(next, i) {
return function() {
fun.debounceNamed('testDebouncedCommand', 20,
function(i) { result = i; called++; }, false)(i);
setTimeout(next, 0);
}
}, function() {})();
var i = setInterval(function() {
if (typeof result === 'undefined') return;
clearInterval(i);
expect(called).to.equal(1, 'debounce call cound');
expect(result).to.equal(10, 'debounce result');
done();
}, 0);
});
it("throttles calls", function(done) {
var called = 0, result = [];
[1,2,3,4].forEach(function(i) {
fun.throttleNamed('testThrottleCommand', 20, function(i) { result.push(i); called++; })(i);
});
setTimeout(function() {
fun.throttleNamed('testThrottleCommand', 20, function(i) { result.push(i); called++; })(5);
}, 80);
setTimeout(function() {
// call 1 immediatelly in the loop,
// call 2 after waiting for timeout with arg from last (fourth) invocation
// call 3 invocation after first throttle
expect(3).to.equal(called, 'throttle call count');
expect([1,4,5]).to.eql(result, 'throttle result');
done();
}, 120);
});
});
describe("queue", function() {
it("queues stuff", async function() {
var resolveDrainRun,
drainRun = new Promise(_resolve => resolveDrainRun = _resolve),
finishedTasks = [],
q = fun.createQueue('testQueue-queue', function(task, callback) {
finishedTasks.push(task); setTimeout(callback, 0); }),
q2 = fun.createQueue('testQueue-queue', function(task, callback) {
expect.fail("redefining worker should not work"); });
expect(q).to.equal(q2, 'id queues not identical');
q.pushAll([1,2,3,4]);
expect(1).to.equal(finishedTasks.length,"tasks prematurely finished?");
q.drain = function() { resolveDrainRun() }
await drainRun;
expect([1,2,3,4]).to.eql(finishedTasks,"tasks not ok");
// expect().assert(!fun._queues.hasOwnProperty('testQueue-queue'), 'queue store not cleaned up');
});
it("associates workers with callbacks", function(done) {
var calls = [];
function worker(thenDo) {
var workerState = 22;
calls.push("workerCalled");
setTimeout(function() {
thenDo(null, ++workerState);
}, 200);
}
function thenDo1(err, arg) { calls.push("thenDo1Called:"+arg); }
function thenDo2(err, arg) { calls.push("thenDo2Called:"+arg); }
function thenDo3(err, arg) { calls.push("thenDo3Called:"+arg); }
function thenDo4(err, arg) { calls.push("thenDo4Called:"+arg); }
var proc = fun.workerWithCallbackQueue('testWorkerWithCallbackQueue', worker).whenDone(thenDo1);
expect(proc).to.equal(fun.workerWithCallbackQueue('testWorkerWithCallbackQueue', worker),'not identical process');
proc.whenDone(thenDo2);
setTimeout(function() { proc.whenDone(thenDo3); }, 100);
waitForFinish1();
function waitForFinish1() {
if (calls.length <= 1) { setTimeout(waitForFinish1, 10); return; }
var expected = ["workerCalled", "thenDo1Called:23", "thenDo2Called:23", "thenDo3Called:23"];
expect(expected).to.eql(calls);
calls = [];
var proc2 = fun.workerWithCallbackQueue('testWorkerWithCallbackQueue', worker).whenDone(thenDo4);
expect().assert(proc2 !== proc, 'new process equals old?');
waitForFinish2();
}
function waitForFinish2() {
if (calls.length <= 1) { setTimeout(waitForFinish2, 10); return; }
var expected = ["workerCalled", "thenDo4Called:23"];
expect(expected).to.eql(calls);
done();
}
});
it("associates workers with callbacks and timesout", function(done) {
var calls = [];
function worker(thenDo) {
setTimeout(function() {
calls.push("workerCalled");
thenDo(null); }, 200);
}
function thenDo1(err, arg) { calls.push("thenDo1Called:" + (err ? err.message : null)); }
function thenDo2(err, arg) { calls.push("thenDo2Called:" + (err ? err.message : null)); }
var proc = fun.workerWithCallbackQueue(
'testWorkerWithCallbackQueueWithTimout',
worker, 100).whenDone(thenDo1);
setTimeout(function() { proc.whenDone(thenDo2); }, 50);
waitForTimeout();
function waitForTimeout() {
if (calls.length <= 1) { setTimeout(waitForTimeout, 10); return; }
var expected = ["thenDo1Called:timeout", "thenDo2Called:timeout"];
expect(expected).to.eql(calls);
done();
};
});
it("associates workers with callbacks and handles errors", function(done) {
var calls = [];
function worker(thenDo) {
var workerState = 22;
calls.push("workerCalled");
throw new Error('foo');
}
function thenDo1(err, arg) { calls.push(err.message); }
function thenDo2(err, arg) { calls.push(err.message); }
fun.workerWithCallbackQueue('testWorkerWithCallbackQueueWithError', worker).whenDone(thenDo1);
fun.workerWithCallbackQueue('testWorkerWithCallbackQueueWithError', worker).whenDone(thenDo2);
waitForError();
function waitForError() {
if (calls.length <= 1) { setTimeout(waitForError, 10); return; }
var expected = ["workerCalled", "foo", "foo"];
expect(expected).to.eql(calls);
done();
};
});
it("associates workers with callbacks and can be canceled", function(done) {
var calls = [];
function worker(thenDo) {
calls.push("workerCalled");
setTimeout(function() { thenDo(null); }, 40);
}
function thenDo1(err, arg) { calls.push("thenDo1Called"); }
function thenDo2(err, arg) { calls.push("thenDo2Called"); }
var proc = fun.workerWithCallbackQueue('testWorkerWithCallbackQueue', worker).whenDone(thenDo1);
proc.cancel();
setTimeout(function() { fun.workerWithCallbackQueue('testWorkerWithCallbackQueue', worker).whenDone(thenDo2); }, 20);
setTimeout(function() {
var expected = ['workerCalled', 'thenDo2Called'];
expect(expected).to.eql(calls);
done();
}, 120);
});
});
describe("wait for", function() {
it("waits", function(done) {
var x = 0, wasCalled, startTime = Date.now(), endTime, timeout;
fun.waitFor(200, function() { return x === 1; }, function (_timeout) {
wasCalled = true; timeout = _timeout; endTime = Date.now();
});
waitForWaitFor();
setTimeout(function() { x = 1; }, 100);
function waitForWaitFor() {
if (!wasCalled) { setTimeout(waitForWaitFor, 20); return; }
expect(timeout).to.equal(undefined, 'timout param not OK: ' + timeout);
var duration = endTime - startTime;
expect(duration).greaterThan(99,'wait duration not OK: ' + duration);
done();
};
});
it("times out", function(done) {
var x = 0, wasCalled, startTime = Date.now(), endTime, timeout;
fun.waitFor(200, function() { return x === 1; /*will never be true*/ }, function (_timeout) {
wasCalled = true; timeout = _timeout; endTime = Date.now();
});
waitForWaitFor();
function waitForWaitFor() {
if (!wasCalled) { setTimeout(waitForWaitFor, 20); return; }
expect(timeout).instanceOf(Error, 'timeout param not OK: ' + timeout);
var duration = endTime - startTime;
expect(duration).greaterThan(199, 'wait duration not OK: ' + duration);
done();
};
});
it("waits without timeout", function(done) {
var x = 0, wasCalled, startTime = Date.now(), endTime, timeout;
fun.waitFor(
function() { return x === 1; },
function (_timeout) {
wasCalled = true; timeout = _timeout; endTime = Date.now(); });
setTimeout(function() { x = 1; }, 400);
waitForWaitFor();
function waitForWaitFor() {
if (!wasCalled) { setTimeout(waitForWaitFor, 20); return; }
expect(timeout).to.equal(undefined);
var duration = endTime - startTime;
expect(duration).to.be.greaterThan(399, 'wait duration not OK: ' + duration);
done();
}
});
});
describe("timing", function() {
it("delays", function(done) {
var run = false;
fun.delay(function() { run = true; }, .8);
setTimeout(function() { expect(run).to.equal(false); }, 500);
setTimeout(function() { expect(run).to.equal(true); done(); }, 820);
});
});
});
describe("function composition", function() {
it("composes functions", function() {
function mult(a,b) { return a * b; }
function add1(a) { return a + 1; }
var composed = fun.compose(mult, add1, String),
result = composed(11, 2);
expect("23" === result, 'compose not OK: ' + result);
});
it("composes async functions", function(done) {
var result, err, test1, test2;
function mult(a,b, thenDo) { thenDo(null, a * b); }
function add1(a, thenDo) { thenDo(null, a + 1); }
var composed = fun.composeAsync(mult, add1);
composed(11, 2, function(err, _result) { result = _result; });
waitFor1();
waitFor2();
waitFor3();
function waitFor1() {
if (!result) { setTimeout(waitFor1, 10); return; }
expect(23).to.equal(result, 'composeAsync not OK: ' + result);
result = null;
test1 = true;
};
function waitFor2() {
if (!test1) { setTimeout(waitFor2, 10); return; }
function a(a,b, thenDo) { thenDo(new Error('ha ha'), a * b); }
function b(a, thenDo) { thenDo(null, a); }
var composed = fun.composeAsync(a, b);
composed(11, 2, function(_err, _result) {
test2 = true;
err = _err;
result = _result;
});
};
function waitFor3() {
if (!test2) { setTimeout(waitFor3, 10); return; }
expect(!result, 'composeAsync result when error expected?: ' + result);
expect(err, 'no error? ' + err);
done();
};
});
describe("composeAsync", () => {
it("works with Error", function(done) {
var aRun = 0, bRun = 0, cRun = 0;
console.log("Dear test runner: an error like \"Object XXX has no method 'barrr'\" is expected!");
fun.composeAsync(
function a(a,b, thenDo) { aRun++; thenDo(null, (a*b).barrr()); },
function b(a, thenDo) { bRun++; thenDo(null, a + 1); }
)(3,4, function(err, result) {
cRun++;
expect(1).to.equal(aRun,'aRun');
expect(0).to.equal(bRun,'bRun');
expect(1).to.equal(cRun,'cRun');
expect(!result, 'result? ' + result);
expect(err instanceof TypeError, 'error? ' + err);
});
waitFor();
function waitFor() {
if (!cRun) { setTimeout(waitFor, 10); return; }
done();
};
});
it("works with errors don't activate callbacks twice", function(done) {
var aRun = 0, bRun = 0, cRun = 0;
fun.composeAsync(
function a(a,b, thenDo) { aRun++; thenDo(null, a * b);
throw new Error('afterthought'); /*throwing this error should not invoke the end handler*/},
function b(a, thenDo) { bRun++; thenDo(null, a + 1); }
)(4,5, function(err, result) {
cRun++;
expect(1).to.equal(aRun,'aRun');
expect(1).to.equal(bRun,'bRun');
expect(1).to.equal(cRun,'cRun');
expect(21).to.equal(result,'result? ' + result);
expect(!err, 'err? ' + err);
});
waitFor();
function waitFor() {
if (!cRun) { setTimeout(waitFor, 30); return; }
done();
};
});
it("does not need end callback", function(done) {
var bNext, bArg;
fun.composeAsync(
function a(next) { next(null, 23); },
function b(arg, next) { bArg = arg; bNext = next; next(); }
)(undefined);
expect(bArg).to.equal(23);
expect(bNext).to.be.a("function");
done();
});
describe("and promises", function() {
it("can mix promises and functions", function(done) {
fun.composeAsync(
Promise.resolve(23),
function a(val, n) { n(null, val + 2); }
)(function(err, value) {
expect(err).to.equal(null);
expect(value).to.equal(25);
done();
});
});
it("can mix promises and functions 2", function(done) {
fun.composeAsync(
function a(n) { n(null, 23); },
Promise.resolve(23)
)(function(err, value) {
expect(err).to.equal(null);
expect(value).to.equal(23);
done();
});
});
it("can deal with promise errors", function(done) {
fun.composeAsync(
Promise.reject("Test Error"),
function a(val, n) { n(null, val + 2); }
)(function(err, value) {
expect(String(err)).to.match(/Test Error/i);
done();
});
});
it("can return promise instead of calling next", function(done) {
fun.composeAsync(
function a() { return Promise.resolve(23); },
function a(val, n) { n(null, val + 3); },
function a(val) { return Promise.resolve(val + 2); }
)(function(err, value) {
expect(value).to.equal(28);
done();
});
});
it("can return failing promise", function(done) {
fun.composeAsync(
function a() { return Promise.reject("Fooo"); }
)(function(err) {
expect(err).to.match(/Foo/i);
done();
});
});
it("is itself a promise", function() {
return Promise.all([
fun.composeAsync(function a(n) { n(null, 23); })()
.then(function(value) { expect(value).to.equal(23); }),
fun.composeAsync(function a() { return Promise.resolve(42); })()
.then(function(value) { expect(value).to.equal(42); }),
fun.composeAsync(Promise.resolve(99))()
.then(function(value) { expect(value).to.equal(99); })
]);
});
it("is itself a promise that catches", function() {
return Promise.all([
fun.composeAsync(function a(n) { n(new Error("Foo")); })()
.catch(function(err) { expect(err).to.match(/Foo/i); }),
fun.composeAsync(Promise.reject(new Error("Bar")))()
.catch(function(err) { expect(err).to.match(/Bar/i); })
]);
});
});
});
});
describe("waitForAll", function() {
it("waits for all functions to be done", function(done) {
fun.waitForAll([
function(next) { setTimeout(function() { next(null, "test", "bar")}, 10); },
function(next) { setTimeout(next, 4); }
], function(err, results) {
expect(err).to.equal(null);
expect(results).to.eql([["test", "bar"], []]);
done();
});
});
it("deals with sync errors", function(done) {
fun.waitForAll([
function(next) { next(null, "test", "bar"); },
function(next) { throw new Error("Foo"); }
], function(err, results) {
expect(String(err)).to.match(/Error: in waitForAll at 1.*(Error: Foo)?/i);
expect(results).to.eql([["test", "bar"], null]);
done();
});
});
it("deals with async errors", function(done) {
fun.waitForAll([
function(next) { next(null, "test", "bar"); },
function(next) { setTimeout(function() { next(new Error("Foo")); }, 10); }
], function(err, results) {
expect(String(err)).to.match(/Error: in waitForAll at 1.*(Error: Foo)?/i);
expect(results).to.eql([["test", "bar"], null]);
done();
});
});
it("times out", function(done) {
fun.waitForAll({timeout: 200}, [
function(next) { setTimeout(function() { next(null); }, 300); },
function(next) { next(null, "test", "bar"); },
function(next) { setTimeout(function() { next(null); }, 400); }
], function(err, results) {
expect(String(err)).to.match(/(Error: in waitForAll at)|(waitForAll timed out, functions at 0, 2 not done)/i);
expect(results).to.eql([null, ["test", "bar"], null]);
done();
});
});
it("without functions it continues immediately", function(done) {
fun.waitForAll([], function(err, results) {
expect(err).to.equal(null);
expect(results).to.have.length(0);
done();
});
});
});
describe("function wrapping", function() {
it("can flip arguments", function() {
function func(a,b,c) { return '' + a + b + c; }
expect('213').to.equal(fun.flip(func)(1,2,3));
});
it("wraps to augment behavior", function() {
var wrapped = fun.wrap(
function(arg1, arg2) { return arg1 + arg2; },
function(proceed, arg1, arg2) {
return proceed(arg1, arg2 + 1) + 1;
});
expect(wrapped(3,4)).to.equal(9);
expect(wrapped.originalFunction(3,4)).to.equal(7);
});
it("wrap binds this", function() {
// correctly bind this
var o = {
x: 3, y: 4,
f: fun.wrap(
function() { return this.x; },
function(proceed) { return proceed() + this.y; })};
expect(o.f()).to.equal(7);
});
it("curries arguments", function() {
function orig(arg1, arg2) { return arg1 + arg2; }
expect(fun.curry(orig, 2)(3)).to.equal(5);
});
it("curry binds this", function() {
// correctly bind this
var o = {x: 3, f: fun.curry(function(a) { return this.x + a; }, 2)};
expect(o.f()).to.equal(5);
});
it("can restrict a function to run only once", function() {
var c = 0;
function counter(arg1) { c++; return arg1 + c; }
var once = fun.once(counter);
once(22); once();
expect(1).to.equal(c);
expect(23).to.equal(once());
});
it("can restrict that only one of multiple a function is run", function(done) {
var log = "";
var either = fun.either(
function() { log += "aRun"; },
function() { log += "bRun"; },
function() { log += "cRun"; });
setTimeout(either[0], 100);
setTimeout(either[1], 40);
setTimeout(either[2], 80);
setTimeout(function() { expect(log).to.equal("bRun"); done(); }, 150)
});
it("can restrict that only one of multiple a function is run via names", async function() {
var log = "", name = "either-test-" + Date.now();
function a() { log += "aRun"; };
function b() { log += "bRun"; };
function c() { log += "cRun"; };
setTimeout(fun.eitherNamed(name, a), 100);
setTimeout(fun.eitherNamed(name, b), 40);
setTimeout(fun.eitherNamed(name, c), 80);
await new Promise(resolve => setTimeout(resolve, 150));
expect(log).to.equal("bRun");
// expect(fun).to.have.property("_eitherNameRegistry");
// expect(fun._eitherNameRegistry).to.not.have.property(name);
});
it("can replace a function for one call", function() {
var log = [],
orig = function() { log.push("orig"); },
replacement = function() { log.push("replacement"); },
obj = {m: orig};
fun.replaceMethodForOneCall(obj, "m", replacement);
obj.m();
obj.m();
obj.m();
expect(log).to.eql(["replacement", "orig", "orig"]);
expect(obj.m).to.equal(orig);
});
});
describe("function creation", function() {
it("creates function from string", function() {
expect(fun.fromString("function(x) { return x + 2; }")(1)).to.equal(3);
});
it("can create scripts for objects", function() {
var obj = {};
fun.asScriptOf(function foo() { return 23; }, obj);
expect(23).to.equal(obj.foo());
});
xit("scripts can call $super", function() {
var klass = function() {};
klass.prototype.foo = function() { return 3; };
var obj = new klass();
fun.asScriptOf(function foo() { return $super() + 23; }, obj);
expect(26).to.equal(obj.foo());
});
});
describe("class realted method access", function() {
var Klass1 = function() {}
Klass1.prototype.foo = function(a, b) { return a + b; };
Klass1.prototype.bar = function(a) { return this.foo(a, 3); };
Klass1.prototype.baz = 23
var Klass2 = function() {}
Klass2.prototype = Object.create(Klass1.prototype);
Klass2.prototype.zork = function() { return 23; };
Klass2.prototype.bar = function(a) { return this.foo(a, 4); };
it("finds method names of class", function() {
expect(fun.functionNames(Klass2)).to.eql(["foo", "bar", "zork"].reverse());
});
it("finds local method names of class", function() {
expect(fun.localFunctionNames(Klass2)).to.eql(["zork", "bar"]);
});
});
});