Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Gecko Bug 1811912] Test cases for RTCIceTransport. #44687

Merged
merged 1 commit into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions webrtc/RTCConfiguration-iceTransportPolicy.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,12 @@
t.add_cleanup(() => offerer.close());

offerer.addEventListener('icecandidate',
e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
e => {
if (e.candidate) {
assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
}
}
);

offerer.addTransceiver('audio');
await offerer.setLocalDescription();
Expand All @@ -142,8 +147,13 @@
t.add_cleanup(() => offerer.close());
t.add_cleanup(() => answerer.close());

answerer.addEventListener('icecandidate',
e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));
offerer.addEventListener('icecandidate',
e => {
if (e.candidate) {
assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
}
}
);

offerer.addTransceiver('audio');
const offer = await offerer.createOffer();
Expand Down Expand Up @@ -175,9 +185,14 @@
offerer.setConfiguration({iceTransportPolicy: 'relay'});

offerer.addEventListener('icecandidate',
e => assert_equals(e.candidate, null, 'Should get no ICE candidates'));

await Promise.all([
e => {
if (e.candidate) {
assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
}
}
);

await Promise.all([
exchangeOfferAnswer(offerer, answerer),
waitForIceStateChange(offerer, ['failed']),
waitForIceStateChange(answerer, ['failed']),
Expand All @@ -201,9 +216,13 @@
exchangeIceCandidates(offerer, answerer);

const checkNoCandidate =
e => assert_equals(e.candidate, null, 'Should get no ICE candidates');
e => {
if (e.candidate) {
assert_equals(e.candidate.candidate, '', 'Should get no ICE candidates')
}
};

offerer.addEventListener('icecandidate', checkNoCandidate);
offerer.addEventListener('icecandidate', checkNoCandidate);

await Promise.all([
exchangeOfferAnswer(offerer, answerer),
Expand Down
282 changes: 280 additions & 2 deletions webrtc/RTCIceTransport.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<title>RTCIceTransport</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="RTCPeerConnection-helper.js"></script>
<script src='RTCConfiguration-helper.js'></script>
<script>
'use strict';

Expand Down Expand Up @@ -58,7 +60,7 @@
assert_true(dtlsTransport instanceof RTCDtlsTransport,
'Expect sctp.transport to be an RTCDtlsTransport');

const iceTransport = dtlsTransport.iceTransport;
const {iceTransport} = dtlsTransport;
assert_true(iceTransport instanceof RTCIceTransport,
'Expect dtlsTransport.transport to be an RTCIceTransport');

Expand Down Expand Up @@ -162,7 +164,7 @@
assert_equals(iceTransport2.role, 'controlled',
`Expect answerer's iceTransport to take the controlled role`);
});
}, 'Two connected iceTransports should has matching local/remote candidates returned');
}, 'Two connected iceTransports should have matching local/remote candidates returned');

promise_test(t => {
const pc1 = new RTCPeerConnection();
Expand Down Expand Up @@ -190,4 +192,280 @@
});
}, 'Unconnected iceTransport should have empty remote candidates and selected pair');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = transceiver.sender.transport;
assert_equals(iceTransport.state, 'new');
assert_equals(iceTransport.gatheringState, 'new');
}, 'RTCIceTransport should be in state "new" initially');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = transceiver.sender.transport;
assert_equals(await nextGatheringState(iceTransport), 'gathering');
assert_equals(await nextGatheringState(iceTransport), 'complete');
}, 'RTCIceTransport should transition to "gathering" then "complete", after sLD');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = transceiver.sender.transport;
assert_equals(await nextGatheringState(iceTransport), 'gathering');
pc1.close();
assert_equals(iceTransport.gatheringState, 'gathering');
const result = await Promise.race([
gatheringStateReached(iceTransport, 'complete'),
new Promise(r => t.step_timeout(r, 1000))]);
assert_equals(result, undefined, `Did not expect a statechange after PC.close(), but got one. state is "${result}"`);
}, 'PC.close() should not cause the RTCIceTransport gathering state to transition to "complete"');

promise_test(async t => {
const pc1 = new RTCPeerConnection({bundlePolicy: 'max-bundle'});
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('test');
// TODO: If the spec settles on exposing the sctp transport in
// have-local-offer, we won't need this audio transceiver hack.
// See https://github.com/w3c/webrtc-pc/issues/2898 and
// https://github.com/w3c/webrtc-pc/issues/2899
const transceiver = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = transceiver.sender.transport;
assert_equals(await nextGatheringState(iceTransport), 'gathering');
assert_equals(await nextGatheringState(iceTransport), 'complete');
// TODO: Maybe, maybe not.
assert_not_equals(pc1.sctp, null, 'pc1.sctp should be set after sLD');
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
assert_equals(pc1.sctp.transport.iceTransport, transceiver.sender.transport.iceTransport);
}, 'RTCIceTransport should transition to "gathering", then "complete" after sLD (DataChannel case)');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());

const {sender} = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
// Copy the SDP before it has candidate attrs
const offer = pc1.localDescription;
const checkingReached = connectionStateReached(sender.transport.iceTransport, 'checking');

let result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]);
assert_equals(result, undefined, `Did not expect a statechange right after sLD(offer), but got one. state is "${result}"`);

await pc2.setRemoteDescription(offer);

const firstPc2CandidatePromise =
new Promise(r => pc2.onicecandidate = e => r(e.candidate));

await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

result = await Promise.race([checkingReached, new Promise(r => t.step_timeout(r, 1000))]);
assert_equals(result, undefined, `Did not expect a statechange callback after sRD(answer), but got one. state is "${result}"`);

const candidate = await firstPc2CandidatePromise;
pc1.addIceCandidate(candidate);

await checkingReached;
}, 'RTCIceTransport should not transition to "checking" until after the answer is set _and_ the first remote candidate is received');


promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const {sender} = pc1.addTransceiver('audio');
exchangeIceCandidates(pc1, pc2);
const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
assert_equals(await nextConnectionState(sender.transport.iceTransport), 'checking');
assert_equals(await nextConnectionState(sender.transport.iceTransport), 'connected');
await gatheringDone;
pc2.close();
await connectionStateReached(sender.transport.iceTransport, 'disconnected');
}, 'RTCIceTransport should transition to "disconnected" if packets stop flowing');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('test');
exchangeIceCandidates(pc1, pc2);
const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const {sctp} = pc1;
assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'checking');
assert_equals(await nextConnectionState(sctp.transport.iceTransport), 'connected');
await gatheringDone;
pc2.close();
await connectionStateReached(sctp.transport.iceTransport, 'disconnected');
}, 'RTCIceTransport should transition to "disconnected" if packets stop flowing (DataChannel case)');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const {sender} = pc1.addTransceiver('audio');
await pc1.setLocalDescription();
const {iceTransport} = sender.transport;
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

pc1.restartIce();

await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
}, 'Local ICE restart should not result in a different ICE transport');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('test');
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const {iceTransport} = pc1.sctp.transport;

pc1.restartIce();

await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
}, 'Local ICE restart should not result in a different ICE transport (DataChannel case)');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const {sender} = pc1.addTransceiver('audio');

await pc1.setLocalDescription();
const {iceTransport} = sender.transport;
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);

pc2.restartIce();

await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc1.localDescription);

assert_equals(sender.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
}, 'Remote ICE restart should not result in a different ICE transport');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.createDataChannel('test');

await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const {iceTransport} = pc1.sctp.transport;

pc2.restartIce();

await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc1.localDescription);

assert_equals(pc1.sctp.transport.iceTransport, iceTransport, 'ICE restart should not result in a different ICE transport');
}, 'Remote ICE restart should not result in a different ICE transport (DataChannel case)');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
// Add two transceivers, one audio and one video. The default bundlePolicy
// ("balanced") will result in each being offered with its own transport,
// but allowing the answerer to bundle the second transceiver on the
// transport of the first, which the answerer will do by default.
const audioTransceiver = pc1.addTransceiver('audio');
const videoTransceiver = pc1.addTransceiver('video');
pc1.createDataChannel('test');

await pc1.setLocalDescription();
const audioIceTransport = audioTransceiver.sender.transport.iceTransport;
const videoIceTransport = videoTransceiver.sender.transport.iceTransport;

assert_not_equals(audioIceTransport, videoIceTransport, 'audio and video should start out with different transports');

await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const sctpIceTransport = pc1.sctp.transport.iceTransport;

assert_equals(videoTransceiver.sender.transport.iceTransport, audioIceTransport, 'After negotiation, the video sender should use the bundle ICE transport from the audio sender');
assert_equals(pc1.sctp.transport.iceTransport, audioIceTransport, 'After negotiation, the datachannel should use the bundle ICE transport from the audio sender');
assert_not_equals(videoIceTransport.state, 'closed', 'Completion of offer/answer should not close the unused ICE transport immediately');

await connectionStateReached(videoIceTransport, 'closed');
}, 'RTCIceTransport should transition to "closed" if the underlying transport is closed because the answer used bundle');

promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const {sender} = pc1.addTransceiver('audio');
exchangeIceCandidates(pc1, pc2);
const gatheringDone = Promise.all([gatheringStateReached(pc1, 'complete'), gatheringStateReached(pc2, 'complete')]);
await pc1.setLocalDescription();
const {iceTransport} = sender.transport;
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
assert_equals(await nextConnectionState(iceTransport), 'checking');
assert_equals(await nextConnectionState(iceTransport), 'connected');
await gatheringDone;

const closedEvent = connectionStateReached(iceTransport, 'closed');
pc1.close();
assert_equals(sender.transport.iceTransport, iceTransport, 'PC.close() should not unset the sender transport');
assert_equals(iceTransport.state, 'closed', 'pc.close() should close the sender transport synchronously');
const result = await Promise.race([closedEvent, new Promise(r => t.step_timeout(r, 1000))]);
assert_equals(result, undefined, 'statechange event should not fire when transitioning to closed due to PC.close()');
}, 'RTCIceTransport should synchronously transition to "closed" with no event if the underlying transport is closed due to PC.close()');

</script>
Loading