Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* -*- 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");
});