275 lines
9.5 KiB
JavaScript
275 lines
9.5 KiB
JavaScript
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set sts=2 sw=2 et tw=80: */
|
|
"use strict";
|
|
|
|
const { ExtensionTaskScheduler } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/ExtensionTaskScheduler.sys.mjs"
|
|
);
|
|
|
|
function assertIsPromise(value, message) {
|
|
let t = typeof value == "object" && value && Cu.getClassName(value, true);
|
|
equal(t, "Promise", message);
|
|
}
|
|
|
|
add_task(function synchronous_return_value() {
|
|
const ets = new ExtensionTaskScheduler();
|
|
const dummyId = "@not_an_extension";
|
|
|
|
let read1 = ets.runReadTask(dummyId, () => 1);
|
|
equal(read1, 1, "runReadTask returns return value");
|
|
|
|
let write1 = ets.runWriteTask(dummyId, () => 1);
|
|
equal(write1, 1, "runWriteTask returns return value");
|
|
});
|
|
|
|
add_task(async function test_throwing_task() {
|
|
const ets = new ExtensionTaskScheduler();
|
|
const dummyId = "@not_an_extension";
|
|
|
|
function funcThrows() {
|
|
throw new Error("funcThrows");
|
|
}
|
|
let rejectedPromise = Promise.reject(new Error("funcRejects"));
|
|
function funcRejects() {
|
|
return rejectedPromise;
|
|
}
|
|
|
|
Assert.throws(
|
|
() => ets.runReadTask(dummyId, funcThrows),
|
|
/funcThrows/,
|
|
"sync task throws"
|
|
);
|
|
|
|
let rv1 = ets.runWriteTask(dummyId, funcRejects);
|
|
equal(rv1, rejectedPromise, "Got original rejected promise for write task");
|
|
let rv2 = ets.runReadTask(dummyId, funcThrows);
|
|
assertIsPromise(rv2, "Got Promise for read task that is blocked on write");
|
|
let calls = [];
|
|
let rv3 = ets.runReadTask(dummyId, () => calls.push(1));
|
|
assertIsPromise(rv3, "Got Promise for read task that does not reject");
|
|
|
|
await Assert.rejects(rv1, /funcRejects/, "task resolves to rejection");
|
|
await Assert.rejects(
|
|
rv2,
|
|
/funcThrows/,
|
|
"task resolves to rejection for synchronously thrown error"
|
|
);
|
|
// The two await above run two rounds of microtasks, which should be plenty
|
|
// to get rv1 to complete, and rv2 + rv3 to run in parallel, and rv3's sync
|
|
// task to complete.
|
|
Assert.deepEqual(calls, [1], "Next task still runs after rejection");
|
|
equal(await rv3, 1, "Got result from non-rejecting task");
|
|
});
|
|
|
|
add_task(async function read_is_parallel__and_write_blocks_read() {
|
|
const ets = new ExtensionTaskScheduler();
|
|
const dummyId = "@not_an_extension";
|
|
|
|
// This returns the internal ExtensionBoundTaskQueue instance. Since there is
|
|
// no extension, it tests the core behavior of the ReadWriteQueue superclass.
|
|
let q = ets.forExtensionId(dummyId);
|
|
equal(q.hasPendingTasks(), false, "Task queue is empty");
|
|
|
|
let calls = [];
|
|
let delayedTask1 = Promise.withResolvers();
|
|
let delayedTask2 = Promise.withResolvers();
|
|
let delayedTask7 = Promise.withResolvers();
|
|
|
|
let rv1 = q.runReadTask(() => calls.push(1) && delayedTask1.promise);
|
|
equal(rv1, delayedTask1.promise, "runReadTask returns original promise (1)");
|
|
equal(q.hasPendingTasks(), true, "Task queue is not empty");
|
|
|
|
let rv2 = q.runReadTask(() => calls.push(2) && delayedTask2.promise);
|
|
equal(rv2, delayedTask2.promise, "runReadTask returns original promise (2)");
|
|
|
|
equal(
|
|
q.runReadTask(() => 123),
|
|
123,
|
|
"runReadTask returns synchronous result immediately"
|
|
);
|
|
|
|
let rv3 = q.runWriteTask(() => calls.push(3));
|
|
let rv4 = q.runReadTask(() => calls.push(4));
|
|
let rv5 = q.runWriteTask(() => calls.push(5));
|
|
let rv6 = q.runWriteTask(() => calls.push(6));
|
|
let rv7 = q.runReadTask(() => calls.push(7) && delayedTask7.promise);
|
|
|
|
Assert.deepEqual(
|
|
calls,
|
|
[1, 2],
|
|
"Write task 3 awaits earlier read tasks and blocks later tasks"
|
|
);
|
|
|
|
delayedTask1.resolve("res1");
|
|
delayedTask2.resolve("res2");
|
|
equal(await rv1, "res1", "Got resolved value for task 1, read");
|
|
equal(await rv2, "res2", "Got resolved value for task 2, read");
|
|
Assert.deepEqual(
|
|
calls,
|
|
[1, 2, 3, 4, 5, 6, 7],
|
|
"After unblocking the read task, all other non-async tasks ran immediately"
|
|
);
|
|
|
|
equal(
|
|
q.runReadTask(() => 456),
|
|
456,
|
|
"Although a previous read task is still pending, it does not block new read"
|
|
);
|
|
|
|
equal(q.hasPendingTasks(), true, "Task queue is not empty");
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue is alive");
|
|
equal(
|
|
ets.forExtensionId(dummyId),
|
|
q,
|
|
"Same queue was reused despite the non-existing extension ID"
|
|
);
|
|
|
|
delayedTask7.resolve("res7");
|
|
equal(await rv7, "res7", "Last read task ran");
|
|
equal(ets._extensionBoundQueues.size, 0, "Queue is gone");
|
|
equal(q.hasPendingTasks(), false, "Task queue is empty");
|
|
|
|
let allReturnValues = [rv1, rv2, rv3, rv4, rv5, rv6, rv7];
|
|
for (let [i, rv] of allReturnValues.entries()) {
|
|
assertIsPromise(rv, `task ${i} returned a Promise`);
|
|
}
|
|
Assert.deepEqual(
|
|
await Promise.all(allReturnValues),
|
|
["res1", "res2", 3, 4, 5, 6, "res7"],
|
|
"All tasks resolved to the expected value"
|
|
);
|
|
});
|
|
|
|
add_task(async function write_blocks_write() {
|
|
const ets = new ExtensionTaskScheduler();
|
|
const dummyId = "@not_an_extension";
|
|
|
|
let q = ets.forExtensionId(dummyId);
|
|
|
|
let calls = [];
|
|
let delayedTask1 = Promise.withResolvers();
|
|
let delayedTask2 = Promise.withResolvers();
|
|
|
|
let rv1 = q.runWriteTask(() => calls.push(1) && delayedTask1.promise);
|
|
equal(rv1, delayedTask1.promise, "runWriteTask (1) returns original promise");
|
|
|
|
let rv2 = q.runWriteTask(() => calls.push(2) && delayedTask2.promise);
|
|
notEqual(rv2, delayedTask2.promise, "runWriteTask (2) returns new Promise");
|
|
Assert.deepEqual(calls, [1], "Second write blocked on first");
|
|
|
|
delayedTask1.resolve("res1");
|
|
delayedTask2.resolve("res2");
|
|
equal(await rv1, "res1", "Got resolved value for task 1, write");
|
|
equal(await rv2, "res2", "Got resolved value for task 2, write");
|
|
|
|
equal(q.hasPendingTasks(), false, "Task queue is empty");
|
|
equal(ets._extensionBoundQueues.size, 0, "Queue is gone");
|
|
});
|
|
|
|
// Tests that the internal task queues are not persisted in memory when the
|
|
// extensionId is not associated with a live extension.
|
|
add_task(async function schedule_non_existing_extension() {
|
|
const ets = new ExtensionTaskScheduler();
|
|
const nonExtensionId = "@does-not-exist";
|
|
let q1 = ets.forExtensionId(nonExtensionId);
|
|
let q2 = ets.forExtensionId(nonExtensionId);
|
|
equal(q1, q2, "Returns same queue even if extension has not loaded yet");
|
|
|
|
function triggerTask() {
|
|
const funcReturns1 = () => 1;
|
|
equal(ets.runReadTask(nonExtensionId, funcReturns1), 1, "Task ran");
|
|
}
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue is there");
|
|
triggerTask();
|
|
equal(ets._extensionBoundQueues.size, 0, "Queue gone after running task");
|
|
|
|
let q3 = ets.forExtensionId(nonExtensionId);
|
|
notEqual(q1, q3, "Queue is different");
|
|
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue is there");
|
|
triggerTask();
|
|
equal(ets._extensionBoundQueues.size, 0, "Queue gone after running task");
|
|
});
|
|
|
|
add_task(async function schedule_existing_extension() {
|
|
const ets = new ExtensionTaskScheduler();
|
|
let extension = ExtensionTestUtils.loadExtension({});
|
|
await extension.startup();
|
|
let extensionId = extension.id;
|
|
|
|
function triggerTask() {
|
|
const funcReturns1 = () => 1;
|
|
equal(ets.runReadTask(extensionId, funcReturns1), 1, "Task ran");
|
|
}
|
|
|
|
let q1 = ets.forExtensionId(extensionId);
|
|
let q2 = ets.forExtensionId(extensionId);
|
|
equal(q1, q2, "Returns same queue for extension");
|
|
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue is there");
|
|
triggerTask();
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue still there after task");
|
|
|
|
let q3 = ets.forExtensionId(extensionId);
|
|
equal(q1, q3, "Queue is the same");
|
|
|
|
await extension.unload();
|
|
equal(ets._extensionBoundQueues.size, 0, "Queue gone after extension unload");
|
|
|
|
triggerTask();
|
|
equal(ets._extensionBoundQueues.size, 0, "Queue still gone after task run");
|
|
});
|
|
|
|
add_task(async function load_and_unload_without_schedule() {
|
|
const ets = new ExtensionTaskScheduler();
|
|
let extension = ExtensionTestUtils.loadExtension({});
|
|
await extension.startup();
|
|
let extensionId = extension.id;
|
|
|
|
let q = ets.forExtensionId(extensionId);
|
|
equal(q.hasPendingTasks(), false, "Task queue is empty");
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue is there");
|
|
|
|
await extension.unload();
|
|
equal(ets._extensionBoundQueues.size, 0, "Queue gone after extension unload");
|
|
});
|
|
|
|
add_task(async function queue_outlives_extension_with_pending_tasks() {
|
|
const extensionId = "@extension_that_reloads";
|
|
const ets = new ExtensionTaskScheduler();
|
|
let extension;
|
|
async function startExtension() {
|
|
extension = ExtensionTestUtils.loadExtension({
|
|
manifest: { browser_specific_settings: { gecko: { id: extensionId } } },
|
|
});
|
|
await extension.startup();
|
|
}
|
|
|
|
let q = ets.forExtensionId(extensionId);
|
|
|
|
await startExtension();
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue still there");
|
|
equal(ets.forExtensionId(extensionId), q, "Queue still same after startup");
|
|
|
|
let delayedTask1 = Promise.withResolvers();
|
|
let delayedTask2 = Promise.withResolvers();
|
|
let rv1 = ets.runWriteTask(extensionId, () => delayedTask1.promise);
|
|
let rv2 = ets.runReadTask(extensionId, () => delayedTask2.promise);
|
|
notEqual(rv2, delayedTask2.promise, "read task blocked on write task");
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue is there after task");
|
|
|
|
await extension.unload();
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue is there despite unload");
|
|
|
|
await startExtension();
|
|
delayedTask1.resolve("res1");
|
|
delayedTask2.resolve("res2");
|
|
equal(await rv1, "res1", "Write task finished");
|
|
equal(await rv2, "res2", "Read task finished");
|
|
equal(ets._extensionBoundQueues.size, 1, "Queue is there despite reload");
|
|
|
|
equal(ets.forExtensionId(extensionId), q, "Queue still same after reload");
|
|
|
|
await extension.unload();
|
|
equal(ets._extensionBoundQueues.size, 0, "Queue not persisted after unload");
|
|
});
|