tor-browser

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

browser_glean_metrics_exist.js (8871B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /* This list allows pre-existing or 'unfixable' JS issues to remain, while we
      5 * detect newly occurring issues in shipping JS. It is a list of regexes
      6 * matching files which have errors:
      7 */
      8 
      9 requestLongerTimeout(2);
     10 
     11 const kAllowlist = new Set([
     12  /browser\/content\/browser\/places\/controller.js$/,
     13 ]);
     14 
     15 // Normally we would use reflect.sys.mjs to get Reflect.parse. However, if
     16 // we do that, then all the AST data is allocated in reflect.sys.mjs's
     17 // zone. That exposes a bug in our GC. The GC collects reflect.sys.mjs's
     18 // zone but not the zone in which our test code lives (since no new
     19 // data is being allocated in it). The cross-compartment wrappers in
     20 // our zone that point to the AST data never get collected, and so the
     21 // AST data itself is never collected. We need to GC both zones at
     22 // once to fix the problem.
     23 const init = Cc["@mozilla.org/jsreflect;1"].createInstance();
     24 init();
     25 
     26 /**
     27 * Check if an error should be ignored due to matching one of the allowlist
     28 * objects.
     29 *
     30 * @param uri the uri to check against the allowlist
     31 * @return true if the uri should be skipped, false otherwise.
     32 */
     33 function uriIsAllowed(uri) {
     34  for (let allowlistItem of kAllowlist) {
     35    if (allowlistItem.test(uri.spec)) {
     36      return true;
     37    }
     38  }
     39  return false;
     40 }
     41 
     42 function recursivelyCheckForGleanCalls(obj, parent = null) {
     43  if (!obj) {
     44    return;
     45  }
     46 
     47  if (Array.isArray(obj)) {
     48    for (let item of obj) {
     49      recursivelyCheckForGleanCalls(item, { obj, parent });
     50    }
     51    return;
     52  }
     53 
     54  for (let key in obj) {
     55    if (key == "loc") {
     56      continue;
     57    }
     58    if (typeof obj[key] == "object") {
     59      recursivelyCheckForGleanCalls(obj[key], { obj, parent });
     60    }
     61  }
     62 
     63  if (obj.type != "Identifier" || obj.name != "Glean") {
     64    return;
     65  }
     66 
     67  function getMemberName(object, child) {
     68    if (
     69      object.type == "MemberExpression" &&
     70      !object.computed &&
     71      object.object === child &&
     72      object.property.type == "Identifier"
     73    ) {
     74      return object.property.name;
     75    }
     76    return "";
     77  }
     78 
     79  function getPrefix(object, child) {
     80    if (
     81      object.type == "MemberExpression" &&
     82      object.computed &&
     83      object.object === child &&
     84      object.property.type == "BinaryExpression" &&
     85      object.property.operator == "+" &&
     86      object.property.left.type == "Literal"
     87    ) {
     88      return object.property.left.value;
     89    }
     90    return "";
     91  }
     92 
     93  let cat = getMemberName(parent.obj, obj);
     94  if (cat) {
     95    if (Glean.hasOwnProperty(cat)) {
     96      ok(true, `The category ${cat} should exist in the global Glean object`);
     97    } else {
     98      record(
     99        false,
    100        `The category ${cat} should exist in the global Glean object`,
    101        undefined,
    102        `${obj.loc.source}:${obj.loc.start.line}`
    103      );
    104      return;
    105    }
    106 
    107    let name = getMemberName(parent.parent.obj, parent.obj);
    108    if (name) {
    109      if (Glean[cat].hasOwnProperty(name)) {
    110        ok(true, `The metric ${name} should exist in the Glean.${cat} object`);
    111      } else {
    112        record(
    113          false,
    114          `The metric ${name} should exist in the Glean.${cat} object`,
    115          undefined,
    116          `${obj.loc.source}:${obj.loc.start.line}`,
    117          // Object metrics are not supported yet in artifact builds (see bug 1883857),
    118          // so we expect some failures.
    119          Services.prefs.getBoolPref("telemetry.fog.artifact_build", false)
    120            ? "fail"
    121            : undefined
    122        );
    123        return;
    124      }
    125 
    126      let methodOrLabel = getMemberName(
    127        parent.parent.parent.obj,
    128        parent.parent.obj
    129      );
    130      if (methodOrLabel) {
    131        if (methodOrLabel in Glean[cat][name]) {
    132          ok(true, `${methodOrLabel} should exist in Glean.${cat}.${name}`);
    133        } else {
    134          record(
    135            false,
    136            `${methodOrLabel} should exist in Glean.${cat}.${name}`,
    137            undefined,
    138            `${obj.loc.source}:${obj.loc.start.line}`
    139          );
    140          return;
    141        }
    142 
    143        let object = Glean[cat][name];
    144        let method = methodOrLabel;
    145        if (typeof Glean[cat][name][methodOrLabel] == "object") {
    146          method = getMemberName(
    147            parent.parent.parent.parent.obj,
    148            parent.parent.parent.obj
    149          );
    150          if (!method) {
    151            return;
    152          }
    153          object = Glean[cat][name][methodOrLabel];
    154        }
    155 
    156        if (method in object) {
    157          ok(true, `${method} exists`);
    158          is(
    159            typeof object[method],
    160            "function",
    161            `${method} should be a function`
    162          );
    163        } else {
    164          record(
    165            false,
    166            `${method} should exist`,
    167            undefined,
    168            `${obj.loc.source}:${obj.loc.start.line}`
    169          );
    170        }
    171      }
    172    } else {
    173      let prefix = getPrefix(parent.parent.obj, parent.obj);
    174      if (prefix) {
    175        if (Object.keys(Glean[cat]).some(key => key.startsWith(prefix))) {
    176          ok(
    177            true,
    178            `some metric names in Glean.${cat} have the prefix ${prefix}`
    179          );
    180        } else {
    181          record(
    182            false,
    183            `some metric names in Glean.${cat} should start with ${prefix}`,
    184            undefined,
    185            `${obj.loc.source}:${obj.loc.start.line}`
    186          );
    187        }
    188      }
    189    }
    190  }
    191 }
    192 
    193 function parsePromise(uri, parseTarget) {
    194  return new Promise(resolve => {
    195    let xhr = new XMLHttpRequest();
    196    xhr.open("GET", uri, true);
    197    xhr.onreadystatechange = function () {
    198      if (this.readyState == this.DONE) {
    199        let scriptText = this.responseText;
    200        if (!scriptText.includes("Glean.")) {
    201          resolve();
    202          return;
    203        }
    204 
    205        try {
    206          info(`Checking ${parseTarget} ${uri}`);
    207          let parseOpts = {
    208            source: uri,
    209            target: parseTarget,
    210          };
    211          recursivelyCheckForGleanCalls(
    212            Reflect.parse(scriptText, parseOpts).body
    213          );
    214        } catch (ex) {
    215          let errorMsg = "Script error reading " + uri + ": " + ex;
    216          ok(false, errorMsg);
    217        }
    218        resolve();
    219      }
    220    };
    221    xhr.onerror = error => {
    222      ok(false, "XHR error reading " + uri + ": " + error);
    223      resolve();
    224    };
    225    xhr.overrideMimeType("application/javascript");
    226    xhr.send(null);
    227  });
    228 }
    229 
    230 add_task(async function checkAllTheJS() {
    231  // In debug builds, even on a fast machine, collecting the file list may take
    232  // more than 30 seconds, and parsing all files may take four more minutes.
    233  // For this reason, this test must be explictly requested in debug builds by
    234  // using the "--setpref parse=<filter>" argument to mach.  You can specify:
    235  //  - A case-sensitive substring of the file name to test (slow).
    236  //  - A single absolute URI printed out by a previous run (fast).
    237  //  - An empty string to run the test on all files (slowest).
    238  let parseRequested = Services.prefs.prefHasUserValue("parse");
    239  let parseValue = parseRequested && Services.prefs.getCharPref("parse");
    240  if (SpecialPowers.isDebugBuild) {
    241    if (!parseRequested) {
    242      ok(
    243        true,
    244        "Test disabled on debug build. To run, execute: ./mach" +
    245          " mochitest-browser --setpref parse=<case_sensitive_filter>" +
    246          " browser/base/content/test/general/browser_parsable_script.js"
    247      );
    248      return;
    249    }
    250    // Request a 15 minutes timeout (30 seconds * 30) for debug builds.
    251    requestLongerTimeout(30);
    252  }
    253 
    254  let uris;
    255  // If an absolute URI is specified on the command line, use it immediately.
    256  if (parseValue && parseValue.includes(":")) {
    257    uris = [NetUtil.newURI(parseValue)];
    258  } else {
    259    let appDir = Services.dirsvc.get("GreD", Ci.nsIFile);
    260    // This asynchronously produces a list of URLs (sadly, mostly sync on our
    261    // test infrastructure because it runs against jarfiles there, and
    262    // our zipreader APIs are all sync)
    263    let startTimeMs = Date.now();
    264    info("Collecting URIs");
    265    uris = await generateURIsFromDirTree(appDir, [".js", ".jsm", ".mjs"]);
    266    info("Collected URIs in " + (Date.now() - startTimeMs) + "ms");
    267 
    268    // Apply the filter specified on the command line, if any.
    269    if (parseValue) {
    270      uris = uris.filter(uri => {
    271        if (uri.spec.includes(parseValue)) {
    272          return true;
    273        }
    274        info("Not checking filtered out " + uri.spec);
    275        return false;
    276      });
    277    }
    278  }
    279 
    280  // We create an array of promises so we can parallelize all our parsing
    281  // and file loading activity:
    282  await PerfTestHelpers.throttledMapPromises(uris, uri => {
    283    if (uriIsAllowed(uri)) {
    284      info("Not checking allowlisted " + uri.spec);
    285      return undefined;
    286    }
    287    return parsePromise(uri.spec, uriIsESModule(uri) ? "module" : "script");
    288  });
    289  ok(true, "All files parsed");
    290 });