Bug 1119593 - Update WebRTC tests to use promises more consistently, r=drno,jib

--HG--
extra : rebase_source : 372f545072260a3b27963eae126ace7e3756f2c3
This commit is contained in:
Martin Thomson 2015-01-21 14:34:13 -08:00
parent 8966c2b84a
commit 52eec4cce1
4 changed files with 1172 additions and 2050 deletions

View File

@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var Cc = SpecialPowers.Cc;
var Ci = SpecialPowers.Ci;
var Cr = SpecialPowers.Cr;
@ -101,18 +103,14 @@ function createMediaElement(type, label) {
*
* @param {Dictionary} constraints
* The constraints for this mozGetUserMedia callback
* @param {Function} onSuccess
* The success callback if the stream is successfully retrieved
* @param {Function} onError
* The error callback if the stream fails to be retrieved
*/
function getUserMedia(constraints, onSuccess, onError) {
function getUserMedia(constraints) {
if (!("fake" in constraints) && FAKE_ENABLED) {
constraints["fake"] = FAKE_ENABLED;
}
info("Call getUserMedia for " + JSON.stringify(constraints));
navigator.mozGetUserMedia(constraints, onSuccess, onError);
return navigator.mediaDevices.getUserMedia(constraints);
}
@ -198,29 +196,47 @@ function checkMediaStreamTracks(constraints, mediaStream) {
mediaStream.getVideoTracks());
}
/**
* Utility methods
*/
/*** Utility methods */
/** The dreadful setTimeout, use sparingly */
function wait(time) {
return new Promise(r => setTimeout(r, time));
}
/** The even more dreadful setInterval, use even more sparingly */
function waitUntil(func, time) {
return new Promise(resolve => {
var interval = setInterval(() => {
if (func()) {
clearInterval(interval);
resolve();
}
}, time || 200);
});
}
/**
* Returns the contents of a blob as text
*
* @param {Blob} blob
The blob to retrieve the contents from
* @param {Function} onSuccess
Callback with the blobs content as parameter
*/
function getBlobContent(blob, onSuccess) {
var reader = new FileReader();
function getBlobContent(blob) {
return new Promise(resolve => {
var reader = new FileReader();
// Listen for 'onloadend' which will always be called after a success or failure
reader.onloadend = function (event) {
onSuccess(event.target.result);
};
// Listen for 'onloadend' which will always be called after a success or failure
reader.onloadend = function (event) {
resolve(event.target.result);
};
reader.readAsText(blob);
reader.readAsText(blob);
});
}
/*** Test control flow methods */
/**
* Generates a callback function fired only under unexpected circumstances
* while running the tests. The generated function kills off the test as well
@ -238,7 +254,7 @@ function generateErrorCallback(message) {
* @param {object} aObj
* The object fired back from the callback
*/
return function (aObj) {
return aObj => {
if (aObj) {
if (aObj.name && aObj.message) {
ok(false, "Unexpected callback for '" + aObj.name +
@ -252,10 +268,15 @@ function generateErrorCallback(message) {
ok(false, "Unexpected callback with message = '" + message +
"' at: " + JSON.stringify(stack));
}
SimpleTest.finish();
throw new Error("Unexpected callback");
}
}
var unexpectedEventArrived;
var unexpectedEventArrivedPromise = new Promise((x, reject) => {
unexpectedEventArrived = reject;
});
/**
* Generates a callback function fired only for unexpected events happening.
*
@ -264,17 +285,248 @@ function generateErrorCallback(message) {
* @param {String} eventName
Name of the unexpected event
*/
function unexpectedEventAndFinish(message, eventName) {
function unexpectedEvent(message, eventName) {
var stack = new Error().stack.split("\n");
stack.shift(); // Don't include this instantiation frame
return function () {
ok(false, "Unexpected event '" + eventName + "' fired with message = '" +
message + "' at: " + JSON.stringify(stack));
SimpleTest.finish();
return e => {
var details = "Unexpected event '" + eventName + "' fired with message = '" +
message + "' at: " + JSON.stringify(stack);
ok(false, details);
unexpectedEventArrived(new Error(details));
}
}
/**
* Implements the event guard pattern used throughout. Each of the 'onxxx'
* attributes on the wrappers can be set with a custom handler. Prior to the
* handler being set, if the event fires, it causes the test execution to halt.
* Once but that handler is used exactly once, and subsequent events will also
* cause test execution to halt.
*
* @param {object} wrapper
* The wrapper on which the psuedo-handler is installed
* @param {object} obj
* The real source of events
* @param {string} event
* The name of the event
* @param {function} redirect
* (Optional) a function that determines what is passed to the event
* handler. By default, the handler is passed the wrapper (as opposed to
* the normal cases where they receive an event object). This redirect
* function is passed the event.
*/
function guardEvent(wrapper, obj, event, redirect) {
redirect = redirect || (e => wrapper);
var onx = 'on' + event;
var unexpected = unexpectedEvent(wrapper, event);
wrapper[onx] = unexpected;
obj[onx] = e => {
info(wrapper + ': "on' + event + '" event fired');
wrapper[onx](redirect(e));
wrapper[onx] = unexpected;
};
}
/**
* This class executes a series of functions in a continuous sequence.
* Promise-bearing functions are executed after the previous promise completes.
*
* @constructor
* @param {object} framework
* A back reference to the framework which makes use of the class. It is
* passed to each command callback.
* @param {function[]} commandList
* Commands to set during initialization
*/
function CommandChain(framework, commandList) {
this._framework = framework;
this.commands = commandList || [ ];
}
CommandChain.prototype = {
/**
* Start the command chain. This returns a promise that always resolves
* cleanly (this catches errors and fails the test case).
*/
execute: function () {
return this.commands.reduce((prev, next, i) => {
if (typeof next !== 'function' || !next.name) {
throw new Error('registered non-function' + next);
}
return prev.then(() => {
info('Run step ' + (i + 1) + ': ' + next.name);
return Promise.race([
next(this._framework),
unexpectedEventArrivedPromise
]);
});
}, Promise.resolve())
.catch(e =>
ok(false, 'Error in test execution: ' + e +
((typeof e.stack === 'string') ?
(' ' + e.stack.split('\n').join(' ... ')) : '')));
},
/**
* Add new commands to the end of the chain
*
* @param {function[]} commands
* List of command functions
*/
append: function(commands) {
this.commands = this.commands.concat(commands);
},
/**
* Returns the index of the specified command in the chain.
*
* @param {function|string} id
* Command function or name
* @returns {number} Index of the command
*/
indexOf: function (id) {
if (typeof id === 'string') {
return this.commands.findIndex(f => f.name === id);
}
return this.commands.indexOf(id);
},
/**
* Inserts the new commands after the specified command.
*
* @param {function|string} id
* Command function or name
* @param {function[]} commands
* List of commands
*/
insertAfter: function (id, commands) {
this._insertHelper(id, commands, 1);
},
/**
* Inserts the new commands before the specified command.
*
* @param {string} id
* Command function or name
* @param {function[]} commands
* List of commands
*/
insertBefore: function (id, commands) {
this._insertHelper(id, commands);
},
_insertHelper: function(id, commands, delta) {
delta = delta || 0;
var index = this.indexOf(id);
if (index >= 0) {
this.commands = [].concat(
this.commands.slice(0, index + delta),
commands,
this.commands.slice(index + delta));
}
},
/**
* Removes the specified command
*
* @param {function|string} id
* Command function or name
* @returns {function[]} Removed commands
*/
remove: function (id) {
var index = this.indexOf(id);
if (index >= 0) {
return this.commands.splice(index, 1);
}
return [];
},
/**
* Removes all commands after the specified one.
*
* @param {function|string} id
* Command function or name
* @returns {function[]} Removed commands
*/
removeAfter: function (id) {
var index = this.indexOf(id);
if (index >= 0) {
return this.commands.splice(index + 1);
}
return [];
},
/**
* Removes all commands before the specified one.
*
* @param {function|string} id
* Command function or name
* @returns {function[]} Removed commands
*/
removeBefore: function (id) {
var index = this.indexOf(id);
if (index >= 0) {
return this.commands.splice(0, index);
}
return [];
},
/**
* Replaces a single command.
*
* @param {function|string} id
* Command function or name
* @param {function[]} commands
* List of commands
* @returns {function[]} Removed commands
*/
replace: function (id, commands) {
this.insertBefore(id, commands);
return this.remove(id);
},
/**
* Replaces all commands after the specified one.
*
* @param {function|string} id
* Command function or name
* @returns {object[]} Removed commands
*/
replaceAfter: function (id, commands) {
var oldCommands = this.removeAfter(id);
this.append(commands);
return oldCommands;
},
/**
* Replaces all commands before the specified one.
*
* @param {function|string} id
* Command function or name
* @returns {object[]} Removed commands
*/
replaceBefore: function (id, commands) {
var oldCommands = this.removeBefore(id);
this.insertBefore(id, commands);
return oldCommands;
},
/**
* Remove all commands whose name match the specified regex.
*
* @param {regex} id_match
* Regular expression to match command names.
*/
filterOut: function (id_match) {
this.commands = this.commands.filter(c => !id_match.test(c.name));
}
};
function IsMacOSX10_6orOlder() {
var is106orOlder = false;
@ -289,4 +541,3 @@ function IsMacOSX10_6orOlder() {
}
return is106orOlder;
}

View File

@ -4,127 +4,57 @@
function makeOffererNonTrickle(chain) {
chain.replace('PC_LOCAL_SETUP_ICE_HANDLER', [
['PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER',
function (test) {
test.pcLocalWaitingForEndOfTrickleIce = false;
// We need to install this callback before calling setLocalDescription
// otherwise we might miss callbacks
test.pcLocal.setupIceCandidateHandler(test, function () {
// We ignore ICE candidates because we want the full offer
} , function (label) {
if (test.pcLocalWaitingForEndOfTrickleIce) {
// This callback is needed for slow environments where ICE
// trickling has not finished before the other side needs the
// full SDP. In this case, this call to test.next() will complete
// the PC_REMOTE_WAIT_FOR_OFFER step (see below).
info("Looks like we were still waiting for Trickle to finish");
// TODO replace this with a Promise
test.next();
}
});
// We can't wait for trickle to finish here as it will only start once
// we have called setLocalDescription in the next step
test.next();
}
]
function PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER(test) {
// We need to install this callback before calling setLocalDescription
// otherwise we might miss callbacks
test.pcLocal.setupIceCandidateHandler(test, () => {});
// We ignore ICE candidates because we want the full offer
}
]);
chain.replace('PC_REMOTE_GET_OFFER', [
['PC_REMOTE_WAIT_FOR_OFFER',
function (test) {
if (test.pcLocal.endOfTrickleIce) {
info("Trickle ICE finished already");
test.next();
} else {
info("Waiting for trickle ICE to finish");
test.pcLocalWaitingForEndOfTrickleIce = true;
// In this case we rely on the callback from
// PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER above to proceed to the next
// step once trickle is finished.
}
}
],
['PC_REMOTE_GET_FULL_OFFER',
function (test) {
function PC_REMOTE_GET_FULL_OFFER(test) {
return test.pcLocal.endOfTrickleIce.then(() => {
test._local_offer = test.pcLocal.localDescription;
test._offer_constraints = test.pcLocal.constraints;
test._offer_options = test.pcLocal.offerOptions;
test.next();
}
]
});
}
]);
chain.insertAfter('PC_REMOTE_SANE_REMOTE_SDP', [
['PC_REMOTE_REQUIRE_REMOTE_SDP_CANDIDATES',
function (test) {
info("test.pcLocal.localDescription.sdp: " + JSON.stringify(test.pcLocal.localDescription.sdp));
info("test._local_offer.sdp" + JSON.stringify(test._local_offer.sdp));
ok(!test.localRequiresTrickleIce, "Local does NOT require trickle");
ok(test._local_offer.sdp.contains("a=candidate"), "offer has ICE candidates")
// TODO check for a=end-of-candidates once implemented
test.next();
}
]
function PC_REMOTE_REQUIRE_REMOTE_SDP_CANDIDATES(test) {
info("test.pcLocal.localDescription.sdp: " + JSON.stringify(test.pcLocal.localDescription.sdp));
info("test._local_offer.sdp" + JSON.stringify(test._local_offer.sdp));
ok(!test.localRequiresTrickleIce, "Local does NOT require trickle");
ok(test._local_offer.sdp.contains("a=candidate"), "offer has ICE candidates")
ok(test._local_offer.sdp.contains("a=end-of-candidates"), "offer has end-of-candidates");
}
]);
}
function makeAnswererNonTrickle(chain) {
chain.replace('PC_REMOTE_SETUP_ICE_HANDLER', [
['PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER',
function (test) {
test.pcRemoteWaitingForEndOfTrickleIce = false;
// We need to install this callback before calling setLocalDescription
// otherwise we might miss callbacks
test.pcRemote.setupIceCandidateHandler(test, function () {
// We ignore ICE candidates because we want the full answer
}, function (label) {
if (test.pcRemoteWaitingForEndOfTrickleIce) {
// This callback is needed for slow environments where ICE
// trickling has not finished before the other side needs the
// full SDP. In this case this callback will call the step after
// PC_LOCAL_WAIT_FOR_ANSWER
info("Looks like we were still waiting for Trickle to finish");
// TODO replace this with a Promise
test.next();
}
});
// We can't wait for trickle to finish here as it will only start once
// we have called setLocalDescription in the next step
test.next();
}
]
function PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER(test) {
// We need to install this callback before calling setLocalDescription
// otherwise we might miss callbacks
test.pcRemote.setupIceCandidateHandler(test, () => {});
// We ignore ICE candidates because we want the full offer
}
]);
chain.replace('PC_LOCAL_GET_ANSWER', [
['PC_LOCAL_WAIT_FOR_ANSWER',
function (test) {
if (test.pcRemote.endOfTrickleIce) {
info("Trickle ICE finished already");
test.next();
} else {
info("Waiting for trickle ICE to finish");
test.pcRemoteWaitingForEndOfTrickleIce = true;
// In this case we rely on the callback from
// PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER above to proceed to the next
// step once trickle is finished.
}
}
],
['PC_LOCAL_GET_FULL_ANSWER',
function (test) {
function PC_LOCAL_GET_FULL_ANSWER(test) {
return test.pcRemote.endOfTrickleIce.then(() => {
test._remote_answer = test.pcRemote.localDescription;
test._answer_constraints = test.pcRemote.constraints;
test.next();
}
]
});
}
]);
chain.insertAfter('PC_LOCAL_SANE_REMOTE_SDP', [
['PC_LOCAL_REQUIRE_REMOTE_SDP_CANDIDATES',
function (test) {
info("test.pcRemote.localDescription.sdp: " + JSON.stringify(test.pcRemote.localDescription.sdp));
info("test._remote_answer.sdp" + JSON.stringify(test._remote_answer.sdp));
ok(!test.remoteRequiresTrickleIce, "Remote does NOT require trickle");
ok(test._remote_answer.sdp.contains("a=candidate"), "answer has ICE candidates")
// TODO check for a=end-of-candidates once implemented
test.next();
}
]
function PC_LOCAL_REQUIRE_REMOTE_SDP_CANDIDATES(test) {
info("test.pcRemote.localDescription.sdp: " + JSON.stringify(test.pcRemote.localDescription.sdp));
info("test._remote_answer.sdp" + JSON.stringify(test._remote_answer.sdp));
ok(!test.remoteRequiresTrickleIce, "Remote does NOT require trickle");
ok(test._remote_answer.sdp.contains("a=candidate"), "answer has ICE candidates")
ok(test._remote_answer.sdp.contains("a=end-of-candidates"), "answer has end-of-candidates");
}
]);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff