testing/mochitest/server.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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('"','&quot;') + '"';
    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"}, "&#160;"),
   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 }

mercurial