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