zotero/test/tests/concurrentCallerTest.js
Abe Jellinek 20e75c3251 Remove Bluebird methods in concurrentCallerTest.js
This is really messy and not ideal, but these tests were written for
Bluebird Promise state inspection, and it's difficult to make them look
nice without that being available.
2025-07-30 22:30:57 -04:00

406 lines
11 KiB
JavaScript

"use strict";
describe("ConcurrentCaller", function () {
const { ConcurrentCaller } = ChromeUtils.importESModule("resource://zotero/concurrentCaller.mjs");
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", async 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 async 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;
await 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 = await 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 set)", async 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 async function () {
if (logger) {
Zotero.debug("Running " + id);
}
running++;
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
await 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);
await Zotero.Promise.delay(1);
var promise2 = caller.start(funcs2);
var promise2Fulfilled = false;
promise2.then(() => promise2Fulfilled = true);
// Wait for first set
var results1 = await promise1;
// Second set shouldn't be done yet
assert.isFalse(promise2Fulfilled);
assert.equal(finished, keys1.length);
assert.equal(results1.length, keys1.length);
assert.sameMembers(await Promise.all(results1), keys1);
assert.isFalse(failed);
})
it("should add functions to existing queue and resolve when all are complete (waiting for later set)", async function () {
var numConcurrent = 2;
var running = 0;
var finished = 0;
var failed = false;
var ids1 = {"1": 100, "2": 45, "3": 200};
var ids2 = {"4": 1, "5": 1};
var makeFunc = function (id, delay) {
return async function () {
if (logger) {
Zotero.debug("Running " + id);
}
running++;
if (running > numConcurrent) {
failed = true;
throw new Error("Too many concurrent tasks");
}
await 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);
var promise1Fulfilled = false;
promise1.then(() => promise1Fulfilled = true);
await Zotero.Promise.delay(10);
var promise2 = caller.start(funcs2);
// Wait for second set
var results2 = await promise2;
// The second set should finish before the first
assert.isFalse(promise1Fulfilled);
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(await results2[0], keys2[0]);
assert.equal(await results2[1], keys2[1]);
assert.isFalse(failed);
})
it("should return a rejected promise if a single passed function fails", async function () {
var numConcurrent = 2;
var caller = new ConcurrentCaller({
numConcurrent,
logger
});
var e = await getPromiseError(caller.start(function () {
throw new Error("Fail");
}));
assert.ok(e);
})
it("should stop on error if stopOnError is set", async 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 async 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;
await 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 promise2Fulfilled = false;
promise2.then(() => promise2Fulfilled = true);
var results1 = await promise1;
assert.isTrue(promise2Fulfilled);
let promise2Value = await promise2;
assert.equal(running, 0);
assert.isBelow(finished, ids1.length);
results1 = await Promise.allSettled(results1);
assert.equal(results1.length, ids1.length);
assert.equal(promise2Value.length, ids2.length);
// 'a' should be fulfilled
assert.isTrue(results1[0].status === 'fulfilled');
// 'g' should be rejected
assert.isTrue(results1[6].status === 'rejected');
// 'm' should be rejected
assert.isTrue(results1[12].status === 'rejected');
// All promises in second batch should be rejected
assert.isTrue((await Promise.allSettled(promise2Value)).every(p => p.status === 'rejected'));
})
it("should not stop on error if stopOnError isn't set", async 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 async 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;
await 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 promise1Fulfilled = false;
promise1.then(() => promise1Fulfilled = true);
var promise2 = caller.start(funcs2);
var promise2Fulfilled = false;
promise2.then(() => promise2Fulfilled = true);
var results2 = await Promise.allSettled(await promise2);
assert.isTrue(promise1Fulfilled);
assert.isTrue(promise2Fulfilled);
var results1 = await Promise.allSettled(await promise1);
assert.equal(running, 0);
assert.equal(finished, ids1.length + ids2.length);
assert.equal(results1.length, ids1.length);
assert.equal(results2.length, ids2.length);
// 'a' should be fulfilled
assert.isTrue(results1[0].status === 'fulfilled');
// 'g' should be rejected
assert.isTrue(results1[6].status === 'rejected');
// 'm' should be fulfilled
assert.isTrue(results1[12].status === 'fulfilled');
// All promises in second batch should be fulfilled
assert.isTrue(results2.every(p => p.status === 'fulfilled'));
})
})
describe("#wait()", function () {
it("should return when all tasks are done", async 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 async 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;
await 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);
var promise1Fulfilled = false;
promise1.then(() => promise1Fulfilled = true);
await Zotero.Promise.delay(10);
var promise2 = caller.start(funcs2);
var promise2Fulfilled = false;
promise2.then(() => promise2Fulfilled = true);
await caller.wait();
await new Promise(resolve => setTimeout(resolve));
assert.isTrue(promise1Fulfilled);
assert.isTrue(promise2Fulfilled);
assert.equal(running, 0);
assert.equal(finished, ids1.length + ids2.length);
})
})
})