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 /* ***** BEGIN LICENSE BLOCK *****
4 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
6 * The contents of this file are subject to the Mozilla Public License Version
7 * 1.1 (the "License"); you may not use this file except in compliance with
8 * the License. You may obtain a copy of the License at
9 * http://www.mozilla.org/MPL/
11 * Software distributed under the License is distributed on an "AS IS" basis,
12 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
13 * for the specific language governing rights and limitations under the
16 * The Original Code is MozJSHTTP code.
18 * The Initial Developer of the Original Code is
19 * Jeff Walden <jwalden+code@mit.edu>.
20 * Portions created by the Initial Developer are Copyright (C) 2006
21 * the Initial Developer. All Rights Reserved.
24 * Robert Sayre <sayrer@gmail.com>
26 * Alternatively, the contents of this file may be used under the terms of
27 * either the GNU General Public License Version 2 or later (the "GPL"), or
28 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
29 * in which case the provisions of the GPL or the LGPL are applicable instead
30 * of those above. If you wish to allow use of your version of this file only
31 * under the terms of either the GPL or the LGPL, and not to allow others to
32 * use your version of this file under the terms of the MPL, indicate your
33 * decision by deleting the provisions above and replace them with the notice
34 * and other provisions required by the GPL or the LGPL. If you do not delete
35 * the provisions above, a recipient may use your version of this file under
36 * the terms of any one of the MPL, the GPL or the LGPL.
38 * ***** END LICENSE BLOCK ***** */
40 // Note that the server script itself already defines Cc, Ci, and Cr for us,
41 // and because they're constants it's not safe to redefine them. Scope leakage
44 const SERVER_PORT
= 8888;
45 var server
; // for use in the shutdown handler, if necessary
50 var tags
= ['A', 'ABBR', 'ACRONYM', 'ADDRESS', 'APPLET', 'AREA', 'B', 'BASE',
51 'BASEFONT', 'BDO', 'BIG', 'BLOCKQUOTE', 'BODY', 'BR', 'BUTTON',
52 'CAPTION', 'CENTER', 'CITE', 'CODE', 'COL', 'COLGROUP', 'DD',
53 'DEL', 'DFN', 'DIR', 'DIV', 'DL', 'DT', 'EM', 'FIELDSET', 'FONT',
54 'FORM', 'FRAME', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
55 'HEAD', 'HR', 'HTML', 'I', 'IFRAME', 'IMG', 'INPUT', 'INS',
56 'ISINDEX', 'KBD', 'LABEL', 'LEGEND', 'LI', 'LINK', 'MAP', 'MENU',
57 'META', 'NOFRAMES', 'NOSCRIPT', 'OBJECT', 'OL', 'OPTGROUP',
58 'OPTION', 'P', 'PARAM', 'PRE', 'Q', 'S', 'SAMP', 'SCRIPT',
59 'SELECT', 'SMALL', 'SPAN', 'STRIKE', 'STRONG', 'STYLE', 'SUB',
60 'SUP', 'TABLE', 'TBODY', 'TD', 'TEXTAREA', 'TFOOT', 'TH', 'THEAD',
61 'TITLE', 'TR', 'TT', 'U', 'UL', 'VAR'];
64 * Below, we'll use makeTagFunc to create a function for each of the
65 * strings in 'tags'. This will allow us to use s-expression like syntax
68 function makeTagFunc(tagName
)
70 return function (attrs
/* rest... */)
72 var startChildren
= 0;
75 // write the start tag and attributes
76 response
+= "<" + tagName
;
77 // if attr is an object, write attributes
78 if (attrs
&& typeof attrs
== 'object') {
80 for (var [key
,value
] in attrs
) {
82 response
+= " " + key
+ '="' + val
.replace('"','"') + '"';
87 // iterate through the rest of the args
88 for (var i
= startChildren
; i
< arguments
.length
; i
++) {
89 if (typeof arguments
[i
] == 'function') {
90 response
+= arguments
[i
]();
92 response
+= arguments
[i
];
96 // write the close tag
97 response
+= "</" + tagName
+ ">\n";
102 function makeTags() {
103 // map our global HTML generation functions
104 for each (var tag
in tags
) {
105 this[tag
] = makeTagFunc(tag
.toLowerCase());
109 // only run the "main" section if httpd.js was loaded ahead of us
110 if (this["nsHttpServer"]) {
116 // We can only have gotten here if the /server/shutdown path was requested,
117 // and we can shut down the xpcshell now that all testing requests have been
129 serverBasePath
= Cc
["@mozilla.org/file/local;1"]
130 .createInstance(Ci
.nsILocalFile
);
131 var procDir
= Cc
["@mozilla.org/file/directory_service;1"]
132 .getService(Ci
.nsIProperties
).get("CurProcD", Ci
.nsIFile
);
133 serverBasePath
.initWithPath(procDir
.parent
.parent
.path
);
134 serverBasePath
.append("_tests");
135 serverBasePath
.append("testing");
136 serverBasePath
.append("mochitest");
138 server
= createMochitestServer(serverBasePath
);
139 server
.start(SERVER_PORT
);
141 // touch a file in the profile directory to indicate we're alive
142 var foStream
= Cc
["@mozilla.org/network/file-output-stream;1"]
143 .createInstance(Ci
.nsIFileOutputStream
);
144 var serverAlive
= Cc
["@mozilla.org/file/local;1"]
145 .createInstance(Ci
.nsILocalFile
);
146 serverAlive
.initWithFile(serverBasePath
);
147 serverAlive
.append("mochitesttestingprofile");
149 // If we're running outside of the test harness, there might
150 // not be a test profile directory present
151 if (serverAlive
.exists()) {
152 serverAlive
.append("server_alive.txt");
153 foStream
.init(serverAlive
,
154 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate
155 data
= "It's alive!";
156 foStream
.write(data
, data
.length
);
163 // The following is threading magic to spin an event loop -- this has to
164 // happen manually in xpcshell for the server to actually work.
166 var thread
= Cc
["@mozilla.org/thread-manager;1"]
169 while (!server
.isStopped())
170 thread
.processNextEvent(true);
172 // Server stopped by /server/shutdown handler -- go through pending events
175 // get rid of any pending requests
176 while (thread
.hasPendingEvents())
177 thread
.processNextEvent(true);
180 /** Creates and returns an HTTP server configured to serve Mochitests. */
181 function createMochitestServer(serverBasePath
)
183 var server
= new nsHttpServer();
185 server
.registerDirectory("/", serverBasePath
);
186 server
.registerPathHandler("/server/shutdown", serverShutdown
);
187 server
.registerContentType("sjs", "sjs"); // .sjs == CGI-like functionality
188 server
.setIndexHandler(defaultDirHandler
);
190 processLocations(server
);
196 * Notifies the HTTP server about all the locations at which it might receive
197 * requests, so that it can properly respond to requests on any of the hosts it
200 function processLocations(server
)
202 var serverLocations
= serverBasePath
.clone();
203 serverLocations
.append("server-locations.txt");
205 const PR_RDONLY
= 0x01;
206 var fis
= new FileInputStream(serverLocations
, PR_RDONLY
, 0444,
207 Ci
.nsIFileInputStream
.CLOSE_ON_EOF
);
209 var lis
= new ConverterInputStream(fis
, "UTF-8", 1024, 0x0);
210 lis
.QueryInterface(Ci
.nsIUnicharLineInputStream
);
213 new RegExp("^([a-z][-a-z0-9+.]*)" +
216 "\\d+\\.\\d+\\.\\d+\\.\\d+" +
218 "(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\\.)*" +
219 "[a-z](?:[-a-z0-9]*[a-z0-9])?" +
230 var seenPrimary
= false;
233 var more
= lis
.readLine(line
);
236 var lineValue
= line
.value
;
237 if (lineValue
.charAt(0) == "#" || lineValue
== "")
240 var match
= LINE_REGEXP
.exec(lineValue
);
242 throw "Syntax error in server-locations.txt, line " + lineno
;
244 var [, scheme
, host
, port
, options
] = match
;
247 if (options
.split(",").indexOf("primary") >= 0)
251 throw "Multiple primary locations in server-locations.txt, " +
255 server
.identity
.setPrimary(scheme
, host
, port
);
261 server
.identity
.add(scheme
, host
, port
);
270 function serverShutdown(metadata
, response
)
272 response
.setStatusLine("1.1", 200, "OK");
273 response
.setHeader("Content-type", "text/plain", false);
275 var body
= "Server shut down.";
276 response
.bodyOutputStream
.write(body
, body
.length
);
278 // Note: this doesn't disrupt the current request.
283 // DIRECTORY LISTINGS
287 * Creates a generator that iterates over the contents of
288 * an nsIFile directory.
290 function dirIter(dir
)
292 var en
= dir
.directoryEntries
;
293 while (en
.hasMoreElements()) {
294 var file
= en
.getNext();
295 yield file
.QueryInterface(Ci
.nsILocalFile
);
300 * Builds an optionally nested object containing links to the
301 * files and directories within dir.
303 function list(requestPath
, directory
, recurse
)
306 var path
= requestPath
;
307 if (path
.charAt(path
.length
- 1) != "/") {
311 var dir
= directory
.QueryInterface(Ci
.nsIFile
);
314 // The SimpleTest directory is hidden
315 var files
= [file
for (file
in dirIter(dir
))
316 if (file
.exists() && file
.path
.indexOf("SimpleTest") == -1)];
318 // Sort files by name, so that tests can be run in a pre-defined order inside
319 // a given directory (see bug 384823)
320 function leafNameComparator(first
, second
) {
321 if (first
.leafName
< second
.leafName
)
323 if (first
.leafName
> second
.leafName
)
327 files
.sort(leafNameComparator
);
329 count
= files
.length
;
330 for each (var file
in files
) {
331 var key
= path
+ file
.leafName
;
333 if (file
.isDirectory()) {
336 if (recurse
&& file
.isDirectory()) {
337 [links
[key
], childCount
] = list(key
, file
, recurse
);
340 if (file
.leafName
.charAt(0) != '.') {
346 return [links
, count
];
350 * Heuristic function that determines whether a given path
351 * is a test case to be executed in the harness, or just
354 function isTest(filename
, pattern
)
357 return pattern
.test(filename
);
359 return filename
.indexOf("test_") > -1 &&
360 filename
.indexOf(".js") == -1 &&
361 filename
.indexOf(".css") == -1 &&
362 !/\^headers\^$/.test(filename
);
366 * Transform nested hashtables of paths to nested HTML lists.
368 function linksToListItems(links
)
372 for (var [link
, value
] in links
) {
373 var classVal
= (!isTest(link
) && !(value
instanceof Object
))
374 ? "non-test invisible"
376 if (value
instanceof Object
) {
377 children
= UL({class: "testdir"}, linksToListItems(value
));
382 var bug_title
= link
.match(/test_bug\S+/);
384 if (bug_title
!= null) {
385 bug_num
= bug_title
[0].match(/\d+/);
388 if ((bug_title
== null) || (bug_num
== null)) {
389 response
+= LI({class: classVal
}, A({href
: link
}, link
), children
);
391 var bug_url
= "https://bugzilla.mozilla.org/show_bug.cgi?id="+bug_num
;
392 response
+= LI({class: classVal
}, A({href
: link
}, link
), " - ", A({href
: bug_url
}, "Bug "+bug_num
), children
);
400 * Transform nested hashtables of paths to a flat table rows.
402 function linksToTableRows(links
)
405 for (var [link
, value
] in links
) {
406 var classVal
= (!isTest(link
) && !(value
instanceof Object
))
407 ? "non-test invisible"
409 if (value
instanceof Object
) {
410 response
+= TR({class: "dir", id
: "tr-" + link
},
411 TD({colspan
: "3"}," "));
412 response
+= linksToTableRows(value
);
414 response
+= TR({class: classVal
, id
: "tr-" + link
},
415 TD("0"), TD("0"), TD("0"));
421 function arrayOfTestFiles(linkArray
, fileArray
, testPattern
) {
422 for (var [link
, value
] in linkArray
) {
423 if (value
instanceof Object
) {
424 arrayOfTestFiles(value
, fileArray
, testPattern
);
425 } else if (isTest(link
, testPattern
)) {
431 * Produce a flat array of test file paths to be executed in the harness.
433 function jsonArrayOfTestFiles(links
)
436 arrayOfTestFiles(links
, testFiles
);
437 testFiles
= ['"' + file
+ '"' for each(file
in testFiles
)];
438 return "[" + testFiles
.join(",\n") + "]";
442 * Produce a normal directory listing.
444 function regularListing(metadata
, response
)
446 var [links
, count
] = list(metadata
.path
,
447 metadata
.getProperty("directory"),
452 TITLE("mochitest index ", metadata
.path
)
456 A({href
: ".."}, "Up a level"),
457 UL(linksToListItems(links
))
464 * Produce a test harness page containing all the test cases
465 * below it, recursively.
467 function testListing(metadata
, response
)
469 var [links
, count
] = list(metadata
.path
,
470 metadata
.getProperty("directory"),
472 dumpn("count: " + count
);
473 var tests
= jsonArrayOfTestFiles(links
);
477 TITLE("MochiTest | ", metadata
.path
),
478 LINK({rel
: "stylesheet",
479 type
: "text/css", href
: "/static/harness.css"}
481 SCRIPT({type
: "text/javascript", src
: "/MochiKit/packed.js"}),
482 SCRIPT({type
: "text/javascript",
483 src
: "/tests/SimpleTest/TestRunner.js"}),
484 SCRIPT({type
: "text/javascript",
485 src
: "/tests/SimpleTest/MozillaFileLogger.js"}),
486 SCRIPT({type
: "text/javascript",
487 src
: "/tests/SimpleTest/quit.js"}),
488 SCRIPT({type
: "text/javascript",
489 src
: "/tests/SimpleTest/setup.js"}),
490 SCRIPT({type
: "text/javascript"},
491 "connect(window, 'onload', hookup); gTestList=" + tests
+ ";"
495 DIV({class: "container"},
496 H2("--> ", A({href
: "#", id
: "runtests"}, "Run Tests"), " <--"),
497 P({style
: "float: right;"},
500 A({href
:"http://www.mochikit.com/"}, "MochiKit"),
504 DIV({class: "status"},
505 H1({id
: "indicator"}, "Status"),
506 H2({id
: "pass"}, "Passed: ", SPAN({id
: "pass-count"},"0")),
507 H2({id
: "fail"}, "Failed: ", SPAN({id
: "fail-count"},"0")),
508 H2({id
: "fail"}, "Todo: ", SPAN({id
: "todo-count"},"0"))
510 DIV({class: "clear"}),
511 DIV({id
: "current-test"},
512 B("Currently Executing: ",
513 SPAN({id
: "current-test-path"}, "_")
516 DIV({class: "clear"}),
517 DIV({class: "frameholder"},
518 IFRAME({scrolling
: "no", id
: "testframe", width
: "500", height
: "300"})
520 DIV({class: "clear"}),
521 DIV({class: "toggle"},
522 A({href
: "#", id
: "toggleNonTests"}, "Show Non-Tests"),
526 TABLE({cellpadding
: 0, cellspacing
: 0, id
: "test-table"},
527 TR(TD("Passed"), TD("Failed"), TD("Todo"),
528 TD({rowspan
: count
+1},
531 linksToListItems(links
)
535 linksToTableRows(links
)
537 DIV({class: "clear"})
545 * Respond to requests that match a file system directory.
546 * Under the tests/ directory, return a test harness page.
548 function defaultDirHandler(metadata
, response
)
550 response
.setStatusLine("1.1", 200, "OK");
551 response
.setHeader("Content-type", "text/html", false);
553 if (metadata
.path
.indexOf("/tests") != 0) {
554 regularListing(metadata
, response
);
556 testListing(metadata
, response
);