| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // 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(); |
| } |
| } |
| } |