Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 // Note that the server script itself already defines Cc, Ci, and Cr for us,
8 // and because they're constants it's not safe to redefine them. Scope leakage
9 // sucks.
11 // Disable automatic network detection, so tests work correctly when
12 // not connected to a network.
13 let (ios = Cc["@mozilla.org/network/io-service;1"]
14 .getService(Ci.nsIIOService2)) {
15 ios.manageOfflineStatus = false;
16 ios.offline = false;
17 }
19 var server; // for use in the shutdown handler, if necessary
21 //
22 // HTML GENERATION
23 //
24 var tags = ['A', 'ABBR', 'ACRONYM', 'ADDRESS', 'APPLET', 'AREA', 'B', 'BASE',
25 'BASEFONT', 'BDO', 'BIG', 'BLOCKQUOTE', 'BODY', 'BR', 'BUTTON',
26 'CAPTION', 'CENTER', 'CITE', 'CODE', 'COL', 'COLGROUP', 'DD',
27 'DEL', 'DFN', 'DIR', 'DIV', 'DL', 'DT', 'EM', 'FIELDSET', 'FONT',
28 'FORM', 'FRAME', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
29 'HEAD', 'HR', 'HTML', 'I', 'IFRAME', 'IMG', 'INPUT', 'INS',
30 'ISINDEX', 'KBD', 'LABEL', 'LEGEND', 'LI', 'LINK', 'MAP', 'MENU',
31 'META', 'NOFRAMES', 'NOSCRIPT', 'OBJECT', 'OL', 'OPTGROUP',
32 'OPTION', 'P', 'PARAM', 'PRE', 'Q', 'S', 'SAMP', 'SCRIPT',
33 'SELECT', 'SMALL', 'SPAN', 'STRIKE', 'STRONG', 'STYLE', 'SUB',
34 'SUP', 'TABLE', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD',
35 'TITLE', 'TR', 'TT', 'U', 'UL', 'VAR'];
37 /**
38 * Below, we'll use makeTagFunc to create a function for each of the
39 * strings in 'tags'. This will allow us to use s-expression like syntax
40 * to create HTML.
41 */
42 function makeTagFunc(tagName)
43 {
44 return function (attrs /* rest... */)
45 {
46 var startChildren = 0;
47 var response = "";
49 // write the start tag and attributes
50 response += "<" + tagName;
51 // if attr is an object, write attributes
52 if (attrs && typeof attrs == 'object') {
53 startChildren = 1;
54 for (var [key,value] in attrs) {
55 var val = "" + value;
56 response += " " + key + '="' + val.replace('"','"') + '"';
57 }
58 }
59 response += ">";
61 // iterate through the rest of the args
62 for (var i = startChildren; i < arguments.length; i++) {
63 if (typeof arguments[i] == 'function') {
64 response += arguments[i]();
65 } else {
66 response += arguments[i];
67 }
68 }
70 // write the close tag
71 response += "</" + tagName + ">\n";
72 return response;
73 }
74 }
76 function makeTags() {
77 // map our global HTML generation functions
78 for each (var tag in tags) {
79 this[tag] = makeTagFunc(tag.toLowerCase());
80 }
81 }
83 var _quitting = false;
85 /** Quit when all activity has completed. */
86 function serverStopped()
87 {
88 _quitting = true;
89 }
91 // only run the "main" section if httpd.js was loaded ahead of us
92 if (this["nsHttpServer"]) {
93 //
94 // SCRIPT CODE
95 //
96 runServer();
98 // We can only have gotten here if the /server/shutdown path was requested.
99 if (_quitting)
100 {
101 dumpn("HTTP server stopped, all pending requests complete");
102 quit(0);
103 }
105 // Impossible as the stop callback should have been called, but to be safe...
106 dumpn("TEST-UNEXPECTED-FAIL | failure to correctly shut down HTTP server");
107 quit(1);
108 }
110 var serverBasePath;
111 var displayResults = true;
113 //
114 // SERVER SETUP
115 //
116 function runServer()
117 {
118 serverBasePath = __LOCATION__.parent;
119 server = createMochitestServer(serverBasePath);
121 //verify server address
122 //if a.b.c.d or 'localhost'
123 if (typeof(_SERVER_ADDR) != "undefined") {
124 if (_SERVER_ADDR == "localhost") {
125 gServerAddress = _SERVER_ADDR;
126 } else {
127 var quads = _SERVER_ADDR.split('.');
128 if (quads.length == 4) {
129 var invalid = false;
130 for (var i=0; i < 4; i++) {
131 if (quads[i] < 0 || quads[i] > 255)
132 invalid = true;
133 }
134 if (!invalid)
135 gServerAddress = _SERVER_ADDR;
136 else
137 throw "invalid _SERVER_ADDR, please specify a valid IP Address";
138 }
139 }
140 } else {
141 throw "please defined _SERVER_ADDR (as an ip address) before running server.js";
142 }
144 if (typeof(_SERVER_PORT) != "undefined") {
145 if (parseInt(_SERVER_PORT) > 0 && parseInt(_SERVER_PORT) < 65536)
146 SERVER_PORT = _SERVER_PORT;
147 } else {
148 throw "please define _SERVER_PORT (as a port number) before running server.js";
149 }
151 // If DISPLAY_RESULTS is not specified, it defaults to true
152 if (typeof(_DISPLAY_RESULTS) != "undefined") {
153 displayResults = _DISPLAY_RESULTS;
154 }
156 server._start(SERVER_PORT, gServerAddress);
158 // touch a file in the profile directory to indicate we're alive
159 var foStream = Cc["@mozilla.org/network/file-output-stream;1"]
160 .createInstance(Ci.nsIFileOutputStream);
161 var serverAlive = Cc["@mozilla.org/file/local;1"]
162 .createInstance(Ci.nsILocalFile);
164 if (typeof(_PROFILE_PATH) == "undefined") {
165 serverAlive.initWithFile(serverBasePath);
166 serverAlive.append("mochitesttestingprofile");
167 } else {
168 serverAlive.initWithPath(_PROFILE_PATH);
169 }
171 // If we're running outside of the test harness, there might
172 // not be a test profile directory present
173 if (serverAlive.exists()) {
174 serverAlive.append("server_alive.txt");
175 foStream.init(serverAlive,
176 0x02 | 0x08 | 0x20, 436, 0); // write, create, truncate
177 data = "It's alive!";
178 foStream.write(data, data.length);
179 foStream.close();
180 }
182 makeTags();
184 //
185 // The following is threading magic to spin an event loop -- this has to
186 // happen manually in xpcshell for the server to actually work.
187 //
188 var thread = Cc["@mozilla.org/thread-manager;1"]
189 .getService()
190 .currentThread;
191 while (!server.isStopped())
192 thread.processNextEvent(true);
194 // Server stopped by /server/shutdown handler -- go through pending events
195 // and return.
197 // get rid of any pending requests
198 while (thread.hasPendingEvents())
199 thread.processNextEvent(true);
200 }
202 /** Creates and returns an HTTP server configured to serve Mochitests. */
203 function createMochitestServer(serverBasePath)
204 {
205 var server = new nsHttpServer();
207 server.registerDirectory("/", serverBasePath);
208 server.registerPathHandler("/server/shutdown", serverShutdown);
209 server.registerPathHandler("/server/debug", serverDebug);
210 server.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
211 server.registerContentType("jar", "application/x-jar");
212 server.registerContentType("ogg", "application/ogg");
213 server.registerContentType("pdf", "application/pdf");
214 server.registerContentType("ogv", "video/ogg");
215 server.registerContentType("oga", "audio/ogg");
216 server.registerContentType("opus", "audio/ogg; codecs=opus");
217 server.registerContentType("dat", "text/plain; charset=utf-8");
218 server.registerContentType("frag", "text/plain"); // .frag == WebGL fragment shader
219 server.registerContentType("vert", "text/plain"); // .vert == WebGL vertex shader
220 server.setIndexHandler(defaultDirHandler);
222 var serverRoot =
223 {
224 getFile: function getFile(path)
225 {
226 var file = serverBasePath.clone().QueryInterface(Ci.nsILocalFile);
227 path.split("/").forEach(function(p) {
228 file.appendRelativePath(p);
229 });
230 return file;
231 },
232 QueryInterface: function(aIID) { return this; }
233 };
235 server.setObjectState("SERVER_ROOT", serverRoot);
237 processLocations(server);
239 return server;
240 }
242 /**
243 * Notifies the HTTP server about all the locations at which it might receive
244 * requests, so that it can properly respond to requests on any of the hosts it
245 * serves.
246 */
247 function processLocations(server)
248 {
249 var serverLocations = serverBasePath.clone();
250 serverLocations.append("server-locations.txt");
252 const PR_RDONLY = 0x01;
253 var fis = new FileInputStream(serverLocations, PR_RDONLY, 292 /* 0444 */,
254 Ci.nsIFileInputStream.CLOSE_ON_EOF);
256 var lis = new ConverterInputStream(fis, "UTF-8", 1024, 0x0);
257 lis.QueryInterface(Ci.nsIUnicharLineInputStream);
259 const LINE_REGEXP =
260 new RegExp("^([a-z][-a-z0-9+.]*)" +
261 "://" +
262 "(" +
263 "\\d+\\.\\d+\\.\\d+\\.\\d+" +
264 "|" +
265 "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" +
266 "[a-z](?:[-a-z0-9]*[a-z0-9])?" +
267 ")" +
268 ":" +
269 "(\\d+)" +
270 "(?:" +
271 "\\s+" +
272 "(\\S+(?:,\\S+)*)" +
273 ")?$");
275 var line = {};
276 var lineno = 0;
277 var seenPrimary = false;
278 do
279 {
280 var more = lis.readLine(line);
281 lineno++;
283 var lineValue = line.value;
284 if (lineValue.charAt(0) == "#" || lineValue == "")
285 continue;
287 var match = LINE_REGEXP.exec(lineValue);
288 if (!match)
289 throw "Syntax error in server-locations.txt, line " + lineno;
291 var [, scheme, host, port, options] = match;
292 if (options)
293 {
294 if (options.split(",").indexOf("primary") >= 0)
295 {
296 if (seenPrimary)
297 {
298 throw "Multiple primary locations in server-locations.txt, " +
299 "line " + lineno;
300 }
302 server.identity.setPrimary(scheme, host, port);
303 seenPrimary = true;
304 continue;
305 }
306 }
308 server.identity.add(scheme, host, port);
309 }
310 while (more);
311 }
314 // PATH HANDLERS
316 // /server/shutdown
317 function serverShutdown(metadata, response)
318 {
319 response.setStatusLine("1.1", 200, "OK");
320 response.setHeader("Content-type", "text/plain", false);
322 var body = "Server shut down.";
323 response.bodyOutputStream.write(body, body.length);
325 dumpn("Server shutting down now...");
326 server.stop(serverStopped);
327 }
329 // /server/debug?[012]
330 function serverDebug(metadata, response)
331 {
332 response.setStatusLine(metadata.httpVersion, 400, "Bad debugging level");
333 if (metadata.queryString.length !== 1)
334 return;
336 var mode;
337 if (metadata.queryString === "0") {
338 // do this now so it gets logged with the old mode
339 dumpn("Server debug logs disabled.");
340 DEBUG = false;
341 DEBUG_TIMESTAMP = false;
342 mode = "disabled";
343 } else if (metadata.queryString === "1") {
344 DEBUG = true;
345 DEBUG_TIMESTAMP = false;
346 mode = "enabled";
347 } else if (metadata.queryString === "2") {
348 DEBUG = true;
349 DEBUG_TIMESTAMP = true;
350 mode = "enabled, with timestamps";
351 } else {
352 return;
353 }
355 response.setStatusLine(metadata.httpVersion, 200, "OK");
356 response.setHeader("Content-type", "text/plain", false);
357 var body = "Server debug logs " + mode + ".";
358 response.bodyOutputStream.write(body, body.length);
359 dumpn(body);
360 }
362 //
363 // DIRECTORY LISTINGS
364 //
366 /**
367 * Creates a generator that iterates over the contents of
368 * an nsIFile directory.
369 */
370 function dirIter(dir)
371 {
372 var en = dir.directoryEntries;
373 while (en.hasMoreElements()) {
374 var file = en.getNext();
375 yield file.QueryInterface(Ci.nsILocalFile);
376 }
377 }
379 /**
380 * Builds an optionally nested object containing links to the
381 * files and directories within dir.
382 */
383 function list(requestPath, directory, recurse)
384 {
385 var count = 0;
386 var path = requestPath;
387 if (path.charAt(path.length - 1) != "/") {
388 path += "/";
389 }
391 var dir = directory.QueryInterface(Ci.nsIFile);
392 var links = {};
394 // The SimpleTest directory is hidden
395 var files = [file for (file in dirIter(dir))
396 if (file.exists() && file.path.indexOf("SimpleTest") == -1)];
398 // Sort files by name, so that tests can be run in a pre-defined order inside
399 // a given directory (see bug 384823)
400 function leafNameComparator(first, second) {
401 if (first.leafName < second.leafName)
402 return -1;
403 if (first.leafName > second.leafName)
404 return 1;
405 return 0;
406 }
407 files.sort(leafNameComparator);
409 count = files.length;
410 for each (var file in files) {
411 var key = path + file.leafName;
412 var childCount = 0;
413 if (file.isDirectory()) {
414 key += "/";
415 }
416 if (recurse && file.isDirectory()) {
417 [links[key], childCount] = list(key, file, recurse);
418 count += childCount;
419 } else {
420 if (file.leafName.charAt(0) != '.') {
421 links[key] = true;
422 }
423 }
424 }
426 return [links, count];
427 }
429 /**
430 * Heuristic function that determines whether a given path
431 * is a test case to be executed in the harness, or just
432 * a supporting file.
433 */
434 function isTest(filename, pattern)
435 {
436 if (pattern)
437 return pattern.test(filename);
439 // File name is a URL style path to a test file, make sure that we check for
440 // tests that start with the appropriate prefix.
441 var testPrefix = typeof(_TEST_PREFIX) == "string" ? _TEST_PREFIX : "test_";
442 var testPattern = new RegExp("^" + testPrefix);
444 pathPieces = filename.split('/');
446 return testPattern.test(pathPieces[pathPieces.length - 1]) &&
447 filename.indexOf(".js") == -1 &&
448 filename.indexOf(".css") == -1 &&
449 !/\^headers\^$/.test(filename);
450 }
452 /**
453 * Transform nested hashtables of paths to nested HTML lists.
454 */
455 function linksToListItems(links)
456 {
457 var response = "";
458 var children = "";
459 for (var [link, value] in links) {
460 var classVal = (!isTest(link) && !(value instanceof Object))
461 ? "non-test invisible"
462 : "test";
463 if (value instanceof Object) {
464 children = UL({class: "testdir"}, linksToListItems(value));
465 } else {
466 children = "";
467 }
469 var bug_title = link.match(/test_bug\S+/);
470 var bug_num = null;
471 if (bug_title != null) {
472 bug_num = bug_title[0].match(/\d+/);
473 }
475 if ((bug_title == null) || (bug_num == null)) {
476 response += LI({class: classVal}, A({href: link}, link), children);
477 } else {
478 var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id="+bug_num;
479 response += LI({class: classVal}, A({href: link}, link), " - ", A({href: bug_url}, "Bug "+bug_num), children);
480 }
482 }
483 return response;
484 }
486 /**
487 * Transform nested hashtables of paths to a flat table rows.
488 */
489 function linksToTableRows(links, recursionLevel)
490 {
491 var response = "";
492 for (var [link, value] in links) {
493 var classVal = (!isTest(link) && !(value instanceof Object))
494 ? "non-test invisible"
495 : "";
497 spacer = "padding-left: " + (10 * recursionLevel) + "px";
499 if (value instanceof Object) {
500 response += TR({class: "dir", id: "tr-" + link },
501 TD({colspan: "3"}, " "),
502 TD({style: spacer},
503 A({href: link}, link)));
504 response += linksToTableRows(value, recursionLevel + 1);
505 } else {
506 var bug_title = link.match(/test_bug\S+/);
507 var bug_num = null;
508 if (bug_title != null) {
509 bug_num = bug_title[0].match(/\d+/);
510 }
511 if ((bug_title == null) || (bug_num == null)) {
512 response += TR({class: classVal, id: "tr-" + link },
513 TD("0"),
514 TD("0"),
515 TD("0"),
516 TD({style: spacer},
517 A({href: link}, link)));
518 } else {
519 var bug_url = "https://bugzilla.mozilla.org/show_bug.cgi?id=" + bug_num;
520 response += TR({class: classVal, id: "tr-" + link },
521 TD("0"),
522 TD("0"),
523 TD("0"),
524 TD({style: spacer},
525 A({href: link}, link), " - ",
526 A({href: bug_url}, "Bug " + bug_num)));
527 }
528 }
529 }
530 return response;
531 }
533 function arrayOfTestFiles(linkArray, fileArray, testPattern) {
534 for (var [link, value] in Iterator(linkArray)) {
535 if (value instanceof Object) {
536 arrayOfTestFiles(value, fileArray, testPattern);
537 } else if (isTest(link, testPattern)) {
538 fileArray.push(link)
539 }
540 }
541 }
542 /**
543 * Produce a flat array of test file paths to be executed in the harness.
544 */
545 function jsonArrayOfTestFiles(links)
546 {
547 var testFiles = [];
548 arrayOfTestFiles(links, testFiles);
549 testFiles = ['"' + file + '"' for each(file in testFiles)];
550 return "[" + testFiles.join(",\n") + "]";
551 }
553 /**
554 * Produce a normal directory listing.
555 */
556 function regularListing(metadata, response)
557 {
558 var [links, count] = list(metadata.path,
559 metadata.getProperty("directory"),
560 false);
561 response.write(
562 HTML(
563 HEAD(
564 TITLE("mochitest index ", metadata.path)
565 ),
566 BODY(
567 BR(),
568 A({href: ".."}, "Up a level"),
569 UL(linksToListItems(links))
570 )
571 )
572 );
573 }
575 /**
576 * Produce a test harness page containing all the test cases
577 * below it, recursively.
578 */
579 function testListing(metadata, response)
580 {
581 var links = {};
582 var count = 0;
583 if (metadata.queryString.indexOf('manifestFile') == -1) {
584 [links, count] = list(metadata.path,
585 metadata.getProperty("directory"),
586 true);
587 }
588 var table_class = metadata.queryString.indexOf("hideResultsTable=1") > -1 ? "invisible": "";
590 let testname = (metadata.queryString.indexOf("testname=") > -1)
591 ? metadata.queryString.match(/testname=([^&]+)/)[1]
592 : "";
594 dumpn("count: " + count);
595 var tests = testname
596 ? "['/" + testname + "']"
597 : jsonArrayOfTestFiles(links);
598 response.write(
599 HTML(
600 HEAD(
601 TITLE("MochiTest | ", metadata.path),
602 LINK({rel: "stylesheet",
603 type: "text/css", href: "/static/harness.css"}
604 ),
605 SCRIPT({type: "text/javascript",
606 src: "/tests/SimpleTest/LogController.js"}),
607 SCRIPT({type: "text/javascript",
608 src: "/tests/SimpleTest/MemoryStats.js"}),
609 SCRIPT({type: "text/javascript",
610 src: "/tests/SimpleTest/TestRunner.js"}),
611 SCRIPT({type: "text/javascript",
612 src: "/tests/SimpleTest/MozillaLogger.js"}),
613 SCRIPT({type: "text/javascript",
614 src: "/chunkifyTests.js"}),
615 SCRIPT({type: "text/javascript",
616 src: "/manifestLibrary.js"}),
617 SCRIPT({type: "text/javascript",
618 src: "/tests/SimpleTest/setup.js"}),
619 SCRIPT({type: "text/javascript"},
620 "window.onload = hookup; gTestList=" + tests + ";"
621 )
622 ),
623 BODY(
624 DIV({class: "container"},
625 H2("--> ", A({href: "#", id: "runtests"}, "Run Tests"), " <--"),
626 P({style: "float: right;"},
627 SMALL(
628 "Based on the ",
629 A({href:"http://www.mochikit.com/"}, "MochiKit"),
630 " unit tests."
631 )
632 ),
633 DIV({class: "status"},
634 H1({id: "indicator"}, "Status"),
635 H2({id: "pass"}, "Passed: ", SPAN({id: "pass-count"},"0")),
636 H2({id: "fail"}, "Failed: ", SPAN({id: "fail-count"},"0")),
637 H2({id: "fail"}, "Todo: ", SPAN({id: "todo-count"},"0"))
638 ),
639 DIV({class: "clear"}),
640 DIV({id: "current-test"},
641 B("Currently Executing: ",
642 SPAN({id: "current-test-path"}, "_")
643 )
644 ),
645 DIV({class: "clear"}),
646 DIV({class: "frameholder"},
647 IFRAME({scrolling: "no", id: "testframe", width: "500", height: "300"})
648 ),
649 DIV({class: "clear"}),
650 DIV({class: "toggle"},
651 A({href: "#", id: "toggleNonTests"}, "Show Non-Tests"),
652 BR()
653 ),
655 (
656 displayResults ?
657 TABLE({cellpadding: 0, cellspacing: 0, class: table_class, id: "test-table"},
658 TR(TD("Passed"), TD("Failed"), TD("Todo"), TD("Test Files")),
659 linksToTableRows(links, 0)
660 ) : ""
661 ),
663 BR(),
664 TABLE({cellpadding: 0, cellspacing: 0, border: 1, bordercolor: "red", id: "fail-table"}
665 ),
667 DIV({class: "clear"})
668 )
669 )
670 )
671 );
672 }
674 /**
675 * Respond to requests that match a file system directory.
676 * Under the tests/ directory, return a test harness page.
677 */
678 function defaultDirHandler(metadata, response)
679 {
680 response.setStatusLine("1.1", 200, "OK");
681 response.setHeader("Content-type", "text/html;charset=utf-8", false);
682 try {
683 if (metadata.path.indexOf("/tests") != 0) {
684 regularListing(metadata, response);
685 } else {
686 testListing(metadata, response);
687 }
688 } catch (ex) {
689 response.write(ex);
690 }
691 }