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 // Webkit Bug Workaround
    470 // THIS SHOULD BE REMOVED WHEN Webkit IS FIXED
    471 // http://code.google.com/p/nativeclient/issues/detail?id=2428
    472 // http://code.google.com/p/chromium/issues/detail?id=103588
    473 
    474 function ForcePluginLoadOnTimeout(elem, tester, timeout) {
    475   tester.log('Registering ForcePluginLoadOnTimeout ' +
    476              '(Bugs: NaCl 2428, Chrome 103588)');
    477 
    478   var started_loading = elem.readyState !== undefined;
    479 
    480   // Remember that the plugin started loading - it may be unloaded by the time
    481   // the callback fires.
    482   elem.addEventListener('load', function() {
    483     started_loading = true;
    484   }, true);
    485 
    486   // Check that the plugin has at least started to load after "timeout" seconds,
    487   // otherwise reload the page.
    488   setTimeout(function() {
    489     if (!started_loading) {
    490       ForceNaClPluginReload(elem, tester);
    491     }
    492   }, timeout);
    493 }
    494 
    495 function ForceNaClPluginReload(elem, tester) {
    496   if (elem.readyState === undefined) {
    497     tester.log('WARNING: WebKit plugin-not-loading error detected; reloading.');
    498     window.location.reload();
    499   }
    500 }
    501 
    502 function NaClWaiter(body_element) {
    503   // Work around how JS binds 'this'
    504   var this_ = this;
    505   var embedsToWaitFor = [];
    506   // embedsLoaded contains list of embeds that have dispatched the
    507   // 'loadend' progress event.
    508   this.embedsLoaded = [];
    509 
    510   this.is_loaded = function(embed) {
    511     for (var i = 0; i < this_.embedsLoaded.length; ++i) {
    512       if (this_.embedsLoaded[i] === embed) {
    513         return true;
    514       }
    515     }
    516     return (embed.readyState == 4) && !this_.has_errored(embed);
    517   }
    518 
    519   this.has_errored = function(embed) {
    520     var msg = embed.lastError;
    521     return embed.lastError != undefined && embed.lastError != '';
    522   }
    523 
    524   // If an argument was passed, it is the body element for registering
    525   // event listeners for the 'loadend' event type.
    526   if (body_element != undefined) {
    527     var eventListener = function(e) {
    528       if (e.type == 'loadend') {
    529         this_.embedsLoaded.push(e.target);
    530       }
    531     }
    532 
    533     body_element.addEventListener('loadend', eventListener, true);
    534   }
    535 
    536   // Takes an arbitrary number of arguments.
    537   this.waitFor = function() {
    538     for (var i = 0; i< arguments.length; i++) {
    539       embedsToWaitFor.push(arguments[i]);
    540     }
    541   }
    542 
    543   this.run = function(doneCallback, pingCallback) {
    544     this.doneCallback = doneCallback;
    545     this.pingCallback = pingCallback;
    546 
    547     // Wait for up to forty seconds for the nexes to load.
    548     // TODO(ncbray) use error handling mechanisms (when they are implemented)
    549     // rather than a timeout.
    550     this.totalWait = 0;
    551     this.maxTotalWait = 40000;
    552     this.retryWait = 10;
    553     this.waitForPlugins();
    554   }
    555 
    556   this.waitForPlugins = function() {
    557     var errored = [];
    558     var loaded = [];
    559     var waiting = [];
    560 
    561     for (var i = 0; i < embedsToWaitFor.length; i++) {
    562       try {
    563         var e = embedsToWaitFor[i];
    564         if (this.has_errored(e)) {
    565           errored.push(e);
    566         } else if (this.is_loaded(e)) {
    567           loaded.push(e);
    568         } else {
    569           waiting.push(e);
    570         }
    571       } catch(err) {
    572         // If the module is badly horked, touching lastError, etc, may except.
    573         errored.push(err);
    574       }
    575     }
    576 
    577     this.totalWait += this.retryWait;
    578 
    579     if (waiting.length == 0) {
    580       this.doneCallback(loaded, errored);
    581     } else if (this.totalWait >= this.maxTotalWait) {
    582       // Timeouts are considered errors.
    583       this.doneCallback(loaded, errored.concat(waiting));
    584     } else {
    585       setTimeout(function() { this_.waitForPlugins(); }, this.retryWait);
    586       // Capped exponential backoff
    587       this.retryWait += this.retryWait/2;
    588       // Paranoid: does setTimeout like floating point numbers?
    589       this.retryWait = Math.round(this.retryWait);
    590       if (this.retryWait > 100)
    591         this.retryWait = 100;
    592       // Prevent the server from thinking the test has died.
    593       if (this.pingCallback)
    594         this.pingCallback();
    595     }
    596   }
    597 }
    598 
    599 
    600 function logLoadStatus(rpc, load_errors_are_test_errors,
    601                        exit_cleanly_is_an_error, loaded, waiting) {
    602   for (var i = 0; i < loaded.length; i++) {
    603     rpc.log(embed_name(loaded[i]) + ' loaded');
    604   }
    605   // Be careful when interacting with horked nexes.
    606   var getCarefully = function (callback) {
    607     try {
    608       return callback();
    609     } catch (err) {
    610       return '<exception>';
    611     }
    612   }
    613 
    614   var errored = false;
    615   for (var j = 0; j < waiting.length; j++) {
    616     // Workaround for WebKit layout bug that caused the NaCl plugin to not
    617     // load.  If we see that the plugin is not loaded after a timeout, we
    618     // forcibly reload the page, thereby triggering layout.  Re-running
    619     // layout should make WebKit instantiate the plugin.  NB: this could
    620     // make the JavaScript-based code go into an infinite loop if the
    621     // WebKit bug becomes deterministic or the NaCl plugin fails after
    622     // loading, but the browser_tester.py code will timeout the test.
    623     //
    624     // http://code.google.com/p/nativeclient/issues/detail?id=2428
    625     //
    626     if (waiting[j].readyState == undefined) {
    627       // alert('Woot');  // -- for manual debugging
    628       rpc.log('WARNING: WebKit plugin-not-loading error detected; reloading.');
    629       window.location.reload();
    630       throw "reload NOW";
    631     }
    632     var name = getCarefully(function(){
    633         return embed_name(waiting[j]);
    634       });
    635     var ready = getCarefully(function(){
    636         var readyStateString =
    637         ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE'];
    638         // An undefined index value will return and undefined result.
    639         return readyStateString[waiting[j].readyState];
    640       });
    641     var last = getCarefully(function(){
    642         return toString(waiting[j].lastError);
    643       });
    644     if (!exit_cleanly_is_an_error) {
    645       // For some tests (e.g. the NaCl SDK examples) it is OK if the test
    646       // exits cleanly when we are waiting for it to load.
    647       //
    648       // In this case, "exiting cleanly" means returning 0 from main, or
    649       // calling exit(0). When this happens, the module "crashes" by posting
    650       // the "crash" message, but it also assigns an exitStatus.
    651       //
    652       // A real crash produces an exitStatus of -1, and if the module is still
    653       // running its exitStatus will be undefined.
    654       var exitStatus = getCarefully(function() {
    655         if (ready === 'DONE') {
    656           return waiting[j].exitStatus;
    657         } else {
    658           return -1;
    659         }
    660       });
    661 
    662       if (exitStatus === 0) {
    663         continue;
    664       }
    665     }
    666     var msg = (name + ' did not load. Status: ' + ready + ' / ' + last);
    667     if (load_errors_are_test_errors) {
    668       rpc.client_error(msg);
    669       errored = true;
    670     } else {
    671       rpc.log(msg);
    672     }
    673   }
    674   return errored;
    675 }
    676 
    677 
    678 // Contains the state for a single test.
    679 function TestStatus(tester, name, async) {
    680   // Work around how JS binds 'this'
    681   var this_ = this;
    682   this.tester = tester;
    683   this.name = name;
    684   this.async = async;
    685   this.running = true;
    686 
    687   this.log = function(message) {
    688     this.tester.rpc.log(this.name, toString(message), !this.running);
    689   }
    690 
    691   this.pass = function() {
    692     // TODO raise if not running.
    693     this.tester.rpc.pass(this.name, !this.running);
    694     this._done();
    695     haltAsyncTest();
    696   }
    697 
    698   this.fail = function(message) {
    699     this.tester.rpc.fail(this.name, message, !this.running);
    700     this._done();
    701     haltAsyncTest();
    702   }
    703 
    704   this._done = function() {
    705     if (this.running) {
    706       this.running = false;
    707       this.tester.testDone(this);
    708     }
    709   }
    710 
    711   this.assert = function(condition, message) {
    712     assert(condition, message, this);
    713   }
    714 
    715   this.assertEqual = function(a, b, message) {
    716     assertEqual(a, b, message, this);
    717   }
    718 
    719   this.callbackWrapper = function(callback, args) {
    720     // A stale callback?
    721     if (!this.running)
    722       return;
    723 
    724     if (args === undefined)
    725       args = [];
    726 
    727     try {
    728       callback.apply(undefined, args);
    729     } catch (err) {
    730       if (typeof err == 'object' && 'type' in err) {
    731         if (err.type == 'test_halt') {
    732           // New-style test
    733           // If we get this exception, we can assume any callbacks or next
    734           // tests have already been scheduled.
    735           return;
    736         } else if (err.type == 'test_fail') {
    737           // Old-style test
    738           // A special exception that terminates the test with a failure
    739           this.tester.rpc.fail(this.name, err.message, !this.running);
    740           this._done();
    741           return;
    742         }
    743       }
    744       // This is not a special type of exception, it is an error.
    745       this.tester.rpc.exception(this.name, err, !this.running);
    746       this._done();
    747       return;
    748     }
    749 
    750     // A normal exit.  Should we move on to the next test?
    751     // Async tests do not move on without an explicit pass.
    752     if (!this.async) {
    753       this.tester.rpc.pass(this.name);
    754       this._done();
    755     }
    756   }
    757 
    758   // Async callbacks should be wrapped so the tester can catch unexpected
    759   // exceptions.
    760   this.wrap = function(callback) {
    761     return function() {
    762       this_.callbackWrapper(callback, arguments);
    763     };
    764   }
    765 
    766   this.setTimeout = function(callback, time) {
    767     setTimeout(this.wrap(callback), time);
    768   }
    769 
    770   this.waitForCallback = function(callbackName, expectedCalls) {
    771     this.log('Waiting for ' + expectedCalls + ' invocations of callback: '
    772                + callbackName);
    773     var gotCallbacks = 0;
    774 
    775     // Deliberately global - this is what the nexe expects.
    776     // TODO(ncbray): consider returning this function, so the test has more
    777     // flexibility. For example, in the test one could count to N
    778     // using a different callback before calling _this_ callback, and
    779     // continuing the test. Also, consider calling user-supplied callback
    780     // when done waiting.
    781     window[callbackName] = this.wrap(function() {
    782       ++gotCallbacks;
    783       this_.log('Received callback ' + gotCallbacks);
    784       if (gotCallbacks == expectedCalls) {
    785         this_.log("Done waiting");
    786         this_.pass();
    787       } else {
    788         // HACK
    789         haltAsyncTest();
    790       }
    791     });
    792 
    793     // HACK if this function is used in a non-async test, make sure we don't
    794     // spuriously pass.  Throwing this exception forces us to behave like an
    795     // async test.
    796     haltAsyncTest();
    797   }
    798 
    799   // This function takes an array of messages and asserts that the nexe
    800   // calls PostMessage with each of these messages, in order.
    801   // Arguments:
    802   //   plugin - The DOM object for the NaCl plugin
    803   //   messages - An array of expected responses
    804   //   callback - An optional callback function that takes the current message
    805   //              string as an argument
    806   this.expectMessageSequence = function(plugin, messages, callback) {
    807     this.assert(messages.length > 0, 'Must provide at least one message');
    808     var local_messages = messages.slice();
    809     var listener = function(message) {
    810       if (message.data.indexOf('@:') == 0) {
    811         // skip debug messages
    812         this_.log('DEBUG: ' + message.data.substr(2));
    813       } else {
    814         this_.assertEqual(message.data, local_messages.shift());
    815         if (callback !== undefined) {
    816           callback(message.data);
    817         }
    818       }
    819       if (local_messages.length == 0) {
    820         this_.pass();
    821       } else {
    822         this_.expectEvent(plugin, 'message', listener);
    823       }
    824     }
    825     this.expectEvent(plugin, 'message', listener);
    826   }
    827 
    828   this.expectEvent = function(src, event_type, listener) {
    829     var wrapper = this.wrap(function(e) {
    830       src.removeEventListener(event_type, wrapper, false);
    831       listener(e);
    832     });
    833     src.addEventListener(event_type, wrapper, false);
    834   }
    835 }
    836 
    837 
    838 function Tester(body_element) {
    839   // Work around how JS binds 'this'
    840   var this_ = this;
    841   // The tests being run.
    842   var tests = [];
    843   this.rpc = new RPCWrapper();
    844   this.waiter = new NaClWaiter(body_element);
    845 
    846   var load_errors_are_test_errors = true;
    847   var exit_cleanly_is_an_error = true;
    848 
    849   var parallel = false;
    850 
    851   //
    852   // BEGIN public interface
    853   //
    854 
    855   this.loadErrorsAreOK = function() {
    856     load_errors_are_test_errors = false;
    857   }
    858 
    859   this.exitCleanlyIsOK = function() {
    860     exit_cleanly_is_an_error = false;
    861   };
    862 
    863   this.log = function(message) {
    864     this.rpc.log(message);
    865   }
    866 
    867   // If this kind of test exits cleanly, it passes
    868   this.addTest = function(name, testFunction) {
    869     tests.push({name: name, callback: testFunction, async: false});
    870   }
    871 
    872   // This kind of test does not pass until "pass" is explicitly called.
    873   this.addAsyncTest = function(name, testFunction) {
    874     tests.push({name: name, callback: testFunction, async: true});
    875   }
    876 
    877   this.run = function() {
    878     this.rpc.startup();
    879     this.startHeartbeat();
    880     this.waiter.run(
    881       function(loaded, waiting) {
    882         var errored = logLoadStatus(this_.rpc, load_errors_are_test_errors,
    883                                     exit_cleanly_is_an_error,
    884                                     loaded, waiting);
    885         if (errored) {
    886           this_.rpc.blankLine();
    887           this_.rpc.log('A nexe load error occured, aborting testing.');
    888           this_._done();
    889         } else {
    890           this_.startTesting();
    891         }
    892       },
    893       function() {
    894         this_.rpc.ping();
    895       }
    896     );
    897   }
    898 
    899   this.runParallel = function() {
    900     parallel = true;
    901     this.run();
    902   }
    903 
    904   // Takes an arbitrary number of arguments.
    905   this.waitFor = function() {
    906     for (var i = 0; i< arguments.length; i++) {
    907       this.waiter.waitFor(arguments[i]);
    908     }
    909   }
    910 
    911   //
    912   // END public interface
    913   //
    914 
    915   this.startHeartbeat = function() {
    916     var rpc = this.rpc;
    917     var heartbeat = function() {
    918       rpc.heartbeat();
    919       setTimeout(heartbeat, 500);
    920     }
    921     heartbeat();
    922   }
    923 
    924   this.launchTest = function(testIndex) {
    925     var testDecl = tests[testIndex];
    926     var currentTest = new TestStatus(this, testDecl.name, testDecl.async);
    927     setTimeout(currentTest.wrap(function() {
    928       this_.rpc.blankLine();
    929       this_.rpc.begin(currentTest.name);
    930       testDecl.callback(currentTest);
    931     }), 0);
    932   }
    933 
    934   this._done = function() {
    935     this.rpc.blankLine();
    936     this.rpc.shutdown();
    937   }
    938 
    939   this.startTesting = function() {
    940     if (tests.length == 0) {
    941       // No tests specified.
    942       this._done();
    943       return;
    944     }
    945 
    946     this.testCount = 0;
    947     if (parallel) {
    948       // Launch all tests.
    949       for (var i = 0; i < tests.length; i++) {
    950         this.launchTest(i);
    951       }
    952     } else {
    953       // Launch the first test.
    954       this.launchTest(0);
    955     }
    956   }
    957 
    958   this.testDone = function(test) {
    959     this.testCount += 1;
    960     if (this.testCount < tests.length) {
    961       if (!parallel) {
    962         // Move on to the next test if they're being run one at a time.
    963         this.launchTest(this.testCount);
    964       }
    965     } else {
    966       this._done();
    967     }
    968   }
    969 }
    970