zotero/test/tests/concurrentCallerTest.js
Dan Stillman 05de47149f Allow marking of errors as handled for Bluebird
Set .handledRejection on an Error object to tell Bluebird that it's been
handled and shouldn't be logged by onPossiblyUnhandledRejection().
2016-04-27 02:32:58 -04:00

385 lines
11 KiB
JavaScript

"use strict";
describe("ConcurrentCaller", function () {
Components.utils.import("resource://zotero/concurrentCaller.js");
var logger = null;
// Uncomment to get debug output
//logger = Zotero.debug;
describe("#start()", function () {
it("should run functions as slots open and wait for them to complete", function* () {
var numConcurrent = 2;
var running = 0;
var finished = 0;
var failed = false;
var ids = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
var funcs = ids.map(function (id) {
return Zotero.Promise.coroutine(function* () {
if (logger) {
Zotero.debug("Running " + id);
}
running++;
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
var min = 10;
var max = 25;
yield Zotero.Promise.delay(
Math.floor(Math.random() * (max - min + 1)) + min
);
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
running--;
finished++;
if (logger) {
Zotero.debug("Finished " + id);
}
return id;
});
})
var caller = new ConcurrentCaller({
numConcurrent,
logger
});
var results = yield caller.start(funcs);
assert.equal(results.length, ids.length);
assert.equal(running, 0);
assert.equal(finished, ids.length);
assert.isFalse(failed);
})
it("should add functions to existing queue and resolve when all are complete (waiting for earlier)", function* () {
var numConcurrent = 2;
var running = 0;
var finished = 0;
var failed = false;
var ids1 = {"1": 5, "2": 10, "3": 7};
var ids2 = {"4": 50, "5": 50};
var makeFunc = function (id, delay) {
return Zotero.Promise.coroutine(function* () {
if (logger) {
Zotero.debug("Running " + id);
}
running++;
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
yield Zotero.Promise.delay(delay);
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
running--;
finished++;
if (logger) {
Zotero.debug("Finished " + id);
}
return id;
});
};
var keys1 = Object.keys(ids1);
var keys2 = Object.keys(ids2);
var funcs1 = Object.keys(ids1).map(id => makeFunc(id, ids1[id]));
var funcs2 = Object.keys(ids2).map(id => makeFunc(id, ids2[id]));
var caller = new ConcurrentCaller({
numConcurrent,
logger
});
var promise1 = caller.start(funcs1);
yield Zotero.Promise.delay(1);
var promise2 = caller.start(funcs2);
var results1 = yield promise1;
// Second set shouldn't be done yet
assert.isFalse(promise2.isFulfilled());
assert.equal(finished, keys1.length);
assert.equal(results1.length, keys1.length);
assert.sameMembers(results1.map(p => p.value()), keys1);
assert.isFalse(failed);
})
it("should add functions to existing queue and resolve when all are complete (waiting for later)", function* () {
var numConcurrent = 2;
var running = 0;
var finished = 0;
var failed = false;
var ids1 = {"1": 50, "2": 15, "3": 60};
var ids2 = {"4": 1, "5": 1};
var makeFunc = function (id, delay) {
return Zotero.Promise.coroutine(function* () {
if (logger) {
Zotero.debug("Running " + id);
}
running++;
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
yield Zotero.Promise.delay(delay);
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
running--;
finished++;
if (logger) {
Zotero.debug("Finished " + id);
}
return id;
});
};
var keys1 = Object.keys(ids1);
var keys2 = Object.keys(ids2);
var funcs1 = Object.keys(ids1).map(id => makeFunc(id, ids1[id]));
var funcs2 = Object.keys(ids2).map(id => makeFunc(id, ids2[id]));
var caller = new ConcurrentCaller({
numConcurrent,
logger
});
var promise1 = caller.start(funcs1);
yield Zotero.Promise.delay(10);
var promise2 = caller.start(funcs2);
var results2 = yield promise2;
// The second set should finish before the first
assert.isFalse(promise1.isFulfilled());
assert.equal(running, 1); // 3 should still be running
assert.equal(finished, 4); // 1, 2, 4, 5
assert.equal(results2.length, keys2.length);
assert.equal(results2[0].value(), keys2[0]);
assert.equal(results2[1].value(), keys2[1]);
assert.isFalse(failed);
})
it("should return a rejected promise if a single passed function fails", function* () {
var numConcurrent = 2;
var caller = new ConcurrentCaller({
numConcurrent,
logger
});
var e = yield getPromiseError(caller.start(function () {
throw new Error("Fail");
}));
assert.ok(e);
})
it("should stop on error if stopOnError is set", function* () {
var numConcurrent = 2;
var running = 0;
var finished = 0;
var failed = false;
var ids1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'];
var ids2 = ['n', 'o', 'p', 'q'];
var makeFunc = function (id) {
return Zotero.Promise.coroutine(function* () {
if (logger) {
Zotero.debug("Running " + id);
}
running++
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
var min = 10;
var max = 25;
yield Zotero.Promise.delay(
Math.floor(Math.random() * (max - min + 1)) + min
);
if (id == 'g') {
running--;
finished++;
Zotero.debug("Throwing " + id);
// This causes an erroneous "possibly unhandled rejection" message in
// Bluebird 2.10.2 that I can't seem to get rid of (and the rejection
// is later handled), so tell Bluebird to ignore it
let e = new Error("Fail");
e.handledRejection = true;
throw e;
}
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
running--;
finished++;
if (logger) {
Zotero.debug("Finished " + id);
}
return id;
});
};
var funcs1 = ids1.map(makeFunc)
var funcs2 = ids2.map(makeFunc)
var caller = new ConcurrentCaller({
numConcurrent,
stopOnError: true,
logger
});
var promise1 = caller.start(funcs1);
var promise2 = caller.start(funcs2);
var results1 = yield promise1;
assert.isTrue(promise2.isFulfilled());
assert.equal(running, 0);
assert.isBelow(finished, ids1.length);
assert.equal(results1.length, ids1.length);
assert.equal(promise2.value().length, ids2.length);
// 'a' should be fulfilled
assert.isTrue(results1[0].isFulfilled());
// 'g' should be rejected
assert.isTrue(results1[6].isRejected());
// 'm' should be rejected
assert.isTrue(results1[12].isRejected());
// All promises in second batch should be rejected
assert.isTrue(promise2.value().every(p => p.isRejected()));
})
it("should not stop on error if stopOnError isn't set", function* () {
var numConcurrent = 2;
var running = 0;
var finished = 0;
var failed = false;
var ids1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'];
var ids2 = ['n', 'o', 'p', 'q'];
var makeFunc = function (id) {
return Zotero.Promise.coroutine(function* () {
if (logger) {
Zotero.debug("Running " + id);
}
running++
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
var min = 10;
var max = 25;
yield Zotero.Promise.delay(
Math.floor(Math.random() * (max - min + 1)) + min
);
if (id == 'g') {
running--;
finished++;
Zotero.debug("Throwing " + id);
// This causes an erroneous "possibly unhandled rejection" message in
// Bluebird 2.10.2 that I can't seem to get rid of (and the rejection
// is later handled), so tell Bluebird to ignore it
let e = new Error("Fail");
e.handledRejection = true;
throw e;
}
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
running--;
finished++;
if (logger) {
Zotero.debug("Finished " + id);
}
return id;
});
};
var funcs1 = ids1.map(makeFunc)
var funcs2 = ids2.map(makeFunc)
var caller = new ConcurrentCaller({
numConcurrent,
logger
});
var promise1 = caller.start(funcs1);
var promise2 = caller.start(funcs2);
var results2 = yield promise2;
assert.isTrue(promise1.isFulfilled());
assert.isTrue(promise2.isFulfilled());
assert.equal(running, 0);
assert.equal(finished, ids1.length + ids2.length);
assert.equal(promise1.value().length, ids1.length);
assert.equal(results2.length, ids2.length);
// 'a' should be fulfilled
assert.isTrue(promise1.value()[0].isFulfilled());
// 'g' should be rejected
assert.isTrue(promise1.value()[6].isRejected());
// 'm' should be fulfilled
assert.isTrue(promise1.value()[12].isFulfilled());
// All promises in second batch should be fulfilled
assert.isTrue(results2.every(p => p.isFulfilled()));
})
})
describe("#wait()", function () {
it("should return when all tasks are done", function* () {
var numConcurrent = 2;
var running = 0;
var finished = 0;
var failed = false;
var ids1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'];
var ids2 = ['n', 'o', 'p', 'q'];
var makeFunc = function (id) {
return Zotero.Promise.coroutine(function* () {
if (logger) {
Zotero.debug("Running " + id);
}
running++;
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
var min = 10;
var max = 25;
yield Zotero.Promise.delay(
Math.floor(Math.random() * (max - min + 1)) + min
);
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
running--;
finished++;
if (logger) {
Zotero.debug("Finished " + id);
}
return id;
});
};
var funcs1 = ids1.map(makeFunc)
var funcs2 = ids2.map(makeFunc)
var caller = new ConcurrentCaller({
numConcurrent,
logger
});
var promise1 = caller.start(funcs1);
yield Zotero.Promise.delay(10);
var promise2 = caller.start(funcs2);
yield caller.wait();
assert.isTrue(promise1.isFulfilled());
assert.isTrue(promise2.isFulfilled());
assert.equal(running, 0);
assert.equal(finished, ids1.length + ids2.length);
})
})
})