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