| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109 | /* * Copyright 2014 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *     http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *//* eslint-disable no-var, unicorn/prefer-at */"use strict";var WebServer = require("./webserver.js").WebServer;var path = require("path");var fs = require("fs");var os = require("os");var puppeteer = require("puppeteer");var url = require("url");var testUtils = require("./testutils.js");const dns = require("dns");const readline = require("readline");const yargs = require("yargs");// Chrome uses host `127.0.0.1` in the browser's websocket endpoint URL while// Firefox uses `localhost`, which before Node.js 17 also resolved to the IPv4// address `127.0.0.1` by Node.js' DNS resolver. However, this behavior changed// in Node.js 17 where the default is to prefer an IPv6 address if one is// offered (which varies based on the OS and/or how the `localhost` hostname// resolution is configured), so it can now also resolve to `::1`. This causes// Firefox to not start anymore since it doesn't bind on the `::1` interface.// To avoid this, we switch Node.js' DNS resolver back to preferring IPv4// since we connect to a local browser anyway. Only do this for Node.js versions// that actually have this API since it got introduced in Node.js 14.18.0 and// it's not relevant for older versions anyway.if (dns.setDefaultResultOrder !== undefined) {  dns.setDefaultResultOrder("ipv4first");}function parseOptions() {  yargs    .usage("Usage: $0")    .option("downloadOnly", {      default: false,      describe: "Download test PDFs without running the tests.",      type: "boolean",    })    .option("fontTest", {      default: false,      describe: "Run the font tests.",      type: "boolean",    })    .option("help", {      alias: "h",      default: false,      describe: "Show this help message.",      type: "boolean",    })    .option("integration", {      default: false,      describe: "Run the integration tests.",      type: "boolean",    })    .option("manifestFile", {      default: "test_manifest.json",      describe: "A path to JSON file in the form of `test_manifest.json`.",      type: "string",    })    .option("masterMode", {      alias: "m",      default: false,      describe: "Run the script in master mode.",      type: "boolean",    })    .option("noChrome", {      default: false,      describe: "Skip Chrome when running tests.",      type: "boolean",    })    .option("noDownload", {      default: false,      describe: "Skip downloading of test PDFs.",      type: "boolean",    })    .option("noPrompts", {      default: false,      describe: "Uses default answers (intended for CLOUD TESTS only!).",      type: "boolean",    })    .option("port", {      default: 0,      describe: "The port the HTTP server should listen on.",      type: "number",    })    .option("reftest", {      default: false,      describe:        "Automatically start reftest showing comparison test failures, if there are any.",      type: "boolean",    })    .option("statsDelay", {      default: 0,      describe:        "The amount of time in milliseconds the browser should wait before starting stats.",      type: "number",    })    .option("statsFile", {      default: "",      describe: "The file where to store stats.",      type: "string",    })    .option("strictVerify", {      default: false,      describe: "Error if verifying the manifest files fails.",      type: "boolean",    })    .option("testfilter", {      alias: "t",      default: [],      describe: "Run specific reftest(s).",      type: "array",    })    .example(      "testfilter",      "$0 -t=issue5567 -t=issue5909\n" +        "Run the reftest identified by issue5567 and issue5909."    )    .option("unitTest", {      default: false,      describe: "Run the unit tests.",      type: "boolean",    })    .option("xfaOnly", {      default: false,      describe: "Only run the XFA reftest(s).",      type: "boolean",    })    .check(argv => {      if (        +argv.reftest + argv.unitTest + argv.fontTest + argv.masterMode <=        1      ) {        return true;      }      throw new Error(        "--reftest, --unitTest, --fontTest, and --masterMode must not be specified together."      );    })    .check(argv => {      if (        +argv.unitTest + argv.fontTest + argv.integration + argv.xfaOnly <=        1      ) {        return true;      }      throw new Error(        "--unitTest, --fontTest, --integration, and --xfaOnly must not be specified together."      );    })    .check(argv => {      if (argv.testfilter && argv.testfilter.length > 0 && argv.xfaOnly) {        throw new Error("--testfilter and --xfaOnly cannot be used together.");      }      return true;    })    .check(argv => {      if (!argv.noDownload || !argv.downloadOnly) {        return true;      }      throw new Error(        "--noDownload and --downloadOnly cannot be used together."      );    })    .check(argv => {      if (!argv.masterMode || argv.manifestFile === "test_manifest.json") {        return true;      }      throw new Error(        "when --masterMode is specified --manifestFile shall be equal to `test_manifest.json`."      );    });  const result = yargs.argv;  if (result.help) {    yargs.showHelp();    process.exit(0);  }  result.testfilter = Array.isArray(result.testfilter)    ? result.testfilter    : [result.testfilter];  return result;}var refsTmpDir = "tmp";var testResultDir = "test_snapshots";var refsDir = "ref";var eqLog = "eq.log";var browserTimeout = 120;function monitorBrowserTimeout(session, onTimeout) {  if (session.timeoutMonitor) {    clearTimeout(session.timeoutMonitor);  }  if (!onTimeout) {    session.timeoutMonitor = null;    return;  }  session.timeoutMonitor = setTimeout(function () {    onTimeout(session);  }, browserTimeout * 1000);}function updateRefImages() {  function sync(removeTmp) {    console.log("  Updating ref/ ... ");    testUtils.copySubtreeSync(refsTmpDir, refsDir);    if (removeTmp) {      testUtils.removeDirSync(refsTmpDir);    }    console.log("done");  }  if (options.noPrompts) {    sync(false); // don't remove tmp/ for botio    return;  }  const reader = readline.createInterface(process.stdin, process.stdout);  reader.question(    "Would you like to update the master copy in ref/? [yn] ",    function (answer) {      if (answer.toLowerCase() === "y") {        sync(true);      } else {        console.log("  OK, not updating.");      }      reader.close();    }  );}function examineRefImages() {  startServer();  const startUrl = `http://${host}:${server.port}/test/resources/reftest-analyzer.html#web=/test/eq.log`;  startBrowser("firefox", startUrl).then(function (browser) {    browser.on("disconnected", function () {      stopServer();      process.exit(0);    });  });}function startRefTest(masterMode, showRefImages) {  function finalize() {    stopServer();    var numErrors = 0;    var numFBFFailures = 0;    var numEqFailures = 0;    var numEqNoSnapshot = 0;    sessions.forEach(function (session) {      numErrors += session.numErrors;      numFBFFailures += session.numFBFFailures;      numEqFailures += session.numEqFailures;      numEqNoSnapshot += session.numEqNoSnapshot;    });    var numFatalFailures = numErrors + numFBFFailures;    console.log();    if (numFatalFailures + numEqFailures > 0) {      console.log("OHNOES!  Some tests failed!");      if (numErrors > 0) {        console.log("  errors: " + numErrors);      }      if (numEqFailures > 0) {        console.log("  different ref/snapshot: " + numEqFailures);      }      if (numFBFFailures > 0) {        console.log("  different first/second rendering: " + numFBFFailures);      }    } else {      console.log("All regression tests passed.");    }    var runtime = (Date.now() - startTime) / 1000;    console.log("Runtime was " + runtime.toFixed(1) + " seconds");    if (options.statsFile) {      fs.writeFileSync(options.statsFile, JSON.stringify(stats, null, 2));    }    if (masterMode) {      if (numEqFailures + numEqNoSnapshot > 0) {        console.log();        console.log("Some eq tests failed or didn't have snapshots.");        console.log("Checking to see if master references can be updated...");        if (numFatalFailures > 0) {          console.log("  No.  Some non-eq tests failed.");        } else {          console.log(            "  Yes!  The references in tmp/ can be synced with ref/."          );          updateRefImages();        }      }    } else if (showRefImages && numEqFailures > 0) {      console.log();      console.log(        `Starting reftest harness to examine ${numEqFailures} eq test failures.`      );      examineRefImages();    }  }  function setup() {    if (fs.existsSync(refsTmpDir)) {      console.error("tmp/ exists -- unable to proceed with testing");      process.exit(1);    }    if (fs.existsSync(eqLog)) {      fs.unlinkSync(eqLog);    }    if (fs.existsSync(testResultDir)) {      testUtils.removeDirSync(testResultDir);    }    startTime = Date.now();    startServer();    server.hooks.POST.push(refTestPostHandler);    onAllSessionsClosed = finalize;    const startUrl = `http://${host}:${server.port}/test/test_slave.html`;    startBrowsers(function (session) {      session.masterMode = masterMode;      session.taskResults = {};      session.tasks = {};      session.remaining = manifest.length;      manifest.forEach(function (item) {        var rounds = item.rounds || 1;        var roundsResults = [];        roundsResults.length = rounds;        session.taskResults[item.id] = roundsResults;        session.tasks[item.id] = item;      });      session.numErrors = 0;      session.numFBFFailures = 0;      session.numEqNoSnapshot = 0;      session.numEqFailures = 0;      monitorBrowserTimeout(session, handleSessionTimeout);    }, makeTestUrl(startUrl));  }  function checkRefsTmp() {    if (masterMode && fs.existsSync(refsTmpDir)) {      if (options.noPrompts) {        testUtils.removeDirSync(refsTmpDir);        setup();        return;      }      console.log("Temporary snapshot dir tmp/ is still around.");      console.log("tmp/ can be removed if it has nothing you need.");      const reader = readline.createInterface(process.stdin, process.stdout);      reader.question(        "SHOULD THIS SCRIPT REMOVE tmp/? THINK CAREFULLY [yn] ",        function (answer) {          if (answer.toLowerCase() === "y") {            testUtils.removeDirSync(refsTmpDir);          }          setup();          reader.close();        }      );    } else {      setup();    }  }  var startTime;  var manifest = getTestManifest();  if (!manifest) {    return;  }  if (options.noDownload) {    checkRefsTmp();  } else {    ensurePDFsDownloaded(checkRefsTmp);  }}function handleSessionTimeout(session) {  if (session.closed) {    return;  }  var browser = session.name;  console.log(    "TEST-UNEXPECTED-FAIL | test failed " +      browser +      " has not responded in " +      browserTimeout +      "s"  );  session.numErrors += session.remaining;  session.remaining = 0;  closeSession(browser);}function getTestManifest() {  var manifest = JSON.parse(fs.readFileSync(options.manifestFile));  const testFilter = options.testfilter.slice(0),    xfaOnly = options.xfaOnly;  if (testFilter.length || xfaOnly) {    manifest = manifest.filter(function (item) {      var i = testFilter.indexOf(item.id);      if (i !== -1) {        testFilter.splice(i, 1);        return true;      }      if (xfaOnly && item.enableXfa) {        return true;      }      return false;    });    if (testFilter.length) {      console.error("Unrecognized test IDs: " + testFilter.join(" "));      return undefined;    }  }  return manifest;}function checkEq(task, results, browser, masterMode) {  var taskId = task.id;  var refSnapshotDir = path.join(refsDir, os.platform(), browser, taskId);  var testSnapshotDir = path.join(    testResultDir,    os.platform(),    browser,    taskId  );  var pageResults = results[0];  var taskType = task.type;  var numEqNoSnapshot = 0;  var numEqFailures = 0;  for (var page = 0; page < pageResults.length; page++) {    if (!pageResults[page]) {      continue;    }    const pageResult = pageResults[page];    let testSnapshot = pageResult.snapshot;    if (testSnapshot && testSnapshot.startsWith("data:image/png;base64,")) {      testSnapshot = Buffer.from(testSnapshot.substring(22), "base64");    } else {      console.error("Valid snapshot was not found.");    }    var refSnapshot = null;    var eq = false;    var refPath = path.join(refSnapshotDir, page + 1 + ".png");    if (!fs.existsSync(refPath)) {      numEqNoSnapshot++;      if (!masterMode) {        console.log("WARNING: no reference snapshot " + refPath);      }    } else {      refSnapshot = fs.readFileSync(refPath);      eq = refSnapshot.toString("hex") === testSnapshot.toString("hex");      if (!eq) {        console.log(          "TEST-UNEXPECTED-FAIL | " +            taskType +            " " +            taskId +            " | in " +            browser +            " | rendering of page " +            (page + 1) +            " != reference rendering"        );        testUtils.ensureDirSync(testSnapshotDir);        fs.writeFileSync(          path.join(testSnapshotDir, page + 1 + ".png"),          testSnapshot        );        fs.writeFileSync(          path.join(testSnapshotDir, page + 1 + "_ref.png"),          refSnapshot        );        // This no longer follows the format of Mozilla reftest output.        const viewportString = `(${pageResult.viewportWidth}x${pageResult.viewportHeight}x${pageResult.outputScale})`;        fs.appendFileSync(          eqLog,          "REFTEST TEST-UNEXPECTED-FAIL | " +            browser +            "-" +            taskId +            "-page" +            (page + 1) +            " | image comparison (==)\n" +            `REFTEST   IMAGE 1 (TEST)${viewportString}: ` +            path.join(testSnapshotDir, page + 1 + ".png") +            "\n" +            `REFTEST   IMAGE 2 (REFERENCE)${viewportString}: ` +            path.join(testSnapshotDir, page + 1 + "_ref.png") +            "\n"        );        numEqFailures++;      }    }    if (masterMode && (!refSnapshot || !eq)) {      var tmpSnapshotDir = path.join(        refsTmpDir,        os.platform(),        browser,        taskId      );      testUtils.ensureDirSync(tmpSnapshotDir);      fs.writeFileSync(        path.join(tmpSnapshotDir, page + 1 + ".png"),        testSnapshot      );    }  }  var session = getSession(browser);  session.numEqNoSnapshot += numEqNoSnapshot;  if (numEqFailures > 0) {    session.numEqFailures += numEqFailures;  } else {    console.log(      "TEST-PASS | " + taskType + " test " + taskId + " | in " + browser    );  }}function checkFBF(task, results, browser, masterMode) {  var numFBFFailures = 0;  var round0 = results[0],    round1 = results[1];  if (round0.length !== round1.length) {    console.error("round 1 and 2 sizes are different");  }  for (var page = 0; page < round1.length; page++) {    var r0Page = round0[page],      r1Page = round1[page];    if (!r0Page) {      continue;    }    if (r0Page.snapshot !== r1Page.snapshot) {      // The FBF tests fail intermittently in Firefox and Google Chrome when run      // on the bots, ignoring `makeref` failures for now; see      //  - https://github.com/mozilla/pdf.js/pull/12368      //  - https://github.com/mozilla/pdf.js/pull/11491      //      // TODO: Figure out why this happens, so that we can remove the hack; see      //       https://github.com/mozilla/pdf.js/issues/12371      if (masterMode) {        console.log(          "TEST-SKIPPED | forward-back-forward test " +            task.id +            " | in " +            browser +            " | page" +            (page + 1)        );        continue;      }      console.log(        "TEST-UNEXPECTED-FAIL | forward-back-forward test " +          task.id +          " | in " +          browser +          " | first rendering of page " +          (page + 1) +          " != second"      );      numFBFFailures++;    }  }  if (numFBFFailures > 0) {    getSession(browser).numFBFFailures += numFBFFailures;  } else {    console.log(      "TEST-PASS | forward-back-forward test " + task.id + " | in " + browser    );  }}function checkLoad(task, results, browser) {  // Load just checks for absence of failure, so if we got here the  // test has passed  console.log("TEST-PASS | load test " + task.id + " | in " + browser);}function checkRefTestResults(browser, id, results) {  var failed = false;  var session = getSession(browser);  var task = session.tasks[id];  results.forEach(function (roundResults, round) {    roundResults.forEach(function (pageResult, page) {      if (!pageResult) {        return; // no results      }      if (pageResult.failure) {        failed = true;        if (fs.existsSync(task.file + ".error")) {          console.log(            "TEST-SKIPPED | PDF was not downloaded " +              id +              " | in " +              browser +              " | page" +              (page + 1) +              " round " +              (round + 1) +              " | " +              pageResult.failure          );        } else {          session.numErrors++;          console.log(            "TEST-UNEXPECTED-FAIL | test failed " +              id +              " | in " +              browser +              " | page" +              (page + 1) +              " round " +              (round + 1) +              " | " +              pageResult.failure          );        }      }    });  });  if (failed) {    return;  }  switch (task.type) {    case "eq":    case "text":      checkEq(task, results, browser, session.masterMode);      break;    case "fbf":      checkFBF(task, results, browser, session.masterMode);      break;    case "load":      checkLoad(task, results, browser);      break;    default:      throw new Error("Unknown test type");  }  // clear memory  results.forEach(function (roundResults, round) {    roundResults.forEach(function (pageResult, page) {      pageResult.snapshot = null;    });  });}function refTestPostHandler(req, res) {  var parsedUrl = url.parse(req.url, true);  var pathname = parsedUrl.pathname;  if (    pathname !== "/tellMeToQuit" &&    pathname !== "/info" &&    pathname !== "/submit_task_results"  ) {    return false;  }  var body = "";  req.on("data", function (data) {    body += data;  });  req.on("end", function () {    res.writeHead(200, { "Content-Type": "text/plain" });    res.end();    var session;    if (pathname === "/tellMeToQuit") {      session = getSession(parsedUrl.query.browser);      monitorBrowserTimeout(session, null);      closeSession(session.name);      return;    }    var data = JSON.parse(body);    if (pathname === "/info") {      console.log(data.message);      return;    }    var browser = data.browser;    var round = data.round;    var id = data.id;    var page = data.page - 1;    var failure = data.failure;    var snapshot = data.snapshot;    var lastPageNum = data.lastPageNum;    session = getSession(browser);    monitorBrowserTimeout(session, handleSessionTimeout);    var taskResults = session.taskResults[id];    if (!taskResults[round]) {      taskResults[round] = [];    }    if (taskResults[round][page]) {      console.error(        "Results for " +          browser +          ":" +          id +          ":" +          round +          ":" +          page +          " were already submitted"      );      // TODO abort testing here?    }    taskResults[round][page] = {      failure,      snapshot,      viewportWidth: data.viewportWidth,      viewportHeight: data.viewportHeight,      outputScale: data.outputScale,    };    if (stats) {      stats.push({        browser,        pdf: id,        page,        round,        stats: data.stats,      });    }    var isDone =      taskResults[taskResults.length - 1] &&      taskResults[taskResults.length - 1][lastPageNum - 1];    if (isDone) {      checkRefTestResults(browser, id, taskResults);      session.remaining--;    }  });  return true;}function onAllSessionsClosedAfterTests(name) {  const startTime = Date.now();  return function () {    stopServer();    var numRuns = 0,      numErrors = 0;    sessions.forEach(function (session) {      numRuns += session.numRuns;      numErrors += session.numErrors;    });    console.log();    console.log("Run " + numRuns + " tests");    if (numErrors > 0) {      console.log("OHNOES!  Some " + name + " tests failed!");      console.log("  " + numErrors + " of " + numRuns + " failed");    } else {      console.log("All " + name + " tests passed.");    }    var runtime = (Date.now() - startTime) / 1000;    console.log(name + " tests runtime was " + runtime.toFixed(1) + " seconds");  };}function makeTestUrl(startUrl) {  return function (browserName) {    const queryParameters =      `?browser=${encodeURIComponent(browserName)}` +      `&manifestFile=${encodeURIComponent("/test/" + options.manifestFile)}` +      `&testFilter=${JSON.stringify(options.testfilter)}` +      `&xfaOnly=${options.xfaOnly}` +      `&delay=${options.statsDelay}` +      `&masterMode=${options.masterMode}`;    return startUrl + queryParameters;  };}function startUnitTest(testUrl, name) {  onAllSessionsClosed = onAllSessionsClosedAfterTests(name);  startServer();  server.hooks.POST.push(unitTestPostHandler);  const startUrl = `http://${host}:${server.port}${testUrl}`;  startBrowsers(function (session) {    session.numRuns = 0;    session.numErrors = 0;  }, makeTestUrl(startUrl));}function startIntegrationTest() {  onAllSessionsClosed = onAllSessionsClosedAfterTests("integration");  startServer();  const { runTests } = require("./integration-boot.js");  startBrowsers(function (session) {    session.numRuns = 0;    session.numErrors = 0;  });  global.integrationBaseUrl = `http://${host}:${server.port}/build/generic/web/viewer.html`;  global.integrationSessions = sessions;  Promise.all(sessions.map(session => session.browserPromise)).then(    async () => {      const results = { runs: 0, failures: 0 };      await runTests(results);      sessions[0].numRuns = results.runs;      sessions[0].numErrors = results.failures;      await Promise.all(sessions.map(session => closeSession(session.name)));    }  );}function unitTestPostHandler(req, res) {  var parsedUrl = url.parse(req.url);  var pathname = parsedUrl.pathname;  if (    pathname !== "/tellMeToQuit" &&    pathname !== "/info" &&    pathname !== "/ttx" &&    pathname !== "/submit_task_results"  ) {    return false;  }  var body = "";  req.on("data", function (data) {    body += data;  });  req.on("end", function () {    if (pathname === "/ttx") {      var translateFont = require("./font/ttxdriver.js").translateFont;      var onCancel = null,        ttxTimeout = 10000;      var timeoutId = setTimeout(function () {        onCancel?.("TTX timeout");      }, ttxTimeout);      translateFont(        body,        function (fn) {          onCancel = fn;        },        function (err, xml) {          clearTimeout(timeoutId);          res.writeHead(200, { "Content-Type": "text/xml" });          res.end(err ? "<error>" + err + "</error>" : xml);        }      );      return;    }    res.writeHead(200, { "Content-Type": "text/plain" });    res.end();    var data = JSON.parse(body);    if (pathname === "/tellMeToQuit") {      closeSession(data.browser);      return;    }    if (pathname === "/info") {      console.log(data.message);      return;    }    var session = getSession(data.browser);    session.numRuns++;    var message =      data.status + " | " + data.description + " | in " + session.name;    if (data.status === "TEST-UNEXPECTED-FAIL") {      session.numErrors++;    }    if (data.error) {      message += " | " + data.error;    }    console.log(message);  });  return true;}async function startBrowser(browserName, startUrl = "") {  const revisions =    require("puppeteer-core/lib/cjs/puppeteer/revisions.js").PUPPETEER_REVISIONS;  const wantedRevision =    browserName === "chrome" ? revisions.chromium : revisions.firefox;  // Remove other revisions than the one we want to use. Updating Puppeteer can  // cause a new revision to be used, and not removing older revisions causes  // the disk to fill up.  const browserFetcher = puppeteer.createBrowserFetcher({    product: browserName,  });  const localRevisions = await browserFetcher.localRevisions();  if (localRevisions.length > 1) {    for (const localRevision of localRevisions) {      if (localRevision !== wantedRevision) {        console.log(`Removing old ${browserName} revision ${localRevision}...`);        await browserFetcher.remove(localRevision);      }    }  }  const options = {    product: browserName,    headless: false,    defaultViewport: null,    ignoreDefaultArgs: ["--disable-extensions"],  };  if (!tempDir) {    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pdfjs-"));  }  const printFile = path.join(tempDir, "print.pdf");  if (browserName === "chrome") {    // avoid crash    options.args = ["--no-sandbox", "--disable-setuid-sandbox"];    // silent printing in a pdf    options.args.push("--kiosk-printing");  }  if (browserName === "firefox") {    options.extraPrefsFirefox = {      // avoid to have a prompt when leaving a page with a form      "dom.disable_beforeunload": true,      // Disable dialog when saving a pdf      "pdfjs.disabled": true,      "browser.helperApps.neverAsk.saveToDisk": "application/pdf",      // Avoid popup when saving is done      "browser.download.always_ask_before_handling_new_types": true,      "browser.download.panel.shown": true,      "browser.download.alwaysOpenPanel": false,      // Save file in output      "browser.download.folderList": 2,      "browser.download.dir": tempDir,      // Print silently in a pdf      "print.always_print_silent": true,      "print.show_print_progress": false,      print_printer: "PDF",      "print.printer_PDF.print_to_file": true,      "print.printer_PDF.print_to_filename": printFile,      // Enable OffscreenCanvas      "gfx.offscreencanvas.enabled": true,      // Disable gpu acceleration      "gfx.canvas.accelerated": false,    };  }  const browser = await puppeteer.launch(options);  if (startUrl) {    const pages = await browser.pages();    const page = pages[0];    await page.goto(startUrl, { timeout: 0, waitUntil: "domcontentloaded" });  }  return browser;}function startBrowsers(initSessionCallback, makeStartUrl = null) {  const browserNames = options.noChrome ? ["firefox"] : ["firefox", "chrome"];  sessions = [];  for (const browserName of browserNames) {    // The session must be pushed first and augmented with the browser once    // it's initialized. The reason for this is that browser initialization    // takes more time when the browser is not found locally yet and we don't    // want `onAllSessionsClosed` to trigger if one of the browsers is done    // and the other one is still initializing, since that would mean that    // once the browser is initialized the server would have stopped already.    // Pushing the session first ensures that `onAllSessionsClosed` will    // only trigger once all browsers are initialized and done.    const session = {      name: browserName,      browser: undefined,      closed: false,    };    sessions.push(session);    const startUrl = makeStartUrl ? makeStartUrl(browserName) : "";    session.browserPromise = startBrowser(browserName, startUrl)      .then(function (browser) {        session.browser = browser;        initSessionCallback?.(session);      })      .catch(function (ex) {        console.log(`Error while starting ${browserName}: ${ex.message}`);        closeSession(browserName);      });  }}function startServer() {  server = new WebServer();  server.host = host;  server.port = options.port;  server.root = "..";  server.cacheExpirationTime = 3600;  server.start();}function stopServer() {  server.stop();}function getSession(browser) {  return sessions.find(session => session.name === browser);}async function closeSession(browser) {  for (const session of sessions) {    if (session.name !== browser) {      continue;    }    if (session.browser !== undefined) {      for (const page of await session.browser.pages()) {        await page.close();      }      await session.browser.close();    }    session.closed = true;    const allClosed = sessions.every(function (s) {      return s.closed;    });    if (allClosed) {      if (tempDir) {        const rimraf = require("rimraf");        rimraf.sync(tempDir);      }      onAllSessionsClosed?.();    }  }}function ensurePDFsDownloaded(callback) {  var downloadUtils = require("./downloadutils.js");  var manifest = getTestManifest();  downloadUtils.downloadManifestFiles(manifest, function () {    downloadUtils.verifyManifestFiles(manifest, function (hasErrors) {      if (hasErrors) {        console.log(          "Unable to verify the checksum for the files that are " +            "used for testing."        );        console.log(          "Please re-download the files, or adjust the MD5 " +            "checksum in the manifest for the files listed above.\n"        );        if (options.strictVerify) {          process.exit(1);        }      }      callback();    });  });}function main() {  if (options.statsFile) {    stats = [];  }  if (options.downloadOnly) {    ensurePDFsDownloaded(function () {});  } else if (options.unitTest) {    // Allows linked PDF files in unit-tests as well.    ensurePDFsDownloaded(function () {      startUnitTest("/test/unit/unit_test.html", "unit");    });  } else if (options.fontTest) {    startUnitTest("/test/font/font_test.html", "font");  } else if (options.integration) {    // Allows linked PDF files in integration-tests as well.    ensurePDFsDownloaded(function () {      startIntegrationTest();    });  } else {    startRefTest(options.masterMode, options.reftest);  }}var server;var sessions;var onAllSessionsClosed;var host = "127.0.0.1";var options = parseOptions();var stats;var tempDir = null;main();
 |