1/*
2 * libjingle
3 * Copyright 2013, Google Inc.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 *  1. Redistributions of source code must retain the above copyright notice,
9 *     this list of conditions and the following disclaimer.
10 *  2. Redistributions in binary form must reproduce the above copyright notice,
11 *     this list of conditions and the following disclaimer in the documentation
12 *     and/or other materials provided with the distribution.
13 *  3. The name of the author may not be used to endorse or promote products
14 *     derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28package org.webrtc;
29
30import junit.framework.TestCase;
31
32import org.junit.Test;
33import org.webrtc.PeerConnection.IceConnectionState;
34import org.webrtc.PeerConnection.IceGatheringState;
35import org.webrtc.PeerConnection.SignalingState;
36
37import java.io.File;
38import java.lang.ref.WeakReference;
39import java.nio.ByteBuffer;
40import java.nio.charset.Charset;
41import java.util.Arrays;
42import java.util.EnumSet;
43import java.util.IdentityHashMap;
44import java.util.LinkedList;
45import java.util.Map;
46import java.util.TreeSet;
47import java.util.concurrent.CountDownLatch;
48import java.util.concurrent.TimeUnit;
49
50/** End-to-end tests for PeerConnection.java. */
51public class PeerConnectionTest extends TestCase {
52  // Set to true to render video.
53  private static final boolean RENDER_TO_GUI = false;
54
55  private static class ObserverExpectations implements PeerConnection.Observer,
56                                            VideoRenderer.Callbacks,
57                                            DataChannel.Observer,
58                                            StatsObserver {
59    private final String name;
60    private int expectedIceCandidates = 0;
61    private int expectedErrors = 0;
62    private int expectedRenegotiations = 0;
63    private int expectedSetSize = 0;
64    private int previouslySeenWidth = 0;
65    private int previouslySeenHeight = 0;
66    private int expectedFramesDelivered = 0;
67    private LinkedList<SignalingState> expectedSignalingChanges =
68        new LinkedList<SignalingState>();
69    private LinkedList<IceConnectionState> expectedIceConnectionChanges =
70        new LinkedList<IceConnectionState>();
71    private LinkedList<IceGatheringState> expectedIceGatheringChanges =
72        new LinkedList<IceGatheringState>();
73    private LinkedList<String> expectedAddStreamLabels =
74        new LinkedList<String>();
75    private LinkedList<String> expectedRemoveStreamLabels =
76        new LinkedList<String>();
77    public LinkedList<IceCandidate> gotIceCandidates =
78        new LinkedList<IceCandidate>();
79    private Map<MediaStream, WeakReference<VideoRenderer>> renderers =
80        new IdentityHashMap<MediaStream, WeakReference<VideoRenderer>>();
81    private DataChannel dataChannel;
82    private LinkedList<DataChannel.Buffer> expectedBuffers =
83        new LinkedList<DataChannel.Buffer>();
84    private LinkedList<DataChannel.State> expectedStateChanges =
85        new LinkedList<DataChannel.State>();
86    private LinkedList<String> expectedRemoteDataChannelLabels =
87        new LinkedList<String>();
88    private int expectedStatsCallbacks = 0;
89    private LinkedList<StatsReport[]> gotStatsReports =
90        new LinkedList<StatsReport[]>();
91
92    public ObserverExpectations(String name) {
93      this.name = name;
94    }
95
96    public synchronized void setDataChannel(DataChannel dataChannel) {
97      assertNull(this.dataChannel);
98      this.dataChannel = dataChannel;
99      this.dataChannel.registerObserver(this);
100      assertNotNull(this.dataChannel);
101    }
102
103    public synchronized void expectIceCandidates(int count) {
104      expectedIceCandidates += count;
105    }
106
107    @Override
108    public synchronized void onIceCandidate(IceCandidate candidate) {
109      --expectedIceCandidates;
110      // We don't assert expectedIceCandidates >= 0 because it's hard to know
111      // how many to expect, in general.  We only use expectIceCandidates to
112      // assert a minimal count.
113      gotIceCandidates.add(candidate);
114    }
115
116    public synchronized void expectError() {
117      ++expectedErrors;
118    }
119
120    @Override
121    public synchronized void onError() {
122      assertTrue(--expectedErrors >= 0);
123    }
124
125    public synchronized void expectSetSize() {
126      if (RENDER_TO_GUI) {
127        // When new frames are delivered to the GUI renderer we don't get
128        // notified of frame size info.
129        return;
130      }
131      ++expectedSetSize;
132    }
133
134    @Override
135    public synchronized void setSize(int width, int height) {
136      assertFalse(RENDER_TO_GUI);
137      assertTrue(--expectedSetSize >= 0);
138      // Because different camera devices (fake & physical) produce different
139      // resolutions, we only sanity-check the set sizes,
140      assertTrue(width > 0);
141      assertTrue(height > 0);
142      if (previouslySeenWidth > 0) {
143        assertEquals(previouslySeenWidth, width);
144        assertEquals(previouslySeenHeight, height);
145      } else {
146        previouslySeenWidth = width;
147        previouslySeenHeight = height;
148      }
149    }
150
151    public synchronized void expectFramesDelivered(int count) {
152      assertFalse(RENDER_TO_GUI);
153      expectedFramesDelivered += count;
154    }
155
156    @Override
157    public synchronized void renderFrame(VideoRenderer.I420Frame frame) {
158      --expectedFramesDelivered;
159    }
160
161    public synchronized void expectSignalingChange(SignalingState newState) {
162      expectedSignalingChanges.add(newState);
163    }
164
165    @Override
166    public synchronized void onSignalingChange(SignalingState newState) {
167      assertEquals(expectedSignalingChanges.removeFirst(), newState);
168    }
169
170    public synchronized void expectIceConnectionChange(
171        IceConnectionState newState) {
172      expectedIceConnectionChanges.add(newState);
173    }
174
175    @Override
176    public synchronized void onIceConnectionChange(
177        IceConnectionState newState) {
178      // TODO(bemasc): remove once delivery of ICECompleted is reliable
179      // (https://code.google.com/p/webrtc/issues/detail?id=3021).
180      if (newState.equals(IceConnectionState.COMPLETED)) {
181        return;
182      }
183
184      assertEquals(expectedIceConnectionChanges.removeFirst(), newState);
185    }
186
187    public synchronized void expectIceGatheringChange(
188        IceGatheringState newState) {
189      expectedIceGatheringChanges.add(newState);
190    }
191
192    @Override
193    public synchronized void onIceGatheringChange(IceGatheringState newState) {
194      // It's fine to get a variable number of GATHERING messages before
195      // COMPLETE fires (depending on how long the test runs) so we don't assert
196      // any particular count.
197      if (newState == IceGatheringState.GATHERING) {
198        return;
199      }
200      assertEquals(expectedIceGatheringChanges.removeFirst(), newState);
201    }
202
203    public synchronized void expectAddStream(String label) {
204      expectedAddStreamLabels.add(label);
205    }
206
207    @Override
208    public synchronized void onAddStream(MediaStream stream) {
209      assertEquals(expectedAddStreamLabels.removeFirst(), stream.label());
210      assertEquals(1, stream.videoTracks.size());
211      assertEquals(1, stream.audioTracks.size());
212      assertTrue(stream.videoTracks.get(0).id().endsWith("VideoTrack"));
213      assertTrue(stream.audioTracks.get(0).id().endsWith("AudioTrack"));
214      assertEquals("video", stream.videoTracks.get(0).kind());
215      assertEquals("audio", stream.audioTracks.get(0).kind());
216      VideoRenderer renderer = createVideoRenderer(this);
217      stream.videoTracks.get(0).addRenderer(renderer);
218      assertNull(renderers.put(
219          stream, new WeakReference<VideoRenderer>(renderer)));
220    }
221
222    public synchronized void expectRemoveStream(String label) {
223      expectedRemoveStreamLabels.add(label);
224    }
225
226    @Override
227    public synchronized void onRemoveStream(MediaStream stream) {
228      assertEquals(expectedRemoveStreamLabels.removeFirst(), stream.label());
229      WeakReference<VideoRenderer> renderer = renderers.remove(stream);
230      assertNotNull(renderer);
231      assertNotNull(renderer.get());
232      assertEquals(1, stream.videoTracks.size());
233      stream.videoTracks.get(0).removeRenderer(renderer.get());
234    }
235
236    public synchronized void expectDataChannel(String label) {
237      expectedRemoteDataChannelLabels.add(label);
238    }
239
240    @Override
241    public synchronized void onDataChannel(DataChannel remoteDataChannel) {
242      assertEquals(expectedRemoteDataChannelLabels.removeFirst(),
243                   remoteDataChannel.label());
244      setDataChannel(remoteDataChannel);
245      assertEquals(DataChannel.State.CONNECTING, dataChannel.state());
246    }
247
248    public synchronized void expectRenegotiationNeeded() {
249      ++expectedRenegotiations;
250    }
251
252    @Override
253    public synchronized void onRenegotiationNeeded() {
254      assertTrue(--expectedRenegotiations >= 0);
255    }
256
257    public synchronized void expectMessage(ByteBuffer expectedBuffer,
258                                           boolean expectedBinary) {
259      expectedBuffers.add(
260          new DataChannel.Buffer(expectedBuffer, expectedBinary));
261    }
262
263    @Override
264    public synchronized void onMessage(DataChannel.Buffer buffer) {
265      DataChannel.Buffer expected = expectedBuffers.removeFirst();
266      assertEquals(expected.binary, buffer.binary);
267      assertTrue(expected.data.equals(buffer.data));
268    }
269
270    @Override
271    public synchronized void onStateChange() {
272      assertEquals(expectedStateChanges.removeFirst(), dataChannel.state());
273    }
274
275    public synchronized void expectStateChange(DataChannel.State state) {
276      expectedStateChanges.add(state);
277    }
278
279    @Override
280    public synchronized void onComplete(StatsReport[] reports) {
281      if (--expectedStatsCallbacks < 0) {
282        throw new RuntimeException("Unexpected stats report: " + reports);
283      }
284      gotStatsReports.add(reports);
285    }
286
287    public synchronized void expectStatsCallback() {
288      ++expectedStatsCallbacks;
289    }
290
291    public synchronized LinkedList<StatsReport[]> takeStatsReports() {
292      LinkedList<StatsReport[]> got = gotStatsReports;
293      gotStatsReports = new LinkedList<StatsReport[]>();
294      return got;
295    }
296
297    // Return a set of expectations that haven't been satisfied yet, possibly
298    // empty if no such expectations exist.
299    public synchronized TreeSet<String> unsatisfiedExpectations() {
300      TreeSet<String> stillWaitingForExpectations = new TreeSet<String>();
301      if (expectedIceCandidates > 0) {  // See comment in onIceCandidate.
302        stillWaitingForExpectations.add("expectedIceCandidates");
303      }
304      if (expectedErrors != 0) {
305        stillWaitingForExpectations.add("expectedErrors: " + expectedErrors);
306      }
307      if (expectedSignalingChanges.size() != 0) {
308        stillWaitingForExpectations.add(
309            "expectedSignalingChanges: " + expectedSignalingChanges.size());
310      }
311      if (expectedIceConnectionChanges.size() != 0) {
312        stillWaitingForExpectations.add("expectedIceConnectionChanges: " +
313                                        expectedIceConnectionChanges.size());
314      }
315      if (expectedIceGatheringChanges.size() != 0) {
316        stillWaitingForExpectations.add("expectedIceGatheringChanges: " +
317                                        expectedIceGatheringChanges.size());
318      }
319      if (expectedAddStreamLabels.size() != 0) {
320        stillWaitingForExpectations.add(
321            "expectedAddStreamLabels: " + expectedAddStreamLabels.size());
322      }
323      if (expectedRemoveStreamLabels.size() != 0) {
324        stillWaitingForExpectations.add(
325            "expectedRemoveStreamLabels: " + expectedRemoveStreamLabels.size());
326      }
327      if (expectedSetSize != 0) {
328        stillWaitingForExpectations.add("expectedSetSize");
329      }
330      if (expectedFramesDelivered > 0) {
331        stillWaitingForExpectations.add(
332            "expectedFramesDelivered: " + expectedFramesDelivered);
333      }
334      if (!expectedBuffers.isEmpty()) {
335        stillWaitingForExpectations.add(
336            "expectedBuffers: " + expectedBuffers.size());
337      }
338      if (!expectedStateChanges.isEmpty()) {
339        stillWaitingForExpectations.add(
340            "expectedStateChanges: " + expectedStateChanges.size());
341      }
342      if (!expectedRemoteDataChannelLabels.isEmpty()) {
343        stillWaitingForExpectations.add("expectedRemoteDataChannelLabels: " +
344                                        expectedRemoteDataChannelLabels.size());
345      }
346      if (expectedStatsCallbacks != 0) {
347        stillWaitingForExpectations.add(
348            "expectedStatsCallbacks: " + expectedStatsCallbacks);
349      }
350      return stillWaitingForExpectations;
351    }
352
353    public void waitForAllExpectationsToBeSatisfied() {
354      // TODO(fischman): problems with this approach:
355      // - come up with something better than a poll loop
356      // - avoid serializing expectations explicitly; the test is not as robust
357      //   as it could be because it must place expectations between wait
358      //   statements very precisely (e.g. frame must not arrive before its
359      //   expectation, and expectation must not be registered so early as to
360      //   stall a wait).  Use callbacks to fire off dependent steps instead of
361      //   explicitly waiting, so there can be just a single wait at the end of
362      //   the test.
363      TreeSet<String> prev = null;
364      TreeSet<String> stillWaitingForExpectations = unsatisfiedExpectations();
365      while (!stillWaitingForExpectations.isEmpty()) {
366        if (!stillWaitingForExpectations.equals(prev)) {
367          System.out.println(
368              name + " still waiting at\n    " +
369              (new Throwable()).getStackTrace()[1] +
370              "\n    for: " +
371              Arrays.toString(stillWaitingForExpectations.toArray()));
372        }
373        try {
374          Thread.sleep(10);
375        } catch (InterruptedException e) {
376          throw new RuntimeException(e);
377        }
378        prev = stillWaitingForExpectations;
379        stillWaitingForExpectations = unsatisfiedExpectations();
380      }
381      if (prev == null) {
382        System.out.println(name + " didn't need to wait at\n    " +
383                           (new Throwable()).getStackTrace()[1]);
384      }
385    }
386  }
387
388  private static class SdpObserverLatch implements SdpObserver {
389    private boolean success = false;
390    private SessionDescription sdp = null;
391    private String error = null;
392    private CountDownLatch latch = new CountDownLatch(1);
393
394    public SdpObserverLatch() {}
395
396    @Override
397    public void onCreateSuccess(SessionDescription sdp) {
398      this.sdp = sdp;
399      onSetSuccess();
400    }
401
402    @Override
403    public void onSetSuccess() {
404      success = true;
405      latch.countDown();
406    }
407
408    @Override
409    public void onCreateFailure(String error) {
410      onSetFailure(error);
411    }
412
413    @Override
414    public void onSetFailure(String error) {
415      this.error = error;
416      latch.countDown();
417    }
418
419    public boolean await() {
420      try {
421        assertTrue(latch.await(1000, TimeUnit.MILLISECONDS));
422        return getSuccess();
423      } catch (Exception e) {
424        throw new RuntimeException(e);
425      }
426    }
427
428    public boolean getSuccess() {
429      return success;
430    }
431
432    public SessionDescription getSdp() {
433      return sdp;
434    }
435
436    public String getError() {
437      return error;
438    }
439  }
440
441  static int videoWindowsMapped = -1;
442
443  private static class TestRenderer implements VideoRenderer.Callbacks {
444    public int width = -1;
445    public int height = -1;
446    public int numFramesDelivered = 0;
447
448    public void setSize(int width, int height) {
449      assertEquals(this.width, -1);
450      assertEquals(this.height, -1);
451      this.width = width;
452      this.height = height;
453    }
454
455    public void renderFrame(VideoRenderer.I420Frame frame) {
456      ++numFramesDelivered;
457    }
458  }
459
460  private static VideoRenderer createVideoRenderer(
461      VideoRenderer.Callbacks videoCallbacks) {
462    if (!RENDER_TO_GUI) {
463      return new VideoRenderer(videoCallbacks);
464    }
465    ++videoWindowsMapped;
466    assertTrue(videoWindowsMapped < 4);
467    int x = videoWindowsMapped % 2 != 0 ? 700 : 0;
468    int y = videoWindowsMapped >= 2 ? 0 : 500;
469    return VideoRenderer.createGui(x, y);
470  }
471
472  // Return a weak reference to test that ownership is correctly held by
473  // PeerConnection, not by test code.
474  private static WeakReference<MediaStream> addTracksToPC(
475      PeerConnectionFactory factory, PeerConnection pc,
476      VideoSource videoSource,
477      String streamLabel, String videoTrackId, String audioTrackId,
478      VideoRenderer.Callbacks videoCallbacks) {
479    MediaStream lMS = factory.createLocalMediaStream(streamLabel);
480    VideoTrack videoTrack =
481        factory.createVideoTrack(videoTrackId, videoSource);
482    assertNotNull(videoTrack);
483    VideoRenderer videoRenderer = createVideoRenderer(videoCallbacks);
484    assertNotNull(videoRenderer);
485    videoTrack.addRenderer(videoRenderer);
486    lMS.addTrack(videoTrack);
487    // Just for fun, let's remove and re-add the track.
488    lMS.removeTrack(videoTrack);
489    lMS.addTrack(videoTrack);
490    lMS.addTrack(factory.createAudioTrack(
491        audioTrackId, factory.createAudioSource(new MediaConstraints())));
492    pc.addStream(lMS, new MediaConstraints());
493    return new WeakReference<MediaStream>(lMS);
494  }
495
496  private static void assertEquals(
497      SessionDescription lhs, SessionDescription rhs) {
498    assertEquals(lhs.type, rhs.type);
499    assertEquals(lhs.description, rhs.description);
500  }
501
502  @Test
503  public void testCompleteSession() throws Exception {
504    doTest();
505  }
506
507  @Test
508  public void testCompleteSessionOnNonMainThread() throws Exception {
509    final Exception[] exceptionHolder = new Exception[1];
510    Thread nonMainThread = new Thread("PeerConnectionTest-nonMainThread") {
511        @Override public void run() {
512          try {
513            doTest();
514          } catch (Exception e) {
515            exceptionHolder[0] = e;
516          }
517        }
518      };
519    nonMainThread.start();
520    nonMainThread.join();
521    if (exceptionHolder[0] != null)
522      throw exceptionHolder[0];
523  }
524
525  private void doTest() throws Exception {
526    CountDownLatch testDone = new CountDownLatch(1);
527    System.gc();  // Encourage any GC-related threads to start up.
528    TreeSet<String> threadsBeforeTest = allThreads();
529
530    PeerConnectionFactory factory = new PeerConnectionFactory();
531    // Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
532    // NOTE: this _must_ happen while |factory| is alive!
533    // Logging.enableTracing(
534    //     "/tmp/PeerConnectionTest-log.txt",
535    //     EnumSet.of(Logging.TraceLevel.TRACE_ALL),
536    //     Logging.Severity.LS_SENSITIVE);
537
538    MediaConstraints pcConstraints = new MediaConstraints();
539    pcConstraints.mandatory.add(
540        new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
541
542    LinkedList<PeerConnection.IceServer> iceServers =
543        new LinkedList<PeerConnection.IceServer>();
544    iceServers.add(new PeerConnection.IceServer(
545        "stun:stun.l.google.com:19302"));
546    iceServers.add(new PeerConnection.IceServer(
547        "turn:fake.example.com", "fakeUsername", "fakePassword"));
548    ObserverExpectations offeringExpectations =
549        new ObserverExpectations("PCTest:offerer");
550    PeerConnection offeringPC = factory.createPeerConnection(
551        iceServers, pcConstraints, offeringExpectations);
552    assertNotNull(offeringPC);
553
554    ObserverExpectations answeringExpectations =
555        new ObserverExpectations("PCTest:answerer");
556    PeerConnection answeringPC = factory.createPeerConnection(
557        iceServers, pcConstraints, answeringExpectations);
558    assertNotNull(answeringPC);
559
560    // We want to use the same camera for offerer & answerer, so create it here
561    // instead of in addTracksToPC.
562    VideoSource videoSource = factory.createVideoSource(
563        VideoCapturer.create(""), new MediaConstraints());
564
565    offeringExpectations.expectSetSize();
566    offeringExpectations.expectRenegotiationNeeded();
567    WeakReference<MediaStream> oLMS = addTracksToPC(
568        factory, offeringPC, videoSource, "offeredMediaStream",
569        "offeredVideoTrack", "offeredAudioTrack", offeringExpectations);
570
571    offeringExpectations.expectRenegotiationNeeded();
572    DataChannel offeringDC = offeringPC.createDataChannel(
573        "offeringDC", new DataChannel.Init());
574    assertEquals("offeringDC", offeringDC.label());
575
576    offeringExpectations.setDataChannel(offeringDC);
577    SdpObserverLatch sdpLatch = new SdpObserverLatch();
578    offeringPC.createOffer(sdpLatch, new MediaConstraints());
579    assertTrue(sdpLatch.await());
580    SessionDescription offerSdp = sdpLatch.getSdp();
581    assertEquals(offerSdp.type, SessionDescription.Type.OFFER);
582    assertFalse(offerSdp.description.isEmpty());
583
584    sdpLatch = new SdpObserverLatch();
585    answeringExpectations.expectSignalingChange(
586        SignalingState.HAVE_REMOTE_OFFER);
587    answeringExpectations.expectAddStream("offeredMediaStream");
588    // SCTP DataChannels are announced via OPEN messages over the established
589    // connection (not via SDP), so answeringExpectations can only register
590    // expecting the channel during ICE, below.
591    answeringPC.setRemoteDescription(sdpLatch, offerSdp);
592    assertEquals(
593        PeerConnection.SignalingState.STABLE, offeringPC.signalingState());
594    assertTrue(sdpLatch.await());
595    assertNull(sdpLatch.getSdp());
596
597    answeringExpectations.expectSetSize();
598    answeringExpectations.expectRenegotiationNeeded();
599    WeakReference<MediaStream> aLMS = addTracksToPC(
600        factory, answeringPC, videoSource, "answeredMediaStream",
601        "answeredVideoTrack", "answeredAudioTrack", answeringExpectations);
602
603    sdpLatch = new SdpObserverLatch();
604    answeringPC.createAnswer(sdpLatch, new MediaConstraints());
605    assertTrue(sdpLatch.await());
606    SessionDescription answerSdp = sdpLatch.getSdp();
607    assertEquals(answerSdp.type, SessionDescription.Type.ANSWER);
608    assertFalse(answerSdp.description.isEmpty());
609
610    offeringExpectations.expectIceCandidates(2);
611    answeringExpectations.expectIceCandidates(2);
612
613    offeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE);
614    answeringExpectations.expectIceGatheringChange(IceGatheringState.COMPLETE);
615
616    sdpLatch = new SdpObserverLatch();
617    answeringExpectations.expectSignalingChange(SignalingState.STABLE);
618    answeringPC.setLocalDescription(sdpLatch, answerSdp);
619    assertTrue(sdpLatch.await());
620    assertNull(sdpLatch.getSdp());
621
622    sdpLatch = new SdpObserverLatch();
623    offeringExpectations.expectSignalingChange(SignalingState.HAVE_LOCAL_OFFER);
624    offeringPC.setLocalDescription(sdpLatch, offerSdp);
625    assertTrue(sdpLatch.await());
626    assertNull(sdpLatch.getSdp());
627    sdpLatch = new SdpObserverLatch();
628    offeringExpectations.expectSignalingChange(SignalingState.STABLE);
629    offeringExpectations.expectAddStream("answeredMediaStream");
630    offeringPC.setRemoteDescription(sdpLatch, answerSdp);
631    assertTrue(sdpLatch.await());
632    assertNull(sdpLatch.getSdp());
633
634    offeringExpectations.waitForAllExpectationsToBeSatisfied();
635    answeringExpectations.waitForAllExpectationsToBeSatisfied();
636
637    assertEquals(offeringPC.getLocalDescription().type, offerSdp.type);
638    assertEquals(offeringPC.getRemoteDescription().type, answerSdp.type);
639    assertEquals(answeringPC.getLocalDescription().type, answerSdp.type);
640    assertEquals(answeringPC.getRemoteDescription().type, offerSdp.type);
641
642    if (!RENDER_TO_GUI) {
643      // Wait for at least some frames to be delivered at each end (number
644      // chosen arbitrarily).
645      offeringExpectations.expectFramesDelivered(10);
646      answeringExpectations.expectFramesDelivered(10);
647      offeringExpectations.expectSetSize();
648      answeringExpectations.expectSetSize();
649    }
650
651    offeringExpectations.expectIceConnectionChange(
652        IceConnectionState.CHECKING);
653    offeringExpectations.expectIceConnectionChange(
654        IceConnectionState.CONNECTED);
655    // TODO(bemasc): uncomment once delivery of ICECompleted is reliable
656    // (https://code.google.com/p/webrtc/issues/detail?id=3021).
657    //
658    // offeringExpectations.expectIceConnectionChange(
659    //     IceConnectionState.COMPLETED);
660    answeringExpectations.expectIceConnectionChange(
661        IceConnectionState.CHECKING);
662    answeringExpectations.expectIceConnectionChange(
663        IceConnectionState.CONNECTED);
664
665    offeringExpectations.expectStateChange(DataChannel.State.OPEN);
666    // See commentary about SCTP DataChannels above for why this is here.
667    answeringExpectations.expectDataChannel("offeringDC");
668    answeringExpectations.expectStateChange(DataChannel.State.OPEN);
669
670    for (IceCandidate candidate : offeringExpectations.gotIceCandidates) {
671      answeringPC.addIceCandidate(candidate);
672    }
673    offeringExpectations.gotIceCandidates.clear();
674    for (IceCandidate candidate : answeringExpectations.gotIceCandidates) {
675      offeringPC.addIceCandidate(candidate);
676    }
677    answeringExpectations.gotIceCandidates.clear();
678
679    offeringExpectations.waitForAllExpectationsToBeSatisfied();
680    answeringExpectations.waitForAllExpectationsToBeSatisfied();
681
682    assertEquals(
683        PeerConnection.SignalingState.STABLE, offeringPC.signalingState());
684    assertEquals(
685        PeerConnection.SignalingState.STABLE, answeringPC.signalingState());
686
687    // Test send & receive UTF-8 text.
688    answeringExpectations.expectMessage(
689        ByteBuffer.wrap("hello!".getBytes(Charset.forName("UTF-8"))), false);
690    DataChannel.Buffer buffer = new DataChannel.Buffer(
691        ByteBuffer.wrap("hello!".getBytes(Charset.forName("UTF-8"))), false);
692    assertTrue(offeringExpectations.dataChannel.send(buffer));
693    answeringExpectations.waitForAllExpectationsToBeSatisfied();
694
695    // Construct this binary message two different ways to ensure no
696    // shortcuts are taken.
697    ByteBuffer expectedBinaryMessage = ByteBuffer.allocateDirect(5);
698    for (byte i = 1; i < 6; ++i) {
699      expectedBinaryMessage.put(i);
700    }
701    expectedBinaryMessage.flip();
702    offeringExpectations.expectMessage(expectedBinaryMessage, true);
703    assertTrue(answeringExpectations.dataChannel.send(
704        new DataChannel.Buffer(
705            ByteBuffer.wrap(new byte[] { 1, 2, 3, 4, 5 }), true)));
706    offeringExpectations.waitForAllExpectationsToBeSatisfied();
707
708    offeringExpectations.expectStateChange(DataChannel.State.CLOSING);
709    answeringExpectations.expectStateChange(DataChannel.State.CLOSING);
710    offeringExpectations.expectStateChange(DataChannel.State.CLOSED);
711    answeringExpectations.expectStateChange(DataChannel.State.CLOSED);
712    answeringExpectations.dataChannel.close();
713    offeringExpectations.dataChannel.close();
714
715    if (RENDER_TO_GUI) {
716      try {
717        Thread.sleep(3000);
718      } catch (Throwable t) {
719        throw new RuntimeException(t);
720      }
721    }
722
723    // TODO(fischman) MOAR test ideas:
724    // - Test that PC.removeStream() works; requires a second
725    //   createOffer/createAnswer dance.
726    // - audit each place that uses |constraints| for specifying non-trivial
727    //   constraints (and ensure they're honored).
728    // - test error cases
729    // - ensure reasonable coverage of _jni.cc is achieved.  Coverage is
730    //   extra-important because of all the free-text (class/method names, etc)
731    //   in JNI-style programming; make sure no typos!
732    // - Test that shutdown mid-interaction is crash-free.
733
734    // Free the Java-land objects, collect them, and sleep a bit to make sure we
735    // don't get late-arrival crashes after the Java-land objects have been
736    // freed.
737    shutdownPC(offeringPC, offeringExpectations);
738    offeringPC = null;
739    shutdownPC(answeringPC, answeringExpectations);
740    answeringPC = null;
741    videoSource.dispose();
742    factory.dispose();
743    System.gc();
744
745    TreeSet<String> threadsAfterTest = allThreads();
746    assertEquals(threadsBeforeTest, threadsAfterTest);
747    Thread.sleep(100);
748  }
749
750  private static void shutdownPC(
751      PeerConnection pc, ObserverExpectations expectations) {
752    expectations.dataChannel.unregisterObserver();
753    expectations.dataChannel.dispose();
754    expectations.expectStatsCallback();
755    assertTrue(pc.getStats(expectations, null));
756    expectations.waitForAllExpectationsToBeSatisfied();
757    expectations.expectIceConnectionChange(IceConnectionState.CLOSED);
758    expectations.expectSignalingChange(SignalingState.CLOSED);
759    pc.close();
760    expectations.waitForAllExpectationsToBeSatisfied();
761    expectations.expectStatsCallback();
762    assertTrue(pc.getStats(expectations, null));
763    expectations.waitForAllExpectationsToBeSatisfied();
764
765    System.out.println("FYI stats: ");
766    int reportIndex = -1;
767    for (StatsReport[] reports : expectations.takeStatsReports()) {
768      System.out.println(" Report #" + (++reportIndex));
769      for (int i = 0; i < reports.length; ++i) {
770        System.out.println("  " + reports[i].toString());
771      }
772    }
773    assertEquals(1, reportIndex);
774    System.out.println("End stats.");
775
776    pc.dispose();
777  }
778
779  // Returns a set of thread IDs belonging to this process, as Strings.
780  private static TreeSet<String> allThreads() {
781    TreeSet<String> threads = new TreeSet<String>();
782    // This pokes at /proc instead of using the Java APIs because we're also
783    // looking for libjingle/webrtc native threads, most of which won't have
784    // attached to the JVM.
785    for (String threadId : (new File("/proc/self/task")).list()) {
786      threads.add(threadId);
787    }
788    return threads;
789  }
790
791  // Return a String form of |strings| joined by |separator|.
792  private static String joinStrings(String separator, TreeSet<String> strings) {
793    StringBuilder builder = new StringBuilder();
794    for (String s : strings) {
795      if (builder.length() > 0) {
796        builder.append(separator);
797      }
798      builder.append(s);
799    }
800    return builder.toString();
801  }
802}
803