From 2751f2ab4c6aa7eebf4bdac1ba72ea41d3975adf Mon Sep 17 00:00:00 2001 From: "perkj@webrtc.org" Date: Sun, 23 Nov 2014 16:00:57 +0000 Subject: This adds an Android apk for running tests on the Java layer of PeerConnection. The only testcase is currently the same test we run on Java standalone. To run the test adb shell am instrument -w org.webrtc.test/android.test.InstrumentationTestRunner R=kjellander@webrtc.org, phoglund@webrtc.org Review URL: https://webrtc-codereview.appspot.com/26219004 git-svn-id: http://webrtc.googlecode.com/svn/trunk@7732 4adac7df-926f-26a2-2b94-8c16560cd09d --- talk/app/webrtc/androidtests/AndroidManifest.xml | 28 + talk/app/webrtc/androidtests/ant.properties | 18 + talk/app/webrtc/androidtests/build.xml | 92 +++ talk/app/webrtc/androidtests/jni/Android.mk | 2 + talk/app/webrtc/androidtests/project.properties | 16 + .../androidtests/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 9397 bytes .../androidtests/res/drawable-ldpi/ic_launcher.png | Bin 0 -> 2729 bytes .../androidtests/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 5237 bytes .../res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 14383 bytes .../app/webrtc/androidtests/res/values/strings.xml | 6 + .../src/org/webrtc/PeerConnectionAndroidTest.java | 49 ++ .../src/org/webrtc/PeerConnectionTest.java | 759 ++++++++++++++++++++ .../libjingle_peerconnection_java_unittest.sh | 2 +- .../src/org/webrtc/PeerConnectionTest.java | 797 --------------------- .../src/org/webrtc/PeerConnectionTestJava.java | 62 ++ 15 files changed, 1033 insertions(+), 798 deletions(-) create mode 100644 talk/app/webrtc/androidtests/AndroidManifest.xml create mode 100644 talk/app/webrtc/androidtests/ant.properties create mode 100644 talk/app/webrtc/androidtests/build.xml create mode 100644 talk/app/webrtc/androidtests/jni/Android.mk create mode 100644 talk/app/webrtc/androidtests/project.properties create mode 100644 talk/app/webrtc/androidtests/res/drawable-hdpi/ic_launcher.png create mode 100644 talk/app/webrtc/androidtests/res/drawable-ldpi/ic_launcher.png create mode 100644 talk/app/webrtc/androidtests/res/drawable-mdpi/ic_launcher.png create mode 100644 talk/app/webrtc/androidtests/res/drawable-xhdpi/ic_launcher.png create mode 100644 talk/app/webrtc/androidtests/res/values/strings.xml create mode 100644 talk/app/webrtc/androidtests/src/org/webrtc/PeerConnectionAndroidTest.java create mode 100644 talk/app/webrtc/java/testcommon/src/org/webrtc/PeerConnectionTest.java delete mode 100644 talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java create mode 100644 talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTestJava.java (limited to 'talk/app') diff --git a/talk/app/webrtc/androidtests/AndroidManifest.xml b/talk/app/webrtc/androidtests/AndroidManifest.xml new file mode 100644 index 0000000000..3bcd99b425 --- /dev/null +++ b/talk/app/webrtc/androidtests/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/talk/app/webrtc/androidtests/ant.properties b/talk/app/webrtc/androidtests/ant.properties new file mode 100644 index 0000000000..bc05353865 --- /dev/null +++ b/talk/app/webrtc/androidtests/ant.properties @@ -0,0 +1,18 @@ +# This file is used to override default values used by the Ant build system. +# +# This file must be checked into Version Control Systems, as it is +# integral to the build system of your project. + +# This file is only used by the Ant script. + +# You can use this to override default values such as +# 'source.dir' for the location of your java source folder and +# 'out.dir' for the location of your output folder. + +# You can also use it define how the release builds are signed by declaring +# the following properties: +# 'key.store' for the location of your keystore and +# 'key.alias' for the name of the key to use. +# The password will be asked during the build when you use the 'release' target. + +source.dir=../java/testcommon/src;src \ No newline at end of file diff --git a/talk/app/webrtc/androidtests/build.xml b/talk/app/webrtc/androidtests/build.xml new file mode 100644 index 0000000000..cb4cb7ac94 --- /dev/null +++ b/talk/app/webrtc/androidtests/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/talk/app/webrtc/androidtests/jni/Android.mk b/talk/app/webrtc/androidtests/jni/Android.mk new file mode 100644 index 0000000000..8e80160039 --- /dev/null +++ b/talk/app/webrtc/androidtests/jni/Android.mk @@ -0,0 +1,2 @@ +# This space intentionally left blank (required for Android build system). + diff --git a/talk/app/webrtc/androidtests/project.properties b/talk/app/webrtc/androidtests/project.properties new file mode 100644 index 0000000000..47b70783f2 --- /dev/null +++ b/talk/app/webrtc/androidtests/project.properties @@ -0,0 +1,16 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-21 + +java.compilerargs=-Xlint:all -Werror diff --git a/talk/app/webrtc/androidtests/res/drawable-hdpi/ic_launcher.png b/talk/app/webrtc/androidtests/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000000..96a442e5b8 Binary files /dev/null and b/talk/app/webrtc/androidtests/res/drawable-hdpi/ic_launcher.png differ diff --git a/talk/app/webrtc/androidtests/res/drawable-ldpi/ic_launcher.png b/talk/app/webrtc/androidtests/res/drawable-ldpi/ic_launcher.png new file mode 100644 index 0000000000..99238729d8 Binary files /dev/null and b/talk/app/webrtc/androidtests/res/drawable-ldpi/ic_launcher.png differ diff --git a/talk/app/webrtc/androidtests/res/drawable-mdpi/ic_launcher.png b/talk/app/webrtc/androidtests/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000000..359047dfa4 Binary files /dev/null and b/talk/app/webrtc/androidtests/res/drawable-mdpi/ic_launcher.png differ diff --git a/talk/app/webrtc/androidtests/res/drawable-xhdpi/ic_launcher.png b/talk/app/webrtc/androidtests/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..71c6d760f0 Binary files /dev/null and b/talk/app/webrtc/androidtests/res/drawable-xhdpi/ic_launcher.png differ diff --git a/talk/app/webrtc/androidtests/res/values/strings.xml b/talk/app/webrtc/androidtests/res/values/strings.xml new file mode 100644 index 0000000000..dfe63f89c7 --- /dev/null +++ b/talk/app/webrtc/androidtests/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + AndroidPeerConnectionTests + + diff --git a/talk/app/webrtc/androidtests/src/org/webrtc/PeerConnectionAndroidTest.java b/talk/app/webrtc/androidtests/src/org/webrtc/PeerConnectionAndroidTest.java new file mode 100644 index 0000000000..228d35c128 --- /dev/null +++ b/talk/app/webrtc/androidtests/src/org/webrtc/PeerConnectionAndroidTest.java @@ -0,0 +1,49 @@ +/* + * libjingle + * Copyright 2014, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.webrtc; + +import android.test.ActivityTestCase; + +public class PeerConnectionAndroidTest extends ActivityTestCase { + private PeerConnectionTest test = new PeerConnectionTest(); + + @Override + protected void setUp() { + assertTrue(PeerConnectionFactory.initializeAndroidGlobals( + getInstrumentation().getContext(), true, + true, null)); + } + + public void testCompleteSession() throws Exception { + // TODO(perkj): Investigate if |test.initializeThreadCheck()| can be used + // on android as well. Currently this check fail. + test.doTest(); + } + + +} diff --git a/talk/app/webrtc/java/testcommon/src/org/webrtc/PeerConnectionTest.java b/talk/app/webrtc/java/testcommon/src/org/webrtc/PeerConnectionTest.java new file mode 100644 index 0000000000..8d962cce97 --- /dev/null +++ b/talk/app/webrtc/java/testcommon/src/org/webrtc/PeerConnectionTest.java @@ -0,0 +1,759 @@ +/* + * libjingle + * Copyright 2013, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.webrtc; + +import org.webrtc.PeerConnection.IceConnectionState; +import org.webrtc.PeerConnection.IceGatheringState; +import org.webrtc.PeerConnection.SignalingState; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.TreeSet; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static junit.framework.Assert.*; + +/** End-to-end tests for PeerConnection.java. */ +public class PeerConnectionTest { + // Set to true to render video. + private static final boolean RENDER_TO_GUI = false; + private TreeSet threadsBeforeTest = null; + + private static class ObserverExpectations implements PeerConnection.Observer, + VideoRenderer.Callbacks, + DataChannel.Observer, + StatsObserver { + private final String name; + private int expectedIceCandidates = 0; + private int expectedErrors = 0; + private int expectedRenegotiations = 0; + private int expectedSetSize = 0; + private int previouslySeenWidth = 0; + private int previouslySeenHeight = 0; + private int expectedFramesDelivered = 0; + private LinkedList expectedSignalingChanges = + new LinkedList(); + private LinkedList expectedIceConnectionChanges = + new LinkedList(); + private LinkedList expectedIceGatheringChanges = + new LinkedList(); + private LinkedList expectedAddStreamLabels = + new LinkedList(); + private LinkedList expectedRemoveStreamLabels = + new LinkedList(); + public LinkedList gotIceCandidates = + new LinkedList(); + private Map> renderers = + new IdentityHashMap>(); + private DataChannel dataChannel; + private LinkedList expectedBuffers = + new LinkedList(); + private LinkedList expectedStateChanges = + new LinkedList(); + private LinkedList expectedRemoteDataChannelLabels = + new LinkedList(); + private int expectedStatsCallbacks = 0; + private LinkedList gotStatsReports = + new LinkedList(); + + public ObserverExpectations(String name) { + this.name = name; + } + + public synchronized void setDataChannel(DataChannel dataChannel) { + assertNull(this.dataChannel); + this.dataChannel = dataChannel; + this.dataChannel.registerObserver(this); + assertNotNull(this.dataChannel); + } + + public synchronized void expectIceCandidates(int count) { + expectedIceCandidates += count; + } + + @Override + public synchronized void onIceCandidate(IceCandidate candidate) { + --expectedIceCandidates; + // We don't assert expectedIceCandidates >= 0 because it's hard to know + // how many to expect, in general. We only use expectIceCandidates to + // assert a minimal count. + gotIceCandidates.add(candidate); + } + + public synchronized void expectSetSize() { + if (RENDER_TO_GUI) { + // When new frames are delivered to the GUI renderer we don't get + // notified of frame size info. + return; + } + ++expectedSetSize; + } + + @Override + public synchronized void setSize(int width, int height) { + assertFalse(RENDER_TO_GUI); + assertTrue(--expectedSetSize >= 0); + // Because different camera devices (fake & physical) produce different + // resolutions, we only sanity-check the set sizes, + assertTrue(width > 0); + assertTrue(height > 0); + if (previouslySeenWidth > 0) { + assertEquals(previouslySeenWidth, width); + assertEquals(previouslySeenHeight, height); + } else { + previouslySeenWidth = width; + previouslySeenHeight = height; + } + } + + public synchronized void expectFramesDelivered(int count) { + assertFalse(RENDER_TO_GUI); + expectedFramesDelivered += count; + } + + @Override + public synchronized void renderFrame(VideoRenderer.I420Frame frame) { + --expectedFramesDelivered; + } + + public synchronized void expectSignalingChange(SignalingState newState) { + expectedSignalingChanges.add(newState); + } + + @Override + public synchronized void onSignalingChange(SignalingState newState) { + assertEquals(expectedSignalingChanges.removeFirst(), newState); + } + + public synchronized void expectIceConnectionChange( + IceConnectionState newState) { + expectedIceConnectionChanges.add(newState); + } + + @Override + public synchronized void onIceConnectionChange( + IceConnectionState newState) { + // TODO(bemasc): remove once delivery of ICECompleted is reliable + // (https://code.google.com/p/webrtc/issues/detail?id=3021). + if (newState.equals(IceConnectionState.COMPLETED)) { + return; + } + + assertEquals(expectedIceConnectionChanges.removeFirst(), newState); + } + + public synchronized void expectIceGatheringChange( + IceGatheringState newState) { + expectedIceGatheringChanges.add(newState); + } + + @Override + public synchronized void onIceGatheringChange(IceGatheringState newState) { + // It's fine to get a variable number of GATHERING messages before + // COMPLETE fires (depending on how long the test runs) so we don't assert + // any particular count. + if (newState == IceGatheringState.GATHERING) { + return; + } + assertEquals(expectedIceGatheringChanges.removeFirst(), newState); + } + + public synchronized void expectAddStream(String label) { + expectedAddStreamLabels.add(label); + } + + @Override + public synchronized void onAddStream(MediaStream stream) { + assertEquals(expectedAddStreamLabels.removeFirst(), stream.label()); + assertEquals(1, stream.videoTracks.size()); + assertEquals(1, stream.audioTracks.size()); + assertTrue(stream.videoTracks.get(0).id().endsWith("VideoTrack")); + assertTrue(stream.audioTracks.get(0).id().endsWith("AudioTrack")); + assertEquals("video", stream.videoTracks.get(0).kind()); + assertEquals("audio", stream.audioTracks.get(0).kind()); + VideoRenderer renderer = createVideoRenderer(this); + stream.videoTracks.get(0).addRenderer(renderer); + assertNull(renderers.put( + stream, new WeakReference(renderer))); + } + + public synchronized void expectRemoveStream(String label) { + expectedRemoveStreamLabels.add(label); + } + + @Override + public synchronized void onRemoveStream(MediaStream stream) { + assertEquals(expectedRemoveStreamLabels.removeFirst(), stream.label()); + WeakReference renderer = renderers.remove(stream); + assertNotNull(renderer); + assertNotNull(renderer.get()); + assertEquals(1, stream.videoTracks.size()); + stream.videoTracks.get(0).removeRenderer(renderer.get()); + } + + public synchronized void expectDataChannel(String label) { + expectedRemoteDataChannelLabels.add(label); + } + + @Override + public synchronized void onDataChannel(DataChannel remoteDataChannel) { + assertEquals(expectedRemoteDataChannelLabels.removeFirst(), + remoteDataChannel.label()); + setDataChannel(remoteDataChannel); + assertEquals(DataChannel.State.CONNECTING, dataChannel.state()); + } + + public synchronized void expectRenegotiationNeeded() { + ++expectedRenegotiations; + } + + @Override + public synchronized void onRenegotiationNeeded() { + assertTrue(--expectedRenegotiations >= 0); + } + + public synchronized void expectMessage(ByteBuffer expectedBuffer, + boolean expectedBinary) { + expectedBuffers.add( + new DataChannel.Buffer(expectedBuffer, expectedBinary)); + } + + @Override + public synchronized void onMessage(DataChannel.Buffer buffer) { + DataChannel.Buffer expected = expectedBuffers.removeFirst(); + assertEquals(expected.binary, buffer.binary); + assertTrue(expected.data.equals(buffer.data)); + } + + @Override + public synchronized void onStateChange() { + assertEquals(expectedStateChanges.removeFirst(), dataChannel.state()); + } + + public synchronized void expectStateChange(DataChannel.State state) { + expectedStateChanges.add(state); + } + + @Override + public synchronized void onComplete(StatsReport[] reports) { + if (--expectedStatsCallbacks < 0) { + throw new RuntimeException("Unexpected stats report: " + reports); + } + gotStatsReports.add(reports); + } + + public synchronized void expectStatsCallback() { + ++expectedStatsCallbacks; + } + + public synchronized LinkedList takeStatsReports() { + LinkedList got = gotStatsReports; + gotStatsReports = new LinkedList(); + return got; + } + + // Return a set of expectations that haven't been satisfied yet, possibly + // empty if no such expectations exist. + public synchronized TreeSet unsatisfiedExpectations() { + TreeSet stillWaitingForExpectations = new TreeSet(); + if (expectedIceCandidates > 0) { // See comment in onIceCandidate. + stillWaitingForExpectations.add("expectedIceCandidates"); + } + if (expectedErrors != 0) { + stillWaitingForExpectations.add("expectedErrors: " + expectedErrors); + } + if (expectedSignalingChanges.size() != 0) { + stillWaitingForExpectations.add( + "expectedSignalingChanges: " + expectedSignalingChanges.size()); + } + if (expectedIceConnectionChanges.size() != 0) { + stillWaitingForExpectations.add("expectedIceConnectionChanges: " + + expectedIceConnectionChanges.size()); + } + if (expectedIceGatheringChanges.size() != 0) { + stillWaitingForExpectations.add("expectedIceGatheringChanges: " + + expectedIceGatheringChanges.size()); + } + if (expectedAddStreamLabels.size() != 0) { + stillWaitingForExpectations.add( + "expectedAddStreamLabels: " + expectedAddStreamLabels.size()); + } + if (expectedRemoveStreamLabels.size() != 0) { + stillWaitingForExpectations.add( + "expectedRemoveStreamLabels: " + expectedRemoveStreamLabels.size()); + } + if (expectedSetSize != 0) { + stillWaitingForExpectations.add("expectedSetSize"); + } + if (expectedFramesDelivered > 0) { + stillWaitingForExpectations.add( + "expectedFramesDelivered: " + expectedFramesDelivered); + } + if (!expectedBuffers.isEmpty()) { + stillWaitingForExpectations.add( + "expectedBuffers: " + expectedBuffers.size()); + } + if (!expectedStateChanges.isEmpty()) { + stillWaitingForExpectations.add( + "expectedStateChanges: " + expectedStateChanges.size()); + } + if (!expectedRemoteDataChannelLabels.isEmpty()) { + stillWaitingForExpectations.add("expectedRemoteDataChannelLabels: " + + expectedRemoteDataChannelLabels.size()); + } + if (expectedStatsCallbacks != 0) { + stillWaitingForExpectations.add( + "expectedStatsCallbacks: " + expectedStatsCallbacks); + } + return stillWaitingForExpectations; + } + + public void waitForAllExpectationsToBeSatisfied() { + // TODO(fischman): problems with this approach: + // - come up with something better than a poll loop + // - avoid serializing expectations explicitly; the test is not as robust + // as it could be because it must place expectations between wait + // statements very precisely (e.g. frame must not arrive before its + // expectation, and expectation must not be registered so early as to + // stall a wait). Use callbacks to fire off dependent steps instead of + // explicitly waiting, so there can be just a single wait at the end of + // the test. + TreeSet prev = null; + TreeSet stillWaitingForExpectations = unsatisfiedExpectations(); + while (!stillWaitingForExpectations.isEmpty()) { + if (!stillWaitingForExpectations.equals(prev)) { + System.out.println( + name + " still waiting at\n " + + (new Throwable()).getStackTrace()[1] + + "\n for: " + + Arrays.toString(stillWaitingForExpectations.toArray())); + } + try { + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + prev = stillWaitingForExpectations; + stillWaitingForExpectations = unsatisfiedExpectations(); + } + if (prev == null) { + System.out.println(name + " didn't need to wait at\n " + + (new Throwable()).getStackTrace()[1]); + } + } + } + + private static class SdpObserverLatch implements SdpObserver { + private boolean success = false; + private SessionDescription sdp = null; + private String error = null; + private CountDownLatch latch = new CountDownLatch(1); + + public SdpObserverLatch() {} + + @Override + public void onCreateSuccess(SessionDescription sdp) { + this.sdp = sdp; + onSetSuccess(); + } + + @Override + public void onSetSuccess() { + success = true; + latch.countDown(); + } + + @Override + public void onCreateFailure(String error) { + onSetFailure(error); + } + + @Override + public void onSetFailure(String error) { + this.error = error; + latch.countDown(); + } + + public boolean await() { + try { + assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); + return getSuccess(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public boolean getSuccess() { + return success; + } + + public SessionDescription getSdp() { + return sdp; + } + + public String getError() { + return error; + } + } + + static int videoWindowsMapped = -1; + + private static class TestRenderer implements VideoRenderer.Callbacks { + public int width = -1; + public int height = -1; + public int numFramesDelivered = 0; + + @Override + public void setSize(int width, int height) { + assertEquals(this.width, -1); + assertEquals(this.height, -1); + this.width = width; + this.height = height; + } + + @Override + public void renderFrame(VideoRenderer.I420Frame frame) { + ++numFramesDelivered; + } + } + + private static VideoRenderer createVideoRenderer( + VideoRenderer.Callbacks videoCallbacks) { + if (!RENDER_TO_GUI) { + return new VideoRenderer(videoCallbacks); + } + ++videoWindowsMapped; + assertTrue(videoWindowsMapped < 4); + int x = videoWindowsMapped % 2 != 0 ? 700 : 0; + int y = videoWindowsMapped >= 2 ? 0 : 500; + return VideoRenderer.createGui(x, y); + } + + // Return a weak reference to test that ownership is correctly held by + // PeerConnection, not by test code. + private static WeakReference addTracksToPC( + PeerConnectionFactory factory, PeerConnection pc, + VideoSource videoSource, + String streamLabel, String videoTrackId, String audioTrackId, + VideoRenderer.Callbacks videoCallbacks) { + MediaStream lMS = factory.createLocalMediaStream(streamLabel); + VideoTrack videoTrack = + factory.createVideoTrack(videoTrackId, videoSource); + assertNotNull(videoTrack); + VideoRenderer videoRenderer = createVideoRenderer(videoCallbacks); + assertNotNull(videoRenderer); + videoTrack.addRenderer(videoRenderer); + lMS.addTrack(videoTrack); + // Just for fun, let's remove and re-add the track. + lMS.removeTrack(videoTrack); + lMS.addTrack(videoTrack); + lMS.addTrack(factory.createAudioTrack( + audioTrackId, factory.createAudioSource(new MediaConstraints()))); + pc.addStream(lMS); + return new WeakReference(lMS); + } + + // Used for making sure thread handles are not leaked. + // Call initializeThreadCheck before a test and finalizeThreadCheck after + // a test. + void initializeThreadCheck() { + System.gc(); // Encourage any GC-related threads to start up. + threadsBeforeTest = allThreads(); + } + + void finalizeThreadCheck() throws Exception { + TreeSet threadsAfterTest = allThreads(); + assertEquals(threadsBeforeTest, threadsAfterTest); + Thread.sleep(100); + } + + void doTest() throws Exception { + PeerConnectionFactory factory = new PeerConnectionFactory(); + // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging. + // NOTE: this _must_ happen while |factory| is alive! + // Logging.enableTracing( + // "/tmp/PeerConnectionTest-log.txt", + // EnumSet.of(Logging.TraceLevel.TRACE_ALL), + // Logging.Severity.LS_SENSITIVE); + + MediaConstraints pcConstraints = new MediaConstraints(); + pcConstraints.mandatory.add( + new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); + + LinkedList iceServers = + new LinkedList(); + iceServers.add(new PeerConnection.IceServer( + "stun:stun.l.google.com:19302")); + iceServers.add(new PeerConnection.IceServer( + "turn:fake.example.com", "fakeUsername", "fakePassword")); + ObserverExpectations offeringExpectations = + new ObserverExpectations("PCTest:offerer"); + PeerConnection offeringPC = factory.createPeerConnection( + iceServers, pcConstraints, offeringExpectations); + assertNotNull(offeringPC); + + ObserverExpectations answeringExpectations = + new ObserverExpectations("PCTest:answerer"); + PeerConnection answeringPC = factory.createPeerConnection( + iceServers, pcConstraints, answeringExpectations); + assertNotNull(answeringPC); + + // We want to use the same camera for offerer & answerer, so create it here + // instead of in addTracksToPC. + VideoSource videoSource = factory.createVideoSource( + VideoCapturer.create(""), new MediaConstraints()); + + offeringExpectations.expectSetSize(); + offeringExpectations.expectRenegotiationNeeded(); + WeakReference oLMS = addTracksToPC( + factory, offeringPC, videoSource, "offeredMediaStream", + "offeredVideoTrack", "offeredAudioTrack", offeringExpectations); + + offeringExpectations.expectRenegotiationNeeded(); + DataChannel offeringDC = offeringPC.createDataChannel( + "offeringDC", new DataChannel.Init()); + assertEquals("offeringDC", offeringDC.label()); + + offeringExpectations.setDataChannel(offeringDC); + SdpObserverLatch sdpLatch = new SdpObserverLatch(); + offeringPC.createOffer(sdpLatch, new MediaConstraints()); + assertTrue(sdpLatch.await()); + SessionDescription offerSdp = sdpLatch.getSdp(); + assertEquals(offerSdp.type, SessionDescription.Type.OFFER); + assertFalse(offerSdp.description.isEmpty()); + + sdpLatch = new SdpObserverLatch(); + answeringExpectations.expectSignalingChange( + SignalingState.HAVE_REMOTE_OFFER); + answeringExpectations.expectAddStream("offeredMediaStream"); + // SCTP DataChannels are announced via OPEN messages over the established + // connection (not via SDP), so answeringExpectations can only register + // expecting the channel during ICE, below. + answeringPC.setRemoteDescription(sdpLatch, offerSdp); + assertEquals( + PeerConnection.SignalingState.STABLE, offeringPC.signalingState()); + assertTrue(sdpLatch.await()); + assertNull(sdpLatch.getSdp()); + + answeringExpectations.expectSetSize(); + answeringExpectations.expectRenegotiationNeeded(); + WeakReference aLMS = addTracksToPC( + factory, answeringPC, videoSource, "answeredMediaStream", + "answeredVideoTrack", "answeredAudioTrack", answeringExpectations); + + sdpLatch = new SdpObserverLatch(); + answeringPC.createAnswer(sdpLatch, new MediaConstraints()); + assertTrue(sdpLatch.await()); + SessionDescription answerSdp = sdpLatch.getSdp(); + assertEquals(answerSdp.type, SessionDescription.Type.ANSWER); + assertFalse(answerSdp.description.isEmpty()); + + offeringExpectations.expectIceCandidates(2); + answeringExpectations.expectIceCandidates(2); + + offeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE); + answeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE); + + sdpLatch = new SdpObserverLatch(); + answeringExpectations.expectSignalingChange(SignalingState.STABLE); + answeringPC.setLocalDescription(sdpLatch, answerSdp); + assertTrue(sdpLatch.await()); + assertNull(sdpLatch.getSdp()); + + sdpLatch = new SdpObserverLatch(); + offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER); + offeringPC.setLocalDescription(sdpLatch, offerSdp); + assertTrue(sdpLatch.await()); + assertNull(sdpLatch.getSdp()); + sdpLatch = new SdpObserverLatch(); + offeringExpectations.expectSignalingChange(SignalingState.STABLE); + offeringExpectations.expectAddStream("answeredMediaStream"); + offeringPC.setRemoteDescription(sdpLatch, answerSdp); + assertTrue(sdpLatch.await()); + assertNull(sdpLatch.getSdp()); + + offeringExpectations.waitForAllExpectationsToBeSatisfied(); + answeringExpectations.waitForAllExpectationsToBeSatisfied(); + + assertEquals(offeringPC.getLocalDescription().type, offerSdp.type); + assertEquals(offeringPC.getRemoteDescription().type, answerSdp.type); + assertEquals(answeringPC.getLocalDescription().type, answerSdp.type); + assertEquals(answeringPC.getRemoteDescription().type, offerSdp.type); + + if (!RENDER_TO_GUI) { + // Wait for at least some frames to be delivered at each end (number + // chosen arbitrarily). + offeringExpectations.expectFramesDelivered(10); + answeringExpectations.expectFramesDelivered(10); + offeringExpectations.expectSetSize(); + answeringExpectations.expectSetSize(); + } + + offeringExpectations.expectIceConnectionChange( + IceConnectionState.CHECKING); + offeringExpectations.expectIceConnectionChange( + IceConnectionState.CONNECTED); + // TODO(bemasc): uncomment once delivery of ICECompleted is reliable + // (https://code.google.com/p/webrtc/issues/detail?id=3021). + // + // offeringExpectations.expectIceConnectionChange( + // IceConnectionState.COMPLETED); + answeringExpectations.expectIceConnectionChange( + IceConnectionState.CHECKING); + answeringExpectations.expectIceConnectionChange( + IceConnectionState.CONNECTED); + + offeringExpectations.expectStateChange(DataChannel.State.OPEN); + // See commentary about SCTP DataChannels above for why this is here. + answeringExpectations.expectDataChannel("offeringDC"); + answeringExpectations.expectStateChange(DataChannel.State.OPEN); + + for (IceCandidate candidate : offeringExpectations.gotIceCandidates) { + answeringPC.addIceCandidate(candidate); + } + offeringExpectations.gotIceCandidates.clear(); + for (IceCandidate candidate : answeringExpectations.gotIceCandidates) { + offeringPC.addIceCandidate(candidate); + } + answeringExpectations.gotIceCandidates.clear(); + + offeringExpectations.waitForAllExpectationsToBeSatisfied(); + answeringExpectations.waitForAllExpectationsToBeSatisfied(); + + assertEquals( + PeerConnection.SignalingState.STABLE, offeringPC.signalingState()); + assertEquals( + PeerConnection.SignalingState.STABLE, answeringPC.signalingState()); + + // Test send & receive UTF-8 text. + answeringExpectations.expectMessage( + ByteBuffer.wrap("hello!".getBytes(Charset.forName("UTF-8"))), false); + DataChannel.Buffer buffer = new DataChannel.Buffer( + ByteBuffer.wrap("hello!".getBytes(Charset.forName("UTF-8"))), false); + assertTrue(offeringExpectations.dataChannel.send(buffer)); + answeringExpectations.waitForAllExpectationsToBeSatisfied(); + + // Construct this binary message two different ways to ensure no + // shortcuts are taken. + ByteBuffer expectedBinaryMessage = ByteBuffer.allocateDirect(5); + for (byte i = 1; i < 6; ++i) { + expectedBinaryMessage.put(i); + } + expectedBinaryMessage.flip(); + offeringExpectations.expectMessage(expectedBinaryMessage, true); + assertTrue(answeringExpectations.dataChannel.send( + new DataChannel.Buffer( + ByteBuffer.wrap(new byte[] { 1, 2, 3, 4, 5 }), true))); + offeringExpectations.waitForAllExpectationsToBeSatisfied(); + + offeringExpectations.expectStateChange(DataChannel.State.CLOSING); + answeringExpectations.expectStateChange(DataChannel.State.CLOSING); + offeringExpectations.expectStateChange(DataChannel.State.CLOSED); + answeringExpectations.expectStateChange(DataChannel.State.CLOSED); + answeringExpectations.dataChannel.close(); + offeringExpectations.dataChannel.close(); + + if (RENDER_TO_GUI) { + try { + Thread.sleep(3000); + } catch (Throwable t) { + throw new RuntimeException(t); + } + } + + // TODO(fischman) MOAR test ideas: + // - Test that PC.removeStream() works; requires a second + // createOffer/createAnswer dance. + // - audit each place that uses |constraints| for specifying non-trivial + // constraints (and ensure they're honored). + // - test error cases + // - ensure reasonable coverage of _jni.cc is achieved. Coverage is + // extra-important because of all the free-text (class/method names, etc) + // in JNI-style programming; make sure no typos! + // - Test that shutdown mid-interaction is crash-free. + + // Free the Java-land objects, collect them, and sleep a bit to make sure we + // don't get late-arrival crashes after the Java-land objects have been + // freed. + shutdownPC(offeringPC, offeringExpectations); + offeringPC = null; + shutdownPC(answeringPC, answeringExpectations); + answeringPC = null; + videoSource.dispose(); + factory.dispose(); + System.gc(); + } + + private static void shutdownPC( + PeerConnection pc, ObserverExpectations expectations) { + expectations.dataChannel.unregisterObserver(); + expectations.dataChannel.dispose(); + expectations.expectStatsCallback(); + assertTrue(pc.getStats(expectations, null)); + expectations.waitForAllExpectationsToBeSatisfied(); + expectations.expectIceConnectionChange(IceConnectionState.CLOSED); + expectations.expectSignalingChange(SignalingState.CLOSED); + pc.close(); + expectations.waitForAllExpectationsToBeSatisfied(); + expectations.expectStatsCallback(); + assertTrue(pc.getStats(expectations, null)); + expectations.waitForAllExpectationsToBeSatisfied(); + + System.out.println("FYI stats: "); + int reportIndex = -1; + for (StatsReport[] reports : expectations.takeStatsReports()) { + System.out.println(" Report #" + (++reportIndex)); + for (int i = 0; i < reports.length; ++i) { + System.out.println(" " + reports[i].toString()); + } + } + assertEquals(1, reportIndex); + System.out.println("End stats."); + + pc.dispose(); + } + + // Returns a set of thread IDs belonging to this process, as Strings. + private static TreeSet allThreads() { + TreeSet threads = new TreeSet(); + // This pokes at /proc instead of using the Java APIs because we're also + // looking for libjingle/webrtc native threads, most of which won't have + // attached to the JVM. + for (String threadId : (new File("/proc/self/task")).list()) { + threads.add(threadId); + } + return threads; + } +} diff --git a/talk/app/webrtc/javatests/libjingle_peerconnection_java_unittest.sh b/talk/app/webrtc/javatests/libjingle_peerconnection_java_unittest.sh index 0ecb7309d8..4f8c74ad07 100644 --- a/talk/app/webrtc/javatests/libjingle_peerconnection_java_unittest.sh +++ b/talk/app/webrtc/javatests/libjingle_peerconnection_java_unittest.sh @@ -44,4 +44,4 @@ export LD_LIBRARY_PATH=`pwd` export JAVA_HOME=GYP_JAVA_HOME ${JAVA_HOME}/bin/java -Xcheck:jni -classpath $CLASSPATH \ - junit.textui.TestRunner org.webrtc.PeerConnectionTest + junit.textui.TestRunner org.webrtc.PeerConnectionTestJava diff --git a/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java b/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java deleted file mode 100644 index 048d92b26f..0000000000 --- a/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTest.java +++ /dev/null @@ -1,797 +0,0 @@ -/* - * libjingle - * Copyright 2013, Google Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO - * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package org.webrtc; - -import junit.framework.TestCase; - -import org.junit.Test; -import org.webrtc.PeerConnection.IceConnectionState; -import org.webrtc.PeerConnection.IceGatheringState; -import org.webrtc.PeerConnection.SignalingState; - -import java.io.File; -import java.lang.ref.WeakReference; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.EnumSet; -import java.util.IdentityHashMap; -import java.util.LinkedList; -import java.util.Map; -import java.util.TreeSet; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** End-to-end tests for PeerConnection.java. */ -public class PeerConnectionTest extends TestCase { - // Set to true to render video. - private static final boolean RENDER_TO_GUI = false; - - private static class ObserverExpectations implements PeerConnection.Observer, - VideoRenderer.Callbacks, - DataChannel.Observer, - StatsObserver { - private final String name; - private int expectedIceCandidates = 0; - private int expectedErrors = 0; - private int expectedRenegotiations = 0; - private int expectedSetSize = 0; - private int previouslySeenWidth = 0; - private int previouslySeenHeight = 0; - private int expectedFramesDelivered = 0; - private LinkedList expectedSignalingChanges = - new LinkedList(); - private LinkedList expectedIceConnectionChanges = - new LinkedList(); - private LinkedList expectedIceGatheringChanges = - new LinkedList(); - private LinkedList expectedAddStreamLabels = - new LinkedList(); - private LinkedList expectedRemoveStreamLabels = - new LinkedList(); - public LinkedList gotIceCandidates = - new LinkedList(); - private Map> renderers = - new IdentityHashMap>(); - private DataChannel dataChannel; - private LinkedList expectedBuffers = - new LinkedList(); - private LinkedList expectedStateChanges = - new LinkedList(); - private LinkedList expectedRemoteDataChannelLabels = - new LinkedList(); - private int expectedStatsCallbacks = 0; - private LinkedList gotStatsReports = - new LinkedList(); - - public ObserverExpectations(String name) { - this.name = name; - } - - public synchronized void setDataChannel(DataChannel dataChannel) { - assertNull(this.dataChannel); - this.dataChannel = dataChannel; - this.dataChannel.registerObserver(this); - assertNotNull(this.dataChannel); - } - - public synchronized void expectIceCandidates(int count) { - expectedIceCandidates += count; - } - - @Override - public synchronized void onIceCandidate(IceCandidate candidate) { - --expectedIceCandidates; - // We don't assert expectedIceCandidates >= 0 because it's hard to know - // how many to expect, in general. We only use expectIceCandidates to - // assert a minimal count. - gotIceCandidates.add(candidate); - } - - public synchronized void expectError() { - ++expectedErrors; - } - - public synchronized void expectSetSize() { - if (RENDER_TO_GUI) { - // When new frames are delivered to the GUI renderer we don't get - // notified of frame size info. - return; - } - ++expectedSetSize; - } - - @Override - public synchronized void setSize(int width, int height) { - assertFalse(RENDER_TO_GUI); - assertTrue(--expectedSetSize >= 0); - // Because different camera devices (fake & physical) produce different - // resolutions, we only sanity-check the set sizes, - assertTrue(width > 0); - assertTrue(height > 0); - if (previouslySeenWidth > 0) { - assertEquals(previouslySeenWidth, width); - assertEquals(previouslySeenHeight, height); - } else { - previouslySeenWidth = width; - previouslySeenHeight = height; - } - } - - public synchronized void expectFramesDelivered(int count) { - assertFalse(RENDER_TO_GUI); - expectedFramesDelivered += count; - } - - @Override - public synchronized void renderFrame(VideoRenderer.I420Frame frame) { - --expectedFramesDelivered; - } - - public synchronized void expectSignalingChange(SignalingState newState) { - expectedSignalingChanges.add(newState); - } - - @Override - public synchronized void onSignalingChange(SignalingState newState) { - assertEquals(expectedSignalingChanges.removeFirst(), newState); - } - - public synchronized void expectIceConnectionChange( - IceConnectionState newState) { - expectedIceConnectionChanges.add(newState); - } - - @Override - public synchronized void onIceConnectionChange( - IceConnectionState newState) { - // TODO(bemasc): remove once delivery of ICECompleted is reliable - // (https://code.google.com/p/webrtc/issues/detail?id=3021). - if (newState.equals(IceConnectionState.COMPLETED)) { - return; - } - - assertEquals(expectedIceConnectionChanges.removeFirst(), newState); - } - - public synchronized void expectIceGatheringChange( - IceGatheringState newState) { - expectedIceGatheringChanges.add(newState); - } - - @Override - public synchronized void onIceGatheringChange(IceGatheringState newState) { - // It's fine to get a variable number of GATHERING messages before - // COMPLETE fires (depending on how long the test runs) so we don't assert - // any particular count. - if (newState == IceGatheringState.GATHERING) { - return; - } - assertEquals(expectedIceGatheringChanges.removeFirst(), newState); - } - - public synchronized void expectAddStream(String label) { - expectedAddStreamLabels.add(label); - } - - @Override - public synchronized void onAddStream(MediaStream stream) { - assertEquals(expectedAddStreamLabels.removeFirst(), stream.label()); - assertEquals(1, stream.videoTracks.size()); - assertEquals(1, stream.audioTracks.size()); - assertTrue(stream.videoTracks.get(0).id().endsWith("VideoTrack")); - assertTrue(stream.audioTracks.get(0).id().endsWith("AudioTrack")); - assertEquals("video", stream.videoTracks.get(0).kind()); - assertEquals("audio", stream.audioTracks.get(0).kind()); - VideoRenderer renderer = createVideoRenderer(this); - stream.videoTracks.get(0).addRenderer(renderer); - assertNull(renderers.put( - stream, new WeakReference(renderer))); - } - - public synchronized void expectRemoveStream(String label) { - expectedRemoveStreamLabels.add(label); - } - - @Override - public synchronized void onRemoveStream(MediaStream stream) { - assertEquals(expectedRemoveStreamLabels.removeFirst(), stream.label()); - WeakReference renderer = renderers.remove(stream); - assertNotNull(renderer); - assertNotNull(renderer.get()); - assertEquals(1, stream.videoTracks.size()); - stream.videoTracks.get(0).removeRenderer(renderer.get()); - } - - public synchronized void expectDataChannel(String label) { - expectedRemoteDataChannelLabels.add(label); - } - - @Override - public synchronized void onDataChannel(DataChannel remoteDataChannel) { - assertEquals(expectedRemoteDataChannelLabels.removeFirst(), - remoteDataChannel.label()); - setDataChannel(remoteDataChannel); - assertEquals(DataChannel.State.CONNECTING, dataChannel.state()); - } - - public synchronized void expectRenegotiationNeeded() { - ++expectedRenegotiations; - } - - @Override - public synchronized void onRenegotiationNeeded() { - assertTrue(--expectedRenegotiations >= 0); - } - - public synchronized void expectMessage(ByteBuffer expectedBuffer, - boolean expectedBinary) { - expectedBuffers.add( - new DataChannel.Buffer(expectedBuffer, expectedBinary)); - } - - @Override - public synchronized void onMessage(DataChannel.Buffer buffer) { - DataChannel.Buffer expected = expectedBuffers.removeFirst(); - assertEquals(expected.binary, buffer.binary); - assertTrue(expected.data.equals(buffer.data)); - } - - @Override - public synchronized void onStateChange() { - assertEquals(expectedStateChanges.removeFirst(), dataChannel.state()); - } - - public synchronized void expectStateChange(DataChannel.State state) { - expectedStateChanges.add(state); - } - - @Override - public synchronized void onComplete(StatsReport[] reports) { - if (--expectedStatsCallbacks < 0) { - throw new RuntimeException("Unexpected stats report: " + reports); - } - gotStatsReports.add(reports); - } - - public synchronized void expectStatsCallback() { - ++expectedStatsCallbacks; - } - - public synchronized LinkedList takeStatsReports() { - LinkedList got = gotStatsReports; - gotStatsReports = new LinkedList(); - return got; - } - - // Return a set of expectations that haven't been satisfied yet, possibly - // empty if no such expectations exist. - public synchronized TreeSet unsatisfiedExpectations() { - TreeSet stillWaitingForExpectations = new TreeSet(); - if (expectedIceCandidates > 0) { // See comment in onIceCandidate. - stillWaitingForExpectations.add("expectedIceCandidates"); - } - if (expectedErrors != 0) { - stillWaitingForExpectations.add("expectedErrors: " + expectedErrors); - } - if (expectedSignalingChanges.size() != 0) { - stillWaitingForExpectations.add( - "expectedSignalingChanges: " + expectedSignalingChanges.size()); - } - if (expectedIceConnectionChanges.size() != 0) { - stillWaitingForExpectations.add("expectedIceConnectionChanges: " + - expectedIceConnectionChanges.size()); - } - if (expectedIceGatheringChanges.size() != 0) { - stillWaitingForExpectations.add("expectedIceGatheringChanges: " + - expectedIceGatheringChanges.size()); - } - if (expectedAddStreamLabels.size() != 0) { - stillWaitingForExpectations.add( - "expectedAddStreamLabels: " + expectedAddStreamLabels.size()); - } - if (expectedRemoveStreamLabels.size() != 0) { - stillWaitingForExpectations.add( - "expectedRemoveStreamLabels: " + expectedRemoveStreamLabels.size()); - } - if (expectedSetSize != 0) { - stillWaitingForExpectations.add("expectedSetSize"); - } - if (expectedFramesDelivered > 0) { - stillWaitingForExpectations.add( - "expectedFramesDelivered: " + expectedFramesDelivered); - } - if (!expectedBuffers.isEmpty()) { - stillWaitingForExpectations.add( - "expectedBuffers: " + expectedBuffers.size()); - } - if (!expectedStateChanges.isEmpty()) { - stillWaitingForExpectations.add( - "expectedStateChanges: " + expectedStateChanges.size()); - } - if (!expectedRemoteDataChannelLabels.isEmpty()) { - stillWaitingForExpectations.add("expectedRemoteDataChannelLabels: " + - expectedRemoteDataChannelLabels.size()); - } - if (expectedStatsCallbacks != 0) { - stillWaitingForExpectations.add( - "expectedStatsCallbacks: " + expectedStatsCallbacks); - } - return stillWaitingForExpectations; - } - - public void waitForAllExpectationsToBeSatisfied() { - // TODO(fischman): problems with this approach: - // - come up with something better than a poll loop - // - avoid serializing expectations explicitly; the test is not as robust - // as it could be because it must place expectations between wait - // statements very precisely (e.g. frame must not arrive before its - // expectation, and expectation must not be registered so early as to - // stall a wait). Use callbacks to fire off dependent steps instead of - // explicitly waiting, so there can be just a single wait at the end of - // the test. - TreeSet prev = null; - TreeSet stillWaitingForExpectations = unsatisfiedExpectations(); - while (!stillWaitingForExpectations.isEmpty()) { - if (!stillWaitingForExpectations.equals(prev)) { - System.out.println( - name + " still waiting at\n " + - (new Throwable()).getStackTrace()[1] + - "\n for: " + - Arrays.toString(stillWaitingForExpectations.toArray())); - } - try { - Thread.sleep(10); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - prev = stillWaitingForExpectations; - stillWaitingForExpectations = unsatisfiedExpectations(); - } - if (prev == null) { - System.out.println(name + " didn't need to wait at\n " + - (new Throwable()).getStackTrace()[1]); - } - } - } - - private static class SdpObserverLatch implements SdpObserver { - private boolean success = false; - private SessionDescription sdp = null; - private String error = null; - private CountDownLatch latch = new CountDownLatch(1); - - public SdpObserverLatch() {} - - @Override - public void onCreateSuccess(SessionDescription sdp) { - this.sdp = sdp; - onSetSuccess(); - } - - @Override - public void onSetSuccess() { - success = true; - latch.countDown(); - } - - @Override - public void onCreateFailure(String error) { - onSetFailure(error); - } - - @Override - public void onSetFailure(String error) { - this.error = error; - latch.countDown(); - } - - public boolean await() { - try { - assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); - return getSuccess(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public boolean getSuccess() { - return success; - } - - public SessionDescription getSdp() { - return sdp; - } - - public String getError() { - return error; - } - } - - static int videoWindowsMapped = -1; - - private static class TestRenderer implements VideoRenderer.Callbacks { - public int width = -1; - public int height = -1; - public int numFramesDelivered = 0; - - public void setSize(int width, int height) { - assertEquals(this.width, -1); - assertEquals(this.height, -1); - this.width = width; - this.height = height; - } - - public void renderFrame(VideoRenderer.I420Frame frame) { - ++numFramesDelivered; - } - } - - private static VideoRenderer createVideoRenderer( - VideoRenderer.Callbacks videoCallbacks) { - if (!RENDER_TO_GUI) { - return new VideoRenderer(videoCallbacks); - } - ++videoWindowsMapped; - assertTrue(videoWindowsMapped < 4); - int x = videoWindowsMapped % 2 != 0 ? 700 : 0; - int y = videoWindowsMapped >= 2 ? 0 : 500; - return VideoRenderer.createGui(x, y); - } - - // Return a weak reference to test that ownership is correctly held by - // PeerConnection, not by test code. - private static WeakReference addTracksToPC( - PeerConnectionFactory factory, PeerConnection pc, - VideoSource videoSource, - String streamLabel, String videoTrackId, String audioTrackId, - VideoRenderer.Callbacks videoCallbacks) { - MediaStream lMS = factory.createLocalMediaStream(streamLabel); - VideoTrack videoTrack = - factory.createVideoTrack(videoTrackId, videoSource); - assertNotNull(videoTrack); - VideoRenderer videoRenderer = createVideoRenderer(videoCallbacks); - assertNotNull(videoRenderer); - videoTrack.addRenderer(videoRenderer); - lMS.addTrack(videoTrack); - // Just for fun, let's remove and re-add the track. - lMS.removeTrack(videoTrack); - lMS.addTrack(videoTrack); - lMS.addTrack(factory.createAudioTrack( - audioTrackId, factory.createAudioSource(new MediaConstraints()))); - pc.addStream(lMS); - return new WeakReference(lMS); - } - - private static void assertEquals( - SessionDescription lhs, SessionDescription rhs) { - assertEquals(lhs.type, rhs.type); - assertEquals(lhs.description, rhs.description); - } - - @Test - public void testCompleteSession() throws Exception { - doTest(); - } - - @Test - public void testCompleteSessionOnNonMainThread() throws Exception { - final Exception[] exceptionHolder = new Exception[1]; - Thread nonMainThread = new Thread("PeerConnectionTest-nonMainThread") { - @Override public void run() { - try { - doTest(); - } catch (Exception e) { - exceptionHolder[0] = e; - } - } - }; - nonMainThread.start(); - nonMainThread.join(); - if (exceptionHolder[0] != null) - throw exceptionHolder[0]; - } - - private void doTest() throws Exception { - CountDownLatch testDone = new CountDownLatch(1); - System.gc(); // Encourage any GC-related threads to start up. - TreeSet threadsBeforeTest = allThreads(); - - PeerConnectionFactory factory = new PeerConnectionFactory(); - // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging. - // NOTE: this _must_ happen while |factory| is alive! - // Logging.enableTracing( - // "/tmp/PeerConnectionTest-log.txt", - // EnumSet.of(Logging.TraceLevel.TRACE_ALL), - // Logging.Severity.LS_SENSITIVE); - - MediaConstraints pcConstraints = new MediaConstraints(); - pcConstraints.mandatory.add( - new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")); - - LinkedList iceServers = - new LinkedList(); - iceServers.add(new PeerConnection.IceServer( - "stun:stun.l.google.com:19302")); - iceServers.add(new PeerConnection.IceServer( - "turn:fake.example.com", "fakeUsername", "fakePassword")); - ObserverExpectations offeringExpectations = - new ObserverExpectations("PCTest:offerer"); - PeerConnection offeringPC = factory.createPeerConnection( - iceServers, pcConstraints, offeringExpectations); - assertNotNull(offeringPC); - - ObserverExpectations answeringExpectations = - new ObserverExpectations("PCTest:answerer"); - PeerConnection answeringPC = factory.createPeerConnection( - iceServers, pcConstraints, answeringExpectations); - assertNotNull(answeringPC); - - // We want to use the same camera for offerer & answerer, so create it here - // instead of in addTracksToPC. - VideoSource videoSource = factory.createVideoSource( - VideoCapturer.create(""), new MediaConstraints()); - - offeringExpectations.expectSetSize(); - offeringExpectations.expectRenegotiationNeeded(); - WeakReference oLMS = addTracksToPC( - factory, offeringPC, videoSource, "offeredMediaStream", - "offeredVideoTrack", "offeredAudioTrack", offeringExpectations); - - offeringExpectations.expectRenegotiationNeeded(); - DataChannel offeringDC = offeringPC.createDataChannel( - "offeringDC", new DataChannel.Init()); - assertEquals("offeringDC", offeringDC.label()); - - offeringExpectations.setDataChannel(offeringDC); - SdpObserverLatch sdpLatch = new SdpObserverLatch(); - offeringPC.createOffer(sdpLatch, new MediaConstraints()); - assertTrue(sdpLatch.await()); - SessionDescription offerSdp = sdpLatch.getSdp(); - assertEquals(offerSdp.type, SessionDescription.Type.OFFER); - assertFalse(offerSdp.description.isEmpty()); - - sdpLatch = new SdpObserverLatch(); - answeringExpectations.expectSignalingChange( - SignalingState.HAVE_REMOTE_OFFER); - answeringExpectations.expectAddStream("offeredMediaStream"); - // SCTP DataChannels are announced via OPEN messages over the established - // connection (not via SDP), so answeringExpectations can only register - // expecting the channel during ICE, below. - answeringPC.setRemoteDescription(sdpLatch, offerSdp); - assertEquals( - PeerConnection.SignalingState.STABLE, offeringPC.signalingState()); - assertTrue(sdpLatch.await()); - assertNull(sdpLatch.getSdp()); - - answeringExpectations.expectSetSize(); - answeringExpectations.expectRenegotiationNeeded(); - WeakReference aLMS = addTracksToPC( - factory, answeringPC, videoSource, "answeredMediaStream", - "answeredVideoTrack", "answeredAudioTrack", answeringExpectations); - - sdpLatch = new SdpObserverLatch(); - answeringPC.createAnswer(sdpLatch, new MediaConstraints()); - assertTrue(sdpLatch.await()); - SessionDescription answerSdp = sdpLatch.getSdp(); - assertEquals(answerSdp.type, SessionDescription.Type.ANSWER); - assertFalse(answerSdp.description.isEmpty()); - - offeringExpectations.expectIceCandidates(2); - answeringExpectations.expectIceCandidates(2); - - offeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE); - answeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE); - - sdpLatch = new SdpObserverLatch(); - answeringExpectations.expectSignalingChange(SignalingState.STABLE); - answeringPC.setLocalDescription(sdpLatch, answerSdp); - assertTrue(sdpLatch.await()); - assertNull(sdpLatch.getSdp()); - - sdpLatch = new SdpObserverLatch(); - offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER); - offeringPC.setLocalDescription(sdpLatch, offerSdp); - assertTrue(sdpLatch.await()); - assertNull(sdpLatch.getSdp()); - sdpLatch = new SdpObserverLatch(); - offeringExpectations.expectSignalingChange(SignalingState.STABLE); - offeringExpectations.expectAddStream("answeredMediaStream"); - offeringPC.setRemoteDescription(sdpLatch, answerSdp); - assertTrue(sdpLatch.await()); - assertNull(sdpLatch.getSdp()); - - offeringExpectations.waitForAllExpectationsToBeSatisfied(); - answeringExpectations.waitForAllExpectationsToBeSatisfied(); - - assertEquals(offeringPC.getLocalDescription().type, offerSdp.type); - assertEquals(offeringPC.getRemoteDescription().type, answerSdp.type); - assertEquals(answeringPC.getLocalDescription().type, answerSdp.type); - assertEquals(answeringPC.getRemoteDescription().type, offerSdp.type); - - if (!RENDER_TO_GUI) { - // Wait for at least some frames to be delivered at each end (number - // chosen arbitrarily). - offeringExpectations.expectFramesDelivered(10); - answeringExpectations.expectFramesDelivered(10); - offeringExpectations.expectSetSize(); - answeringExpectations.expectSetSize(); - } - - offeringExpectations.expectIceConnectionChange( - IceConnectionState.CHECKING); - offeringExpectations.expectIceConnectionChange( - IceConnectionState.CONNECTED); - // TODO(bemasc): uncomment once delivery of ICECompleted is reliable - // (https://code.google.com/p/webrtc/issues/detail?id=3021). - // - // offeringExpectations.expectIceConnectionChange( - // IceConnectionState.COMPLETED); - answeringExpectations.expectIceConnectionChange( - IceConnectionState.CHECKING); - answeringExpectations.expectIceConnectionChange( - IceConnectionState.CONNECTED); - - offeringExpectations.expectStateChange(DataChannel.State.OPEN); - // See commentary about SCTP DataChannels above for why this is here. - answeringExpectations.expectDataChannel("offeringDC"); - answeringExpectations.expectStateChange(DataChannel.State.OPEN); - - for (IceCandidate candidate : offeringExpectations.gotIceCandidates) { - answeringPC.addIceCandidate(candidate); - } - offeringExpectations.gotIceCandidates.clear(); - for (IceCandidate candidate : answeringExpectations.gotIceCandidates) { - offeringPC.addIceCandidate(candidate); - } - answeringExpectations.gotIceCandidates.clear(); - - offeringExpectations.waitForAllExpectationsToBeSatisfied(); - answeringExpectations.waitForAllExpectationsToBeSatisfied(); - - assertEquals( - PeerConnection.SignalingState.STABLE, offeringPC.signalingState()); - assertEquals( - PeerConnection.SignalingState.STABLE, answeringPC.signalingState()); - - // Test send & receive UTF-8 text. - answeringExpectations.expectMessage( - ByteBuffer.wrap("hello!".getBytes(Charset.forName("UTF-8"))), false); - DataChannel.Buffer buffer = new DataChannel.Buffer( - ByteBuffer.wrap("hello!".getBytes(Charset.forName("UTF-8"))), false); - assertTrue(offeringExpectations.dataChannel.send(buffer)); - answeringExpectations.waitForAllExpectationsToBeSatisfied(); - - // Construct this binary message two different ways to ensure no - // shortcuts are taken. - ByteBuffer expectedBinaryMessage = ByteBuffer.allocateDirect(5); - for (byte i = 1; i < 6; ++i) { - expectedBinaryMessage.put(i); - } - expectedBinaryMessage.flip(); - offeringExpectations.expectMessage(expectedBinaryMessage, true); - assertTrue(answeringExpectations.dataChannel.send( - new DataChannel.Buffer( - ByteBuffer.wrap(new byte[] { 1, 2, 3, 4, 5 }), true))); - offeringExpectations.waitForAllExpectationsToBeSatisfied(); - - offeringExpectations.expectStateChange(DataChannel.State.CLOSING); - answeringExpectations.expectStateChange(DataChannel.State.CLOSING); - offeringExpectations.expectStateChange(DataChannel.State.CLOSED); - answeringExpectations.expectStateChange(DataChannel.State.CLOSED); - answeringExpectations.dataChannel.close(); - offeringExpectations.dataChannel.close(); - - if (RENDER_TO_GUI) { - try { - Thread.sleep(3000); - } catch (Throwable t) { - throw new RuntimeException(t); - } - } - - // TODO(fischman) MOAR test ideas: - // - Test that PC.removeStream() works; requires a second - // createOffer/createAnswer dance. - // - audit each place that uses |constraints| for specifying non-trivial - // constraints (and ensure they're honored). - // - test error cases - // - ensure reasonable coverage of _jni.cc is achieved. Coverage is - // extra-important because of all the free-text (class/method names, etc) - // in JNI-style programming; make sure no typos! - // - Test that shutdown mid-interaction is crash-free. - - // Free the Java-land objects, collect them, and sleep a bit to make sure we - // don't get late-arrival crashes after the Java-land objects have been - // freed. - shutdownPC(offeringPC, offeringExpectations); - offeringPC = null; - shutdownPC(answeringPC, answeringExpectations); - answeringPC = null; - videoSource.dispose(); - factory.dispose(); - System.gc(); - - TreeSet threadsAfterTest = allThreads(); - assertEquals(threadsBeforeTest, threadsAfterTest); - Thread.sleep(100); - } - - private static void shutdownPC( - PeerConnection pc, ObserverExpectations expectations) { - expectations.dataChannel.unregisterObserver(); - expectations.dataChannel.dispose(); - expectations.expectStatsCallback(); - assertTrue(pc.getStats(expectations, null)); - expectations.waitForAllExpectationsToBeSatisfied(); - expectations.expectIceConnectionChange(IceConnectionState.CLOSED); - expectations.expectSignalingChange(SignalingState.CLOSED); - pc.close(); - expectations.waitForAllExpectationsToBeSatisfied(); - expectations.expectStatsCallback(); - assertTrue(pc.getStats(expectations, null)); - expectations.waitForAllExpectationsToBeSatisfied(); - - System.out.println("FYI stats: "); - int reportIndex = -1; - for (StatsReport[] reports : expectations.takeStatsReports()) { - System.out.println(" Report #" + (++reportIndex)); - for (int i = 0; i < reports.length; ++i) { - System.out.println(" " + reports[i].toString()); - } - } - assertEquals(1, reportIndex); - System.out.println("End stats."); - - pc.dispose(); - } - - // Returns a set of thread IDs belonging to this process, as Strings. - private static TreeSet allThreads() { - TreeSet threads = new TreeSet(); - // This pokes at /proc instead of using the Java APIs because we're also - // looking for libjingle/webrtc native threads, most of which won't have - // attached to the JVM. - for (String threadId : (new File("/proc/self/task")).list()) { - threads.add(threadId); - } - return threads; - } - - // Return a String form of |strings| joined by |separator|. - private static String joinStrings(String separator, TreeSet strings) { - StringBuilder builder = new StringBuilder(); - for (String s : strings) { - if (builder.length() > 0) { - builder.append(separator); - } - builder.append(s); - } - return builder.toString(); - } -} diff --git a/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTestJava.java b/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTestJava.java new file mode 100644 index 0000000000..2f0419c426 --- /dev/null +++ b/talk/app/webrtc/javatests/src/org/webrtc/PeerConnectionTestJava.java @@ -0,0 +1,62 @@ +/* + * libjingle + * Copyright 2014, Google Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.webrtc; + + +import junit.framework.TestCase; + +/** End-to-end tests for PeerConnection.java. */ +public class PeerConnectionTestJava extends TestCase { + private PeerConnectionTest test = new PeerConnectionTest(); + @Test + public void testCompleteSession() throws Exception { + test.initializeThreadCheck(); + test.doTest(); + test.finalizeThreadCheck(); + } + + @Test + public void testCompleteSessionOnNonMainThread() throws Exception { + final Exception[] exceptionHolder = new Exception[1]; + Thread nonMainThread = new Thread("PeerConnectionTest-nonMainThread") { + @Override public void run() { + try { + test.initializeThreadCheck(); + test.doTest(); + test.finalizeThreadCheck(); + } catch (Exception e) { + exceptionHolder[0] = e; + } + } + }; + nonMainThread.start(); + nonMainThread.join(); + if (exceptionHolder[0] != null) + throw exceptionHolder[0]; + } +} \ No newline at end of file -- cgit v1.2.3