test.js 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. /*
  2. * Copyright 2014 Mozilla Foundation
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. /* eslint-disable no-var, unicorn/prefer-at */
  17. "use strict";
  18. var WebServer = require("./webserver.js").WebServer;
  19. var path = require("path");
  20. var fs = require("fs");
  21. var os = require("os");
  22. var puppeteer = require("puppeteer");
  23. var url = require("url");
  24. var testUtils = require("./testutils.js");
  25. const dns = require("dns");
  26. const readline = require("readline");
  27. const yargs = require("yargs");
  28. // Chrome uses host `127.0.0.1` in the browser's websocket endpoint URL while
  29. // Firefox uses `localhost`, which before Node.js 17 also resolved to the IPv4
  30. // address `127.0.0.1` by Node.js' DNS resolver. However, this behavior changed
  31. // in Node.js 17 where the default is to prefer an IPv6 address if one is
  32. // offered (which varies based on the OS and/or how the `localhost` hostname
  33. // resolution is configured), so it can now also resolve to `::1`. This causes
  34. // Firefox to not start anymore since it doesn't bind on the `::1` interface.
  35. // To avoid this, we switch Node.js' DNS resolver back to preferring IPv4
  36. // since we connect to a local browser anyway. Only do this for Node.js versions
  37. // that actually have this API since it got introduced in Node.js 14.18.0 and
  38. // it's not relevant for older versions anyway.
  39. if (dns.setDefaultResultOrder !== undefined) {
  40. dns.setDefaultResultOrder("ipv4first");
  41. }
  42. function parseOptions() {
  43. yargs
  44. .usage("Usage: $0")
  45. .option("downloadOnly", {
  46. default: false,
  47. describe: "Download test PDFs without running the tests.",
  48. type: "boolean",
  49. })
  50. .option("fontTest", {
  51. default: false,
  52. describe: "Run the font tests.",
  53. type: "boolean",
  54. })
  55. .option("help", {
  56. alias: "h",
  57. default: false,
  58. describe: "Show this help message.",
  59. type: "boolean",
  60. })
  61. .option("integration", {
  62. default: false,
  63. describe: "Run the integration tests.",
  64. type: "boolean",
  65. })
  66. .option("manifestFile", {
  67. default: "test_manifest.json",
  68. describe: "A path to JSON file in the form of `test_manifest.json`.",
  69. type: "string",
  70. })
  71. .option("masterMode", {
  72. alias: "m",
  73. default: false,
  74. describe: "Run the script in master mode.",
  75. type: "boolean",
  76. })
  77. .option("noChrome", {
  78. default: false,
  79. describe: "Skip Chrome when running tests.",
  80. type: "boolean",
  81. })
  82. .option("noDownload", {
  83. default: false,
  84. describe: "Skip downloading of test PDFs.",
  85. type: "boolean",
  86. })
  87. .option("noPrompts", {
  88. default: false,
  89. describe: "Uses default answers (intended for CLOUD TESTS only!).",
  90. type: "boolean",
  91. })
  92. .option("port", {
  93. default: 0,
  94. describe: "The port the HTTP server should listen on.",
  95. type: "number",
  96. })
  97. .option("reftest", {
  98. default: false,
  99. describe:
  100. "Automatically start reftest showing comparison test failures, if there are any.",
  101. type: "boolean",
  102. })
  103. .option("statsDelay", {
  104. default: 0,
  105. describe:
  106. "The amount of time in milliseconds the browser should wait before starting stats.",
  107. type: "number",
  108. })
  109. .option("statsFile", {
  110. default: "",
  111. describe: "The file where to store stats.",
  112. type: "string",
  113. })
  114. .option("strictVerify", {
  115. default: false,
  116. describe: "Error if verifying the manifest files fails.",
  117. type: "boolean",
  118. })
  119. .option("testfilter", {
  120. alias: "t",
  121. default: [],
  122. describe: "Run specific reftest(s).",
  123. type: "array",
  124. })
  125. .example(
  126. "testfilter",
  127. "$0 -t=issue5567 -t=issue5909\n" +
  128. "Run the reftest identified by issue5567 and issue5909."
  129. )
  130. .option("unitTest", {
  131. default: false,
  132. describe: "Run the unit tests.",
  133. type: "boolean",
  134. })
  135. .option("xfaOnly", {
  136. default: false,
  137. describe: "Only run the XFA reftest(s).",
  138. type: "boolean",
  139. })
  140. .check(argv => {
  141. if (
  142. +argv.reftest + argv.unitTest + argv.fontTest + argv.masterMode <=
  143. 1
  144. ) {
  145. return true;
  146. }
  147. throw new Error(
  148. "--reftest, --unitTest, --fontTest, and --masterMode must not be specified together."
  149. );
  150. })
  151. .check(argv => {
  152. if (
  153. +argv.unitTest + argv.fontTest + argv.integration + argv.xfaOnly <=
  154. 1
  155. ) {
  156. return true;
  157. }
  158. throw new Error(
  159. "--unitTest, --fontTest, --integration, and --xfaOnly must not be specified together."
  160. );
  161. })
  162. .check(argv => {
  163. if (argv.testfilter && argv.testfilter.length > 0 && argv.xfaOnly) {
  164. throw new Error("--testfilter and --xfaOnly cannot be used together.");
  165. }
  166. return true;
  167. })
  168. .check(argv => {
  169. if (!argv.noDownload || !argv.downloadOnly) {
  170. return true;
  171. }
  172. throw new Error(
  173. "--noDownload and --downloadOnly cannot be used together."
  174. );
  175. })
  176. .check(argv => {
  177. if (!argv.masterMode || argv.manifestFile === "test_manifest.json") {
  178. return true;
  179. }
  180. throw new Error(
  181. "when --masterMode is specified --manifestFile shall be equal to `test_manifest.json`."
  182. );
  183. });
  184. const result = yargs.argv;
  185. if (result.help) {
  186. yargs.showHelp();
  187. process.exit(0);
  188. }
  189. result.testfilter = Array.isArray(result.testfilter)
  190. ? result.testfilter
  191. : [result.testfilter];
  192. return result;
  193. }
  194. var refsTmpDir = "tmp";
  195. var testResultDir = "test_snapshots";
  196. var refsDir = "ref";
  197. var eqLog = "eq.log";
  198. var browserTimeout = 120;
  199. function monitorBrowserTimeout(session, onTimeout) {
  200. if (session.timeoutMonitor) {
  201. clearTimeout(session.timeoutMonitor);
  202. }
  203. if (!onTimeout) {
  204. session.timeoutMonitor = null;
  205. return;
  206. }
  207. session.timeoutMonitor = setTimeout(function () {
  208. onTimeout(session);
  209. }, browserTimeout * 1000);
  210. }
  211. function updateRefImages() {
  212. function sync(removeTmp) {
  213. console.log(" Updating ref/ ... ");
  214. testUtils.copySubtreeSync(refsTmpDir, refsDir);
  215. if (removeTmp) {
  216. testUtils.removeDirSync(refsTmpDir);
  217. }
  218. console.log("done");
  219. }
  220. if (options.noPrompts) {
  221. sync(false); // don't remove tmp/ for botio
  222. return;
  223. }
  224. const reader = readline.createInterface(process.stdin, process.stdout);
  225. reader.question(
  226. "Would you like to update the master copy in ref/? [yn] ",
  227. function (answer) {
  228. if (answer.toLowerCase() === "y") {
  229. sync(true);
  230. } else {
  231. console.log(" OK, not updating.");
  232. }
  233. reader.close();
  234. }
  235. );
  236. }
  237. function examineRefImages() {
  238. startServer();
  239. const startUrl = `http://${host}:${server.port}/test/resources/reftest-analyzer.html#web=/test/eq.log`;
  240. startBrowser("firefox", startUrl).then(function (browser) {
  241. browser.on("disconnected", function () {
  242. stopServer();
  243. process.exit(0);
  244. });
  245. });
  246. }
  247. function startRefTest(masterMode, showRefImages) {
  248. function finalize() {
  249. stopServer();
  250. var numErrors = 0;
  251. var numFBFFailures = 0;
  252. var numEqFailures = 0;
  253. var numEqNoSnapshot = 0;
  254. sessions.forEach(function (session) {
  255. numErrors += session.numErrors;
  256. numFBFFailures += session.numFBFFailures;
  257. numEqFailures += session.numEqFailures;
  258. numEqNoSnapshot += session.numEqNoSnapshot;
  259. });
  260. var numFatalFailures = numErrors + numFBFFailures;
  261. console.log();
  262. if (numFatalFailures + numEqFailures > 0) {
  263. console.log("OHNOES! Some tests failed!");
  264. if (numErrors > 0) {
  265. console.log(" errors: " + numErrors);
  266. }
  267. if (numEqFailures > 0) {
  268. console.log(" different ref/snapshot: " + numEqFailures);
  269. }
  270. if (numFBFFailures > 0) {
  271. console.log(" different first/second rendering: " + numFBFFailures);
  272. }
  273. } else {
  274. console.log("All regression tests passed.");
  275. }
  276. var runtime = (Date.now() - startTime) / 1000;
  277. console.log("Runtime was " + runtime.toFixed(1) + " seconds");
  278. if (options.statsFile) {
  279. fs.writeFileSync(options.statsFile, JSON.stringify(stats, null, 2));
  280. }
  281. if (masterMode) {
  282. if (numEqFailures + numEqNoSnapshot > 0) {
  283. console.log();
  284. console.log("Some eq tests failed or didn't have snapshots.");
  285. console.log("Checking to see if master references can be updated...");
  286. if (numFatalFailures > 0) {
  287. console.log(" No. Some non-eq tests failed.");
  288. } else {
  289. console.log(
  290. " Yes! The references in tmp/ can be synced with ref/."
  291. );
  292. updateRefImages();
  293. }
  294. }
  295. } else if (showRefImages && numEqFailures > 0) {
  296. console.log();
  297. console.log(
  298. `Starting reftest harness to examine ${numEqFailures} eq test failures.`
  299. );
  300. examineRefImages();
  301. }
  302. }
  303. function setup() {
  304. if (fs.existsSync(refsTmpDir)) {
  305. console.error("tmp/ exists -- unable to proceed with testing");
  306. process.exit(1);
  307. }
  308. if (fs.existsSync(eqLog)) {
  309. fs.unlinkSync(eqLog);
  310. }
  311. if (fs.existsSync(testResultDir)) {
  312. testUtils.removeDirSync(testResultDir);
  313. }
  314. startTime = Date.now();
  315. startServer();
  316. server.hooks.POST.push(refTestPostHandler);
  317. onAllSessionsClosed = finalize;
  318. const startUrl = `http://${host}:${server.port}/test/test_slave.html`;
  319. startBrowsers(function (session) {
  320. session.masterMode = masterMode;
  321. session.taskResults = {};
  322. session.tasks = {};
  323. session.remaining = manifest.length;
  324. manifest.forEach(function (item) {
  325. var rounds = item.rounds || 1;
  326. var roundsResults = [];
  327. roundsResults.length = rounds;
  328. session.taskResults[item.id] = roundsResults;
  329. session.tasks[item.id] = item;
  330. });
  331. session.numErrors = 0;
  332. session.numFBFFailures = 0;
  333. session.numEqNoSnapshot = 0;
  334. session.numEqFailures = 0;
  335. monitorBrowserTimeout(session, handleSessionTimeout);
  336. }, makeTestUrl(startUrl));
  337. }
  338. function checkRefsTmp() {
  339. if (masterMode && fs.existsSync(refsTmpDir)) {
  340. if (options.noPrompts) {
  341. testUtils.removeDirSync(refsTmpDir);
  342. setup();
  343. return;
  344. }
  345. console.log("Temporary snapshot dir tmp/ is still around.");
  346. console.log("tmp/ can be removed if it has nothing you need.");
  347. const reader = readline.createInterface(process.stdin, process.stdout);
  348. reader.question(
  349. "SHOULD THIS SCRIPT REMOVE tmp/? THINK CAREFULLY [yn] ",
  350. function (answer) {
  351. if (answer.toLowerCase() === "y") {
  352. testUtils.removeDirSync(refsTmpDir);
  353. }
  354. setup();
  355. reader.close();
  356. }
  357. );
  358. } else {
  359. setup();
  360. }
  361. }
  362. var startTime;
  363. var manifest = getTestManifest();
  364. if (!manifest) {
  365. return;
  366. }
  367. if (options.noDownload) {
  368. checkRefsTmp();
  369. } else {
  370. ensurePDFsDownloaded(checkRefsTmp);
  371. }
  372. }
  373. function handleSessionTimeout(session) {
  374. if (session.closed) {
  375. return;
  376. }
  377. var browser = session.name;
  378. console.log(
  379. "TEST-UNEXPECTED-FAIL | test failed " +
  380. browser +
  381. " has not responded in " +
  382. browserTimeout +
  383. "s"
  384. );
  385. session.numErrors += session.remaining;
  386. session.remaining = 0;
  387. closeSession(browser);
  388. }
  389. function getTestManifest() {
  390. var manifest = JSON.parse(fs.readFileSync(options.manifestFile));
  391. const testFilter = options.testfilter.slice(0),
  392. xfaOnly = options.xfaOnly;
  393. if (testFilter.length || xfaOnly) {
  394. manifest = manifest.filter(function (item) {
  395. var i = testFilter.indexOf(item.id);
  396. if (i !== -1) {
  397. testFilter.splice(i, 1);
  398. return true;
  399. }
  400. if (xfaOnly && item.enableXfa) {
  401. return true;
  402. }
  403. return false;
  404. });
  405. if (testFilter.length) {
  406. console.error("Unrecognized test IDs: " + testFilter.join(" "));
  407. return undefined;
  408. }
  409. }
  410. return manifest;
  411. }
  412. function checkEq(task, results, browser, masterMode) {
  413. var taskId = task.id;
  414. var refSnapshotDir = path.join(refsDir, os.platform(), browser, taskId);
  415. var testSnapshotDir = path.join(
  416. testResultDir,
  417. os.platform(),
  418. browser,
  419. taskId
  420. );
  421. var pageResults = results[0];
  422. var taskType = task.type;
  423. var numEqNoSnapshot = 0;
  424. var numEqFailures = 0;
  425. for (var page = 0; page < pageResults.length; page++) {
  426. if (!pageResults[page]) {
  427. continue;
  428. }
  429. const pageResult = pageResults[page];
  430. let testSnapshot = pageResult.snapshot;
  431. if (testSnapshot && testSnapshot.startsWith("data:image/png;base64,")) {
  432. testSnapshot = Buffer.from(testSnapshot.substring(22), "base64");
  433. } else {
  434. console.error("Valid snapshot was not found.");
  435. }
  436. var refSnapshot = null;
  437. var eq = false;
  438. var refPath = path.join(refSnapshotDir, page + 1 + ".png");
  439. if (!fs.existsSync(refPath)) {
  440. numEqNoSnapshot++;
  441. if (!masterMode) {
  442. console.log("WARNING: no reference snapshot " + refPath);
  443. }
  444. } else {
  445. refSnapshot = fs.readFileSync(refPath);
  446. eq = refSnapshot.toString("hex") === testSnapshot.toString("hex");
  447. if (!eq) {
  448. console.log(
  449. "TEST-UNEXPECTED-FAIL | " +
  450. taskType +
  451. " " +
  452. taskId +
  453. " | in " +
  454. browser +
  455. " | rendering of page " +
  456. (page + 1) +
  457. " != reference rendering"
  458. );
  459. testUtils.ensureDirSync(testSnapshotDir);
  460. fs.writeFileSync(
  461. path.join(testSnapshotDir, page + 1 + ".png"),
  462. testSnapshot
  463. );
  464. fs.writeFileSync(
  465. path.join(testSnapshotDir, page + 1 + "_ref.png"),
  466. refSnapshot
  467. );
  468. // This no longer follows the format of Mozilla reftest output.
  469. const viewportString = `(${pageResult.viewportWidth}x${pageResult.viewportHeight}x${pageResult.outputScale})`;
  470. fs.appendFileSync(
  471. eqLog,
  472. "REFTEST TEST-UNEXPECTED-FAIL | " +
  473. browser +
  474. "-" +
  475. taskId +
  476. "-page" +
  477. (page + 1) +
  478. " | image comparison (==)\n" +
  479. `REFTEST IMAGE 1 (TEST)${viewportString}: ` +
  480. path.join(testSnapshotDir, page + 1 + ".png") +
  481. "\n" +
  482. `REFTEST IMAGE 2 (REFERENCE)${viewportString}: ` +
  483. path.join(testSnapshotDir, page + 1 + "_ref.png") +
  484. "\n"
  485. );
  486. numEqFailures++;
  487. }
  488. }
  489. if (masterMode && (!refSnapshot || !eq)) {
  490. var tmpSnapshotDir = path.join(
  491. refsTmpDir,
  492. os.platform(),
  493. browser,
  494. taskId
  495. );
  496. testUtils.ensureDirSync(tmpSnapshotDir);
  497. fs.writeFileSync(
  498. path.join(tmpSnapshotDir, page + 1 + ".png"),
  499. testSnapshot
  500. );
  501. }
  502. }
  503. var session = getSession(browser);
  504. session.numEqNoSnapshot += numEqNoSnapshot;
  505. if (numEqFailures > 0) {
  506. session.numEqFailures += numEqFailures;
  507. } else {
  508. console.log(
  509. "TEST-PASS | " + taskType + " test " + taskId + " | in " + browser
  510. );
  511. }
  512. }
  513. function checkFBF(task, results, browser, masterMode) {
  514. var numFBFFailures = 0;
  515. var round0 = results[0],
  516. round1 = results[1];
  517. if (round0.length !== round1.length) {
  518. console.error("round 1 and 2 sizes are different");
  519. }
  520. for (var page = 0; page < round1.length; page++) {
  521. var r0Page = round0[page],
  522. r1Page = round1[page];
  523. if (!r0Page) {
  524. continue;
  525. }
  526. if (r0Page.snapshot !== r1Page.snapshot) {
  527. // The FBF tests fail intermittently in Firefox and Google Chrome when run
  528. // on the bots, ignoring `makeref` failures for now; see
  529. // - https://github.com/mozilla/pdf.js/pull/12368
  530. // - https://github.com/mozilla/pdf.js/pull/11491
  531. //
  532. // TODO: Figure out why this happens, so that we can remove the hack; see
  533. // https://github.com/mozilla/pdf.js/issues/12371
  534. if (masterMode) {
  535. console.log(
  536. "TEST-SKIPPED | forward-back-forward test " +
  537. task.id +
  538. " | in " +
  539. browser +
  540. " | page" +
  541. (page + 1)
  542. );
  543. continue;
  544. }
  545. console.log(
  546. "TEST-UNEXPECTED-FAIL | forward-back-forward test " +
  547. task.id +
  548. " | in " +
  549. browser +
  550. " | first rendering of page " +
  551. (page + 1) +
  552. " != second"
  553. );
  554. numFBFFailures++;
  555. }
  556. }
  557. if (numFBFFailures > 0) {
  558. getSession(browser).numFBFFailures += numFBFFailures;
  559. } else {
  560. console.log(
  561. "TEST-PASS | forward-back-forward test " + task.id + " | in " + browser
  562. );
  563. }
  564. }
  565. function checkLoad(task, results, browser) {
  566. // Load just checks for absence of failure, so if we got here the
  567. // test has passed
  568. console.log("TEST-PASS | load test " + task.id + " | in " + browser);
  569. }
  570. function checkRefTestResults(browser, id, results) {
  571. var failed = false;
  572. var session = getSession(browser);
  573. var task = session.tasks[id];
  574. results.forEach(function (roundResults, round) {
  575. roundResults.forEach(function (pageResult, page) {
  576. if (!pageResult) {
  577. return; // no results
  578. }
  579. if (pageResult.failure) {
  580. failed = true;
  581. if (fs.existsSync(task.file + ".error")) {
  582. console.log(
  583. "TEST-SKIPPED | PDF was not downloaded " +
  584. id +
  585. " | in " +
  586. browser +
  587. " | page" +
  588. (page + 1) +
  589. " round " +
  590. (round + 1) +
  591. " | " +
  592. pageResult.failure
  593. );
  594. } else {
  595. session.numErrors++;
  596. console.log(
  597. "TEST-UNEXPECTED-FAIL | test failed " +
  598. id +
  599. " | in " +
  600. browser +
  601. " | page" +
  602. (page + 1) +
  603. " round " +
  604. (round + 1) +
  605. " | " +
  606. pageResult.failure
  607. );
  608. }
  609. }
  610. });
  611. });
  612. if (failed) {
  613. return;
  614. }
  615. switch (task.type) {
  616. case "eq":
  617. case "text":
  618. checkEq(task, results, browser, session.masterMode);
  619. break;
  620. case "fbf":
  621. checkFBF(task, results, browser, session.masterMode);
  622. break;
  623. case "load":
  624. checkLoad(task, results, browser);
  625. break;
  626. default:
  627. throw new Error("Unknown test type");
  628. }
  629. // clear memory
  630. results.forEach(function (roundResults, round) {
  631. roundResults.forEach(function (pageResult, page) {
  632. pageResult.snapshot = null;
  633. });
  634. });
  635. }
  636. function refTestPostHandler(req, res) {
  637. var parsedUrl = url.parse(req.url, true);
  638. var pathname = parsedUrl.pathname;
  639. if (
  640. pathname !== "/tellMeToQuit" &&
  641. pathname !== "/info" &&
  642. pathname !== "/submit_task_results"
  643. ) {
  644. return false;
  645. }
  646. var body = "";
  647. req.on("data", function (data) {
  648. body += data;
  649. });
  650. req.on("end", function () {
  651. res.writeHead(200, { "Content-Type": "text/plain" });
  652. res.end();
  653. var session;
  654. if (pathname === "/tellMeToQuit") {
  655. session = getSession(parsedUrl.query.browser);
  656. monitorBrowserTimeout(session, null);
  657. closeSession(session.name);
  658. return;
  659. }
  660. var data = JSON.parse(body);
  661. if (pathname === "/info") {
  662. console.log(data.message);
  663. return;
  664. }
  665. var browser = data.browser;
  666. var round = data.round;
  667. var id = data.id;
  668. var page = data.page - 1;
  669. var failure = data.failure;
  670. var snapshot = data.snapshot;
  671. var lastPageNum = data.lastPageNum;
  672. session = getSession(browser);
  673. monitorBrowserTimeout(session, handleSessionTimeout);
  674. var taskResults = session.taskResults[id];
  675. if (!taskResults[round]) {
  676. taskResults[round] = [];
  677. }
  678. if (taskResults[round][page]) {
  679. console.error(
  680. "Results for " +
  681. browser +
  682. ":" +
  683. id +
  684. ":" +
  685. round +
  686. ":" +
  687. page +
  688. " were already submitted"
  689. );
  690. // TODO abort testing here?
  691. }
  692. taskResults[round][page] = {
  693. failure,
  694. snapshot,
  695. viewportWidth: data.viewportWidth,
  696. viewportHeight: data.viewportHeight,
  697. outputScale: data.outputScale,
  698. };
  699. if (stats) {
  700. stats.push({
  701. browser,
  702. pdf: id,
  703. page,
  704. round,
  705. stats: data.stats,
  706. });
  707. }
  708. var isDone =
  709. taskResults[taskResults.length - 1] &&
  710. taskResults[taskResults.length - 1][lastPageNum - 1];
  711. if (isDone) {
  712. checkRefTestResults(browser, id, taskResults);
  713. session.remaining--;
  714. }
  715. });
  716. return true;
  717. }
  718. function onAllSessionsClosedAfterTests(name) {
  719. const startTime = Date.now();
  720. return function () {
  721. stopServer();
  722. var numRuns = 0,
  723. numErrors = 0;
  724. sessions.forEach(function (session) {
  725. numRuns += session.numRuns;
  726. numErrors += session.numErrors;
  727. });
  728. console.log();
  729. console.log("Run " + numRuns + " tests");
  730. if (numErrors > 0) {
  731. console.log("OHNOES! Some " + name + " tests failed!");
  732. console.log(" " + numErrors + " of " + numRuns + " failed");
  733. } else {
  734. console.log("All " + name + " tests passed.");
  735. }
  736. var runtime = (Date.now() - startTime) / 1000;
  737. console.log(name + " tests runtime was " + runtime.toFixed(1) + " seconds");
  738. };
  739. }
  740. function makeTestUrl(startUrl) {
  741. return function (browserName) {
  742. const queryParameters =
  743. `?browser=${encodeURIComponent(browserName)}` +
  744. `&manifestFile=${encodeURIComponent("/test/" + options.manifestFile)}` +
  745. `&testFilter=${JSON.stringify(options.testfilter)}` +
  746. `&xfaOnly=${options.xfaOnly}` +
  747. `&delay=${options.statsDelay}` +
  748. `&masterMode=${options.masterMode}`;
  749. return startUrl + queryParameters;
  750. };
  751. }
  752. function startUnitTest(testUrl, name) {
  753. onAllSessionsClosed = onAllSessionsClosedAfterTests(name);
  754. startServer();
  755. server.hooks.POST.push(unitTestPostHandler);
  756. const startUrl = `http://${host}:${server.port}${testUrl}`;
  757. startBrowsers(function (session) {
  758. session.numRuns = 0;
  759. session.numErrors = 0;
  760. }, makeTestUrl(startUrl));
  761. }
  762. function startIntegrationTest() {
  763. onAllSessionsClosed = onAllSessionsClosedAfterTests("integration");
  764. startServer();
  765. const { runTests } = require("./integration-boot.js");
  766. startBrowsers(function (session) {
  767. session.numRuns = 0;
  768. session.numErrors = 0;
  769. });
  770. global.integrationBaseUrl = `http://${host}:${server.port}/build/generic/web/viewer.html`;
  771. global.integrationSessions = sessions;
  772. Promise.all(sessions.map(session => session.browserPromise)).then(
  773. async () => {
  774. const results = { runs: 0, failures: 0 };
  775. await runTests(results);
  776. sessions[0].numRuns = results.runs;
  777. sessions[0].numErrors = results.failures;
  778. await Promise.all(sessions.map(session => closeSession(session.name)));
  779. }
  780. );
  781. }
  782. function unitTestPostHandler(req, res) {
  783. var parsedUrl = url.parse(req.url);
  784. var pathname = parsedUrl.pathname;
  785. if (
  786. pathname !== "/tellMeToQuit" &&
  787. pathname !== "/info" &&
  788. pathname !== "/ttx" &&
  789. pathname !== "/submit_task_results"
  790. ) {
  791. return false;
  792. }
  793. var body = "";
  794. req.on("data", function (data) {
  795. body += data;
  796. });
  797. req.on("end", function () {
  798. if (pathname === "/ttx") {
  799. var translateFont = require("./font/ttxdriver.js").translateFont;
  800. var onCancel = null,
  801. ttxTimeout = 10000;
  802. var timeoutId = setTimeout(function () {
  803. onCancel?.("TTX timeout");
  804. }, ttxTimeout);
  805. translateFont(
  806. body,
  807. function (fn) {
  808. onCancel = fn;
  809. },
  810. function (err, xml) {
  811. clearTimeout(timeoutId);
  812. res.writeHead(200, { "Content-Type": "text/xml" });
  813. res.end(err ? "<error>" + err + "</error>" : xml);
  814. }
  815. );
  816. return;
  817. }
  818. res.writeHead(200, { "Content-Type": "text/plain" });
  819. res.end();
  820. var data = JSON.parse(body);
  821. if (pathname === "/tellMeToQuit") {
  822. closeSession(data.browser);
  823. return;
  824. }
  825. if (pathname === "/info") {
  826. console.log(data.message);
  827. return;
  828. }
  829. var session = getSession(data.browser);
  830. session.numRuns++;
  831. var message =
  832. data.status + " | " + data.description + " | in " + session.name;
  833. if (data.status === "TEST-UNEXPECTED-FAIL") {
  834. session.numErrors++;
  835. }
  836. if (data.error) {
  837. message += " | " + data.error;
  838. }
  839. console.log(message);
  840. });
  841. return true;
  842. }
  843. async function startBrowser(browserName, startUrl = "") {
  844. const revisions =
  845. require("puppeteer-core/lib/cjs/puppeteer/revisions.js").PUPPETEER_REVISIONS;
  846. const wantedRevision =
  847. browserName === "chrome" ? revisions.chromium : revisions.firefox;
  848. // Remove other revisions than the one we want to use. Updating Puppeteer can
  849. // cause a new revision to be used, and not removing older revisions causes
  850. // the disk to fill up.
  851. const browserFetcher = puppeteer.createBrowserFetcher({
  852. product: browserName,
  853. });
  854. const localRevisions = await browserFetcher.localRevisions();
  855. if (localRevisions.length > 1) {
  856. for (const localRevision of localRevisions) {
  857. if (localRevision !== wantedRevision) {
  858. console.log(`Removing old ${browserName} revision ${localRevision}...`);
  859. await browserFetcher.remove(localRevision);
  860. }
  861. }
  862. }
  863. const options = {
  864. product: browserName,
  865. headless: false,
  866. defaultViewport: null,
  867. ignoreDefaultArgs: ["--disable-extensions"],
  868. };
  869. if (!tempDir) {
  870. tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pdfjs-"));
  871. }
  872. const printFile = path.join(tempDir, "print.pdf");
  873. if (browserName === "chrome") {
  874. // avoid crash
  875. options.args = ["--no-sandbox", "--disable-setuid-sandbox"];
  876. // silent printing in a pdf
  877. options.args.push("--kiosk-printing");
  878. }
  879. if (browserName === "firefox") {
  880. options.extraPrefsFirefox = {
  881. // avoid to have a prompt when leaving a page with a form
  882. "dom.disable_beforeunload": true,
  883. // Disable dialog when saving a pdf
  884. "pdfjs.disabled": true,
  885. "browser.helperApps.neverAsk.saveToDisk": "application/pdf",
  886. // Avoid popup when saving is done
  887. "browser.download.always_ask_before_handling_new_types": true,
  888. "browser.download.panel.shown": true,
  889. "browser.download.alwaysOpenPanel": false,
  890. // Save file in output
  891. "browser.download.folderList": 2,
  892. "browser.download.dir": tempDir,
  893. // Print silently in a pdf
  894. "print.always_print_silent": true,
  895. "print.show_print_progress": false,
  896. print_printer: "PDF",
  897. "print.printer_PDF.print_to_file": true,
  898. "print.printer_PDF.print_to_filename": printFile,
  899. // Enable OffscreenCanvas
  900. "gfx.offscreencanvas.enabled": true,
  901. // Disable gpu acceleration
  902. "gfx.canvas.accelerated": false,
  903. };
  904. }
  905. const browser = await puppeteer.launch(options);
  906. if (startUrl) {
  907. const pages = await browser.pages();
  908. const page = pages[0];
  909. await page.goto(startUrl, { timeout: 0, waitUntil: "domcontentloaded" });
  910. }
  911. return browser;
  912. }
  913. function startBrowsers(initSessionCallback, makeStartUrl = null) {
  914. const browserNames = options.noChrome ? ["firefox"] : ["firefox", "chrome"];
  915. sessions = [];
  916. for (const browserName of browserNames) {
  917. // The session must be pushed first and augmented with the browser once
  918. // it's initialized. The reason for this is that browser initialization
  919. // takes more time when the browser is not found locally yet and we don't
  920. // want `onAllSessionsClosed` to trigger if one of the browsers is done
  921. // and the other one is still initializing, since that would mean that
  922. // once the browser is initialized the server would have stopped already.
  923. // Pushing the session first ensures that `onAllSessionsClosed` will
  924. // only trigger once all browsers are initialized and done.
  925. const session = {
  926. name: browserName,
  927. browser: undefined,
  928. closed: false,
  929. };
  930. sessions.push(session);
  931. const startUrl = makeStartUrl ? makeStartUrl(browserName) : "";
  932. session.browserPromise = startBrowser(browserName, startUrl)
  933. .then(function (browser) {
  934. session.browser = browser;
  935. initSessionCallback?.(session);
  936. })
  937. .catch(function (ex) {
  938. console.log(`Error while starting ${browserName}: ${ex.message}`);
  939. closeSession(browserName);
  940. });
  941. }
  942. }
  943. function startServer() {
  944. server = new WebServer();
  945. server.host = host;
  946. server.port = options.port;
  947. server.root = "..";
  948. server.cacheExpirationTime = 3600;
  949. server.start();
  950. }
  951. function stopServer() {
  952. server.stop();
  953. }
  954. function getSession(browser) {
  955. return sessions.find(session => session.name === browser);
  956. }
  957. async function closeSession(browser) {
  958. for (const session of sessions) {
  959. if (session.name !== browser) {
  960. continue;
  961. }
  962. if (session.browser !== undefined) {
  963. for (const page of await session.browser.pages()) {
  964. await page.close();
  965. }
  966. await session.browser.close();
  967. }
  968. session.closed = true;
  969. const allClosed = sessions.every(function (s) {
  970. return s.closed;
  971. });
  972. if (allClosed) {
  973. if (tempDir) {
  974. const rimraf = require("rimraf");
  975. rimraf.sync(tempDir);
  976. }
  977. onAllSessionsClosed?.();
  978. }
  979. }
  980. }
  981. function ensurePDFsDownloaded(callback) {
  982. var downloadUtils = require("./downloadutils.js");
  983. var manifest = getTestManifest();
  984. downloadUtils.downloadManifestFiles(manifest, function () {
  985. downloadUtils.verifyManifestFiles(manifest, function (hasErrors) {
  986. if (hasErrors) {
  987. console.log(
  988. "Unable to verify the checksum for the files that are " +
  989. "used for testing."
  990. );
  991. console.log(
  992. "Please re-download the files, or adjust the MD5 " +
  993. "checksum in the manifest for the files listed above.\n"
  994. );
  995. if (options.strictVerify) {
  996. process.exit(1);
  997. }
  998. }
  999. callback();
  1000. });
  1001. });
  1002. }
  1003. function main() {
  1004. if (options.statsFile) {
  1005. stats = [];
  1006. }
  1007. if (options.downloadOnly) {
  1008. ensurePDFsDownloaded(function () {});
  1009. } else if (options.unitTest) {
  1010. // Allows linked PDF files in unit-tests as well.
  1011. ensurePDFsDownloaded(function () {
  1012. startUnitTest("/test/unit/unit_test.html", "unit");
  1013. });
  1014. } else if (options.fontTest) {
  1015. startUnitTest("/test/font/font_test.html", "font");
  1016. } else if (options.integration) {
  1017. // Allows linked PDF files in integration-tests as well.
  1018. ensurePDFsDownloaded(function () {
  1019. startIntegrationTest();
  1020. });
  1021. } else {
  1022. startRefTest(options.masterMode, options.reftest);
  1023. }
  1024. }
  1025. var server;
  1026. var sessions;
  1027. var onAllSessionsClosed;
  1028. var host = "127.0.0.1";
  1029. var options = parseOptions();
  1030. var stats;
  1031. var tempDir = null;
  1032. main();