Bug 1877642 - Disable browser_fullscreen-tab-close-race.js on apple_silicon !debug...
[gecko.git] / testing / mochitest / server.js
blob934f0b281cb7b3a8bc4346b730d36aaa3ab698d4
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 // We expect these to be defined in the global scope by runtest.py.
8 /* global __LOCATION__, _PROFILE_PATH, _SERVER_PORT, _SERVER_ADDR, _DISPLAY_RESULTS,
9           _TEST_PREFIX, _HTTPD_PATH */
10 // Defined by xpcshell
11 /* global quit */
13 /* eslint-disable mozilla/use-chromeutils-generateqi */
15 // Set up a protocol substituion so that we can load the httpd.js file.
16 let protocolHandler = Services.io
17   .getProtocolHandler("resource")
18   .QueryInterface(Ci.nsIResProtocolHandler);
19 let httpdJSPath = PathUtils.toFileURI(_HTTPD_PATH);
21 protocolHandler.setSubstitution(
22   "httpd-server",
23   Services.io.newURI(httpdJSPath)
25 const { HttpServer, dumpn, setDebuggingStatus } = ChromeUtils.importESModule(
26   "resource://httpd-server/httpd.sys.mjs"
29 protocolHandler.setSubstitution(
30   "mochitest-server",
31   Services.io.newFileURI(__LOCATION__.parent)
33 /* import-globals-from mochitestListingsUtils.js */
34 Services.scriptloader.loadSubScript(
35   "resource://mochitest-server/mochitestListingsUtils.js",
36   this
39 const CC = Components.Constructor;
41 const FileInputStream = CC(
42   "@mozilla.org/network/file-input-stream;1",
43   "nsIFileInputStream",
44   "init"
46 const ConverterInputStream = CC(
47   "@mozilla.org/intl/converter-input-stream;1",
48   "nsIConverterInputStream",
49   "init"
52 // Disable automatic network detection, so tests work correctly when
53 // not connected to a network.
54 // eslint-disable-next-line mozilla/use-services
55 var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
56 ios.manageOfflineStatus = false;
57 ios.offline = false;
59 var server; // for use in the shutdown handler, if necessary
61 var _quitting = false;
63 /** Quit when all activity has completed. */
64 function serverStopped() {
65   _quitting = true;
69 // SCRIPT CODE
71 runServer();
73 // We can only have gotten here if the /server/shutdown path was requested.
74 if (_quitting) {
75   dumpn("HTTP server stopped, all pending requests complete");
76   quit(0);
79 // Impossible as the stop callback should have been called, but to be safe...
80 dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server");
81 quit(1);
83 var serverBasePath;
84 var displayResults = true;
86 var gServerAddress;
87 var SERVER_PORT;
90 // SERVER SETUP
92 function runServer() {
93   serverBasePath = __LOCATION__.parent;
94   server = createMochitestServer(serverBasePath);
96   // verify server address
97   // if a.b.c.d or 'localhost'
98   if (typeof _SERVER_ADDR != "undefined") {
99     if (_SERVER_ADDR == "localhost") {
100       gServerAddress = _SERVER_ADDR;
101     } else {
102       var quads = _SERVER_ADDR.split(".");
103       if (quads.length == 4) {
104         var invalid = false;
105         for (var i = 0; i < 4; i++) {
106           if (quads[i] < 0 || quads[i] > 255) {
107             invalid = true;
108           }
109         }
110         if (!invalid) {
111           gServerAddress = _SERVER_ADDR;
112         } else {
113           throw new Error(
114             "invalid _SERVER_ADDR, please specify a valid IP Address"
115           );
116         }
117       }
118     }
119   } else {
120     throw new Error(
121       "please define _SERVER_ADDR (as an ip address) before running server.js"
122     );
123   }
125   if (typeof _SERVER_PORT != "undefined") {
126     if (parseInt(_SERVER_PORT) > 0 && parseInt(_SERVER_PORT) < 65536) {
127       SERVER_PORT = _SERVER_PORT;
128     }
129   } else {
130     throw new Error(
131       "please define _SERVER_PORT (as a port number) before running server.js"
132     );
133   }
135   // If DISPLAY_RESULTS is not specified, it defaults to true
136   if (typeof _DISPLAY_RESULTS != "undefined") {
137     displayResults = _DISPLAY_RESULTS;
138   }
140   server._start(SERVER_PORT, gServerAddress);
142   // touch a file in the profile directory to indicate we're alive
143   var foStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
144     Ci.nsIFileOutputStream
145   );
146   var serverAlive = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
148   if (typeof _PROFILE_PATH == "undefined") {
149     serverAlive.initWithFile(serverBasePath);
150     serverAlive.append("mochitesttestingprofile");
151   } else {
152     serverAlive.initWithPath(_PROFILE_PATH);
153   }
155   // Create a file to inform the harness that the server is ready
156   if (serverAlive.exists()) {
157     serverAlive.append("server_alive.txt");
158     foStream.init(serverAlive, 0x02 | 0x08 | 0x20, 436, 0); // write, create, truncate
159     var data = "It's alive!";
160     foStream.write(data, data.length);
161     foStream.close();
162   } else {
163     throw new Error(
164       "Failed to create server_alive.txt because " +
165         serverAlive.path +
166         " could not be found."
167     );
168   }
170   makeTags();
172   //
173   // The following is threading magic to spin an event loop -- this has to
174   // happen manually in xpcshell for the server to actually work.
175   //
176   var thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread;
177   while (!server.isStopped()) {
178     thread.processNextEvent(true);
179   }
181   // Server stopped by /server/shutdown handler -- go through pending events
182   // and return.
184   // get rid of any pending requests
185   while (thread.hasPendingEvents()) {
186     thread.processNextEvent(true);
187   }
190 /** Creates and returns an HTTP server configured to serve Mochitests. */
191 function createMochitestServer(serverBasePath) {
192   var server = new HttpServer();
194   server.registerDirectory("/", serverBasePath);
195   server.registerPathHandler("/server/shutdown", serverShutdown);
196   server.registerPathHandler("/server/debug", serverDebug);
197   server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
198   server.registerContentType("jar", "application/x-jar");
199   server.registerContentType("ogg", "application/ogg");
200   server.registerContentType("pdf", "application/pdf");
201   server.registerContentType("ogv", "video/ogg");
202   server.registerContentType("oga", "audio/ogg");
203   server.registerContentType("opus", "audio/ogg; codecs=opus");
204   server.registerContentType("dat", "text/plain; charset=utf-8");
205   server.registerContentType("frag", "text/plain"); // .frag == WebGL fragment shader
206   server.registerContentType("vert", "text/plain"); // .vert == WebGL vertex shader
207   server.registerContentType("wasm", "application/wasm");
208   server.setIndexHandler(defaultDirHandler);
210   var serverRoot = {
211     getFile: function getFile(path) {
212       var file = serverBasePath.clone().QueryInterface(Ci.nsIFile);
213       path.split("/").forEach(function (p) {
214         file.appendRelativePath(p);
215       });
216       return file;
217     },
218     QueryInterface() {
219       return this;
220     },
221   };
223   server.setObjectState("SERVER_ROOT", serverRoot);
225   processLocations(server);
227   return server;
231  * Notifies the HTTP server about all the locations at which it might receive
232  * requests, so that it can properly respond to requests on any of the hosts it
233  * serves.
234  */
235 function processLocations(server) {
236   var serverLocations = serverBasePath.clone();
237   serverLocations.append("server-locations.txt");
239   const PR_RDONLY = 0x01;
240   var fis = new FileInputStream(
241     serverLocations,
242     PR_RDONLY,
243     292 /* 0444 */,
244     Ci.nsIFileInputStream.CLOSE_ON_EOF
245   );
247   var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
248   lis.QueryInterface(Ci.nsIUnicharLineInputStream);
250   const LINE_REGEXP = new RegExp(
251     "^([a-z][-a-z0-9+.]*)" +
252       "://" +
253       "(" +
254       "\\d+\\.\\d+\\.\\d+\\.\\d+" +
255       "|" +
256       "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" +
257       "[a-z](?:[-a-z0-9]*[a-z0-9])?" +
258       ")" +
259       ":" +
260       "(\\d+)" +
261       "(?:" +
262       "\\s+" +
263       "(\\S+(?:,\\S+)*)" +
264       ")?$"
265   );
267   var line = {};
268   var lineno = 0;
269   var seenPrimary = false;
270   do {
271     var more = lis.readLine(line);
272     lineno++;
274     var lineValue = line.value;
275     if (lineValue.charAt(0) == "#" || lineValue == "") {
276       continue;
277     }
279     var match = LINE_REGEXP.exec(lineValue);
280     if (!match) {
281       throw new Error("Syntax error in server-locations.txt, line " + lineno);
282     }
284     var [, scheme, host, port, options] = match;
285     if (options) {
286       if (options.split(",").includes("primary")) {
287         if (seenPrimary) {
288           throw new Error(
289             "Multiple primary locations in server-locations.txt, " +
290               "line " +
291               lineno
292           );
293         }
295         server.identity.setPrimary(scheme, host, port);
296         seenPrimary = true;
297         continue;
298       }
299     }
301     server.identity.add(scheme, host, port);
302   } while (more);
305 // PATH HANDLERS
307 // /server/shutdown
308 function serverShutdown(metadata, response) {
309   response.setStatusLine("1.1", 200, "OK");
310   response.setHeader("Content-type", "text/plain", false);
312   var body = "Server shut down.";
313   response.bodyOutputStream.write(body, body.length);
315   dumpn("Server shutting down now...");
316   server.stop(serverStopped);
319 // /server/debug?[012]
320 function serverDebug(metadata, response) {
321   response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level");
322   if (metadata.queryString.length !== 1) {
323     return;
324   }
326   var mode;
327   if (metadata.queryString === "0") {
328     // do this now so it gets logged with the old mode
329     dumpn("Server debug logs disabled.");
330     setDebuggingStatus(false, false);
331     mode = "disabled";
332   } else if (metadata.queryString === "1") {
333     setDebuggingStatus(true, false);
334     mode = "enabled";
335   } else if (metadata.queryString === "2") {
336     setDebuggingStatus(true, true);
337     mode = "enabled, with timestamps";
338   } else {
339     return;
340   }
342   response.setStatusLine(metadata.httpVersion, 200, "OK");
343   response.setHeader("Content-type", "text/plain", false);
344   var body = "Server debug logs " + mode + ".";
345   response.bodyOutputStream.write(body, body.length);
346   dumpn(body);
350  * Produce a normal directory listing.
351  */
352 function regularListing(metadata, response) {
353   var [links] = list(metadata.path, metadata.getProperty("directory"), false);
354   response.write(
355     "<!DOCTYPE html>\n" +
356       HTML(
357         HEAD(TITLE("mochitest index ", metadata.path)),
358         BODY(BR(), A({ href: ".." }, "Up a level"), UL(linksToListItems(links)))
359       )
360   );
364  * Read a manifestFile located at the root of the server's directory and turn
365  * it into an object for creating a table of clickable links for each test.
366  */
367 function convertManifestToTestLinks(root, manifest) {
368   const { NetUtil } = ChromeUtils.importESModule(
369     "resource://gre/modules/NetUtil.sys.mjs"
370   );
372   var manifestFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
373   manifestFile.initWithFile(serverBasePath);
374   manifestFile.append(manifest);
376   var manifestStream = Cc[
377     "@mozilla.org/network/file-input-stream;1"
378   ].createInstance(Ci.nsIFileInputStream);
379   manifestStream.init(manifestFile, -1, 0, 0);
381   var manifestObj = JSON.parse(
382     NetUtil.readInputStreamToString(manifestStream, manifestStream.available())
383   );
384   var paths = manifestObj.tests;
385   var pathPrefix = "/" + root + "/";
386   return [
387     paths.reduce(function (t, p) {
388       t[pathPrefix + p.path] = true;
389       return t;
390     }, {}),
391     paths.length,
392   ];
396  * Produce a test harness page containing all the test cases
397  * below it, recursively.
398  */
399 function testListing(metadata, response) {
400   var links = {};
401   var count = 0;
402   if (!metadata.queryString.includes("manifestFile")) {
403     [links, count] = list(
404       metadata.path,
405       metadata.getProperty("directory"),
406       true
407     );
408   } else if (typeof Components != "undefined") {
409     var manifest = metadata.queryString.match(/manifestFile=([^&]+)/)[1];
411     [links, count] = convertManifestToTestLinks(
412       metadata.path.split("/")[1],
413       manifest
414     );
415   }
417   var table_class =
418     metadata.queryString.indexOf("hideResultsTable=1") > -1 ? "invisible" : "";
420   let testname =
421     metadata.queryString.indexOf("testname=") > -1
422       ? metadata.queryString.match(/testname=([^&]+)/)[1]
423       : "";
425   dumpn("count: " + count);
426   var tests = testname ? "['/" + testname + "']" : jsonArrayOfTestFiles(links);
427   response.write(
428     HTML(
429       HEAD(
430         TITLE("MochiTest | ", metadata.path),
431         LINK({
432           rel: "stylesheet",
433           type: "text/css",
434           href: "/static/harness.css",
435         }),
436         SCRIPT({
437           type: "text/javascript",
438           src: "/tests/SimpleTest/LogController.js",
439         }),
440         SCRIPT({
441           type: "text/javascript",
442           src: "/tests/SimpleTest/MemoryStats.js",
443         }),
444         SCRIPT({
445           type: "text/javascript",
446           src: "/tests/SimpleTest/TestRunner.js",
447         }),
448         SCRIPT({
449           type: "text/javascript",
450           src: "/tests/SimpleTest/MozillaLogger.js",
451         }),
452         SCRIPT({ type: "text/javascript", src: "/chunkifyTests.js" }),
453         SCRIPT({ type: "text/javascript", src: "/manifestLibrary.js" }),
454         SCRIPT({ type: "text/javascript", src: "/tests/SimpleTest/setup.js" }),
455         SCRIPT(
456           { type: "text/javascript" },
457           "window.onload =  hookup; gTestList=" + tests + ";"
458         )
459       ),
460       BODY(
461         DIV(
462           { class: "container" },
463           H2("--> ", A({ href: "#", id: "runtests" }, "Run Tests"), " <--"),
464           P(
465             { style: "float: right;" },
466             SMALL(
467               "Based on the ",
468               A({ href: "http://www.mochikit.com/" }, "MochiKit"),
469               " unit tests."
470             )
471           ),
472           DIV(
473             { class: "status" },
474             H1({ id: "indicator" }, "Status"),
475             H2({ id: "pass" }, "Passed: ", SPAN({ id: "pass-count" }, "0")),
476             H2({ id: "fail" }, "Failed: ", SPAN({ id: "fail-count" }, "0")),
477             H2({ id: "fail" }, "Todo: ", SPAN({ id: "todo-count" }, "0"))
478           ),
479           DIV({ class: "clear" }),
480           DIV(
481             { id: "current-test" },
482             B("Currently Executing: ", SPAN({ id: "current-test-path" }, "_"))
483           ),
484           DIV({ class: "clear" }),
485           DIV(
486             { class: "frameholder" },
487             IFRAME({
488               scrolling: "no",
489               id: "testframe",
490               allow: "geolocation 'src'",
491               allowfullscreen: true,
492             })
493           ),
494           DIV({ class: "clear" }),
495           DIV(
496             { class: "toggle" },
497             A({ href: "#", id: "toggleNonTests" }, "Show Non-Tests"),
498             BR()
499           ),
501           displayResults
502             ? TABLE(
503                 {
504                   cellpadding: 0,
505                   cellspacing: 0,
506                   class: table_class,
507                   id: "test-table",
508                 },
509                 TR(TD("Passed"), TD("Failed"), TD("Todo"), TD("Test Files")),
510                 linksToTableRows(links, 0)
511               )
512             : "",
513           BR(),
514           TABLE({
515             cellpadding: 0,
516             cellspacing: 0,
517             border: 1,
518             bordercolor: "red",
519             id: "fail-table",
520           }),
522           DIV({ class: "clear" })
523         )
524       )
525     )
526   );
530  * Respond to requests that match a file system directory.
531  * Under the tests/ directory, return a test harness page.
532  */
533 function defaultDirHandler(metadata, response) {
534   response.setStatusLine("1.1", 200, "OK");
535   response.setHeader("Content-type", "text/html;charset=utf-8", false);
536   try {
537     if (metadata.path.indexOf("/tests") != 0) {
538       regularListing(metadata, response);
539     } else {
540       testListing(metadata, response);
541     }
542   } catch (ex) {
543     response.write(ex);
544   }