// Root URL of the fake hub, see the `data` dir in the tests. const FAKE_HUB = "chrome://mochitests/content/browser/toolkit/components/ml/tests/browser/data"; const FAKE_URL_TEMPLATE = "{model}/resolve/{revision}";
/** *Testthatwecanretrievethecorrectcontentwithoutacallback.
*/
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 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."
);
Assert.equal(
contents.join(""),
responseContent, "The response content should match."
);
});
/** *Testthatmulti-aggregatoronlycallthecallbackfortheprovidedtypes.
*/
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);
});
/** *Testthatmulti-aggregatoraggregatecorrectly.
*/
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];
// 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");
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"
);
// Test cases const testCases = [
{
url: "https://huggingface.co/mozilla/",
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows all models and versions from Mozilla on Hugging Face",
},
{
url: "https://huggingface.co/mozilla/distilvit/blob/v0.5.0/",
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows a specific model and version from Mozilla on Hugging Face",
},
{
url: "https://huggingface.co/mozilla/restricted-model",
expected: { allowed: false, rejectionType: RejectionType.DENIED },
description: "Denies a specific restricted model from Mozilla on Hugging Face",
},
{
url: "https://model-hub.mozilla.org/some-model",
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows any model from Mozilla's model hub",
},
{
url: "https://my.cool.hub/",
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to an unapproved hub URL",
},
{
url: "https://sub.localhost/myhub",
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to a subdomain of an approved domain",
},
{
url: "https://model-hub.mozilla.org.evil.com",
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to URL with allowed domain as a subdomain in another domain",
},
{
url: "httpsz://localhost/myhub",
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",
},
{
url: "https://user@huggingface.co/mozilla/",
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to URL with user info",
},
{
url: "ftp://localhost/myhub/",
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to URL with disallowed scheme but allowed host",
},
{
url: "https://model-hub.mozilla.org.hack/",
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to domain containing an allowed domain",
},
{
url: "https:///huggingface.co/mozilla/",
expected: { allowed: false, rejectionType: RejectionType.DISALLOWED },
description: "Denies access to URL with triple slashes, just type correctly",
},
{
url: "https://localhost:8080/myhub",
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to URL with port specified",
},
{
url: "http://localhost/myhub",
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to URL with HTTP scheme on localhost",
},
{
url: "https://model-hub.mozilla.org/",
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to Mozilla's approved model hub URL",
},
{
url: "chrome://gre/somewhere/in/the/code/base",
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to internal resource URL in code base",
},
{
url: "http://localhost:37001/Xenova/all-M",
expected: { allowed: true, rejectionType: RejectionType.NONE },
description: "Allows access to URL with localhost with a port",
},
{
url: "https://user@localhost/Xenova/all-M",
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}`
);
}
});
/** *TesttheBlockListManagerwithasingleblockedn-gramsatwordboundaries *
*/
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"
);
});
/** *TesttheBlockListManagerwithmultipleblockedn-gramsatwordboundaries *
*/
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"
);
});
/** *TesttheBlockListManagerwithmultipleblockedn-gramsanywhere *
*/
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"
);
});
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"
);
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"
);
// 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);
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,circularrefs,andlimits. *DonotassumeMap/Setshapes;justvalidateuseful,boundedoutput.
*/
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");
});
// 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—slowpathviathrowinggetter;depthlimitsandsafety. *Keepthechecks,butdon’tassumespecificsummarizationtokens.
*/
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;
}
// 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—alwaysreturnsastring,evenforhostileproxies.
*/
add_task(function test_stringifyForLog_always_string_with_proxy() { // Proxy that throws on get const target = {}; const proxy = new Proxy(target, {
get() { thrownew Error("trap");
},
ownKeys() { // JSON.stringify inspects keys; make that safe too. thrownew Error("ownKeys trap");
},
});
// a file is stored by engineOne
await cache.put({
engineId: engineOne,
taskName: crypto.randomUUID(),
model: "org/model",
revision: "v1",
file: "file.txt",
data: createBlob(),
headers: null,
});
// The file is read by engineTwo
let retrievedData = await cache.getFile({
engineId: engineTwo,
model: "org/model",
revision: "v1",
file: "file.txt",
});
Assert.deepEqual(
retrievedData[0],
testData, "The retrieved data should match the stored data."
);
// We should have two engines associated with the model file
let retrievedFiles = await cache.listFiles({ model, revision }); Assert.equal(retrievedFiles.metadata.engineIds.length, 2);
// if we delete the model by engineOne, it will still be around for engineTwo
await MLUninstallService.uninstall({ engineIds: [engineOne], hub }); Assert.equal(UtilsMLEngine.getInstance(engineOne), null);
retrievedData = await cache.getFile({
engineId: engineTwo,
model: "org/model",
revision: "v1",
file: "file.txt",
}); Assert.deepEqual(
retrievedData[0],
testData, "The retrieved data should match the stored data."
);
// We should now have one engine associated with the model file
retrievedFiles = await cache.listFiles({ model, revision }); Assert.equal(retrievedFiles.metadata.engineIds.length, 1); Assert.equal(retrievedFiles.metadata.engineIds[0], engineTwo);
// now deleting via engineTwo
await MLUninstallService.uninstall({ engineIds: [engineTwo], hub }); Assert.equal(UtilsMLEngine.getInstance(engineTwo), null);
// at this point we should not have anymore files const dataAfterDelete = await cache.getFile({
engineId: engineOne,
model: "org/model",
revision: "v1",
file: "file.txt",
}); Assert.equal(
dataAfterDelete, null, "The data for the deleted model should not exist."
);
// Now we should have no more engine Assert.equal(await cache._testGetData(cache.enginesStoreName), null);
// a file is stored by engineOne
await cache.put({
engineId: engineOne,
taskName: crypto.randomUUID(),
model: "org/model",
revision: "v1",
file: "file.txt",
data: createBlob(),
headers: null,
});
// The file is read by engineTwo
let retrievedData = await cache.getFile({
engineId: engineTwo,
model: "org/model",
revision: "v1",
file: "file.txt",
});
Assert.deepEqual(
retrievedData[0],
testData, "The retrieved data should match the stored data."
);
// We should have two engines associated with the model file
let retrievedFiles = await cache.listFiles({ model, revision }); Assert.equal(retrievedFiles.metadata.engineIds.length, 2);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.