Home | History | Annotate | Download | only in browserdata
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 
      6 function $(id) {
      7   return document.getElementById(id);
      8 }
      9 
     10 
     11 function createNaClEmbed(args) {
     12   var fallback = function(value, default_value) {
     13     return value !== undefined ? value : default_value;
     14   };
     15   var embed = document.createElement('embed');
     16   embed.id = args.id;
     17   embed.src = args.src;
     18   embed.type = fallback(args.type, 'application/x-nacl');
     19   // JavaScript inconsistency: this is equivalent to class=... in HTML.
     20   embed.className = fallback(args.className, 'naclModule');
     21   embed.width = fallback(args.width, 0);
     22   embed.height = fallback(args.height, 0);
     23   return embed;
     24 }
     25 
     26 
     27 function decodeURIArgs(encoded) {
     28   var args = {};
     29   if (encoded.length > 0) {
     30     var pairs = encoded.replace(/\+/g, ' ').split('&');
     31     for (var p = 0; p < pairs.length; p++) {
     32       var pair = pairs[p].split('=');
     33       if (pair.length != 2) {
     34         throw "Malformed argument key/value pair: '" + pairs[p] + "'";
     35       }
     36       args[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
     37     }
     38   }
     39   return args;
     40 }
     41 
     42 
     43 function addDefaultsToArgs(defaults, args) {
     44   for (var key in defaults) {
     45     if (!(key in args)) {
     46       args[key] = defaults[key];
     47     }
     48   }
     49 }
     50 
     51 
     52 // Return a dictionary of arguments for the test.  These arguments are passed
     53 // in the query string of the main page's URL.  Any time this function is used,
     54 // default values should be provided for every argument.  In some cases a test
     55 // may be run without an expected query string (manual testing, for example.)
     56 // Careful: all the keys and values in the dictionary are strings.  You will
     57 // need to manually parse any non-string values you wish to use.
     58 function getTestArguments(defaults) {
     59   var encoded = window.location.search.substring(1);
     60   var args = decodeURIArgs(encoded);
     61   if (defaults !== undefined) {
     62     addDefaultsToArgs(defaults, args);
     63   }
     64   return args;
     65 }
     66 
     67 
     68 function exceptionToLogText(e) {
     69   if (typeof e == 'object' && 'message' in e && 'stack' in e) {
     70     return e.message + '\n' + e.stack.toString();
     71   } else if (typeof(e) == 'string') {
     72     return e;
     73   } else {
     74     return toString(e)
     75   }
     76 }
     77 
     78 
     79 // Logs test results to the server using URL-encoded RPC.
     80 // Also logs the same test results locally into the DOM.
     81 function RPCWrapper() {
     82   // Work around how JS binds 'this'
     83   var this_ = this;
     84   // It is assumed RPC will work unless proven otherwise.
     85   this.rpc_available = true;
     86   // Set to true if any test fails.
     87   this.ever_failed = false;
     88   // Async calls can make it faster, but it can also change order of events.
     89   this.async = false;
     90 
     91   // Called if URL-encoded RPC gets a 404, can't find the server, etc.
     92   function handleRPCFailure(name, message) {
     93     // This isn't treated as a testing error - the test can be run without a
     94     // web server that understands RPC.
     95     this_.logLocal('RPC failure for ' + name + ': ' + message + ' - If you ' +
     96                    'are running this test manually, this is not a problem.',
     97                    'gray');
     98     this_.disableRPC();
     99   }
    100 
    101   function handleRPCResponse(name, req) {
    102     if (req.status == 200) {
    103       if (req.responseText == 'Die, please') {
    104         // TODO(eugenis): this does not end the browser process on Mac.
    105         window.close();
    106       } else if (req.responseText != 'OK') {
    107         this_.logLocal('Unexpected RPC response to ' + name + ': \'' +
    108                        req.responseText + '\' - If you are running this test ' +
    109                        'manually, this is not a problem.', 'gray');
    110         this_.disableRPC();
    111       }
    112     } else {
    113       handleRPCFailure(name, req.status.toString());
    114     }
    115   }
    116 
    117   // Performs a URL-encoded RPC call, given a function name and a dictionary
    118   // (actually just an object - it's a JS idiom) of parameters.
    119   function rpcCall(name, params) {
    120     if (window.domAutomationController !== undefined) {
    121       // Running as a Chrome browser_test.
    122       var msg = {type: name};
    123       for (var pname in params) {
    124         msg[pname] = params[pname];
    125       }
    126       domAutomationController.setAutomationId(0);
    127       domAutomationController.send(JSON.stringify(msg));
    128     } else if (this_.rpc_available) {
    129       // Construct the URL for the RPC request.
    130       var args = [];
    131       for (var pname in params) {
    132         pvalue = params[pname];
    133         args.push(encodeURIComponent(pname) + '=' + encodeURIComponent(pvalue));
    134       }
    135       var url = '/TESTER/' + name + '?' + args.join('&');
    136       var req = new XMLHttpRequest();
    137       // Async result handler
    138       if (this_.async) {
    139         req.onreadystatechange = function() {
    140           if (req.readyState == XMLHttpRequest.DONE) {
    141             handleRPCResponse(name, req);
    142           }
    143         }
    144       }
    145       try {
    146         req.open('GET', url, this_.async);
    147         req.send();
    148         if (!this_.async) {
    149           handleRPCResponse(name, req);
    150         }
    151       } catch (err) {
    152         handleRPCFailure(name, err.toString());
    153       }
    154     }
    155   }
    156 
    157   // Pretty prints an error into the DOM.
    158   this.logLocalError = function(message) {
    159     this.logLocal(message, 'red');
    160     this.visualError();
    161   }
    162 
    163   // If RPC isn't working, disable it to stop error message spam.
    164   this.disableRPC = function() {
    165     if (this.rpc_available) {
    166       this.rpc_available = false;
    167       this.logLocal('Disabling RPC', 'gray');
    168     }
    169   }
    170 
    171   this.startup = function() {
    172     // TODO(ncbray) move into test runner
    173     this.num_passed = 0;
    174     this.num_failed = 0;
    175     this.num_errors = 0;
    176     this._log('[STARTUP]');
    177   }
    178 
    179   this.shutdown = function() {
    180     if (this.num_passed == 0 && this.num_failed == 0 && this.num_errors == 0) {
    181       this.client_error('No tests were run. This may be a bug.');
    182     }
    183     var full_message = '[SHUTDOWN] ';
    184     full_message += this.num_passed + ' passed';
    185     full_message += ', ' + this.num_failed + ' failed';
    186     full_message += ', ' + this.num_errors + ' errors';
    187     this.logLocal(full_message);
    188     rpcCall('Shutdown', {message: full_message, passed: !this.ever_failed});
    189 
    190     if (this.ever_failed) {
    191       this.localOutput.style.border = '2px solid #FF0000';
    192     } else {
    193       this.localOutput.style.border = '2px solid #00FF00';
    194     }
    195   }
    196 
    197   this.ping = function() {
    198     rpcCall('Ping', {});
    199   }
    200 
    201   this.heartbeat = function() {
    202     rpcCall('JavaScriptIsAlive', {});
    203   }
    204 
    205   this.client_error = function(message) {
    206     this.num_errors += 1;
    207     this.visualError();
    208     var full_message = '\n[CLIENT_ERROR] ' + exceptionToLogText(message)
    209     // The client error could have been generated by logging - be careful.
    210     try {
    211       this._log(full_message, 'red');
    212     } catch (err) {
    213       // There's not much that can be done, at this point.
    214     }
    215   }
    216 
    217   this.begin = function(test_name) {
    218     var full_message = '[' + test_name + ' BEGIN]'
    219     this._log(full_message, 'blue');
    220   }
    221 
    222   this._log = function(message, color, from_completed_test) {
    223     if (typeof(message) != 'string') {
    224       message = toString(message);
    225     }
    226 
    227     // For event-driven tests, output may come after the test has finished.
    228     // Display this in a special way to assist debugging.
    229     if (from_completed_test) {
    230       color = 'orange';
    231       message = 'completed test: ' + message;
    232     }
    233 
    234     this.logLocal(message, color);
    235     rpcCall('TestLog', {message: message});
    236   }
    237 
    238   this.log = function(test_name, message, from_completed_test) {
    239     if (message == undefined) {
    240       // This is a log message that is not assosiated with a test.
    241       // What we though was the test name is actually the message.
    242       this._log(test_name);
    243     } else {
    244       if (typeof(message) != 'string') {
    245         message = toString(message);
    246       }
    247       var full_message = '[' + test_name + ' LOG] ' + message;
    248       this._log(full_message, 'black', from_completed_test);
    249     }
    250   }
    251 
    252   this.fail = function(test_name, message, from_completed_test) {
    253     this.num_failed += 1;
    254     this.visualError();
    255     var full_message = '[' + test_name + ' FAIL] ' + message
    256     this._log(full_message, 'red', from_completed_test);
    257   }
    258 
    259   this.exception = function(test_name, err, from_completed_test) {
    260     this.num_errors += 1;
    261     this.visualError();
    262     var message = exceptionToLogText(err);
    263     var full_message = '[' + test_name + ' EXCEPTION] ' + message;
    264     this._log(full_message, 'purple', from_completed_test);
    265   }
    266 
    267   this.pass = function(test_name, from_completed_test) {
    268     this.num_passed += 1;
    269     var full_message = '[' + test_name + ' PASS]';
    270     this._log(full_message, 'green', from_completed_test);
    271   }
    272 
    273   this.blankLine = function() {
    274     this._log('');
    275   }
    276 
    277   // Allows users to log time data that will be parsed and re-logged
    278   // for chrome perf-bot graphs / performance regression testing.
    279   // See: native_client/tools/process_perf_output.py
    280   this.logTimeData = function(event, timeMS) {
    281     this.log('NaClPerf [' + event + '] ' + timeMS + ' millisecs');
    282   }
    283 
    284   this.visualError = function() {
    285     // Changing the color is defered until testing is done
    286     this.ever_failed = true;
    287   }
    288 
    289   this.logLineLocal = function(text, color) {
    290     text = text.replace(/\s+$/, '');
    291     if (text == '') {
    292       this.localOutput.appendChild(document.createElement('br'));
    293     } else {
    294       var mNode = document.createTextNode(text);
    295       var div = document.createElement('div');
    296       // Preserve whitespace formatting.
    297       div.style['white-space'] = 'pre';
    298       if (color != undefined) {
    299         div.style.color = color;
    300       }
    301       div.appendChild(mNode);
    302       this.localOutput.appendChild(div);
    303     }
    304   }
    305 
    306   this.logLocal = function(message, color) {
    307     var lines = message.split('\n');
    308     for (var i = 0; i < lines.length; i++) {
    309       this.logLineLocal(lines[i], color);
    310     }
    311   }
    312 
    313   // Create a place in the page to output test results
    314   this.localOutput = document.createElement('div');
    315   this.localOutput.id = 'testresults';
    316   this.localOutput.style.border = '2px solid #0000FF';
    317   this.localOutput.style.padding = '10px';
    318   document.body.appendChild(this.localOutput);
    319 }
    320 
    321 
    322 //
    323 // BEGIN functions for testing
    324 //
    325 
    326 
    327 function fail(message, info, test_status) {
    328   var parts = [];
    329   if (message != undefined) {
    330     parts.push(message);
    331   }
    332   if (info != undefined) {
    333     parts.push('(' + info + ')');
    334   }
    335   var full_message = parts.join(' ');
    336 
    337   if (test_status !== undefined) {
    338     // New-style test
    339     test_status.fail(full_message);
    340   } else {
    341     // Old-style test
    342     throw {type: 'test_fail', message: full_message};
    343   }
    344 }
    345 
    346 
    347 function assert(condition, message, test_status) {
    348   if (!condition) {
    349     fail(message, toString(condition), test_status);
    350   }
    351 }
    352 
    353 
    354 // This is accepted best practice for checking if an object is an array.
    355 function isArray(obj) {
    356   return Object.prototype.toString.call(obj) === '[object Array]';
    357 }
    358 
    359 
    360 function toString(obj) {
    361   if (typeof(obj) == 'string') {
    362     return '\'' + obj + '\'';
    363   }
    364   try {
    365     return obj.toString();
    366   } catch (err) {
    367     try {
    368       // Arrays should do this automatically, but there is a known bug where
    369       // NaCl gets array types wrong.  .toString will fail on these objects.
    370       return obj.join(',');
    371     } catch (err) {
    372       if (obj == undefined) {
    373         return 'undefined';
    374       } else {
    375         // There is no way to create a textual representation of this object.
    376         return '[UNPRINTABLE]';
    377       }
    378     }
    379   }
    380 }
    381 
    382 
    383 // Old-style, but new-style tests use it indirectly.
    384 // (The use of the "test" parameter indicates a new-style test.  This is a
    385 // temporary hack to avoid code duplication.)
    386 function assertEqual(a, b, message, test_status) {
    387   if (isArray(a) && isArray(b)) {
    388     assertArraysEqual(a, b, message, test_status);
    389   } else if (a !== b) {
    390     fail(message, toString(a) + ' != ' + toString(b), test_status);
    391   }
    392 }
    393 
    394 
    395 // Old-style, but new-style tests use it indirectly.
    396 // (The use of the "test" parameter indicates a new-style test.  This is a
    397 // temporary hack to avoid code duplication.)
    398 function assertArraysEqual(a, b, message, test_status) {
    399   var dofail = function() {
    400     fail(message, toString(a) + ' != ' + toString(b), test_status);
    401   }
    402   if (a.length != b.length) {
    403     dofail();
    404   }
    405   for (var i = 0; i < a.length; i++) {
    406     if (a[i] !== b[i]) {
    407       dofail();
    408     }
    409   }
    410 }
    411 
    412 
    413 // Ideally there'd be some way to identify what exception was thrown, but JS
    414 // exceptions are fairly ad-hoc.
    415 // TODO(ncbray) allow manual validation of exception types?
    416 function assertRaises(func, message, test_status) {
    417   try {
    418     func();
    419   } catch (err) {
    420     return;
    421   }
    422   fail(message, 'did not raise', test_status);
    423 }
    424 
    425 
    426 //
    427 // END functions for testing
    428 //
    429 
    430 
    431 function haltAsyncTest() {
    432   throw {type: 'test_halt'};
    433 }
    434 
    435 
    436 function begins_with(s, prefix) {
    437   if (s.length >= prefix.length) {
    438     return s.substr(0, prefix.length) == prefix;
    439   } else {
    440     return false;
    441   }
    442 }
    443 
    444 
    445 function ends_with(s, suffix) {
    446   if (s.length >= suffix.length) {
    447     return s.substr(s.length - suffix.length, suffix.length) == suffix;
    448   } else {
    449     return false;
    450   }
    451 }
    452 
    453 
    454 function embed_name(embed) {
    455   if (embed.name != undefined) {
    456     if (embed.id != undefined) {
    457       return embed.name + ' / ' + embed.id;
    458     } else {
    459       return embed.name;
    460     }
    461   } else if (embed.id != undefined) {
    462     return embed.id;
    463   } else {
    464     return '[no name]';
    465   }
    466 }
    467 
    468 
    469 // Write data to the filesystem. This will only work if the browser_tester was
    470 // initialized with --output_dir.
    471 function outputFile(name, data, onload, onerror) {
    472   var xhr = new XMLHttpRequest();
    473   xhr.onload = onload;
    474   xhr.onerror = onerror;
    475   xhr.open('POST', name, true);
    476   xhr.send(data);
    477 }
    478 
    479 
    480 // Webkit Bug Workaround
    481 // THIS SHOULD BE REMOVED WHEN Webkit IS FIXED
    482 // http://code.google.com/p/nativeclient/issues/detail?id=2428
    483 // http://code.google.com/p/chromium/issues/detail?id=103588
    484 
    485 function ForcePluginLoadOnTimeout(elem, tester, timeout) {
    486   tester.log('Registering ForcePluginLoadOnTimeout ' +
    487              '(Bugs: NaCl 2428, Chrome 103588)');
    488 
    489   var started_loading = elem.readyState !== undefined;
    490 
    491   // Remember that the plugin started loading - it may be unloaded by the time
    492   // the callback fires.
    493   elem.addEventListener('load', function() {
    494     started_loading = true;
    495   }, true);
    496 
    497   // Check that the plugin has at least started to load after "timeout" seconds,
    498   // otherwise reload the page.
    499   setTimeout(function() {
    500     if (!started_loading) {
    501       ForceNaClPluginReload(elem, tester);
    502     }
    503   }, timeout);
    504 }
    505 
    506 function ForceNaClPluginReload(elem, tester) {
    507   if (elem.readyState === undefined) {
    508     tester.log('WARNING: WebKit plugin-not-loading error detected; reloading.');
    509     window.location.reload();
    510   }
    511 }
    512 
    513 function NaClWaiter(body_element) {
    514   // Work around how JS binds 'this'
    515   var this_ = this;
    516   var embedsToWaitFor = [];
    517   // embedsLoaded contains list of embeds that have dispatched the
    518   // 'loadend' progress event.
    519   this.embedsLoaded = [];
    520 
    521   this.is_loaded = function(embed) {
    522     for (var i = 0; i < this_.embedsLoaded.length; ++i) {
    523       if (this_.embedsLoaded[i] === embed) {
    524         return true;
    525       }
    526     }
    527     return (embed.readyState == 4) && !this_.has_errored(embed);
    528   }
    529 
    530   this.has_errored = function(embed) {
    531     var msg = embed.lastError;
    532     return embed.lastError != undefined && embed.lastError != '';
    533   }
    534 
    535   // If an argument was passed, it is the body element for registering
    536   // event listeners for the 'loadend' event type.
    537   if (body_element != undefined) {
    538     var eventListener = function(e) {
    539       if (e.type == 'loadend') {
    540         this_.embedsLoaded.push(e.target);
    541       }
    542     }
    543 
    544     body_element.addEventListener('loadend', eventListener, true);
    545   }
    546 
    547   // Takes an arbitrary number of arguments.
    548   this.waitFor = function() {
    549     for (var i = 0; i< arguments.length; i++) {
    550       embedsToWaitFor.push(arguments[i]);
    551     }
    552   }
    553 
    554   this.run = function(doneCallback, pingCallback) {
    555     this.doneCallback = doneCallback;
    556     this.pingCallback = pingCallback;
    557 
    558     // Wait for up to forty seconds for the nexes to load.
    559     // TODO(ncbray) use error handling mechanisms (when they are implemented)
    560     // rather than a timeout.
    561     this.totalWait = 0;
    562     this.maxTotalWait = 40000;
    563     this.retryWait = 10;
    564     this.waitForPlugins();
    565   }
    566 
    567   this.waitForPlugins = function() {
    568     var errored = [];
    569     var loaded = [];
    570     var waiting = [];
    571 
    572     for (var i = 0; i < embedsToWaitFor.length; i++) {
    573       try {
    574         var e = embedsToWaitFor[i];
    575         if (this.has_errored(e)) {
    576           errored.push(e);
    577         } else if (this.is_loaded(e)) {
    578           loaded.push(e);
    579         } else {
    580           waiting.push(e);
    581         }
    582       } catch(err) {
    583         // If the module is badly horked, touching lastError, etc, may except.
    584         errored.push(err);
    585       }
    586     }
    587 
    588     this.totalWait += this.retryWait;
    589 
    590     if (waiting.length == 0) {
    591       this.doneCallback(loaded, errored);
    592     } else if (this.totalWait >= this.maxTotalWait) {
    593       // Timeouts are considered errors.
    594       this.doneCallback(loaded, errored.concat(waiting));
    595     } else {
    596       setTimeout(function() { this_.waitForPlugins(); }, this.retryWait);
    597       // Capped exponential backoff
    598       this.retryWait += this.retryWait/2;
    599       // Paranoid: does setTimeout like floating point numbers?
    600       this.retryWait = Math.round(this.retryWait);
    601       if (this.retryWait > 100)
    602         this.retryWait = 100;
    603       // Prevent the server from thinking the test has died.
    604       if (this.pingCallback)
    605         this.pingCallback();
    606     }
    607   }
    608 }
    609 
    610 
    611 function logLoadStatus(rpc, load_errors_are_test_errors,
    612                        exit_cleanly_is_an_error, loaded, waiting) {
    613   for (var i = 0; i < loaded.length; i++) {
    614     rpc.log(embed_name(loaded[i]) + ' loaded');
    615   }
    616   // Be careful when interacting with horked nexes.
    617   var getCarefully = function (callback) {
    618     try {
    619       return callback();
    620     } catch (err) {
    621       return '<exception>';
    622     }
    623   }
    624 
    625   var errored = false;
    626   for (var j = 0; j < waiting.length; j++) {
    627     // Workaround for WebKit layout bug that caused the NaCl plugin to not
    628     // load.  If we see that the plugin is not loaded after a timeout, we
    629     // forcibly reload the page, thereby triggering layout.  Re-running
    630     // layout should make WebKit instantiate the plugin.  NB: this could
    631     // make the JavaScript-based code go into an infinite loop if the
    632     // WebKit bug becomes deterministic or the NaCl plugin fails after
    633     // loading, but the browser_tester.py code will timeout the test.
    634     //
    635     // http://code.google.com/p/nativeclient/issues/detail?id=2428
    636     //
    637     if (waiting[j].readyState == undefined) {
    638       // alert('Woot');  // -- for manual debugging
    639       rpc.log('WARNING: WebKit plugin-not-loading error detected; reloading.');
    640       window.location.reload();
    641       throw "reload NOW";
    642     }
    643     var name = getCarefully(function(){
    644         return embed_name(waiting[j]);
    645       });
    646     var ready = getCarefully(function(){
    647         var readyStateString =
    648         ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE'];
    649         // An undefined index value will return and undefined result.
    650         return readyStateString[waiting[j].readyState];
    651       });
    652     var last = getCarefully(function(){
    653         return toString(waiting[j].lastError);
    654       });
    655     if (!exit_cleanly_is_an_error) {
    656       // For some tests (e.g. the NaCl SDK examples) it is OK if the test
    657       // exits cleanly when we are waiting for it to load.
    658       //
    659       // In this case, "exiting cleanly" means returning 0 from main, or
    660       // calling exit(0). When this happens, the module "crashes" by posting
    661       // the "crash" message, but it also assigns an exitStatus.
    662       //
    663       // A real crash produces an exitStatus of -1, and if the module is still
    664       // running its exitStatus will be undefined.
    665       var exitStatus = getCarefully(function() {
    666         if (ready === 'DONE') {
    667           return waiting[j].exitStatus;
    668         } else {
    669           return -1;
    670         }
    671       });
    672 
    673       if (exitStatus === 0) {
    674         continue;
    675       }
    676     }
    677     var msg = (name + ' did not load. Status: ' + ready + ' / ' + last);
    678     if (load_errors_are_test_errors) {
    679       rpc.client_error(msg);
    680       errored = true;
    681     } else {
    682       rpc.log(msg);
    683     }
    684   }
    685   return errored;
    686 }
    687 
    688 
    689 // Contains the state for a single test.
    690 function TestStatus(tester, name, async) {
    691   // Work around how JS binds 'this'
    692   var this_ = this;
    693   this.tester = tester;
    694   this.name = name;
    695   this.async = async;
    696   this.running = true;
    697 
    698   this.log = function(message) {
    699     this.tester.rpc.log(this.name, toString(message), !this.running);
    700   }
    701 
    702   this.pass = function() {
    703     // TODO raise if not running.
    704     this.tester.rpc.pass(this.name, !this.running);
    705     this._done();
    706     haltAsyncTest();
    707   }
    708 
    709   this.fail = function(message) {
    710     this.tester.rpc.fail(this.name, message, !this.running);
    711     this._done();
    712     haltAsyncTest();
    713   }
    714 
    715   this._done = function() {
    716     if (this.running) {
    717       this.running = false;
    718       this.tester.testDone(this);
    719     }
    720   }
    721 
    722   this.assert = function(condition, message) {
    723     assert(condition, message, this);
    724   }
    725 
    726   this.assertEqual = function(a, b, message) {
    727     assertEqual(a, b, message, this);
    728   }
    729 
    730   this.callbackWrapper = function(callback, args) {
    731     // A stale callback?
    732     if (!this.running)
    733       return;
    734 
    735     if (args === undefined)
    736       args = [];
    737 
    738     try {
    739       callback.apply(undefined, args);
    740     } catch (err) {
    741       if (typeof err == 'object' && 'type' in err) {
    742         if (err.type == 'test_halt') {
    743           // New-style test
    744           // If we get this exception, we can assume any callbacks or next
    745           // tests have already been scheduled.
    746           return;
    747         } else if (err.type == 'test_fail') {
    748           // Old-style test
    749           // A special exception that terminates the test with a failure
    750           this.tester.rpc.fail(this.name, err.message, !this.running);
    751           this._done();
    752           return;
    753         }
    754       }
    755       // This is not a special type of exception, it is an error.
    756       this.tester.rpc.exception(this.name, err, !this.running);
    757       this._done();
    758       return;
    759     }
    760 
    761     // A normal exit.  Should we move on to the next test?
    762     // Async tests do not move on without an explicit pass.
    763     if (!this.async) {
    764       this.tester.rpc.pass(this.name);
    765       this._done();
    766     }
    767   }
    768 
    769   // Async callbacks should be wrapped so the tester can catch unexpected
    770   // exceptions.
    771   this.wrap = function(callback) {
    772     return function() {
    773       this_.callbackWrapper(callback, arguments);
    774     };
    775   }
    776 
    777   this.setTimeout = function(callback, time) {
    778     setTimeout(this.wrap(callback), time);
    779   }
    780 
    781   this.waitForCallback = function(callbackName, expectedCalls) {
    782     this.log('Waiting for ' + expectedCalls + ' invocations of callback: '
    783                + callbackName);
    784     var gotCallbacks = 0;
    785 
    786     // Deliberately global - this is what the nexe expects.
    787     // TODO(ncbray): consider returning this function, so the test has more
    788     // flexibility. For example, in the test one could count to N
    789     // using a different callback before calling _this_ callback, and
    790     // continuing the test. Also, consider calling user-supplied callback
    791     // when done waiting.
    792     window[callbackName] = this.wrap(function() {
    793       ++gotCallbacks;
    794       this_.log('Received callback ' + gotCallbacks);
    795       if (gotCallbacks == expectedCalls) {
    796         this_.log("Done waiting");
    797         this_.pass();
    798       } else {
    799         // HACK
    800         haltAsyncTest();
    801       }
    802     });
    803 
    804     // HACK if this function is used in a non-async test, make sure we don't
    805     // spuriously pass.  Throwing this exception forces us to behave like an
    806     // async test.
    807     haltAsyncTest();
    808   }
    809 
    810   // This function takes an array of messages and asserts that the nexe
    811   // calls PostMessage with each of these messages, in order.
    812   // Arguments:
    813   //   plugin - The DOM object for the NaCl plugin
    814   //   messages - An array of expected responses
    815   //   callback - An optional callback function that takes the current message
    816   //              string as an argument
    817   this.expectMessageSequence = function(plugin, messages, callback) {
    818     this.assert(messages.length > 0, 'Must provide at least one message');
    819     var local_messages = messages.slice();
    820     var listener = function(message) {
    821       if (message.data.indexOf('@:') == 0) {
    822         // skip debug messages
    823         this_.log('DEBUG: ' + message.data.substr(2));
    824       } else {
    825         this_.assertEqual(message.data, local_messages.shift());
    826         if (callback !== undefined) {
    827           callback(message.data);
    828         }
    829       }
    830       if (local_messages.length == 0) {
    831         this_.pass();
    832       } else {
    833         this_.expectEvent(plugin, 'message', listener);
    834       }
    835     }
    836     this.expectEvent(plugin, 'message', listener);
    837   }
    838 
    839   this.expectEvent = function(src, event_type, listener) {
    840     var wrapper = this.wrap(function(e) {
    841       src.removeEventListener(event_type, wrapper, false);
    842       listener(e);
    843     });
    844     src.addEventListener(event_type, wrapper, false);
    845   }
    846 }
    847 
    848 
    849 function Tester(body_element) {
    850   // Work around how JS binds 'this'
    851   var this_ = this;
    852   // The tests being run.
    853   var tests = [];
    854   this.rpc = new RPCWrapper();
    855   this.waiter = new NaClWaiter(body_element);
    856 
    857   var load_errors_are_test_errors = true;
    858   var exit_cleanly_is_an_error = true;
    859 
    860   var parallel = false;
    861 
    862   //
    863   // BEGIN public interface
    864   //
    865 
    866   this.loadErrorsAreOK = function() {
    867     load_errors_are_test_errors = false;
    868   }
    869 
    870   this.exitCleanlyIsOK = function() {
    871     exit_cleanly_is_an_error = false;
    872   };
    873 
    874   this.log = function(message) {
    875     this.rpc.log(message);
    876   }
    877 
    878   // If this kind of test exits cleanly, it passes
    879   this.addTest = function(name, testFunction) {
    880     tests.push({name: name, callback: testFunction, async: false});
    881   }
    882 
    883   // This kind of test does not pass until "pass" is explicitly called.
    884   this.addAsyncTest = function(name, testFunction) {
    885     tests.push({name: name, callback: testFunction, async: true});
    886   }
    887 
    888   this.run = function() {
    889     this.rpc.startup();
    890     this.startHeartbeat();
    891     this.waiter.run(
    892       function(loaded, waiting) {
    893         var errored = logLoadStatus(this_.rpc, load_errors_are_test_errors,
    894                                     exit_cleanly_is_an_error,
    895                                     loaded, waiting);
    896         if (errored) {
    897           this_.rpc.blankLine();
    898           this_.rpc.log('A nexe load error occured, aborting testing.');
    899           this_._done();
    900         } else {
    901           this_.startTesting();
    902         }
    903       },
    904       function() {
    905         this_.rpc.ping();
    906       }
    907     );
    908   }
    909 
    910   this.runParallel = function() {
    911     parallel = true;
    912     this.run();
    913   }
    914 
    915   // Takes an arbitrary number of arguments.
    916   this.waitFor = function() {
    917     for (var i = 0; i< arguments.length; i++) {
    918       this.waiter.waitFor(arguments[i]);
    919     }
    920   }
    921 
    922   //
    923   // END public interface
    924   //
    925 
    926   this.startHeartbeat = function() {
    927     var rpc = this.rpc;
    928     var heartbeat = function() {
    929       rpc.heartbeat();
    930       setTimeout(heartbeat, 500);
    931     }
    932     heartbeat();
    933   }
    934 
    935   this.launchTest = function(testIndex) {
    936     var testDecl = tests[testIndex];
    937     var currentTest = new TestStatus(this, testDecl.name, testDecl.async);
    938     setTimeout(currentTest.wrap(function() {
    939       this_.rpc.blankLine();
    940       this_.rpc.begin(currentTest.name);
    941       testDecl.callback(currentTest);
    942     }), 0);
    943   }
    944 
    945   this._done = function() {
    946     this.rpc.blankLine();
    947     this.rpc.shutdown();
    948   }
    949 
    950   this.startTesting = function() {
    951     if (tests.length == 0) {
    952       // No tests specified.
    953       this._done();
    954       return;
    955     }
    956 
    957     this.testCount = 0;
    958     if (parallel) {
    959       // Launch all tests.
    960       for (var i = 0; i < tests.length; i++) {
    961         this.launchTest(i);
    962       }
    963     } else {
    964       // Launch the first test.
    965       this.launchTest(0);
    966     }
    967   }
    968 
    969   this.testDone = function(test) {
    970     this.testCount += 1;
    971     if (this.testCount < tests.length) {
    972       if (!parallel) {
    973         // Move on to the next test if they're being run one at a time.
    974         this.launchTest(this.testCount);
    975       }
    976     } else {
    977       this._done();
    978     }
    979   }
    980 }
    981