diff options
Diffstat (limited to 'testing/mochitest/server.js')
-rw-r--r-- | testing/mochitest/server.js | 759 |
1 files changed, 759 insertions, 0 deletions
diff --git a/testing/mochitest/server.js b/testing/mochitest/server.js new file mode 100644 index 000000000..112cb2200 --- /dev/null +++ b/testing/mochitest/server.js @@ -0,0 +1,759 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note that the server script itself already defines Cc, Ci, and Cr for us, +// and because they're constants it's not safe to redefine them. Scope leakage +// sucks. + +// Disable automatic network detection, so tests work correctly when +// not connected to a network. +var ios = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService2); +ios.manageOfflineStatus = false; +ios.offline = false; + +var server; // for use in the shutdown handler, if necessary + +// +// HTML GENERATION +// +var tags = ['A', 'ABBR', 'ACRONYM', 'ADDRESS', 'APPLET', 'AREA', 'B', 'BASE', + 'BASEFONT', 'BDO', 'BIG', 'BLOCKQUOTE', 'BODY', 'BR', 'BUTTON', + 'CAPTION', 'CENTER', 'CITE', 'CODE', 'COL', 'COLGROUP', 'DD', + 'DEL', 'DFN', 'DIR', 'DIV', 'DL', 'DT', 'EM', 'FIELDSET', 'FONT', + 'FORM', 'FRAME', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', + 'HEAD', 'HR', 'HTML', 'I', 'IFRAME', 'IMG', 'INPUT', 'INS', + 'ISINDEX', 'KBD', 'LABEL', 'LEGEND', 'LI', 'LINK', 'MAP', 'MENU', + 'META', 'NOFRAMES', 'NOSCRIPT', 'OBJECT', 'OL', 'OPTGROUP', + 'OPTION', 'P', 'PARAM', 'PRE', 'Q', 'S', 'SAMP', 'SCRIPT', + 'SELECT', 'SMALL', 'SPAN', 'STRIKE', 'STRONG', 'STYLE', 'SUB', + 'SUP', 'TABLE', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD', + 'TITLE', 'TR', 'TT', 'U', 'UL', 'VAR']; + +/** + * Below, we'll use makeTagFunc to create a function for each of the + * strings in 'tags'. This will allow us to use s-expression like syntax + * to create HTML. + */ +function makeTagFunc(tagName) +{ + return function (attrs /* rest... */) + { + var startChildren = 0; + var response = ""; + + // write the start tag and attributes + response += "<" + tagName; + // if attr is an object, write attributes + if (attrs && typeof attrs == 'object') { + startChildren = 1; + + for (let key in attrs) { + const value = attrs[key]; + var val = "" + value; + response += " " + key + '="' + val.replace('"','"') + '"'; + } + } + response += ">"; + + // iterate through the rest of the args + for (var i = startChildren; i < arguments.length; i++) { + if (typeof arguments[i] == 'function') { + response += arguments[i](); + } else { + response += arguments[i]; + } + } + + // write the close tag + response += "</" + tagName + ">\n"; + return response; + } +} + +function makeTags() { + // map our global HTML generation functions + for (let tag of tags) { + this[tag] = makeTagFunc(tag.toLowerCase()); + } +} + +var _quitting = false; + +/** Quit when all activity has completed. */ +function serverStopped() +{ + _quitting = true; +} + +// only run the "main" section if httpd.js was loaded ahead of us +if (this["nsHttpServer"]) { + // + // SCRIPT CODE + // + runServer(); + + // We can only have gotten here if the /server/shutdown path was requested. + if (_quitting) + { + dumpn("HTTP server stopped, all pending requests complete"); + quit(0); + } + + // Impossible as the stop callback should have been called, but to be safe... + dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server"); + quit(1); +} + +var serverBasePath; +var displayResults = true; + +var gServerAddress; +var SERVER_PORT; + +// +// SERVER SETUP +// +function runServer() +{ + serverBasePath = __LOCATION__.parent; + server = createMochitestServer(serverBasePath); + + //verify server address + //if a.b.c.d or 'localhost' + if (typeof(_SERVER_ADDR) != "undefined") { + if (_SERVER_ADDR == "localhost") { + gServerAddress = _SERVER_ADDR; + } else { + var quads = _SERVER_ADDR.split('.'); + if (quads.length == 4) { + var invalid = false; + for (var i=0; i < 4; i++) { + if (quads[i] < 0 || quads[i] > 255) + invalid = true; + } + if (!invalid) + gServerAddress = _SERVER_ADDR; + else + throw "invalid _SERVER_ADDR, please specify a valid IP Address"; + } + } + } else { + throw "please defined _SERVER_ADDR (as an ip address) before running server.js"; + } + + if (typeof(_SERVER_PORT) != "undefined") { + if (parseInt(_SERVER_PORT) > 0 && parseInt(_SERVER_PORT) < 65536) + SERVER_PORT = _SERVER_PORT; + } else { + throw "please define _SERVER_PORT (as a port number) before running server.js"; + } + + // If DISPLAY_RESULTS is not specified, it defaults to true + if (typeof(_DISPLAY_RESULTS) != "undefined") { + displayResults = _DISPLAY_RESULTS; + } + + server._start(SERVER_PORT, gServerAddress); + + // touch a file in the profile directory to indicate we're alive + var foStream = Cc["@mozilla.org/network/file-output-stream;1"] + .createInstance(Ci.nsIFileOutputStream); + var serverAlive = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsILocalFile); + + if (typeof(_PROFILE_PATH) == "undefined") { + serverAlive.initWithFile(serverBasePath); + serverAlive.append("mochitesttestingprofile"); + } else { + serverAlive.initWithPath(_PROFILE_PATH); + } + + // If we're running outside of the test harness, there might + // not be a test profile directory present + if (serverAlive.exists()) { + serverAlive.append("server_alive.txt"); + foStream.init(serverAlive, + 0x02 | 0x08 | 0x20, 436, 0); // write, create, truncate + var data = "It's alive!"; + foStream.write(data, data.length); + foStream.close(); + } + + makeTags(); + + // + // The following is threading magic to spin an event loop -- this has to + // happen manually in xpcshell for the server to actually work. + // + var thread = Cc["@mozilla.org/thread-manager;1"] + .getService() + .currentThread; + while (!server.isStopped()) + thread.processNextEvent(true); + + // Server stopped by /server/shutdown handler -- go through pending events + // and return. + + // get rid of any pending requests + while (thread.hasPendingEvents()) + thread.processNextEvent(true); +} + +/** Creates and returns an HTTP server configured to serve Mochitests. */ +function createMochitestServer(serverBasePath) +{ + var server = new nsHttpServer(); + + server.registerDirectory("/", serverBasePath); + server.registerPathHandler("/server/shutdown", serverShutdown); + server.registerPathHandler("/server/debug", serverDebug); + server.registerPathHandler("/nested_oop", nestedTest); + server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality + server.registerContentType("jar", "application/x-jar"); + server.registerContentType("ogg", "application/ogg"); + server.registerContentType("pdf", "application/pdf"); + server.registerContentType("ogv", "video/ogg"); + server.registerContentType("oga", "audio/ogg"); + server.registerContentType("opus", "audio/ogg; codecs=opus"); + server.registerContentType("dat", "text/plain; charset=utf-8"); + server.registerContentType("frag", "text/plain"); // .frag == WebGL fragment shader + server.registerContentType("vert", "text/plain"); // .vert == WebGL vertex shader + server.setIndexHandler(defaultDirHandler); + + var serverRoot = + { + getFile: function getFile(path) + { + var file = serverBasePath.clone().QueryInterface(Ci.nsILocalFile); + path.split("/").forEach(function(p) { + file.appendRelativePath(p); + }); + return file; + }, + QueryInterface: function(aIID) { return this; } + }; + + server.setObjectState("SERVER_ROOT", serverRoot); + + processLocations(server); + + return server; +} + +/** + * Notifies the HTTP server about all the locations at which it might receive + * requests, so that it can properly respond to requests on any of the hosts it + * serves. + */ +function processLocations(server) +{ + var serverLocations = serverBasePath.clone(); + serverLocations.append("server-locations.txt"); + + const PR_RDONLY = 0x01; + var fis = new FileInputStream(serverLocations, PR_RDONLY, 292 /* 0444 */, + Ci.nsIFileInputStream.CLOSE_ON_EOF); + + var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0); + lis.QueryInterface(Ci.nsIUnicharLineInputStream); + + const LINE_REGEXP = + new RegExp("^([a-z][-a-z0-9+.]*)" + + "://" + + "(" + + "\\d+\\.\\d+\\.\\d+\\.\\d+" + + "|" + + "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" + + "[a-z](?:[-a-z0-9]*[a-z0-9])?" + + ")" + + ":" + + "(\\d+)" + + "(?:" + + "\\s+" + + "(\\S+(?:,\\S+)*)" + + ")?$"); + + var line = {}; + var lineno = 0; + var seenPrimary = false; + do + { + var more = lis.readLine(line); + lineno++; + + var lineValue = line.value; + if (lineValue.charAt(0) == "#" || lineValue == "") + continue; + + var match = LINE_REGEXP.exec(lineValue); + if (!match) + throw "Syntax error in server-locations.txt, line " + lineno; + + var [, scheme, host, port, options] = match; + if (options) + { + if (options.split(",").indexOf("primary") >= 0) + { + if (seenPrimary) + { + throw "Multiple primary locations in server-locations.txt, " + + "line " + lineno; + } + + server.identity.setPrimary(scheme, host, port); + seenPrimary = true; + continue; + } + } + + server.identity.add(scheme, host, port); + } + while (more); +} + +// PATH HANDLERS + +// /server/shutdown +function serverShutdown(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-type", "text/plain", false); + + var body = "Server shut down."; + response.bodyOutputStream.write(body, body.length); + + dumpn("Server shutting down now..."); + server.stop(serverStopped); +} + +// /server/debug?[012] +function serverDebug(metadata, response) +{ + response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level"); + if (metadata.queryString.length !== 1) + return; + + var mode; + if (metadata.queryString === "0") { + // do this now so it gets logged with the old mode + dumpn("Server debug logs disabled."); + DEBUG = false; + DEBUG_TIMESTAMP = false; + mode = "disabled"; + } else if (metadata.queryString === "1") { + DEBUG = true; + DEBUG_TIMESTAMP = false; + mode = "enabled"; + } else if (metadata.queryString === "2") { + DEBUG = true; + DEBUG_TIMESTAMP = true; + mode = "enabled, with timestamps"; + } else { + return; + } + + response.setStatusLine(metadata.httpVersion, 200, "OK"); + response.setHeader("Content-type", "text/plain", false); + var body = "Server debug logs " + mode + "."; + response.bodyOutputStream.write(body, body.length); + dumpn(body); +} + +// +// DIRECTORY LISTINGS +// + +/** + * Creates a generator that iterates over the contents of + * an nsIFile directory. + */ +function* dirIter(dir) +{ + var en = dir.directoryEntries; + while (en.hasMoreElements()) { + var file = en.getNext(); + yield file.QueryInterface(Ci.nsILocalFile); + } +} + +/** + * Builds an optionally nested object containing links to the + * files and directories within dir. + */ +function list(requestPath, directory, recurse) +{ + var count = 0; + var path = requestPath; + if (path.charAt(path.length - 1) != "/") { + path += "/"; + } + + var dir = directory.QueryInterface(Ci.nsIFile); + var links = {}; + + // The SimpleTest directory is hidden + let files = []; + for (let file of dirIter(dir)) { + if (file.exists() && file.path.indexOf("SimpleTest") == -1) { + files.push(file); + } + } + + // Sort files by name, so that tests can be run in a pre-defined order inside + // a given directory (see bug 384823) + function leafNameComparator(first, second) { + if (first.leafName < second.leafName) + return -1; + if (first.leafName > second.leafName) + return 1; + return 0; + } + files.sort(leafNameComparator); + + count = files.length; + for (let file of files) { + var key = path + file.leafName; + var childCount = 0; + if (file.isDirectory()) { + key += "/"; + } + if (recurse && file.isDirectory()) { + [links[key], childCount] = list(key, file, recurse); + count += childCount; + } else { + if (file.leafName.charAt(0) != '.') { + links[key] = {'test': {'url': key, 'expected': 'pass'}}; + } + } + } + + return [links, count]; +} + +/** + * Heuristic function that determines whether a given path + * is a test case to be executed in the harness, or just + * a supporting file. + */ +function isTest(filename, pattern) +{ + if (pattern) + return pattern.test(filename); + + // File name is a URL style path to a test file, make sure that we check for + // tests that start with the appropriate prefix. + var testPrefix = typeof(_TEST_PREFIX) == "string" ? _TEST_PREFIX : "test_"; + var testPattern = new RegExp("^" + testPrefix); + + var pathPieces = filename.split('/'); + + return testPattern.test(pathPieces[pathPieces.length - 1]) && + filename.indexOf(".js") == -1 && + filename.indexOf(".css") == -1 && + !/\^headers\^$/.test(filename); +} + +/** + * Transform nested hashtables of paths to nested HTML lists. + */ +function linksToListItems(links) +{ + var response = ""; + var children = ""; + for (let link in links) { + const value = links[link]; + var classVal = (!isTest(link) && !(value instanceof Object)) + ? "non-test invisible" + : "test"; + if (value instanceof Object) { + children = UL({class: "testdir"}, linksToListItems(value)); + } else { + children = ""; + } + + var bug_title = link.match(/test_bug\S+/); + var bug_num = null; + if (bug_title != null) { + bug_num = bug_title[0].match(/\d+/); + } + + if ((bug_title == null) || (bug_num == null)) { + response += LI({class: classVal}, A({href: link}, link), children); + } else { + var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id="+bug_num; + response += LI({class: classVal}, A({href: link}, link), " - ", A({href: bug_url}, "Bug "+bug_num), children); + } + + } + return response; +} + +/** + * Transform nested hashtables of paths to a flat table rows. + */ +function linksToTableRows(links, recursionLevel) +{ + var response = ""; + for (let link in links) { + const value = links[link]; + var classVal = (!isTest(link) && ((value instanceof Object) && ('test' in value))) + ? "non-test invisible" + : ""; + + var spacer = "padding-left: " + (10 * recursionLevel) + "px"; + + if ((value instanceof Object) && !('test' in value)) { + response += TR({class: "dir", id: "tr-" + link }, + TD({colspan: "3"}, " "), + TD({style: spacer}, + A({href: link}, link))); + response += linksToTableRows(value, recursionLevel + 1); + } else { + var bug_title = link.match(/test_bug\S+/); + var bug_num = null; + if (bug_title != null) { + bug_num = bug_title[0].match(/\d+/); + } + if ((bug_title == null) || (bug_num == null)) { + response += TR({class: classVal, id: "tr-" + link }, + TD("0"), + TD("0"), + TD("0"), + TD({style: spacer}, + A({href: link}, link))); + } else { + var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num; + response += TR({class: classVal, id: "tr-" + link }, + TD("0"), + TD("0"), + TD("0"), + TD({style: spacer}, + A({href: link}, link), " - ", + A({href: bug_url}, "Bug " + bug_num))); + } + } + } + return response; +} + +function arrayOfTestFiles(linkArray, fileArray, testPattern) { + for (let link in linkArray) { + const value = linkArray[link]; + if ((value instanceof Object) && !('test' in value)) { + arrayOfTestFiles(value, fileArray, testPattern); + } else if (isTest(link, testPattern) && (value instanceof Object)) { + fileArray.push(value['test']) + } + } +} +/** + * Produce a flat array of test file paths to be executed in the harness. + */ +function jsonArrayOfTestFiles(links) +{ + var testFiles = []; + arrayOfTestFiles(links, testFiles); + testFiles = testFiles.map(function(file) { return '"' + file['url'] + '"'; }); + + return "[" + testFiles.join(",\n") + "]"; +} + +/** + * Produce a normal directory listing. + */ +function regularListing(metadata, response) +{ + var [links, count] = list(metadata.path, + metadata.getProperty("directory"), + false); + response.write( + HTML( + HEAD( + TITLE("mochitest index ", metadata.path) + ), + BODY( + BR(), + A({href: ".."}, "Up a level"), + UL(linksToListItems(links)) + ) + ) + ); +} + +/** + * Read a manifestFile located at the root of the server's directory and turn + * it into an object for creating a table of clickable links for each test. + */ +function convertManifestToTestLinks(root, manifest) +{ + Cu.import("resource://gre/modules/NetUtil.jsm"); + + var manifestFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + manifestFile.initWithFile(serverBasePath); + manifestFile.append(manifest); + + var manifestStream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream); + manifestStream.init(manifestFile, -1, 0, 0); + + var manifestObj = JSON.parse(NetUtil.readInputStreamToString(manifestStream, + manifestStream.available())); + var paths = manifestObj.tests; + var pathPrefix = '/' + root + '/' + return [paths.reduce(function(t, p) { t[pathPrefix + p.path] = true; return t; }, {}), + paths.length]; +} + +/** + * Produce a test harness page that has one remote iframe + */ +function nestedTest(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-type", "text/html;charset=utf-8", false); + response.write( + HTML( + HEAD( + TITLE("Mochitest | ", metadata.path), + LINK({rel: "stylesheet", + type: "text/css", href: "/static/harness.css"}), + SCRIPT({type: "text/javascript", + src: "/nested_setup.js"}), + SCRIPT({type: "text/javascript"}, + "window.onload = addPermissions; gTestURL = '/tests?" + metadata.queryString + "';") + ), + BODY( + DIV({class: "container"}, + DIV({class: "frameholder", id: "holder-div"}) + ) + ))); +} + +/** + * Produce a test harness page containing all the test cases + * below it, recursively. + */ +function testListing(metadata, response) +{ + var links = {}; + var count = 0; + if (metadata.queryString.indexOf('manifestFile') == -1) { + [links, count] = list(metadata.path, + metadata.getProperty("directory"), + true); + } else if (typeof(Components) != undefined) { + var manifest = metadata.queryString.match(/manifestFile=([^&]+)/)[1]; + + [links, count] = convertManifestToTestLinks(metadata.path.split('/')[1], + manifest); + } + + var table_class = metadata.queryString.indexOf("hideResultsTable=1") > -1 ? "invisible": ""; + + let testname = (metadata.queryString.indexOf("testname=") > -1) + ? metadata.queryString.match(/testname=([^&]+)/)[1] + : ""; + + dumpn("count: " + count); + var tests = testname + ? "['/" + testname + "']" + : jsonArrayOfTestFiles(links); + response.write( + HTML( + HEAD( + TITLE("MochiTest | ", metadata.path), + LINK({rel: "stylesheet", + type: "text/css", href: "/static/harness.css"} + ), + SCRIPT({type: "text/javascript", + src: "/tests/SimpleTest/StructuredLog.jsm"}), + SCRIPT({type: "text/javascript", + src: "/tests/SimpleTest/LogController.js"}), + SCRIPT({type: "text/javascript", + src: "/tests/SimpleTest/MemoryStats.js"}), + SCRIPT({type: "text/javascript", + src: "/tests/SimpleTest/TestRunner.js"}), + SCRIPT({type: "text/javascript", + src: "/tests/SimpleTest/MozillaLogger.js"}), + SCRIPT({type: "text/javascript", + src: "/chunkifyTests.js"}), + SCRIPT({type: "text/javascript", + src: "/manifestLibrary.js"}), + SCRIPT({type: "text/javascript", + src: "/tests/SimpleTest/setup.js"}), + SCRIPT({type: "text/javascript"}, + "window.onload = hookup; gTestList=" + tests + ";" + ) + ), + BODY( + DIV({class: "container"}, + H2("--> ", A({href: "#", id: "runtests"}, "Run Tests"), " <--"), + P({style: "float: right;"}, + SMALL( + "Based on the ", + A({href:"http://www.mochikit.com/"}, "MochiKit"), + " unit tests." + ) + ), + DIV({class: "status"}, + H1({id: "indicator"}, "Status"), + H2({id: "pass"}, "Passed: ", SPAN({id: "pass-count"},"0")), + H2({id: "fail"}, "Failed: ", SPAN({id: "fail-count"},"0")), + H2({id: "fail"}, "Todo: ", SPAN({id: "todo-count"},"0")) + ), + DIV({class: "clear"}), + DIV({id: "current-test"}, + B("Currently Executing: ", + SPAN({id: "current-test-path"}, "_") + ) + ), + DIV({class: "clear"}), + DIV({class: "frameholder"}, + IFRAME({scrolling: "no", id: "testframe", "allowfullscreen": true}) + ), + DIV({class: "clear"}), + DIV({class: "toggle"}, + A({href: "#", id: "toggleNonTests"}, "Show Non-Tests"), + BR() + ), + + ( + displayResults ? + TABLE({cellpadding: 0, cellspacing: 0, class: table_class, id: "test-table"}, + TR(TD("Passed"), TD("Failed"), TD("Todo"), TD("Test Files")), + linksToTableRows(links, 0) + ) : "" + ), + + BR(), + TABLE({cellpadding: 0, cellspacing: 0, border: 1, bordercolor: "red", id: "fail-table"} + ), + + DIV({class: "clear"}) + ) + ) + ) + ); +} + +/** + * Respond to requests that match a file system directory. + * Under the tests/ directory, return a test harness page. + */ +function defaultDirHandler(metadata, response) +{ + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-type", "text/html;charset=utf-8", false); + try { + if (metadata.path.indexOf("/tests") != 0) { + regularListing(metadata, response); + } else { + testListing(metadata, response); + } + } catch (ex) { + response.write(ex); + } +} |