diff options
Diffstat (limited to 'intl/l10n/test/test_l10nregistry_fuzzed.js')
-rw-r--r-- | intl/l10n/test/test_l10nregistry_fuzzed.js | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/intl/l10n/test/test_l10nregistry_fuzzed.js b/intl/l10n/test/test_l10nregistry_fuzzed.js new file mode 100644 index 0000000000..f99c3b61e5 --- /dev/null +++ b/intl/l10n/test/test_l10nregistry_fuzzed.js @@ -0,0 +1,205 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This test is a fuzzing test for the L10nRegistry API. It was written to find + * a hard to reproduce bug in the L10nRegistry code. If it fails, place the seed + * from the failing run in the code directly below to make it consistently reproducible. + */ +let seed = Math.floor(Math.random() * 1e9); + +console.log(`Starting a fuzzing run with seed: ${seed}.`); +console.log("To reproduce this test locally, re-run it locally with:"); +console.log(`let seed = ${seed};`); + +/** + * A simple non-robust psuedo-random number generator. + * + * It is implemented using a Lehmer random number generator. + * https://en.wikipedia.org/wiki/16,807 + * + * @returns {number} Ranged [0, 1) + */ +function prng() { + const multiplier = 16807; + const prime = 2147483647; + seed = seed * multiplier % prime + return (seed - 1) / prime +} + +/** + * Generate a name like "mock-dmsxfodrqboljmxdeayt". + * @returns {string} + */ +function generateRandomName() { + let name = 'mock-' + const letters = "abcdefghijklmnopqrstuvwxyz"; + for (let i = 0; i < 20; i++) { + name += letters[Math.floor(prng() * letters.length)]; + } + return name; +} + +/** + * Picks one item from an array. + * + * @param {Array<T>} + * @returns {T} + */ +function pickOne(list) { + return list[Math.floor(prng() * list.length)] +} + +/** + * Picks a random subset from an array. + * + * @param {Array<T>} + * @returns {Array<T>} + */ +function pickN(list, count) { + list = list.slice(); + const result = []; + for (let i = 0; i < count && i < list.length; i++) { + // Pick a random item. + const index = Math.floor(prng() * list.length); + + // Swap item to the end. + const a = list[index]; + const b = list[list.length - 1]; + list[index] = b; + list[list.length - 1] = a + + // Now that the random item is on the end, pop it off and add it to the results. + result.push(list.pop()); + } + + return result +} + +/** + * Generate a random number + * @param {number} min + * @param {number} max + * @returns {number} + */ +function random(min, max) { + const delta = max - min; + return min + delta * prng(); +} + +/** + * Generate a random number generator with a distribution more towards the lower end. + * @param {number} min + * @param {number} max + * @returns {number} + */ +function randomPow(min, max) { + const delta = max - min; + const r = prng() + return min + delta * r * r; +} + +add_task(async function test_fuzzing_sources() { + const iterations = 100; + const maxSources = 10; + + const metasources = ["app", "langpack", ""]; + const availableLocales = ["en", "en-US", "pl", "en-CA", "es-AR", "es-ES"]; + + const l10nReg = new L10nRegistry(); + + for (let i = 0; i < iterations; i++) { + console.log("----------------------------------------------------------------------"); + console.log("Iteration", i); + let sourceCount = randomPow(0, maxSources); + + const mocks = []; + const fs = []; + + const locales = new Set(); + const filenames = new Set(); + + for (let j = 0; j < sourceCount; j++) { + const locale = pickOne(availableLocales); + locales.add(locale); + + let metasource = pickOne(metasources); + if (metasource === "langpack") { + metasource = `${metasource}-${locale}` + } + + const dir = generateRandomName(); + const filename = generateRandomName() + j + ".ftl"; + const path = `${dir}/${locale}/${filename}` + const name = metasource || "app"; + const source = "key = value"; + + filenames.add(filename); + + console.log("Add source", { name, metasource, path, source }); + fs.push({ path, source }); + + mocks.push([ + name, // name + metasource, // metasource, + [locale], // locales, + dir + "/{locale}/", + fs + ]) + } + + l10nReg.registerSources(mocks.map(args => L10nFileSource.createMock(...args))); + + const bundleLocales = pickN([...locales], random(1, 4)); + const bundleFilenames = pickN([...filenames], random(1, 10)); + + console.log("generateBundles", {bundleLocales, bundleFilenames}); + const bundles = l10nReg.generateBundles( + bundleLocales, + bundleFilenames + ); + + function next() { + console.log("Getting next bundle"); + const bundle = bundles.next() + console.log("Next bundle obtained", bundle); + return bundle; + } + + const ops = [ + // Increase the frequency of next being called. + next, + next, + next, + () => { + const newMocks = []; + for (const mock of pickN(mocks, random(0, 3))) { + const newMock = mock.slice(); + newMocks.push(newMock) + } + console.log("l10nReg.updateSources"); + l10nReg.updateSources(newMocks.map(mock => L10nFileSource.createMock(...mock))); + }, + () => { + console.log("l10nReg.clearSources"); + l10nReg.clearSources(); + } + ]; + + console.log("Start the operation loop"); + while (true) { + console.log("Next operation"); + const op = pickOne(ops); + const result = await op(); + if (result?.done) { + // The iterator completed. + break; + } + } + + console.log("Clear sources"); + l10nReg.clearSources(); + } + + ok(true, "The L10nRegistry fuzzing did not crash.") +}); |