koroutine
Version:
Small, lightweight coroutine scheduler for node based on ES6 generators
500 lines (415 loc) • 13.4 kB
JavaScript
;
const koroutine = require('..');
// koroutine.enableBreadcrumbs(false);
// koroutine.setErrorHandler(() => {});
function asyncCallSuccess (input, callback, delay) {
setTimeout(function () {
callback(null, input, 'arg-1', 'arg-2');
}, delay);
}
function asyncCallError (input, callback, delay) {
setTimeout(function () {
callback(new Error(input));
}, delay);
}
function* sequentialCallSuccess (input, delay) {
return yield asyncCallSuccess(input, koroutine.callback(), delay);
}
function* sequentialCallError (input, delay) {
return yield asyncCallError(input, koroutine.callback(), delay);
}
function* testSequentialCalls (test) {
test.expect(4);
let result = yield* sequentialCallSuccess('input-1', 200);
test.deepEqual(result, ['input-1', 'arg-1', 'arg-2']);
result = yield* sequentialCallSuccess('input-2', 100);
test.deepEqual(result, ['input-2', 'arg-1', 'arg-2']);
try {
yield* sequentialCallError('error-1', 150);
} catch (e) {
test.equal(e.message, 'error-1');
test.equal(e.cause, 'Exception');
}
test.done();
}
function* alwaysThrows () {
throw new Error('ERROR');
}
exports['Test koroutine error handler function'] = function (test) {
koroutine.run(alwaysThrows(), {errorHandler: () => test.done()});
};
exports['Test koroutine.callback() fail when koroutine not started'] = function (test) {
try {
asyncCallSuccess('input-1', koroutine.callback(), 100);
test.equals(1, 2);
} catch (ex) {
test.done();
}
};
function testNotGeneratorFunction () {
return function () {
console.log('####################### SHOULD NEVER GET PRINTED!!! #######################');
};
}
exports['Test koroutine.run() fail without iterator'] = function (test) {
try {
koroutine.run(testNotGeneratorFunction());
test.equals(1, 2);
} catch (ex) {
test.done();
}
};
function* dummy () {
yield true;
}
function* reentrantCoroutine () {
koroutine.run(dummy());
}
exports['Test reentrant koroutine '] = function (test) {
koroutine.setErrorHandler((e) => test.done());
try {
koroutine.run(reentrantCoroutine());
} catch (ex) {
test.done();
}
};
exports['Test sequential async calls'] = function (test) {
koroutine.run(testSequentialCalls(test));
};
function* testParallelCalls (test) {
test.expect(5);
const f1 = koroutine.future();
asyncCallSuccess('input-1', f1, 200);
const f2 = koroutine.future();
asyncCallSuccess('input-2', f2, 300);
const f3 = koroutine.future();
asyncCallError('error-1', f3, 100);
const numErrors = yield* koroutine.join(f1, f2, f3);
test.deepEqual(f1.data, ['input-1', 'arg-1', 'arg-2']);
test.deepEqual(f2.data, ['input-2', 'arg-1', 'arg-2']);
test.equal(f3.error.message, 'error-1');
test.equal(f3.error.cause, 'Exception');
test.equal(numErrors, 1);
test.done();
}
exports['Test parallel async calls'] = function (test) {
koroutine.run(testParallelCalls(test));
};
function* testMixedCalls (test) {
test.expect(8);
const f1 = koroutine.future();
asyncCallSuccess('input-1', f1, 10);
const f2 = koroutine.future();
asyncCallSuccess('input-2', f2, 100);
const f3 = koroutine.future();
asyncCallSuccess('input-3', f3, 200);
const f4 = koroutine.future();
asyncCallSuccess('input-4', f4, 300);
const f5 = koroutine.future();
asyncCallSuccess('input-5', f5, 800);
let numErrors = yield* koroutine.join(f1, f2);
test.equal(numErrors, 0);
test.deepEqual(f1.data, ['input-1', 'arg-1', 'arg-2']);
test.deepEqual(f2.data, ['input-2', 'arg-1', 'arg-2']);
let result = yield* sequentialCallSuccess('input-4', 400);
test.deepEqual(result, ['input-4', 'arg-1', 'arg-2']);
numErrors = yield* koroutine.join(f3, f4, f5);
test.equal(numErrors, 0);
test.deepEqual(f3.data, ['input-3', 'arg-1', 'arg-2']);
test.deepEqual(f4.data, ['input-4', 'arg-1', 'arg-2']);
test.deepEqual(f5.data, ['input-5', 'arg-1', 'arg-2']);
test.done();
}
exports['Test futures mixed with yield'] = function (test) {
koroutine.run(testMixedCalls(test));
};
function* callbackTimeout (test) {
try {
yield asyncCallSuccess('input-1', koroutine.callback(10), 100);
test.equal(1, 2);
} catch (e) {
test.done();
}
}
exports['Test callback timeout'] = function (test) {
koroutine.run(callbackTimeout(test));
};
function* callbackTimeoutsRace (test) {
test.expect(2);
const result1 = yield asyncCallSuccess('input-1', koroutine.callback(50), 10);
test.deepEqual(result1, ['input-1', 'arg-1', 'arg-2']);
const result2 = yield asyncCallSuccess('input-2', koroutine.callback(), 400);
test.deepEqual(result2, ['input-2', 'arg-1', 'arg-2']);
test.done();
}
exports['Test multiple callbacks timeout race condition'] = function (test) {
koroutine.run(callbackTimeoutsRace(test));
};
exports['Test futures mixed with yield'] = function (test) {
koroutine.run(testMixedCalls(test));
};
exports['Test future timeout'] = function (test) {
koroutine.run((function* (test) {
test.expect(4);
const f1 = koroutine.future(100);
asyncCallSuccess('input-1', f1, 8000);
const f2 = koroutine.future(2000);
asyncCallSuccess('input-2', f2, 300);
const f3 = koroutine.future(200);
asyncCallError('error-1', f3, 8000);
const numErrors = yield* koroutine.join(f1, f2, f3);
test.equal(numErrors, 2);
test.equal(f1.error.cause, 'TimedOut');
test.deepEqual(f2.data, ['input-2', 'arg-1', 'arg-2']);
test.equal(f3.error.cause, 'TimedOut');
test.done();
})(test));
};
exports['Test koroutine timeout with future'] = function (test) {
koroutine.run((function* (test) {
test.expect(1);
try {
const ft = koroutine.future(100);
asyncCallSuccess('input-2', ft, 300);
yield* koroutine.join(ft);
test.equals(1, 2);
} catch (e) {
test.equal(e.cause, 'TimedOut');
test.done();
}
})(test), {timeout: 10});
};
function* testSequentialCallTimeout (test) {
test.expect(1);
try {
yield* sequentialCallSuccess('input-1', 2000);
} catch (e) {
test.equal(e.cause, 'TimedOut');
}
test.done();
}
exports['Test sequential call timeout'] = function (test) {
koroutine.run(testSequentialCallTimeout(test), {timeout: 10});
};
function* testParallelCallsTimeout (test) {
test.expect(2);
const f1 = koroutine.future();
asyncCallSuccess('input-1', f1, 200);
const f2 = koroutine.future();
asyncCallSuccess('input-2', f2, 600);
try {
yield* koroutine.join(f1, f2);
} catch (e) {
test.equal(e.cause, 'TimedOut');
test.deepEqual(f1.data, ['input-1', 'arg-1', 'arg-2']);
}
test.done();
}
exports['Test parallel calls timeout'] = function (test) {
koroutine.run(testParallelCallsTimeout(test), {timeout: 400});
};
exports['Test continue after error'] = function (test) {
koroutine.run((function* (test) {
test.expect(3);
try {
yield* sequentialCallError('error-1', 150);
test.equals(1, 2);
} catch (e) {
test.equal(e.message, 'error-1');
test.equal(e.cause, 'Exception');
}
let result = yield* sequentialCallSuccess('input-2', 100);
test.deepEqual(result, ['input-2', 'arg-1', 'arg-2']);
test.done();
})(test));
};
function* testSleep (test) {
test.expect(1);
const start = Date.now();
yield koroutine.sleep(100);
const end = Date.now();
test.ok((end - start) >= 90, (end - start));
test.done();
};
exports['Test sleep'] = function (test) {
koroutine.run(testSleep(test));
};
function* testDefer (test) {
test.expect(3);
let count = 0;
yield koroutine.defer();
test.equal(count, 0);
setImmediate(function () {
count++;
});
setImmediate(function () {
count++;
});
test.equal(count, 0);
yield koroutine.defer();
test.equal(count, 2);
test.done();
}
exports['Test defer'] = function (test) {
koroutine.run(testDefer(test));
};
function testDoneLatch (test, limit) {
let count = 0;
const origDone = test.done;
return function () {
count += 1;
if (count >= limit) {
origDone();
}
};
}
function testExpectAccumulator (test) {
let accCount = 0;
const origExpect = test.expect;
return function (count) {
accCount += count;
origExpect(accCount);
};
}
exports['Test sequential async calls in multiple simultaneous coroutines'] = function (test) {
test.expect = testExpectAccumulator(test);
test.done = testDoneLatch(test, 3);
koroutine.run(testSequentialCalls(test));
koroutine.run(testSequentialCalls(test));
koroutine.run(testSequentialCalls(test));
};
exports['Test parallel async calls in multiple simultaneous coroutines'] = function (test) {
test.expect = testExpectAccumulator(test);
test.done = testDoneLatch(test, 3);
koroutine.run(testParallelCalls(test), {timeout: 8000});
koroutine.run(testParallelCalls(test), {timeout: 8000});
koroutine.run(testParallelCalls(test), {timeout: 8000});
};
exports['Test mixed calls in multiple simultaneous coroutines'] = function (test) {
test.expect = testExpectAccumulator(test);
test.done = testDoneLatch(test, 6);
koroutine.run(testSequentialCalls(test), {timeout: 2000});
koroutine.run(testParallelCalls(test), {timeout: 1000});
koroutine.run(testSequentialCalls(test), {timeout: 2000});
koroutine.run(testParallelCalls(test), {timeout: 1000});
koroutine.run(testParallelCalls(test), {timeout: 1000});
koroutine.run(testSequentialCalls(test), {timeout: 2000});
};
function* testCoroutineCurrentState (test, delay, input) {
test.expect(1);
koroutine.state.my_var = input;
yield koroutine.sleep(delay);
test.equal(koroutine.state.my_var, input);
test.done();
};
exports['Test koroutine current context'] = function (test) {
test.expect = testExpectAccumulator(test);
test.done = testDoneLatch(test, 3);
koroutine.run(testCoroutineCurrentState(test, 100, 'first'));
koroutine.run(testCoroutineCurrentState(test, 200, 'second'));
koroutine.run(testCoroutineCurrentState(test, 150, 'third'));
};
function* testExceptionThrower (test) {
throw new Error('Uncaught Exception!');
}
exports['Test coroutine throwing uncaught exception'] = function (test) {
koroutine.setErrorHandler((e) => {
test.ok(e);
test.done();
});
koroutine.run(testExceptionThrower(test));
};
exports['Test coroutine throwing error with timeout'] = function (test) {
koroutine.setErrorHandler((e) => {
test.ok(e);
test.done();
});
koroutine.run(testExceptionThrower(test), {timeout: 10});
};
exports['Test future() fail without active koroutine'] = function (test) {
try {
koroutine.future();
test.equals(1, 2);
} catch (e) {
test.done();
}
};
exports['Test sleep() fail without active koroutine'] = function (test) {
try {
koroutine.sleep(10);
test.equals(1, 2);
} catch (e) {
test.done();
}
};
exports['Test defer() fail without active koroutine'] = function (test) {
try {
koroutine.defer();
test.equals(1, 2);
} catch (e) {
test.done();
}
};
function* testStackTracing (mesg) {
yield* sequentialCallSuccess('input-1', 100);
yield* sequentialCallSuccess('input-2', 100);
throw new Error(mesg);
}
exports['Test with stack tracing disabled'] = function (test) {
const errHandlerFn = function (err) {
const log = err.stack;
test.expect(5);
test.ok(err.message.startsWith('Unhandled exception in koroutine'));
test.ok(log.includes('next (native)'));
test.equal((log.match(/testStackTracing/g) || []).length, 1);
test.ok(!log.includes('Resumed'));
test.ok(log.split('\n').length > 5);
test.done();
};
koroutine.setErrorHandler(errHandlerFn);
koroutine.run(testStackTracing('test stack tracing disabled'));
};
exports['Test with stack tracing enabled globally'] = function (test) {
const errHandlerFn = function (err) {
const log = err.stack;
// console.log(log);
test.expect(5);
test.ok(err.message.startsWith('Unhandled exception in koroutine'));
test.ok(!log.includes('next (native)'));
test.equal((log.match(/testStackTracing/g) || []).length, 3);
test.ok(log.includes('test-stack-tracing suspended at'));
test.ok(log.split('\n').length > 5);
test.done();
};
koroutine.setErrorHandler(errHandlerFn);
koroutine.enableBreadcrumbs(true);
koroutine.run(testStackTracing('test stack tracing enabled'), {name: 'test-stack-tracing'});
};
exports['Test with stack tracing enabled locally'] = function (test) {
const errHandlerFn = function (err) {
const log = err.stack;
// console.log(log);
test.expect(5);
test.ok(err.message.startsWith('Unhandled exception in koroutine'));
test.ok(!log.includes('next (native)'));
test.equal((log.match(/testStackTracing/g) || []).length, 3);
test.ok(log.includes('test-stack-tracing suspended at'));
test.ok(log.split('\n').length > 5);
test.done();
};
koroutine.setErrorHandler(errHandlerFn);
koroutine.enableBreadcrumbs(false);
koroutine.run(testStackTracing('test stack tracing enabled'), {enableBreadcrumbs: true, name: 'test-stack-tracing'});
};
function* multipleLiveCallback () {
koroutine.callback();
yield* sequentialCallSuccess('input-1', 100);
}
exports['Test multiple live callback fail'] = function (test) {
try {
koroutine.run(multipleLiveCallback(), {errorHandler: (err) => { throw err; }});
test.equals(1, 2);
} catch (e) {
test.done();
}
};