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.
7 return document
.getElementById(id
);
11 function createNaClEmbed(args
) {
12 var fallback = function(value
, default_value
) {
13 return value
!== undefined ? value
: default_value
;
15 var embed
= document
.createElement('embed');
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);
27 function decodeURIArgs(encoded
) {
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
] + "'";
36 args
[decodeURIComponent(pair
[0])] = decodeURIComponent(pair
[1]);
43 function addDefaultsToArgs(defaults
, args
) {
44 for (var key
in defaults
) {
46 args
[key
] = defaults
[key
];
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
);
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') {
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'
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.
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.',
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.
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');
113 handleRPCFailure(name
, req
.status
.toString());
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
];
126 domAutomationController
.setAutomationId(0);
127 domAutomationController
.send(JSON
.stringify(msg
));
128 } else if (this_
.rpc_available
) {
129 // Construct the URL for the RPC request.
131 for (var pname
in params
) {
132 pvalue
= params
[pname
];
133 args
.push(encodeURIComponent(pname
) + '=' + encodeURIComponent(pvalue
));
135 var url
= '/TESTER/' + name
+ '?' + args
.join('&');
136 var req
= new XMLHttpRequest();
137 // Async result handler
139 req
.onreadystatechange = function() {
140 if (req
.readyState
== XMLHttpRequest
.DONE
) {
141 handleRPCResponse(name
, req
);
146 req
.open('GET', url
, this_
.async
);
149 handleRPCResponse(name
, req
);
152 handleRPCFailure(name
, err
.toString());
157 // Pretty prints an error into the DOM.
158 this.logLocalError = function(message
) {
159 this.logLocal(message
, 'red');
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');
171 this.startup = function() {
172 // TODO(ncbray) move into test runner
176 this._log('[STARTUP]');
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.');
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';
193 this.localOutput
.style
.border
= '2px solid #00FF00';
197 this.ping = function() {
201 this.heartbeat = function() {
202 rpcCall('JavaScriptIsAlive', {});
205 this.client_error = function(message
) {
206 this.num_errors
+= 1;
208 var full_message
= '\n[CLIENT_ERROR] ' + exceptionToLogText(message
)
209 // The client error could have been generated by logging - be careful.
211 this._log(full_message
, 'red');
213 // There's not much that can be done, at this point.
217 this.begin = function(test_name
) {
218 var full_message
= '[' + test_name
+ ' BEGIN]'
219 this._log(full_message
, 'blue');
222 this._log = function(message
, color
, from_completed_test
) {
223 if (typeof(message
) != 'string') {
224 message
= toString(message
);
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
) {
231 message
= 'completed test: ' + message
;
234 this.logLocal(message
, color
);
235 rpcCall('TestLog', {message
: message
});
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
);
244 if (typeof(message
) != 'string') {
245 message
= toString(message
);
247 var full_message
= '[' + test_name
+ ' LOG] ' + message
;
248 this._log(full_message
, 'black', from_completed_test
);
252 this.fail = function(test_name
, message
, from_completed_test
) {
253 this.num_failed
+= 1;
255 var full_message
= '[' + test_name
+ ' FAIL] ' + message
256 this._log(full_message
, 'red', from_completed_test
);
259 this.exception = function(test_name
, err
, from_completed_test
) {
260 this.num_errors
+= 1;
262 var message
= exceptionToLogText(err
);
263 var full_message
= '[' + test_name
+ ' EXCEPTION] ' + message
;
264 this._log(full_message
, 'purple', from_completed_test
);
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
);
273 this.blankLine = function() {
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');
284 this.visualError = function() {
285 // Changing the color is defered until testing is done
286 this.ever_failed
= true;
289 this.logLineLocal = function(text
, color
) {
290 text
= text
.replace(/\s+$/, '');
292 this.localOutput
.appendChild(document
.createElement('br'));
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
;
301 div
.appendChild(mNode
);
302 this.localOutput
.appendChild(div
);
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
);
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
) {
329 if (message
!= undefined) {
332 if (info
!= undefined) {
333 parts
.push('(' + info
+ ')');
335 var full_message
= parts
.join(' ');
337 if (test_status
!== undefined) {
339 test_status
.fail(full_message
);
342 throw {type
: 'test_fail', message
: full_message
};
347 function assert(condition
, message
, test_status
) {
349 fail(message
, toString(condition
), test_status
);
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
+ '\'';
365 return obj
.toString();
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(',');
372 if (obj
== undefined) {
375 // There is no way to create a textual representation of this object.
376 return '[UNPRINTABLE]';
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
);
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
);
402 if (a
.length
!= b
.length
) {
405 for (var i
= 0; i
< a
.length
; i
++) {
413 function assertRegexMatches(str
, re
, message
, test_status
) {
414 if (!str
.match(re
)) {
415 fail(message
, toString(str
) + ' doesn\'t match ' + toString(re
.toString()),
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
) {
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
;
453 function ends_with(s
, suffix
) {
454 if (s
.length
>= suffix
.length
) {
455 return s
.substr(s
.length
- suffix
.length
, suffix
.length
) == suffix
;
462 function embed_name(embed
) {
463 if (embed
.name
!= undefined) {
464 if (embed
.id
!= undefined) {
465 return embed
.name
+ ' / ' + embed
.id
;
469 } else if (embed
.id
!= undefined) {
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();
482 xhr
.onerror
= onerror
;
483 xhr
.open('POST', name
, true);
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;
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
);
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();
521 function NaClWaiter(body_element
) {
522 // Work around how JS binds '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
) {
535 return (embed
.readyState
== 4) && !this_
.has_errored(embed
);
538 this.has_errored = function(embed
) {
539 var msg
= embed
.lastError
;
540 return embed
.lastError
!= undefined && embed
.lastError
!= '';
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
);
552 body_element
.addEventListener('loadend', eventListener
, true);
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
]);
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.
570 this.maxTotalWait
= 40000;
572 this.waitForPlugins();
575 this.waitForPlugins = function() {
580 for (var i
= 0; i
< embedsToWaitFor
.length
; i
++) {
582 var e
= embedsToWaitFor
[i
];
583 if (this.has_errored(e
)) {
585 } else if (this.is_loaded(e
)) {
591 // If the module is badly horked, touching lastError, etc, may except.
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
));
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
)
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');
624 // Be careful when interacting with horked nexes.
625 var getCarefully = function (callback
) {
629 return '<exception>';
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.
643 // http://code.google.com/p/nativeclient/issues/detail?id=2428
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();
651 var name
= getCarefully(function(){
652 return embed_name(waiting
[j
]);
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
];
660 var last
= getCarefully(function(){
661 return toString(waiting
[j
].lastError
);
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.
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.
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
;
681 if (exitStatus
=== 0) {
685 var msg
= (name
+ ' did not load. Status: ' + ready
+ ' / ' + last
);
686 if (load_errors_are_test_errors
) {
687 rpc
.client_error(msg
);
697 // Contains the state for a single test.
698 function TestStatus(tester
, name
, async
) {
699 // Work around how JS binds 'this'
701 this.tester
= tester
;
706 this.log = function(message
) {
707 this.tester
.rpc
.log(this.name
, toString(message
), !this.running
);
710 this.pass = function() {
711 // TODO raise if not running.
712 this.tester
.rpc
.pass(this.name
, !this.running
);
717 this.fail = function(message
) {
718 this.tester
.rpc
.fail(this.name
, message
, !this.running
);
723 this._done = function() {
725 this.running
= false;
726 this.tester
.testDone(this);
730 this.assert = function(condition
, message
) {
731 assert(condition
, message
, this);
734 this.assertEqual = function(a
, b
, message
) {
735 assertEqual(a
, b
, message
, this);
738 this.assertRegexMatches = function(a
, b
, message
) {
739 assertRegexMatches(a
, b
, message
, this);
742 this.callbackWrapper = function(callback
, args
) {
747 if (args
=== undefined)
751 callback
.apply(undefined, args
);
753 if (typeof err
== 'object' && 'type' in err
) {
754 if (err
.type
== 'test_halt') {
756 // If we get this exception, we can assume any callbacks or next
757 // tests have already been scheduled.
759 } else if (err
.type
== 'test_fail') {
761 // A special exception that terminates the test with a failure
762 this.tester
.rpc
.fail(this.name
, err
.message
, !this.running
);
767 // This is not a special type of exception, it is an error.
768 this.tester
.rpc
.exception(this.name
, err
, !this.running
);
773 // A normal exit. Should we move on to the next test?
774 // Async tests do not move on without an explicit pass.
776 this.tester
.rpc
.pass(this.name
);
781 // Async callbacks should be wrapped so the tester can catch unexpected
783 this.wrap = function(callback
) {
785 this_
.callbackWrapper(callback
, arguments
);
789 this.setTimeout = function(callback
, time
) {
790 setTimeout(this.wrap(callback
), time
);
793 this.waitForCallback = function(callbackName
, expectedCalls
) {
794 this.log('Waiting for ' + expectedCalls
+ ' invocations of callback: '
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() {
806 this_
.log('Received callback ' + gotCallbacks
);
807 if (gotCallbacks
== expectedCalls
) {
808 this_
.log("Done waiting");
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
822 // This function takes an array of messages and asserts that the nexe
823 // calls PostMessage with each of these messages, in order.
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));
837 this_
.assertEqual(message
.data
, local_messages
.shift());
838 if (callback
!== undefined) {
839 callback(message
.data
);
842 if (local_messages
.length
== 0) {
845 this_
.expectEvent(plugin
, 'message', listener
);
848 this.expectEvent(plugin
, 'message', listener
);
851 this.expectEvent = function(src
, event_type
, listener
) {
852 var wrapper
= this.wrap(function(e
) {
853 src
.removeEventListener(event_type
, wrapper
, false);
856 src
.addEventListener(event_type
, wrapper
, false);
861 function Tester(body_element
) {
862 // Work around how JS binds 'this'
864 // The tests being run.
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;
875 // BEGIN public interface
878 this.loadErrorsAreOK = function() {
879 load_errors_are_test_errors
= false;
882 this.exitCleanlyIsOK = function() {
883 exit_cleanly_is_an_error
= false;
886 this.log = function(message
) {
887 this.rpc
.log(message
);
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});
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});
900 this.run = function() {
902 this.startHeartbeat();
904 function(loaded
, waiting
) {
905 var errored
= logLoadStatus(this_
.rpc
, load_errors_are_test_errors
,
906 exit_cleanly_is_an_error
,
909 this_
.rpc
.blankLine();
910 this_
.rpc
.log('A nexe load error occured, aborting testing.');
913 this_
.startTesting();
922 this.runParallel = function() {
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
]);
935 // END public interface
938 this.startHeartbeat = function() {
940 var heartbeat = function() {
942 setTimeout(heartbeat
, 500);
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
);
957 this._done = function() {
958 this.rpc
.blankLine();
962 this.startTesting = function() {
963 if (tests
.length
== 0) {
964 // No tests specified.
972 for (var i
= 0; i
< tests
.length
; i
++) {
976 // Launch the first test.
981 this.testDone = function(test
) {
983 if (this.testCount
< tests
.length
) {
985 // Move on to the next test if they're being run one at a time.
986 this.launchTest(this.testCount
);