/* -*- 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/. */
// We expect these to be defined in the global scope by runtest.py.
/* global __LOCATION__, _PROFILE_PATH, _SERVER_PORT, _SERVER_ADDR, _DISPLAY_RESULTS,
_TEST_PREFIX, _HTTPD_PATH */
// Defined by xpcshell
/* global quit */
/* eslint-disable mozilla/use-chromeutils-generateqi */
// Set up a protocol substituion so that we can load the httpd.js file.
let protocolHandler = Services.io
.getProtocolHandler(
"resource")
.QueryInterface(Ci.nsIResProtocolHandler);
let httpdJSPath = PathUtils.toFileURI(_HTTPD_PATH);
protocolHandler.setSubstitution(
"httpd-server",
Services.io.newURI(httpdJSPath)
);
const { HttpServer, dumpn, setDebuggingStatus } = ChromeUtils.importESModule(
"resource://httpd-server/httpd.sys.mjs"
);
protocolHandler.setSubstitution(
"mochitest-server",
Services.io.newFileURI(__LOCATION__.parent)
);
/* import-globals-from mochitestListingsUtils.js */
Services.scriptloader.loadSubScript(
"resource://mochitest-server/mochitestListingsUtils.js",
this
);
const CC = Components.Constructor;
const FileInputStream = CC(
"@mozilla.org/network/file-input-stream;1",
"nsIFileInputStream",
"init"
);
const ConverterInputStream = CC(
"@mozilla.org/intl/converter-input-stream;1",
"nsIConverterInputStream",
"init"
);
// Disable automatic network detection, so tests work correctly when
// not connected to a network.
// eslint-disable-next-line mozilla/use-services
var ios = Cc[
"@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
ios.manageOfflineStatus =
false;
ios.offline =
false;
var server;
// for use in the shutdown handler, if necessary
var _quitting =
false;
/** Quit when all activity has completed. */
function serverStopped() {
_quitting =
true;
}
//
// 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 new Error(
"invalid _SERVER_ADDR, please specify a valid IP Address"
);
}
}
}
}
else {
throw new Error(
"please define _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 new Error(
"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.nsIFile);
if (
typeof _PROFILE_PATH ==
"undefined") {
serverAlive.initWithFile(serverBasePath);
serverAlive.append(
"mochitesttestingprofile");
}
else {
serverAlive.initWithPath(_PROFILE_PATH);
}
// Create a file to inform the harness that the server is ready
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();
}
else {
throw new Error(
"Failed to create server_alive.txt because " +
serverAlive.path +
" could not be found."
);
}
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 HttpServer();
server.registerDirectory(
"/", serverBasePath);
server.registerPathHandler(
"/server/shutdown", serverShutdown);
server.registerPathHandler(
"/server/debug", serverDebug);
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.registerContentType(
"wasm",
"application/wasm");
server.setIndexHandler(defaultDirHandler);
var serverRoot = {
getFile:
function getFile(path) {
var file = serverBasePath.clone().QueryInterface(Ci.nsIFile);
path.split(
"/").forEach(
function (p) {
file.appendRelativePath(p);
});
return file;
},
QueryInterface() {
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 new Error(
"Syntax error in server-locations.txt, line " + lineno);
}
var [, scheme, host, port, options] = match;
if (options) {
if (options.split(
",").includes(
"primary")) {
if (seenPrimary) {
throw new Error(
"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.");
setDebuggingStatus(
false,
false);
mode =
"disabled";
}
else if (metadata.queryString ===
"1") {
setDebuggingStatus(
true,
false);
mode =
"enabled";
}
else if (metadata.queryString ===
"2") {
setDebuggingStatus(
true,
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);
}
/**
* Produce a normal directory listing.
*/
function regularListing(metadata, response) {
var [links] = list(metadata.path, metadata.getProperty(
"directory"),
false);
response.write(
"\n" +
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) {
const { NetUtil } = ChromeUtils.importESModule(
"resource://gre/modules/NetUtil.sys.mjs"
);
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 containing all the test cases
* below it, recursively.
*/
function testListing(metadata, response) {
var links = {};
var count = 0;
if (!metadata.queryString.includes(
"manifestFile")) {
[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/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",
allow:
"geolocation 'src'",
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);
}
}