Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
"use strict";
const {
MultiProgressAggregator,
ProgressAndStatusCallbackParams,
ProgressStatusText,
readResponse,
URLChecker,
RejectionType,
BlockListManager,
RemoteSettingsManager,
isAddonEngineId,
addonIdToEngineId,
engineIdToAddonId,
stringifyForLog,
} = ChromeUtils.importESModule("chrome://global/content/ml/Utils.sys.mjs");
/**
* Test that we can retrieve the correct content without a callback.
*/
add_task(async function test_correct_response_no_callback() {
const content = "This is the expected response.";
const blob = new Blob([content]);
const response = new Response(blob, {
headers: new Headers({ "Content-Length": blob.size }),
});
const responseArray = await readResponse(response);
const responseContent = new TextDecoder().decode(responseArray);
Assert.equal(content, responseContent, "The response content should match.");
});
/**
* Test that we can retrieve the correct content with a callback.
*/
add_task(async function test_correct_response_callback() {
const content = "This is the expected response.";
const blob = new Blob([content]);
const response = new Response(blob, {
headers: new Headers({ "Content-Length": blob.size }),
});
const responseArray = await readResponse(response, data => {
data;
});
const responseContent = new TextDecoder().decode(responseArray);
Assert.equal(content, responseContent, "The response content should match.");
});
/**
* Test that we can retrieve the correct content with a content-lenght lower than the actual len
*/
add_task(async function test_correct_response_content_length_under_reported() {
const content = "This is the expected response.";
const blob = new Blob([content]);
const response = new Response(blob, {
headers: new Headers({
"Content-Length": 1,
}),
});
const responseArray = await readResponse(response, data => {
data;
});
const responseContent = new TextDecoder().decode(responseArray);
Assert.equal(content, responseContent, "The response content should match.");
});
/**
* Test that we can retrieve the correct content with a content-lenght larger than the actual len
*/
add_task(async function test_correct_response_content_length_over_reported() {
const content = "This is the expected response.";
const blob = new Blob([content]);
const response = new Response(blob, {
headers: new Headers({
"Content-Length": 2 * blob.size + 20,
}),
});
const responseArray = await readResponse(response, data => {
data;
});
const responseContent = new TextDecoder().decode(responseArray);
Assert.equal(content, responseContent, "The response content should match.");
});
/**
* Test that we can retrieve and the callback provide correct information
*/
add_task(async function test_correct_response_callback_correct() {
const contents = ["Carrot", "Broccoli", "Tomato", "Spinach"];
let contentSizes = [];
let totalSize = 0;
for (const value of contents) {
contentSizes.push(new Blob([value]).size);
totalSize += contentSizes[contentSizes.length - 1];
}
const numChunks = contents.length;
let encoder = new TextEncoder();
// const stream = ReadableStream.from(contents);
let streamId = -1;
const stream = new ReadableStream({
pull(controller) {
streamId += 1;
if (streamId < numChunks) {
controller.enqueue(encoder.encode(contents[streamId]));
} else {
controller.close();
}
},
});
const response = new Response(stream, {
headers: new Headers({
"Content-Length": totalSize,
}),
});
let chunkId = -1;
let expectedTotalLoaded = 0;
const responseArray = await readResponse(response, data => {
chunkId += 1;
// The callback is called on time with no data loaded and just the total
if (chunkId == 0) {
Assert.deepEqual(
{
total: data.total,
currentLoaded: data.currentLoaded,
totalLoaded: data.totalLoaded,
},
{
total: totalSize,
currentLoaded: 0,
totalLoaded: 0,
},
"The callback should be called on time with an estimate of the total size and no data read. "
);
} else {
Assert.less(
chunkId - 1,
numChunks,
"The number of times the callback is called should be lower than the number of chunks"
);
expectedTotalLoaded += contentSizes[chunkId - 1];
Assert.deepEqual(
{
total: data.total,
currentLoaded: data.currentLoaded,
totalLoaded: data.totalLoaded,
},
{
total: totalSize,
currentLoaded: contentSizes[chunkId - 1],
totalLoaded: expectedTotalLoaded,
},
"The reported value by the callback should match the correct values"
);
}
});
Assert.equal(
chunkId,
numChunks,
"The callback should be called exactly as many times as the number of chunks."
);
const responseContent = new TextDecoder().decode(
responseArray.buffer.slice(
responseArray.byteOffset,
responseArray.byteLength + responseArray.byteOffset
)
);
Assert.equal(
contents.join(""),
responseContent,
"The response content should match."
);
});
/**
* Test that multi-aggregator only call the callback for the provided types.
*/
add_task(async function test_multi_aggregator_watchtypes() {
let numCalls = 0;
let aggregator = new MultiProgressAggregator({
progressCallback: _ => {
numCalls += 1;
},
watchedTypes: ["t1"],
});
aggregator.aggregateCallback(
new ProgressAndStatusCallbackParams({
type: "download",
})
);
Assert.equal(numCalls, 0);
aggregator.aggregateCallback(
new ProgressAndStatusCallbackParams({
type: "t1",
})
);
Assert.equal(numCalls, 1);
});
/**
* Test that multi-aggregator aggregate correctly.
*/
add_task(async function test_multi_aggregator() {
// Ids for all available tasks. Should be unique per element.
const taskIds = ["A", "B", "C", "D", "E", "F"];
// The type for each available tasks.
const taskTypes = ["t1", "t1", "t2", "t2", "t3", "t3"];
// The total size available for each task
const taskSizes = [5, 11, 13, 17, 19, 23];
// The chunk sizes. The sum for indices with same chunk task index (according to chunkTaskIndex)
// should be equal to the corresponding size in taskSizes
const chunkSizes = [2, 3, 5, 6, 11, 7, 12, 6, 8, 9, 9, 10];
// Task index for each chunk. Index in the array taskIds. Order was chosen so that we can simulate
// overlaps in tasks.
const chunkTaskIndex = [0, 0, 1, 2, 5, 2, 5, 1, 3, 4, 3, 4];
// Indicating how much has been loaded for the task so far.
const chunkTaskLoaded = [2, 5, 5, 6, 11, 13, 23, 11, 8, 9, 17, 19];
// Whether the
const chunkIsFinal = [0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1];
let numDone = 0;
let currentData = null;
let expectedTotalToLoad = 0;
let numCalls = 0;
let expectedNumCalls = 0;
let expectedTotalLoaded = 0;
let taskIdsWithDataSet = new Set();
const aggregator = new MultiProgressAggregator({
progressCallback: data => {
currentData = data;
numCalls += 1;
if (data.statusText == ProgressStatusText.DONE) {
numDone += 1;
}
},
watchedTypes: ["t1", "t2", "t3"],
});
// Initiate and advertise the size for each task
for (const i in taskTypes) {
currentData = null;
expectedNumCalls += 1;
aggregator.aggregateCallback(
new ProgressAndStatusCallbackParams({
type: taskTypes[i],
statusText: ProgressStatusText.INITIATE,
id: taskIds[i],
total: taskSizes[i],
})
);
Assert.equal(currentData.progress, 0, "Progress is 0%");
Assert.ok(currentData, "Received data should be defined");
Assert.deepEqual(
{
statusText: currentData?.statusText,
type: currentData?.type,
id: currentData?.id,
totalObjectsSeen: currentData?.metadata?.totalObjectsSeen,
numDone,
numCalls,
},
{
statusText: ProgressStatusText.INITIATE,
type: taskTypes[i],
id: taskIds[i],
totalObjectsSeen: taskIdsWithDataSet.size,
numDone: 0,
numCalls: expectedNumCalls,
},
"Data received after initiate should be correct"
);
currentData = null;
expectedNumCalls += 1;
aggregator.aggregateCallback(
new ProgressAndStatusCallbackParams({
type: taskTypes[i],
statusText: ProgressStatusText.SIZE_ESTIMATE,
id: taskIds[i],
total: taskSizes[i],
})
);
Assert.ok(currentData, "Received data should be defined");
expectedTotalToLoad += taskSizes[i];
taskIdsWithDataSet.add(taskIds[i]);
Assert.deepEqual(
{
numDone,
numCalls,
total: currentData.total,
totalObjectsSeen: currentData?.metadata?.totalObjectsSeen,
},
{
numDone: 0,
total: expectedTotalToLoad,
numCalls: expectedNumCalls,
totalObjectsSeen: taskIdsWithDataSet.size,
},
"Data received after size estimate should be correct."
);
}
// Send progress status for each chunk.
for (const chunkIndex in chunkTaskIndex) {
let taskIndex = chunkTaskIndex[chunkIndex];
currentData = null;
expectedNumCalls += 1;
expectedTotalLoaded += chunkSizes[chunkIndex];
aggregator.aggregateCallback(
new ProgressAndStatusCallbackParams({
type: taskTypes[taskIndex],
statusText: ProgressStatusText.IN_PROGRESS,
id: taskIds[taskIndex],
total: taskSizes[taskIndex],
currentLoaded: chunkSizes[chunkIndex],
totalLoaded: chunkTaskLoaded[chunkIndex],
})
);
Assert.ok(currentData, "Received data should be defined");
Assert.deepEqual(
{
numDone,
numCalls,
total: currentData?.total,
totalObjectsSeen: currentData?.metadata?.totalObjectsSeen,
currentLoaded: currentData?.currentLoaded,
totalLoaded: currentData?.totalLoaded,
},
{
numDone: 0,
numCalls: expectedNumCalls,
total: expectedTotalToLoad,
totalObjectsSeen: taskIdsWithDataSet.size,
currentLoaded: chunkSizes[chunkIndex],
totalLoaded: expectedTotalLoaded,
},
"Data received after in progress should be correct"
);
// Notify of task is done
if (chunkIsFinal[chunkIndex]) {
currentData = null;
aggregator.aggregateCallback(
new ProgressAndStatusCallbackParams({
type: taskTypes[taskIndex],
statusText: ProgressStatusText.IN_PROGRESS,
id: taskIds[taskIndex],
total: taskSizes[taskIndex],
currentLoaded: chunkSizes[chunkIndex],
totalLoaded: chunkTaskLoaded[chunkIndex],
})
);
expectedNumCalls += 1;
Assert.deepEqual(
{
numDone,
numCalls,
total: currentData?.total,
totalObjectsSeen: currentData?.metadata?.totalObjectsSeen,
currentLoaded: currentData?.currentLoaded,
totalLoaded: currentData?.totalLoaded,
},
{
numDone: 0,
numCalls: expectedNumCalls,
total: expectedTotalToLoad,
totalObjectsSeen: taskIdsWithDataSet.size,
currentLoaded: chunkSizes[chunkIndex],
totalLoaded: expectedTotalLoaded,
},
"Extra data beyond what is expected or a process should not affect total downloaded"
);
currentData = null;
expectedNumCalls += 1;
aggregator.aggregateCallback(
new ProgressAndStatusCallbackParams({
type: taskTypes[taskIndex],
statusText: ProgressStatusText.DONE,
id: taskIds[taskIndex],
total: taskSizes[chunkIndex],
})
);
Assert.ok(currentData, "Received data should be defined");
Assert.deepEqual(
{ total: currentData.total, numCalls },
{ total: expectedTotalToLoad, numCalls: expectedNumCalls },
"Data received after completed tasks should be correct"
);
}
}
Assert.equal(currentData.progress, 100, "Progress is 100%");
Assert.equal(numDone, 1, "Done status should be received");
});
/**
* Tests the URLChecker class
*
*/
add_task(async function testURLChecker() {
// Example of result from remote settings
const list = [
{ filter: "ALLOW", urlPrefix: "https://huggingface.co/mozilla/" },
{ filter: "ALLOW", urlPrefix: "https://model-hub.mozilla.org/" },
{
filter: "ALLOW",
urlPrefix:
},
{
filter: "ALLOW",
},
{
filter: "DENY",
},
{
filter: "ALLOW",
},
];
const checker = new URLChecker(list);
// Test cases
const testCases = [
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description:
"Allows all models and versions from Mozilla on Hugging Face",
},
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description:
"Allows a specific model and version from Mozilla on Hugging Face",
},
{
expected: { allowed: false, rejectionType: RejectionType.DENIED },
description:
"Denies a specific restricted model from Mozilla on Hugging Face",
},
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows any model from Mozilla's model hub",
},
{
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to an unapproved hub URL",
},
{
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to a subdomain of an approved domain",
},
{
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description:
"Denies access to URL with allowed domain as a subdomain in another domain",
},
{
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access with a similar-looking scheme",
},
{
url: "https://localhost./",
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to URL with trailing dot in domain",
},
{
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to URL with user info",
},
{
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description:
"Denies access to URL with disallowed scheme but allowed host",
},
{
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to domain containing an allowed domain",
},
{
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description:
"Denies access to URL with triple slashes, just type correctly",
},
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to URL with port specified",
},
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to URL with HTTP scheme on localhost",
},
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to Mozilla's approved model hub URL",
},
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to internal resource URL in code base",
},
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to URL with localhost with a port",
},
{
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to URL with localhost with a user",
},
];
for (const { url, expected, description } of testCases) {
const result = checker.allowedURL(url);
Assert.deepEqual(
result,
expected,
`URL check for '${url}' should return ${JSON.stringify(
expected
)}: ${description}`
);
}
});
/**
* Test the Block List Manager with a single blocked n-grams at word boundaries
*
*/
add_task(async function testBlockListManager_single_blocked_word() {
const manager = new BlockListManager({
blockNgrams: [BlockListManager.encodeBase64("would like")],
});
Assert.equal(
manager.blockNgramSet.values().next().value,
"would like",
"decoded blocked n-grams should match the original value"
);
Assert.equal(
BlockListManager.decodeBase64(BlockListManager.encodeBase64("would like")),
"would like",
"round trip encode/decode should give original input"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People are here" }),
false,
"should have no match if blocked n-gram not in input"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People would likes" }),
false,
"should have no match even if only part of blocked n-gram in input"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People would do like" }),
false,
"should have no match if they are other words separating a blocked 2-grams"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People wouldlike" }),
false,
"should have no match if text contains blocked n-grams but without the spaces"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People would_like" }),
false,
"should have no match text contain special characters between a blocked 2-grams"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People would liketo " }),
false,
"should have no match if the blocked 2-grams is not at word boundary"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People are here and would like go" }),
true,
"should match if blocked 2-grams in input"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "would like to do it" }),
true,
"should match if blocked 2-grams is at the beginning of input"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "I need to would like." }),
true,
"should match if blocked 2-grams is at end of input even with punctuation"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "I need to would like" }),
true,
"should match if blocked 2-grams is at end of input"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "I need to would like " }),
true,
"should match even if blocked 2-grams has extra spaces after it"
);
});
/**
* Test the Block List Manager with multiple blocked n-grams at word boundaries
*
*/
add_task(async function testBlockListManager_multiple_blocked_ngrams() {
const manager = new BlockListManager({
blockNgrams: [
BlockListManager.encodeBase64("would like"),
BlockListManager.encodeBase64("vast"),
BlockListManager.encodeBase64("blocked"),
],
});
Assert.equal(
manager.matchAtWordBoundary({ text: "People are here" }),
false,
"should have no match if blocked n-grams are not present"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People wouldlike iblocked" }),
false,
"should have no match if blocked n-grams are not at words boundary"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "People are here and blocked" }),
true,
"should match if blocked n-grams are at word boundary"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "would like to do it" }),
true,
"should match for all blocked n-grams in the list"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "I need to would like blocked." }),
true,
"should match for all blocked n-grams in the list"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "I have a vast amount." }),
true,
"should match for all blocked n-grams in the list"
);
});
/**
* Test the Block List Manager with multiple blocked n-grams anywhere
*
*/
add_task(async function testBlockListManager_anywhere() {
const manager = new BlockListManager({
blockNgrams: [
BlockListManager.encodeBase64("would like"),
BlockListManager.encodeBase64("vast"),
BlockListManager.encodeBase64("blocked"),
],
});
Assert.equal(
manager.matchAnywhere({ text: "People are here" }),
false,
"should have no match if blocked n-grams are not present"
);
Assert.equal(
manager.matchAnywhere({ text: "People wouldlike iblocked" }),
true,
"should match even if blocked n-grams are not at word boundary"
);
Assert.equal(
manager.matchAnywhere({ text: "People are here and blocked" }),
true,
"should match for all blocked n-grams"
);
Assert.equal(
manager.matchAnywhere({ text: "would like to do it" }),
true,
"should match for all blocked n-grams"
);
Assert.equal(
manager.matchAnywhere({ text: "I need to would like blocked." }),
true,
"should match for all blocked n-grams"
);
Assert.equal(
manager.matchAnywhere({ text: "I have a vast amount." }),
true,
"should match for all blocked n-grams"
);
Assert.equal(
manager.matchAnywhere({ text: "I have avast amount." }),
true,
"should match for all blocked n-grams even if not at word boundary"
);
});
/**
* Get test data for remote settings manager test.
*
*/
async function getMockedRemoteSettingsClients() {
const client1 = await createRemoteClient({
collectionName: "test-block-list",
records: [
{
version: "1.0.0",
name: "test-link-preview-en",
blockList: ["cGVyc29u"], // person
language: "en",
id: "1",
},
{
version: "1.0.0",
name: "test-link-preview-fr",
blockList: ["bW9p"], // moi
language: "fr",
id: "2",
},
{
version: "1.0.0",
name: "base-fr",
blockList: [],
language: "fr",
id: "3",
},
{
version: "2.0.0",
name: "test-link-preview-en",
blockList: ["b25l"], // one
language: "en",
id: "4",
},
{
version: "1.1.0",
name: "test-link-preview-en",
blockList: ["dHdv"], // two
language: "en",
id: "5",
},
],
});
const client2 = await createRemoteClient({
collectionName: "test-request-options",
records: [
{
version: "1.0.0",
featureId: "test-link-preview",
options: JSON.stringify({ param1: "value1", number: 2 }),
id: "10",
},
{
version: "1.0.0",
featureId: "test-ml-suggest",
options: JSON.stringify({ suggest: "val" }),
id: "11",
},
{
version: "1.0.0",
featureId: "ml-i2t",
options: JSON.stringify({ size: 2 }),
id: "12",
},
{
version: "2.0.0",
featureId: "test-link-preview",
options: JSON.stringify({ param2: "value2", number2: 20 }),
id: "13",
},
{
version: "1.1.0",
featureId: "test-link-preview",
options: JSON.stringify({ param1: "value2", number: 3 }),
id: "14",
},
],
});
const client3 = await createRemoteClient({
collectionName: "test-inference-options",
records: [
{
featureId: "test-link-preview",
id: "20",
},
{
featureId: "test-ml-suggest",
id: "21",
},
],
});
return {
"test-block-list": client1,
"test-request-options": client2,
"test-inference-options": client3,
};
}
/**
* Test the Remote Settings Manager
*
*/
add_task(async function testRemoteSettingsManager() {
RemoteSettingsManager.mockRemoteSettings(
await getMockedRemoteSettingsClients()
);
let data = await RemoteSettingsManager.getRemoteData({
collectionName: "test-block-list",
filters: {
name: "test-link-preview-en",
},
majorVersion: 1,
});
Assert.deepEqual(
data,
{
version: "1.1.0",
name: "test-link-preview-en",
blockList: ["dHdv"],
language: "en",
id: "5",
},
"should retrieve the latest revision ignoring previous one"
);
data = await RemoteSettingsManager.getRemoteData({
collectionName: "test-block-list",
filters: {
name: "test-link-preview-en",
language: "en",
},
majorVersion: 2,
});
Assert.deepEqual(
data,
{
version: "2.0.0",
name: "test-link-preview-en",
blockList: ["b25l"],
language: "en",
id: "4",
},
"should retrieve the exact revision"
);
data = await RemoteSettingsManager.getRemoteData({
collectionName: "test-request-options",
filters: {
featureId: "test-link-preview",
},
majorVersion: 1,
lookupKey: record => record.featureId,
});
Assert.deepEqual(
data,
{
version: "1.1.0",
featureId: "test-link-preview",
options: JSON.stringify({ param1: "value2", number: 3 }),
id: "14",
},
"should retrieve from the correct collection even in presence of multiple"
);
data = await RemoteSettingsManager.getRemoteData({
collectionName: "test-inference-options",
filters: {
featureId: "test-link-preview",
},
});
Assert.deepEqual(
data,
{
featureId: "test-link-preview",
id: "20",
},
"should retrieve from the correct collection even in presence of multiple"
);
data = await RemoteSettingsManager.getRemoteData(
{
collectionName: "test-request-options",
filters: {
featureId: "test-link-previewP",
},
majorVersion: 1,
lookupKey: record => record.featureId,
},
"should work with lookupKey"
);
Assert.equal(data, null);
RemoteSettingsManager.removeMocks();
});
/**
* Test the Remote data Manager
*
*/
add_task(async function testBlockListManagerWithRS() {
RemoteSettingsManager.mockRemoteSettings(
await getMockedRemoteSettingsClients()
);
let manager = await BlockListManager.initializeFromRemoteSettings({
blockListName: "test-link-preview-en",
language: "en",
fallbackToDefault: false,
majorVersion: 1,
collectionName: "test-block-list",
});
Assert.equal(
manager.matchAtWordBoundary({ text: "two guy is here" }),
true,
"should retrieve the correct list and match <two> for the blocked word"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "one person is here, moi" }),
false,
"should retrieve the correct list and match nothing"
);
await Assert.rejects(
BlockListManager.initializeFromRemoteSettings({
blockListName: "test-link-preview-en-non-existing",
language: "en",
fallbackToDefault: false,
majorVersion: 1,
collectionName: "test-block-list",
}),
/./,
"should fails since the block list does not exist"
);
manager = await BlockListManager.initializeFromRemoteSettings({
blockListName: "test-link-preview-en-non-existing",
language: "en",
fallbackToDefault: true,
majorVersion: 1,
collectionName: "test-block-list",
});
Assert.equal(
manager.matchAtWordBoundary({
text: "the bells are ringing, ding dong ...",
}),
true,
"should not fail but fallback to default list with a match for dong"
);
Assert.equal(
manager.matchAtWordBoundary({ text: "two person is here, moi" }),
false,
"should not fail but fallback to default list with no matches found"
);
RemoteSettingsManager.removeMocks();
});
add_task(function test_addon_engine_id_utilities() {
const prefix = "ML-ENGINE-";
// Valid addon ID
const addonId = "custom-addon";
const engineId = addonIdToEngineId(addonId);
Assert.equal(
engineId,
`${prefix}${addonId}`,
"addonIdToEngineId should add the correct prefix"
);
Assert.ok(
isAddonEngineId(engineId),
"isAddonEngineId should detect prefixed engine ID"
);
Assert.equal(
engineIdToAddonId(engineId),
addonId,
"engineIdToAddonId should return original addon ID"
);
// Invalid engine ID
const invalidEngineId = "ENGINE-custom-addon";
Assert.ok(
!isAddonEngineId(invalidEngineId),
"isAddonEngineId should reject non-prefixed engine ID"
);
Assert.equal(
engineIdToAddonId(invalidEngineId),
null,
"engineIdToAddonId should return null for invalid ID"
);
// Edge case: empty string
Assert.ok(!isAddonEngineId(""), "isAddonEngineId should reject empty string");
Assert.equal(
engineIdToAddonId(""),
null,
"engineIdToAddonId should return null for empty string"
);
});
/**
* stringifyForLog — primitives and core types.
* Accept both JSON and summary-string outputs.
*/
add_task(function test_stringifyForLog_primitives_and_core_types() {
// Primitives (avoid top-level undefined since some impls return undefined)
Assert.equal(stringifyForLog(42), "42");
Assert.equal(stringifyForLog(true), "true");
Assert.equal(stringifyForLog(null), "null");
Assert.equal(stringifyForLog("hi"), '"hi"');
// BigInt & Symbol — could be quoted (JSON) or not (slow path)
const bi = stringifyForLog(1n);
Assert.ok(bi === "1" || bi === '"1"', "BigInt should stringify");
const sym = stringifyForLog(Symbol("x"));
Assert.ok(
sym === "Symbol(x)" || sym === '"Symbol(x)"',
"Symbol should stringify (quoted or not)"
);
// Date & RegExp — either embedded in JSON or as substrings in a summary
const sample = { d: new Date("2020-01-01T00:00:00.000Z"), r: /abc/i };
const s = stringifyForLog(sample);
let parsed = null;
try {
parsed = JSON.parse(s);
} catch {}
if (parsed) {
// Date must be somewhere (either as ISO string or an object fallback)
Assert.ok(
parsed.d === "2020-01-01T00:00:00.000Z" || typeof parsed.d === "object",
"Date should be preserved as ISO or object"
);
// RegExp may collapse to {} in JSON; accept either the literal string or {}
Assert.ok(
parsed.r === "/abc/i" ||
(typeof parsed.r === "object" && Object.keys(parsed.r).length === 0),
'RegExp should be "/abc/i" or an empty object'
);
} else {
// Summary string path
Assert.ok(
s.includes("2020-01-01T00:00:00.000Z"),
"Summary should contain ISO date"
);
Assert.ok(s.includes("/abc/i"), "Summary should contain RegExp literal");
}
// Function — always a short descriptor
function foo() {}
const f = stringifyForLog({ f: foo });
let parsedF = null;
try {
parsedF = JSON.parse(f);
} catch {}
const fVal = parsedF ? parsedF.f : f;
Assert.ok(
String(fVal).includes("[Function"),
"Function should be described briefly"
);
// Error — JSON path may produce {}, summary path should contain 'Error: boom'
const err = new Error("boom");
const e = stringifyForLog({ e: err });
let parsedE = null;
try {
parsedE = JSON.parse(e);
} catch {}
if (parsedE && typeof parsedE.e === "object" && parsedE.e) {
// Accept either detailed fields or an empty object — do not require raw text.
const hasFields =
parsedE.e.name === "Error" ||
parsedE.e.message === "boom" ||
(parsedE.e.stack && typeof parsedE.e.stack === "string");
const emptyObj = Object.keys(parsedE.e).length === 0;
Assert.ok(
hasFields || emptyObj,
"Error should be detailed or {} in JSON mode"
);
} else {
// Summary string path (non-JSON) should include something meaningful.
Assert.ok(
e.includes("Error") && e.includes("boom"),
"Summary should include error text"
);
}
});
/**
* stringifyForLog — collections, circular refs, and limits.
* Do not assume Map/Set shapes; just validate useful, bounded output.
*/
add_task(function test_stringifyForLog_collections_and_limits() {
const m = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
const st = new Set([10, 20, 30]);
const ta = new Uint8Array(5);
const ab = new ArrayBuffer(7);
const obj = { m, st, ta, ab, cyc: null };
obj.cyc = obj; // circular
const out = stringifyForLog(obj, { maxKeysPerLevel: 2 });
// Always a string and non-empty
Assert.equal(typeof out, "string");
Assert.greater(out.length, 0);
// TypedArray / ArrayBuffer summaries should appear as strings in either path
Assert.ok(
out.includes("Uint8Array(5 bytes)"),
"TypedArray size summary present"
);
// Circular marker must be present
Assert.ok(out.includes("[Circular]"), "Circular marker present");
// Map/Set can be summarized or JSON-ified or collapse to {}/[]; we only require *some* hint.
// Accept any of these hints to avoid coupling to a specific implementation.
const mapHints =
out.includes("Map(") || out.includes('"m":') || out.includes("m:");
const setHints =
out.includes("Set(") || out.includes('"st":') || out.includes("st:");
Assert.ok(mapHints, "Output should carry some hint of the Map");
Assert.ok(setHints, "Output should carry some hint of the Set");
});
/**
* stringifyForLog — output truncation guard.
* Compute suffix length from the actual output to avoid off-by-one.
*/
add_task(function test_stringifyForLog_output_truncation() {
const maxOutputLength = 200;
const huge = "x".repeat(5000);
const out = stringifyForLog({ huge }, { maxOutputLength });
// Must end with a truncation marker
Assert.ok(/…\[truncated]$/.test(out), "Should end with truncation suffix");
// Validate that the prefix was clipped to maxOutputLength characters.
const suffix = "…[truncated]";
Assert.equal(out.length, maxOutputLength + suffix.length);
});
/**
* stringifyForLog — slow path via throwing getter; depth limits and safety.
* Keep the checks, but don’t assume specific summarization tokens.
*/
add_task(function test_stringifyForLog_slow_path_throwing_getter_and_depth() {
const deep = { level: 0 };
let cur = deep;
for (let i = 1; i <= 5; i++) {
cur.child = { level: i };
cur = cur.child;
}
const host = {};
Object.defineProperty(host, "bad", {
enumerable: true,
get() {
throw new Error("nope");
},
});
host.deep = deep;
const out = stringifyForLog(host, {
maxDepth: 2,
maxKeysPerLevel: 50,
maxOutputLength: 5000,
});
// Throwing getter recorded
Assert.ok(
out.includes('"bad": [Thrown:'),
"Throwing getter should be caught and annotated"
);
// Depth limit — don’t pin to exact token; just ensure some summarization occurred
Assert.ok(
/\[Object [^\]]+\]|\[Array\(\d+\)\]/.test(out),
"Deep object should be summarized at maxDepth"
);
});
/**
* stringifyForLog — always returns a string, even for hostile proxies.
*/
add_task(function test_stringifyForLog_always_string_with_proxy() {
// Proxy that throws on get
const target = {};
const proxy = new Proxy(target, {
get() {
throw new Error("trap");
},
ownKeys() {
// JSON.stringify inspects keys; make that safe too.
throw new Error("ownKeys trap");
},
});
const out = stringifyForLog(proxy);
Assert.equal(typeof out, "string");
Assert.ok(
out.includes("[Uninspectable:") ||
out.includes("Proxy") ||
out.includes("[Thrown:"),
"Should degrade gracefully on hostile proxies"
);
});
/**
* stringifyForLog — top-level BigInt should not throw and should stringify.
* Accept both quoted and unquoted representations.
*/
add_task(function test_stringifyForLog_top_level_bigint() {
const out = stringifyForLog(9007199254740993n);
Assert.ok(
out === "9007199254740993" || out === '"9007199254740993"',
"Top-level BigInt should stringify (quoted or not)"
);
});