Process Alt-Svc headers.
[chromium-blink-merge.git] / ppapi / native_client / tools / browser_tester / browserdata / nacltest.js
blobf5e7c971daa08d354d61bcb5a440c48f274b578a
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.
6 function $(id) {
7   return document.getElementById(id);
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;
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;
43 function addDefaultsToArgs(defaults, args) {
44   for (var key in defaults) {
45     if (!(key in args)) {
46       args[key] = defaults[key];
47     }
48   }
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;
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   }
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;
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   }
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   }
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   }
157   // Pretty prints an error into the DOM.
158   this.logLocalError = function(message) {
159     this.logLocal(message, 'red');
160     this.visualError();
161   }
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   }
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   }
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});
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   }
197   this.ping = function() {
198     rpcCall('Ping', {});
199   }
201   this.heartbeat = function() {
202     rpcCall('JavaScriptIsAlive', {});
203   }
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   }
217   this.begin = function(test_name) {
218     var full_message = '[' + test_name + ' BEGIN]'
219     this._log(full_message, 'blue');
220   }
222   this._log = function(message, color, from_completed_test) {
223     if (typeof(message) != 'string') {
224       message = toString(message);
225     }
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     }
234     this.logLocal(message, color);
235     rpcCall('TestLog', {message: message});
236   }
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   }
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   }
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   }
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   }
273   this.blankLine = function() {
274     this._log('');
275   }
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   }
284   this.visualError = function() {
285     // Changing the color is defered until testing is done
286     this.ever_failed = true;
287   }
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   }
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   }
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);
323 // BEGIN functions for testing
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(' ');
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   }
347 function assert(condition, message, test_status) {
348   if (!condition) {
349     fail(message, toString(condition), test_status);
350   }
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]';
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   }
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   }
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   }
413 function assertRegexMatches(str, re, message, test_status) {
414   if (!str.match(re)) {
415     fail(message, toString(str) + ' doesn\'t match ' + toString(re.toString()),
416          test_status);
417   }
421 // Ideally there'd be some way to identify what exception was thrown, but JS
422 // exceptions are fairly ad-hoc.
423 // TODO(ncbray) allow manual validation of exception types?
424 function assertRaises(func, message, test_status) {
425   try {
426     func();
427   } catch (err) {
428     return;
429   }
430   fail(message, 'did not raise', test_status);
435 // END functions for testing
439 function haltAsyncTest() {
440   throw {type: 'test_halt'};
444 function begins_with(s, prefix) {
445   if (s.length >= prefix.length) {
446     return s.substr(0, prefix.length) == prefix;
447   } else {
448     return false;
449   }
453 function ends_with(s, suffix) {
454   if (s.length >= suffix.length) {
455     return s.substr(s.length - suffix.length, suffix.length) == suffix;
456   } else {
457     return false;
458   }
462 function embed_name(embed) {
463   if (embed.name != undefined) {
464     if (embed.id != undefined) {
465       return embed.name + ' / ' + embed.id;
466     } else {
467       return embed.name;
468     }
469   } else if (embed.id != undefined) {
470     return embed.id;
471   } else {
472     return '[no name]';
473   }
477 // Write data to the filesystem. This will only work if the browser_tester was
478 // initialized with --output_dir.
479 function outputFile(name, data, onload, onerror) {
480   var xhr = new XMLHttpRequest();
481   xhr.onload = onload;
482   xhr.onerror = onerror;
483   xhr.open('POST', name, true);
484   xhr.send(data);
488 // Webkit Bug Workaround
489 // THIS SHOULD BE REMOVED WHEN Webkit IS FIXED
490 // http://code.google.com/p/nativeclient/issues/detail?id=2428
491 // http://code.google.com/p/chromium/issues/detail?id=103588
493 function ForcePluginLoadOnTimeout(elem, tester, timeout) {
494   tester.log('Registering ForcePluginLoadOnTimeout ' +
495              '(Bugs: NaCl 2428, Chrome 103588)');
497   var started_loading = elem.readyState !== undefined;
499   // Remember that the plugin started loading - it may be unloaded by the time
500   // the callback fires.
501   elem.addEventListener('load', function() {
502     started_loading = true;
503   }, true);
505   // Check that the plugin has at least started to load after "timeout" seconds,
506   // otherwise reload the page.
507   setTimeout(function() {
508     if (!started_loading) {
509       ForceNaClPluginReload(elem, tester);
510     }
511   }, timeout);
514 function ForceNaClPluginReload(elem, tester) {
515   if (elem.readyState === undefined) {
516     tester.log('WARNING: WebKit plugin-not-loading error detected; reloading.');
517     window.location.reload();
518   }
521 function NaClWaiter(body_element) {
522   // Work around how JS binds 'this'
523   var this_ = this;
524   var embedsToWaitFor = [];
525   // embedsLoaded contains list of embeds that have dispatched the
526   // 'loadend' progress event.
527   this.embedsLoaded = [];
529   this.is_loaded = function(embed) {
530     for (var i = 0; i < this_.embedsLoaded.length; ++i) {
531       if (this_.embedsLoaded[i] === embed) {
532         return true;
533       }
534     }
535     return (embed.readyState == 4) && !this_.has_errored(embed);
536   }
538   this.has_errored = function(embed) {
539     var msg = embed.lastError;
540     return embed.lastError != undefined && embed.lastError != '';
541   }
543   // If an argument was passed, it is the body element for registering
544   // event listeners for the 'loadend' event type.
545   if (body_element != undefined) {
546     var eventListener = function(e) {
547       if (e.type == 'loadend') {
548         this_.embedsLoaded.push(e.target);
549       }
550     }
552     body_element.addEventListener('loadend', eventListener, true);
553   }
555   // Takes an arbitrary number of arguments.
556   this.waitFor = function() {
557     for (var i = 0; i< arguments.length; i++) {
558       embedsToWaitFor.push(arguments[i]);
559     }
560   }
562   this.run = function(doneCallback, pingCallback) {
563     this.doneCallback = doneCallback;
564     this.pingCallback = pingCallback;
566     // Wait for up to forty seconds for the nexes to load.
567     // TODO(ncbray) use error handling mechanisms (when they are implemented)
568     // rather than a timeout.
569     this.totalWait = 0;
570     this.maxTotalWait = 40000;
571     this.retryWait = 10;
572     this.waitForPlugins();
573   }
575   this.waitForPlugins = function() {
576     var errored = [];
577     var loaded = [];
578     var waiting = [];
580     for (var i = 0; i < embedsToWaitFor.length; i++) {
581       try {
582         var e = embedsToWaitFor[i];
583         if (this.has_errored(e)) {
584           errored.push(e);
585         } else if (this.is_loaded(e)) {
586           loaded.push(e);
587         } else {
588           waiting.push(e);
589         }
590       } catch(err) {
591         // If the module is badly horked, touching lastError, etc, may except.
592         errored.push(err);
593       }
594     }
596     this.totalWait += this.retryWait;
598     if (waiting.length == 0) {
599       this.doneCallback(loaded, errored);
600     } else if (this.totalWait >= this.maxTotalWait) {
601       // Timeouts are considered errors.
602       this.doneCallback(loaded, errored.concat(waiting));
603     } else {
604       setTimeout(function() { this_.waitForPlugins(); }, this.retryWait);
605       // Capped exponential backoff
606       this.retryWait += this.retryWait/2;
607       // Paranoid: does setTimeout like floating point numbers?
608       this.retryWait = Math.round(this.retryWait);
609       if (this.retryWait > 100)
610         this.retryWait = 100;
611       // Prevent the server from thinking the test has died.
612       if (this.pingCallback)
613         this.pingCallback();
614     }
615   }
619 function logLoadStatus(rpc, load_errors_are_test_errors,
620                        exit_cleanly_is_an_error, loaded, waiting) {
621   for (var i = 0; i < loaded.length; i++) {
622     rpc.log(embed_name(loaded[i]) + ' loaded');
623   }
624   // Be careful when interacting with horked nexes.
625   var getCarefully = function (callback) {
626     try {
627       return callback();
628     } catch (err) {
629       return '<exception>';
630     }
631   }
633   var errored = false;
634   for (var j = 0; j < waiting.length; j++) {
635     // Workaround for WebKit layout bug that caused the NaCl plugin to not
636     // load.  If we see that the plugin is not loaded after a timeout, we
637     // forcibly reload the page, thereby triggering layout.  Re-running
638     // layout should make WebKit instantiate the plugin.  NB: this could
639     // make the JavaScript-based code go into an infinite loop if the
640     // WebKit bug becomes deterministic or the NaCl plugin fails after
641     // loading, but the browser_tester.py code will timeout the test.
642     //
643     // http://code.google.com/p/nativeclient/issues/detail?id=2428
644     //
645     if (waiting[j].readyState == undefined) {
646       // alert('Woot');  // -- for manual debugging
647       rpc.log('WARNING: WebKit plugin-not-loading error detected; reloading.');
648       window.location.reload();
649       throw "reload NOW";
650     }
651     var name = getCarefully(function(){
652         return embed_name(waiting[j]);
653       });
654     var ready = getCarefully(function(){
655         var readyStateString =
656         ['UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE'];
657         // An undefined index value will return and undefined result.
658         return readyStateString[waiting[j].readyState];
659       });
660     var last = getCarefully(function(){
661         return toString(waiting[j].lastError);
662       });
663     if (!exit_cleanly_is_an_error) {
664       // For some tests (e.g. the NaCl SDK examples) it is OK if the test
665       // exits cleanly when we are waiting for it to load.
666       //
667       // In this case, "exiting cleanly" means returning 0 from main, or
668       // calling exit(0). When this happens, the module "crashes" by posting
669       // the "crash" message, but it also assigns an exitStatus.
670       //
671       // A real crash produces an exitStatus of -1, and if the module is still
672       // running its exitStatus will be undefined.
673       var exitStatus = getCarefully(function() {
674         if (ready === 'DONE') {
675           return waiting[j].exitStatus;
676         } else {
677           return -1;
678         }
679       });
681       if (exitStatus === 0) {
682         continue;
683       }
684     }
685     var msg = (name + ' did not load. Status: ' + ready + ' / ' + last);
686     if (load_errors_are_test_errors) {
687       rpc.client_error(msg);
688       errored = true;
689     } else {
690       rpc.log(msg);
691     }
692   }
693   return errored;
697 // Contains the state for a single test.
698 function TestStatus(tester, name, async) {
699   // Work around how JS binds 'this'
700   var this_ = this;
701   this.tester = tester;
702   this.name = name;
703   this.async = async;
704   this.running = true;
706   this.log = function(message) {
707     this.tester.rpc.log(this.name, toString(message), !this.running);
708   }
710   this.pass = function() {
711     // TODO raise if not running.
712     this.tester.rpc.pass(this.name, !this.running);
713     this._done();
714     haltAsyncTest();
715   }
717   this.fail = function(message) {
718     this.tester.rpc.fail(this.name, message, !this.running);
719     this._done();
720     haltAsyncTest();
721   }
723   this._done = function() {
724     if (this.running) {
725       this.running = false;
726       this.tester.testDone(this);
727     }
728   }
730   this.assert = function(condition, message) {
731     assert(condition, message, this);
732   }
734   this.assertEqual = function(a, b, message) {
735     assertEqual(a, b, message, this);
736   }
738   this.assertRegexMatches = function(a, b, message) {
739     assertRegexMatches(a, b, message, this);
740   }
742   this.callbackWrapper = function(callback, args) {
743     // A stale callback?
744     if (!this.running)
745       return;
747     if (args === undefined)
748       args = [];
750     try {
751       callback.apply(undefined, args);
752     } catch (err) {
753       if (typeof err == 'object' && 'type' in err) {
754         if (err.type == 'test_halt') {
755           // New-style test
756           // If we get this exception, we can assume any callbacks or next
757           // tests have already been scheduled.
758           return;
759         } else if (err.type == 'test_fail') {
760           // Old-style test
761           // A special exception that terminates the test with a failure
762           this.tester.rpc.fail(this.name, err.message, !this.running);
763           this._done();
764           return;
765         }
766       }
767       // This is not a special type of exception, it is an error.
768       this.tester.rpc.exception(this.name, err, !this.running);
769       this._done();
770       return;
771     }
773     // A normal exit.  Should we move on to the next test?
774     // Async tests do not move on without an explicit pass.
775     if (!this.async) {
776       this.tester.rpc.pass(this.name);
777       this._done();
778     }
779   }
781   // Async callbacks should be wrapped so the tester can catch unexpected
782   // exceptions.
783   this.wrap = function(callback) {
784     return function() {
785       this_.callbackWrapper(callback, arguments);
786     };
787   }
789   this.setTimeout = function(callback, time) {
790     setTimeout(this.wrap(callback), time);
791   }
793   this.waitForCallback = function(callbackName, expectedCalls) {
794     this.log('Waiting for ' + expectedCalls + ' invocations of callback: '
795                + callbackName);
796     var gotCallbacks = 0;
798     // Deliberately global - this is what the nexe expects.
799     // TODO(ncbray): consider returning this function, so the test has more
800     // flexibility. For example, in the test one could count to N
801     // using a different callback before calling _this_ callback, and
802     // continuing the test. Also, consider calling user-supplied callback
803     // when done waiting.
804     window[callbackName] = this.wrap(function() {
805       ++gotCallbacks;
806       this_.log('Received callback ' + gotCallbacks);
807       if (gotCallbacks == expectedCalls) {
808         this_.log("Done waiting");
809         this_.pass();
810       } else {
811         // HACK
812         haltAsyncTest();
813       }
814     });
816     // HACK if this function is used in a non-async test, make sure we don't
817     // spuriously pass.  Throwing this exception forces us to behave like an
818     // async test.
819     haltAsyncTest();
820   }
822   // This function takes an array of messages and asserts that the nexe
823   // calls PostMessage with each of these messages, in order.
824   // Arguments:
825   //   plugin - The DOM object for the NaCl plugin
826   //   messages - An array of expected responses
827   //   callback - An optional callback function that takes the current message
828   //              string as an argument
829   this.expectMessageSequence = function(plugin, messages, callback) {
830     this.assert(messages.length > 0, 'Must provide at least one message');
831     var local_messages = messages.slice();
832     var listener = function(message) {
833       if (message.data.indexOf('@:') == 0) {
834         // skip debug messages
835         this_.log('DEBUG: ' + message.data.substr(2));
836       } else {
837         this_.assertEqual(message.data, local_messages.shift());
838         if (callback !== undefined) {
839           callback(message.data);
840         }
841       }
842       if (local_messages.length == 0) {
843         this_.pass();
844       } else {
845         this_.expectEvent(plugin, 'message', listener);
846       }
847     }
848     this.expectEvent(plugin, 'message', listener);
849   }
851   this.expectEvent = function(src, event_type, listener) {
852     var wrapper = this.wrap(function(e) {
853       src.removeEventListener(event_type, wrapper, false);
854       listener(e);
855     });
856     src.addEventListener(event_type, wrapper, false);
857   }
861 function Tester(body_element) {
862   // Work around how JS binds 'this'
863   var this_ = this;
864   // The tests being run.
865   var tests = [];
866   this.rpc = new RPCWrapper();
867   this.waiter = new NaClWaiter(body_element);
869   var load_errors_are_test_errors = true;
870   var exit_cleanly_is_an_error = true;
872   var parallel = false;
874   //
875   // BEGIN public interface
876   //
878   this.loadErrorsAreOK = function() {
879     load_errors_are_test_errors = false;
880   }
882   this.exitCleanlyIsOK = function() {
883     exit_cleanly_is_an_error = false;
884   };
886   this.log = function(message) {
887     this.rpc.log(message);
888   }
890   // If this kind of test exits cleanly, it passes
891   this.addTest = function(name, testFunction) {
892     tests.push({name: name, callback: testFunction, async: false});
893   }
895   // This kind of test does not pass until "pass" is explicitly called.
896   this.addAsyncTest = function(name, testFunction) {
897     tests.push({name: name, callback: testFunction, async: true});
898   }
900   this.run = function() {
901     this.rpc.startup();
902     this.startHeartbeat();
903     this.waiter.run(
904       function(loaded, waiting) {
905         var errored = logLoadStatus(this_.rpc, load_errors_are_test_errors,
906                                     exit_cleanly_is_an_error,
907                                     loaded, waiting);
908         if (errored) {
909           this_.rpc.blankLine();
910           this_.rpc.log('A nexe load error occured, aborting testing.');
911           this_._done();
912         } else {
913           this_.startTesting();
914         }
915       },
916       function() {
917         this_.rpc.ping();
918       }
919     );
920   }
922   this.runParallel = function() {
923     parallel = true;
924     this.run();
925   }
927   // Takes an arbitrary number of arguments.
928   this.waitFor = function() {
929     for (var i = 0; i< arguments.length; i++) {
930       this.waiter.waitFor(arguments[i]);
931     }
932   }
934   //
935   // END public interface
936   //
938   this.startHeartbeat = function() {
939     var rpc = this.rpc;
940     var heartbeat = function() {
941       rpc.heartbeat();
942       setTimeout(heartbeat, 500);
943     }
944     heartbeat();
945   }
947   this.launchTest = function(testIndex) {
948     var testDecl = tests[testIndex];
949     var currentTest = new TestStatus(this, testDecl.name, testDecl.async);
950     setTimeout(currentTest.wrap(function() {
951       this_.rpc.blankLine();
952       this_.rpc.begin(currentTest.name);
953       testDecl.callback(currentTest);
954     }), 0);
955   }
957   this._done = function() {
958     this.rpc.blankLine();
959     this.rpc.shutdown();
960   }
962   this.startTesting = function() {
963     if (tests.length == 0) {
964       // No tests specified.
965       this._done();
966       return;
967     }
969     this.testCount = 0;
970     if (parallel) {
971       // Launch all tests.
972       for (var i = 0; i < tests.length; i++) {
973         this.launchTest(i);
974       }
975     } else {
976       // Launch the first test.
977       this.launchTest(0);
978     }
979   }
981   this.testDone = function(test) {
982     this.testCount += 1;
983     if (this.testCount < tests.length) {
984       if (!parallel) {
985         // Move on to the next test if they're being run one at a time.
986         this.launchTest(this.testCount);
987       }
988     } else {
989       this._done();
990     }
991   }