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,
10 // Defined by xpcshell
13 /* import-globals-from ../../netwerk/test/httpserver/httpd.js */
14 /* eslint-disable mozilla/use-chromeutils-generateqi */
16 // Disable automatic network detection, so tests work correctly when
17 // not connected to a network.
18 // eslint-disable-next-line mozilla/use-services
19 var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
20 ios.manageOfflineStatus = false;
23 var server; // for use in the shutdown handler, if necessary
28 /* global A, ABBR, ACRONYM, ADDRESS, APPLET, AREA, B, BASE,
29 BASEFONT, BDO, BIG, BLOCKQUOTE, BODY, BR, BUTTON,
30 CAPTION, CENTER, CITE, CODE, COL, COLGROUP, DD,
31 DEL, DFN, DIR, DIV, DL, DT, EM, FIELDSET, FONT,
32 FORM, FRAME, FRAMESET, H1, H2, H3, H4, H5, H6,
33 HEAD, HR, HTML, I, IFRAME, IMG, INPUT, INS,
34 ISINDEX, KBD, LABEL, LEGEND, LI, LINK, MAP, MENU,
35 META, NOFRAMES, NOSCRIPT, OBJECT, OL, OPTGROUP,
36 OPTION, P, PARAM, PRE, Q, S, SAMP, SCRIPT,
37 SELECT, SMALL, SPAN, STRIKE, STRONG, STYLE, SUB,
38 SUP, TABLE, TBODY, TD, TEXTAREA, TFOOT, TH, THEAD,
39 TITLE, TR, TT, U, UL, VAR */
135 * Below, we'll use makeTagFunc to create a function for each of the
136 * strings in 'tags'. This will allow us to use s-expression like syntax
139 function makeTagFunc(tagName) {
140 return function (attrs /* rest... */) {
141 var startChildren = 0;
144 // write the start tag and attributes
145 response += "<" + tagName;
146 // if attr is an object, write attributes
147 if (attrs && typeof attrs == "object") {
150 for (let key in attrs) {
151 const value = attrs[key];
152 var val = "" + value;
153 response += " " + key + '="' + val.replace('"', """) + '"';
158 // iterate through the rest of the args
159 for (var i = startChildren; i < arguments.length; i++) {
160 if (typeof arguments[i] == "function") {
161 response += arguments[i]();
163 response += arguments[i];
167 // write the close tag
168 response += "</" + tagName + ">\n";
173 function makeTags() {
174 // map our global HTML generation functions
175 for (let tag of tags) {
176 this[tag] = makeTagFunc(tag.toLowerCase());
180 var _quitting = false;
182 /** Quit when all activity has completed. */
183 function serverStopped() {
187 // only run the "main" section if httpd.js was loaded ahead of us
188 if (this.nsHttpServer) {
194 // We can only have gotten here if the /server/shutdown path was requested.
196 dumpn("HTTP server stopped, all pending requests complete");
200 // Impossible as the stop callback should have been called, but to be safe...
201 dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server");
206 var displayResults = true;
214 function runServer() {
215 serverBasePath = __LOCATION__.parent;
216 server = createMochitestServer(serverBasePath);
218 // verify server address
219 // if a.b.c.d or 'localhost'
220 if (typeof _SERVER_ADDR != "undefined") {
221 if (_SERVER_ADDR == "localhost") {
222 gServerAddress = _SERVER_ADDR;
224 var quads = _SERVER_ADDR.split(".");
225 if (quads.length == 4) {
227 for (var i = 0; i < 4; i++) {
228 if (quads[i] < 0 || quads[i] > 255) {
233 gServerAddress = _SERVER_ADDR;
236 "invalid _SERVER_ADDR, please specify a valid IP Address"
243 "please define _SERVER_ADDR (as an ip address) before running server.js"
247 if (typeof _SERVER_PORT != "undefined") {
248 if (parseInt(_SERVER_PORT) > 0 && parseInt(_SERVER_PORT) < 65536) {
249 SERVER_PORT = _SERVER_PORT;
253 "please define _SERVER_PORT (as a port number) before running server.js"
257 // If DISPLAY_RESULTS is not specified, it defaults to true
258 if (typeof _DISPLAY_RESULTS != "undefined") {
259 displayResults = _DISPLAY_RESULTS;
262 server._start(SERVER_PORT, gServerAddress);
264 // touch a file in the profile directory to indicate we're alive
265 var foStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
266 Ci.nsIFileOutputStream
268 var serverAlive = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
270 if (typeof _PROFILE_PATH == "undefined") {
271 serverAlive.initWithFile(serverBasePath);
272 serverAlive.append("mochitesttestingprofile");
274 serverAlive.initWithPath(_PROFILE_PATH);
277 // Create a file to inform the harness that the server is ready
278 if (serverAlive.exists()) {
279 serverAlive.append("server_alive.txt");
280 foStream.init(serverAlive, 0x02 | 0x08 | 0x20, 436, 0); // write, create, truncate
281 var data = "It's alive!";
282 foStream.write(data, data.length);
286 "Failed to create server_alive.txt because " +
288 " could not be found."
295 // The following is threading magic to spin an event loop -- this has to
296 // happen manually in xpcshell for the server to actually work.
298 var thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread;
299 while (!server.isStopped()) {
300 thread.processNextEvent(true);
303 // Server stopped by /server/shutdown handler -- go through pending events
306 // get rid of any pending requests
307 while (thread.hasPendingEvents()) {
308 thread.processNextEvent(true);
312 /** Creates and returns an HTTP server configured to serve Mochitests. */
313 function createMochitestServer(serverBasePath) {
314 var server = new nsHttpServer();
316 server.registerDirectory("/", serverBasePath);
317 server.registerPathHandler("/server/shutdown", serverShutdown);
318 server.registerPathHandler("/server/debug", serverDebug);
319 server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
320 server.registerContentType("jar", "application/x-jar");
321 server.registerContentType("ogg", "application/ogg");
322 server.registerContentType("pdf", "application/pdf");
323 server.registerContentType("ogv", "video/ogg");
324 server.registerContentType("oga", "audio/ogg");
325 server.registerContentType("opus", "audio/ogg; codecs=opus");
326 server.registerContentType("dat", "text/plain; charset=utf-8");
327 server.registerContentType("frag", "text/plain"); // .frag == WebGL fragment shader
328 server.registerContentType("vert", "text/plain"); // .vert == WebGL vertex shader
329 server.registerContentType("wasm", "application/wasm");
330 server.setIndexHandler(defaultDirHandler);
333 getFile: function getFile(path) {
334 var file = serverBasePath.clone().QueryInterface(Ci.nsIFile);
335 path.split("/").forEach(function (p) {
336 file.appendRelativePath(p);
340 QueryInterface(aIID) {
345 server.setObjectState("SERVER_ROOT", serverRoot);
347 processLocations(server);
353 * Notifies the HTTP server about all the locations at which it might receive
354 * requests, so that it can properly respond to requests on any of the hosts it
357 function processLocations(server) {
358 var serverLocations = serverBasePath.clone();
359 serverLocations.append("server-locations.txt");
361 const PR_RDONLY = 0x01;
362 var fis = new FileInputStream(
366 Ci.nsIFileInputStream.CLOSE_ON_EOF
369 var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
370 lis.QueryInterface(Ci.nsIUnicharLineInputStream);
372 const LINE_REGEXP = new RegExp(
373 "^([a-z][-a-z0-9+.]*)" +
376 "\\d+\\.\\d+\\.\\d+\\.\\d+" +
378 "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" +
379 "[a-z](?:[-a-z0-9]*[a-z0-9])?" +
391 var seenPrimary = false;
393 var more = lis.readLine(line);
396 var lineValue = line.value;
397 if (lineValue.charAt(0) == "#" || lineValue == "") {
401 var match = LINE_REGEXP.exec(lineValue);
403 throw new Error("Syntax error in server-locations.txt, line " + lineno);
406 var [, scheme, host, port, options] = match;
408 if (options.split(",").includes("primary")) {
411 "Multiple primary locations in server-locations.txt, " +
417 server.identity.setPrimary(scheme, host, port);
423 server.identity.add(scheme, host, port);
430 function serverShutdown(metadata, response) {
431 response.setStatusLine("1.1", 200, "OK");
432 response.setHeader("Content-type", "text/plain", false);
434 var body = "Server shut down.";
435 response.bodyOutputStream.write(body, body.length);
437 dumpn("Server shutting down now...");
438 server.stop(serverStopped);
441 // /server/debug?[012]
442 function serverDebug(metadata, response) {
443 response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level");
444 if (metadata.queryString.length !== 1) {
449 if (metadata.queryString === "0") {
450 // do this now so it gets logged with the old mode
451 dumpn("Server debug logs disabled.");
453 DEBUG_TIMESTAMP = false;
455 } else if (metadata.queryString === "1") {
457 DEBUG_TIMESTAMP = false;
459 } else if (metadata.queryString === "2") {
461 DEBUG_TIMESTAMP = true;
462 mode = "enabled, with timestamps";
467 response.setStatusLine(metadata.httpVersion, 200, "OK");
468 response.setHeader("Content-type", "text/plain", false);
469 var body = "Server debug logs " + mode + ".";
470 response.bodyOutputStream.write(body, body.length);
475 // DIRECTORY LISTINGS
479 * Creates a generator that iterates over the contents of
480 * an nsIFile directory.
482 function* dirIter(dir) {
483 var en = dir.directoryEntries;
484 while (en.hasMoreElements()) {
490 * Builds an optionally nested object containing links to the
491 * files and directories within dir.
493 function list(requestPath, directory, recurse) {
495 var path = requestPath;
496 if (path.charAt(path.length - 1) != "/") {
500 var dir = directory.QueryInterface(Ci.nsIFile);
503 // The SimpleTest directory is hidden
505 for (let file of dirIter(dir)) {
506 if (file.exists() && !file.path.includes("SimpleTest")) {
511 // Sort files by name, so that tests can be run in a pre-defined order inside
512 // a given directory (see bug 384823)
513 function leafNameComparator(first, second) {
514 if (first.leafName < second.leafName) {
517 if (first.leafName > second.leafName) {
522 files.sort(leafNameComparator);
524 count = files.length;
525 for (let file of files) {
526 var key = path + file.leafName;
528 if (file.isDirectory()) {
531 if (recurse && file.isDirectory()) {
532 [links[key], childCount] = list(key, file, recurse);
534 } else if (file.leafName.charAt(0) != ".") {
535 links[key] = { test: { url: key, expected: "pass" } };
539 return [links, count];
543 * Heuristic function that determines whether a given path
544 * is a test case to be executed in the harness, or just
547 function isTest(filename, pattern) {
549 return pattern.test(filename);
552 // File name is a URL style path to a test file, make sure that we check for
553 // tests that start with the appropriate prefix.
554 var testPrefix = typeof _TEST_PREFIX == "string" ? _TEST_PREFIX : "test_";
555 var testPattern = new RegExp("^" + testPrefix);
557 var pathPieces = filename.split("/");
560 testPattern.test(pathPieces[pathPieces.length - 1]) &&
561 !filename.includes(".js") &&
562 !filename.includes(".css") &&
563 !/\^headers\^$/.test(filename)
568 * Transform nested hashtables of paths to nested HTML lists.
570 function linksToListItems(links) {
573 for (let link in links) {
574 const value = links[link];
576 !isTest(link) && !(value instanceof Object)
577 ? "non-test invisible"
579 if (value instanceof Object) {
580 children = UL({ class: "testdir" }, linksToListItems(value));
585 var bug_title = link.match(/test_bug\S+/);
587 if (bug_title != null) {
588 bug_num = bug_title[0].match(/\d+/);
591 if (bug_title == null || bug_num == null) {
592 response += LI({ class: classVal }, A({ href: link }, link), children);
594 var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num;
597 A({ href: link }, link),
599 A({ href: bug_url }, "Bug " + bug_num),
608 * Transform nested hashtables of paths to a flat table rows.
610 function linksToTableRows(links, recursionLevel) {
612 for (let link in links) {
613 const value = links[link];
615 !isTest(link) && value instanceof Object && "test" in value
616 ? "non-test invisible"
619 var spacer = "padding-left: " + 10 * recursionLevel + "px";
621 if (value instanceof Object && !("test" in value)) {
623 { class: "dir", id: "tr-" + link },
624 TD({ colspan: "3" }, " "),
625 TD({ style: spacer }, A({ href: link }, link))
627 response += linksToTableRows(value, recursionLevel + 1);
629 var bug_title = link.match(/test_bug\S+/);
631 if (bug_title != null) {
632 bug_num = bug_title[0].match(/\d+/);
634 if (bug_title == null || bug_num == null) {
636 { class: classVal, id: "tr-" + link },
640 TD({ style: spacer }, A({ href: link }, link))
643 var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num;
645 { class: classVal, id: "tr-" + link },
651 A({ href: link }, link),
653 A({ href: bug_url }, "Bug " + bug_num)
662 function arrayOfTestFiles(linkArray, fileArray, testPattern) {
663 for (let link in linkArray) {
664 const value = linkArray[link];
665 if (value instanceof Object && !("test" in value)) {
666 arrayOfTestFiles(value, fileArray, testPattern);
667 } else if (isTest(link, testPattern) && value instanceof Object) {
668 fileArray.push(value.test);
673 * Produce a flat array of test file paths to be executed in the harness.
675 function jsonArrayOfTestFiles(links) {
677 arrayOfTestFiles(links, testFiles);
678 testFiles = testFiles.map(function (file) {
679 return '"' + file.url + '"';
682 return "[" + testFiles.join(",\n") + "]";
686 * Produce a normal directory listing.
688 function regularListing(metadata, response) {
689 var [links] = list(metadata.path, metadata.getProperty("directory"), false);
692 HEAD(TITLE("mochitest index ", metadata.path)),
693 BODY(BR(), A({ href: ".." }, "Up a level"), UL(linksToListItems(links)))
699 * Read a manifestFile located at the root of the server's directory and turn
700 * it into an object for creating a table of clickable links for each test.
702 function convertManifestToTestLinks(root, manifest) {
703 const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
705 var manifestFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
706 manifestFile.initWithFile(serverBasePath);
707 manifestFile.append(manifest);
709 var manifestStream = Cc[
710 "@mozilla.org/network/file-input-stream;1"
711 ].createInstance(Ci.nsIFileInputStream);
712 manifestStream.init(manifestFile, -1, 0, 0);
714 var manifestObj = JSON.parse(
715 NetUtil.readInputStreamToString(manifestStream, manifestStream.available())
717 var paths = manifestObj.tests;
718 var pathPrefix = "/" + root + "/";
720 paths.reduce(function (t, p) {
721 t[pathPrefix + p.path] = true;
729 * Produce a test harness page containing all the test cases
730 * below it, recursively.
732 function testListing(metadata, response) {
735 if (!metadata.queryString.includes("manifestFile")) {
736 [links, count] = list(
738 metadata.getProperty("directory"),
741 } else if (typeof Components != "undefined") {
742 var manifest = metadata.queryString.match(/manifestFile=([^&]+)/)[1];
744 [links, count] = convertManifestToTestLinks(
745 metadata.path.split("/")[1],
751 metadata.queryString.indexOf("hideResultsTable=1") > -1 ? "invisible" : "";
754 metadata.queryString.indexOf("testname=") > -1
755 ? metadata.queryString.match(/testname=([^&]+)/)[1]
758 dumpn("count: " + count);
759 var tests = testname ? "['/" + testname + "']" : jsonArrayOfTestFiles(links);
763 TITLE("MochiTest | ", metadata.path),
767 href: "/static/harness.css",
770 type: "text/javascript",
771 src: "/tests/SimpleTest/LogController.js",
774 type: "text/javascript",
775 src: "/tests/SimpleTest/MemoryStats.js",
778 type: "text/javascript",
779 src: "/tests/SimpleTest/TestRunner.js",
782 type: "text/javascript",
783 src: "/tests/SimpleTest/MozillaLogger.js",
785 SCRIPT({ type: "text/javascript", src: "/chunkifyTests.js" }),
786 SCRIPT({ type: "text/javascript", src: "/manifestLibrary.js" }),
787 SCRIPT({ type: "text/javascript", src: "/tests/SimpleTest/setup.js" }),
789 { type: "text/javascript" },
790 "window.onload = hookup; gTestList=" + tests + ";"
795 { class: "container" },
796 H2("--> ", A({ href: "#", id: "runtests" }, "Run Tests"), " <--"),
798 { style: "float: right;" },
801 A({ href: "http://www.mochikit.com/" }, "MochiKit"),
807 H1({ id: "indicator" }, "Status"),
808 H2({ id: "pass" }, "Passed: ", SPAN({ id: "pass-count" }, "0")),
809 H2({ id: "fail" }, "Failed: ", SPAN({ id: "fail-count" }, "0")),
810 H2({ id: "fail" }, "Todo: ", SPAN({ id: "todo-count" }, "0"))
812 DIV({ class: "clear" }),
814 { id: "current-test" },
815 B("Currently Executing: ", SPAN({ id: "current-test-path" }, "_"))
817 DIV({ class: "clear" }),
819 { class: "frameholder" },
820 IFRAME({ scrolling: "no", id: "testframe", allowfullscreen: true })
822 DIV({ class: "clear" }),
825 A({ href: "#", id: "toggleNonTests" }, "Show Non-Tests"),
837 TR(TD("Passed"), TD("Failed"), TD("Todo"), TD("Test Files")),
838 linksToTableRows(links, 0)
850 DIV({ class: "clear" })
858 * Respond to requests that match a file system directory.
859 * Under the tests/ directory, return a test harness page.
861 function defaultDirHandler(metadata, response) {
862 response.setStatusLine("1.1", 200, "OK");
863 response.setHeader("Content-type", "text/html;charset=utf-8", false);
865 if (metadata.path.indexOf("/tests") != 0) {
866 regularListing(metadata, response);
868 testListing(metadata, response);