tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

JSObjectsTestUtils.sys.mjs (10563B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/
      3 */
      4 
      5 import { ObjectUtils } from "resource://gre/modules/ObjectUtils.sys.mjs";
      6 import { TEST_PAGE_HTML, CONTEXTS, AllObjects } from "resource://testing-common/AllJavascriptTypes.mjs";
      7 import { AddonTestUtils } from "resource://testing-common/AddonTestUtils.sys.mjs"
      8 
      9 // Name of the environment variable to set while running the test to update the expected values
     10 const UPDATE_SNAPSHOT_ENV = "UPDATE_SNAPSHOT";
     11 
     12 export { CONTEXTS } from "resource://testing-common/AllJavascriptTypes.mjs";
     13 
     14 // To avoid totally unrelated exceptions about missing appinfo when running from xpcshell tests
     15 const isXpcshell = Services.env.exists("XPCSHELL_TEST_PROFILE_DIR");
     16 if (isXpcshell) {
     17  AddonTestUtils.createAppInfo(
     18    "xpcshell@tests.mozilla.org",
     19    "XPCShell",
     20    "42",
     21    "42"
     22  );
     23 }
     24 
     25 let gTestScope;
     26 
     27 /**
     28 * Initialize the test helper.
     29 *
     30 * @param {object} testScope
     31 *        XPCShell or mochitest test scope (i.e. the global object of the test currently executed)
     32 */
     33 function init(testScope) {
     34  if (!testScope?.gTestPath && !testScope?.Assert) {
     35    throw new Error("`JSObjectsTestUtils.init()` should be called with the (xpcshell or mochitest) test global object");
     36  }
     37  gTestScope = testScope;
     38 
     39  if ("gTestPath" in testScope) {
     40    AddonTestUtils.initMochitest(testScope);
     41  } else {
     42    AddonTestUtils.init(testScope);
     43  }
     44 
     45  const server = AddonTestUtils.createHttpServer({
     46    hosts: ["example.com"],
     47  });
     48 
     49  server.registerPathHandler("/", (request, response) => {
     50    response.setHeader("Content-Type", "text/html");
     51    response.write(TEST_PAGE_HTML);
     52  });
     53 
     54  // Lookup for all preferences to toggle in order to have all the expected objects type functional
     55  const prefValues = new Map();
     56  for (const { prefs } of AllObjects) {
     57    if (!prefs) {
     58      continue;
     59    }
     60    for (const elt of prefs) {
     61      if (elt.length != 2) {
     62        throw new Error("Each pref should be an array of two element [prefName, prefValue]. Got: "+elt);
     63      }
     64      const [ name, value ] = elt;
     65      const otherValue = prefValues.get(name);
     66      if (otherValue && otherValue != value) {
     67        throw new Error(`Two javascript values in AllJavascriptTypes.mjs are expecting different values for '${name}' preference. (${otherValue} vs ${value})`);
     68      }
     69      prefValues.set(name, value);
     70      if (typeof(value) == "boolean") {
     71        Services.prefs.setBoolPref(name, value);
     72        gTestScope.registerCleanupFunction(() => {
     73          Services.prefs.clearUserPref(name);
     74        });
     75      } else {
     76        throw new Error("Unsupported pref type: "+name+" = "+value);
     77      }
     78    }
     79  }
     80 }
     81 
     82 let gExpectedValuesFilePath;
     83 let gCurrentTestFolderUrl;
     84 
     85 const chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
     86  Ci.nsIChromeRegistry
     87 );
     88 function loadExpectedValues(expectedValuesFileName) {
     89  const isUpdate = Services.env.get(UPDATE_SNAPSHOT_ENV) == "true";
     90  dump(`JS Objects test: ${isUpdate ? "Update" : "Check"} ${expectedValuesFileName}\n`);
     91 
     92  // Depending on the test suite, mochitest will expose `gTextPath` which is a chrome://
     93  // for the current test file.
     94  // Otherwise xpcshell will expose `resource://test/` for the current test folder.
     95  gCurrentTestFolderUrl = "gTestPath" in gTestScope 
     96      ? gTestScope.gTestPath.substr(0, gTestScope.gTestPath.lastIndexOf("/")) + "/"
     97      : "resource://test/";
     98 
     99  // Build the URL for the test data file
    100  const url = gCurrentTestFolderUrl + expectedValuesFileName;
    101 
    102  // Resolve the test data file URL into a file absolute path 
    103  if (url.startsWith("chrome")) {
    104    const chromeURL = Services.io.newURI(url);
    105    gExpectedValuesFilePath = chromeRegistry
    106      .convertChromeURL(chromeURL)
    107      .QueryInterface(Ci.nsIFileURL).file.path;
    108  } else if (url.startsWith("resource")) {
    109    const resURL = Services.io.newURI(url);
    110    const resHandler = Services.io.getProtocolHandler("resource")
    111      .QueryInterface(Ci.nsIResProtocolHandler);
    112    gExpectedValuesFilePath = Services.io.newURI(resHandler.resolveURI(resURL)).QueryInterface(Ci.nsIFileURL).file.path;
    113  }
    114 
    115  if (!isUpdate) {
    116    dump(`Loading test data file: ${url}\n`);
    117    return ChromeUtils.importESModule(url).default;
    118  }
    119 
    120  return null;
    121 }
    122 
    123 async function mayBeSaveExpectedValues(actualValues) {
    124  const isUpdate = Services.env.get(UPDATE_SNAPSHOT_ENV) == "true";
    125  if (!isUpdate) {
    126    return;
    127  }
    128  if (!actualValues?.length) {
    129    return;
    130  }
    131 
    132  const filePath = gExpectedValuesFilePath;
    133  const assertionValues = [];
    134  let i = 0;
    135 
    136  for (const objectDescription of AllObjects) {
    137    if (objectDescription.disabled) {
    138      continue;
    139    }
    140 
    141    if (i >= actualValues.length) {
    142      throw new Error("Unexpected discrepencies between the reported evaled strings and expected values");
    143    }
    144 
    145    const value = actualValues[i++]
    146 
    147    // Ignore this JS object as the test function did not return any actual value.
    148    // We assume none of the tests would store "undefined" as a target value.
    149    if (value == undefined) {
    150      continue;
    151    }
    152 
    153    let evaled = objectDescription.expression;
    154    // Remove any first empty line
    155    evaled = evaled.replace(/^\s*\n/, "");
    156    // remove the unnecessary indentation
    157    const m = evaled.match(/^( +)/);
    158    if (m && m[1]) {
    159      const regexp = new RegExp("^"+m[1], "gm");
    160      evaled = evaled.replace(regexp, "");
    161    }
    162    // Ensure prefixing all new lines in the evaled string with "  //"
    163    // to keep it being in a code comment.
    164    evaled = evaled.replace(/\r?\n/g, "\n  // ");
    165 
    166    assertionValues.push(
    167      "  // " + evaled +
    168        "\n" +
    169        // Ident each newline to match the array item indentation
    170        JSON.stringify(value, null, 2).replace(/^/gm, "  ") +
    171        ","
    172    );
    173  }
    174 
    175  if (i != actualValues.length) {
    176    throw new Error("Unexpected discrepencies between the reported evaled strings and expected values");
    177  }
    178 
    179  const fileContent = `/* Any copyright is dedicated to the Public Domain.
    180  http://creativecommons.org/publicdomain/zero/1.0/ */
    181 
    182 /*
    183 * THIS FILE IS AUTOGENERATED. DO NOT MODIFY BY HAND.
    184 *
    185 * More info in https://firefox-source-docs.mozilla.org/devtools/tests/js-object-tests.html
    186 */
    187 
    188 export default [
    189 ${assertionValues.join("\n\n")}
    190 ];`;
    191  dump("Writing: " + fileContent + " in " + filePath + "\n");
    192  await IOUtils.write(filePath, new TextEncoder().encode(fileContent));
    193 }
    194 
    195 async function testOrUpdateExpectedValues(expectedValuesFileName, actualValues) {
    196  let expectedValues = loadExpectedValues(expectedValuesFileName);
    197  if (expectedValues) {
    198    // Clone the Array as we are going to mutate it via Array.shift().
    199    expectedValues = [...expectedValues];
    200 
    201    const testPath = "gtestPath" in gTestScope ? gTestScope.gTestPath.replace("chrome://mochitest/content/browser/", "") : "path/to/your/xpcshell/test";
    202    const failureMessage = `This is a JavaScript value processing test, which includes an automatically generated snapshot file (${expectedValuesFileName}).\n` +
    203      "You may update this file by running:`\n" +
    204      `  $ mach test ${testPath} --headless --setenv ${UPDATE_SNAPSHOT_ENV}=true\n` +
    205      "And then carefuly review if the result is valid regarding your ongoing changes.\n" +
    206      "`More info in https://firefox-source-docs.mozilla.org/devtools/tests/js-object-tests.html\n";
    207 
    208    let failed = false;
    209    let i = 0;
    210    for (const objectDescription of AllObjects) {
    211      if (objectDescription.disabled) {
    212        continue;
    213      }
    214      const actual = actualValues[i++];
    215      const expression = objectDescription.expression;
    216 
    217      // Ignore this JS object as the test function did not return any actual value.
    218      // We assume none of the tests would store "undefined" as a target value.
    219      if (actual == undefined) {
    220        continue;
    221      }
    222 
    223      const expected = expectedValues.shift();
    224 
    225      const isMochitest = "gTestPath" in gTestScope;
    226      try {
    227        gTestScope.Assert.deepEqual(actual, expected, `Got expected output for "${expression}"`);
    228      } catch(e) {
    229        // deepEqual only throws in case of differences when running in XPCShell tests. Mochitest won't throw and keep running.
    230        // XPCShell will stop at the first failing assertion, so ensure showing our failure message and ok() will throw and stop the test.
    231        if (!isMochitest) {
    232          gTestScope.Assert.ok(false, failureMessage);
    233        }
    234        throw e;
    235      }
    236      // As mochitest won't throw when calling deepEqual with differences in the objects,
    237      // we have to recompute the difference in order to know if any of the tests failed.
    238      if (isMochitest && !failed && !ObjectUtils.deepEqual(actual, expected)) {
    239        failed = true;
    240      }
    241    }
    242 
    243    if (failed) {
    244      const failMessage = "This is a JavaScript value processing test, which includes an automatically generated snapshot file.\n" +
    245        "If the change made to that snapshot file makes sense, you may simply update them by running:`\n" +
    246        `  $ mach test ${testPath} --headless --setenv ${UPDATE_SNAPSHOT_ENV}=true\n` +
    247        "`More info in devtools/shared/tests/objects/README.md\n";
    248      gTestScope.Assert.ok(false, failMessage);
    249    }
    250  }
    251 
    252  mayBeSaveExpectedValues(actualValues);
    253 }
    254 
    255 async function runTest(expectedValuesFileName, testFunction) {
    256  if (!gTestScope) {
    257    throw new Error("`JSObjectsTestUtils.init()` should be called before `runTest()`");
    258  }
    259  if (typeof (expectedValuesFileName) != "string") {
    260    throw new Error("`JSObjectsTestUtils.runTest()` first argument should be a data file name");
    261  }
    262  if (typeof (testFunction) != "function") {
    263    throw new Error("`JSObjectsTestUtils.runTest()` second argument should be a test function");
    264  }
    265 
    266  const actualValues = [];
    267 
    268  if (!gTestScope) {
    269    throw new Error("`JSObjectsTestUtils.init()` should be called before `runTest()`");
    270  }
    271 
    272  for (const objectDescription of AllObjects) {
    273    if (objectDescription.disabled) {
    274      continue;
    275    }
    276 
    277    const { context, expression } = objectDescription;
    278    if (!Object.values(CONTEXTS).includes(context)) {
    279      throw new Error("Missing, or invalid context in: " + JSON.stringify(objectDescription));
    280    }
    281 
    282    if (!expression) {
    283      throw new Error("Missing a value in: " + JSON.stringify(objectDescription));
    284    }
    285 
    286    const actual = await testFunction({ context, expression });
    287    actualValues.push(actual);
    288  }
    289 
    290  testOrUpdateExpectedValues(expectedValuesFileName, actualValues);
    291 }
    292 
    293 export const JSObjectsTestUtils = { init, runTest, testOrUpdateExpectedValues };