| /* |
| * Copyright 2026 The WebRTC project authors. All Rights Reserved. |
| * |
| * Use of this source code is governed by a BSD-style license |
| * that can be found in the LICENSE file in the root of the source |
| * tree. An additional intellectual property rights grant can be found |
| * in the file PATENTS. All contributing project authors may |
| * be found in the AUTHORS file in the root of the source tree. |
| */ |
| |
| package org.webrtc; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.fail; |
| |
| import androidx.test.runner.AndroidJUnit4; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicLong; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.robolectric.annotation.Config; |
| |
| @RunWith(AndroidJUnit4.class) |
| @Config(manifest = Config.NONE) |
| public class NativeLifecycleLockTest { |
| private static final long FAKE_PTR = 0x12345678L; |
| private NativeLifecycleLock lifecycleLock; |
| |
| @Before |
| public void setUp() { |
| lifecycleLock = new NativeLifecycleLock("TestClass", FAKE_PTR); |
| } |
| |
| @Test |
| public void testCallWithPointerReturnsValue() { |
| String result = lifecycleLock.call(ptr -> { |
| assertThat(ptr).isEqualTo(FAKE_PTR); |
| return "success"; |
| }); |
| assertThat(result).isEqualTo("success"); |
| } |
| |
| @Test |
| public void testCallNoArgReturnsValue() { |
| String result = lifecycleLock.call(() -> "success"); |
| assertThat(result).isEqualTo("success"); |
| } |
| |
| @Test |
| public void testRunWithPointerExecutes() { |
| AtomicLong seenPtr = new AtomicLong(0); |
| lifecycleLock.run(seenPtr::set); |
| assertThat(seenPtr.get()).isEqualTo(FAKE_PTR); |
| } |
| |
| @Test |
| public void testRunNoArgExecutes() { |
| AtomicBoolean executed = new AtomicBoolean(false); |
| lifecycleLock.run(() -> executed.set(true)); |
| assertThat(executed.get()).isTrue(); |
| } |
| |
| @Test |
| public void testGetNativePointer() { |
| assertThat(lifecycleLock.getNativePointer()).isEqualTo(FAKE_PTR); |
| } |
| |
| @Test |
| public void testIsDisposedInitiallyFalse() { |
| assertThat(lifecycleLock.isDisposed()).isFalse(); |
| } |
| |
| @Test |
| public void testDisposeExecutesDisposerAndSetsDisposed() { |
| AtomicLong disposedPtr = new AtomicLong(0); |
| lifecycleLock.dispose(disposedPtr::set); |
| |
| assertThat(disposedPtr.get()).isEqualTo(FAKE_PTR); |
| assertThat(lifecycleLock.isDisposed()).isTrue(); |
| } |
| |
| @Test |
| public void testDisposeIsIdempotent() { |
| AtomicInteger callCount = new AtomicInteger(0); |
| lifecycleLock.dispose(ptr -> callCount.incrementAndGet()); |
| lifecycleLock.dispose(ptr -> callCount.incrementAndGet()); |
| |
| assertThat(callCount.get()).isEqualTo(1); |
| assertThat(lifecycleLock.isDisposed()).isTrue(); |
| } |
| |
| @Test(expected = IllegalStateException.class) |
| public void testCallWithPointerAfterDisposeThrowsIllegalStateException() { |
| lifecycleLock.dispose(ptr -> {}); |
| lifecycleLock.call(ptr -> "failed"); |
| } |
| |
| @Test(expected = IllegalStateException.class) |
| public void testCallNoArgAfterDisposeThrowsIllegalStateException() { |
| lifecycleLock.dispose(ptr -> {}); |
| lifecycleLock.call(() -> "failed"); |
| } |
| |
| @Test(expected = IllegalStateException.class) |
| public void testRunWithPointerAfterDisposeThrowsIllegalStateException() { |
| lifecycleLock.dispose(ptr -> {}); |
| lifecycleLock.run(ptr -> {}); |
| } |
| |
| @Test(expected = IllegalStateException.class) |
| public void testRunNoArgAfterDisposeThrowsIllegalStateException() { |
| lifecycleLock.dispose(ptr -> {}); |
| lifecycleLock.run(() -> {}); |
| } |
| |
| @Test(expected = IllegalStateException.class) |
| public void testGetNativePointerAfterDisposeThrowsIllegalStateException() { |
| lifecycleLock.dispose(ptr -> {}); |
| lifecycleLock.getNativePointer(); |
| } |
| |
| @Test |
| public void testExceptionMessageContainsClassName() { |
| lifecycleLock.dispose(ptr -> {}); |
| try { |
| lifecycleLock.call(ptr -> "failed"); |
| fail("Expected IllegalStateException"); |
| } catch (IllegalStateException e) { |
| assertThat(e).hasMessageThat().contains("TestClass has been disposed."); |
| } |
| } |
| |
| @Test |
| public void testDisposeBlocksUntilInFlightCallFinishes() throws Exception { |
| CountDownLatch inFlightStarted = new CountDownLatch(1); |
| CountDownLatch allowInFlightToComplete = new CountDownLatch(1); |
| CountDownLatch disposeFinished = new CountDownLatch(1); |
| AtomicBoolean callCompleted = new AtomicBoolean(false); |
| |
| Thread threadA = new Thread(() -> { |
| lifecycleLock.run(() -> { |
| inFlightStarted.countDown(); |
| try { |
| allowInFlightToComplete.await(5, TimeUnit.SECONDS); |
| callCompleted.set(true); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| } |
| }); |
| }); |
| threadA.start(); |
| |
| assertThat(inFlightStarted.await(5, TimeUnit.SECONDS)).isTrue(); |
| |
| Thread threadB = new Thread(() -> { |
| lifecycleLock.dispose(ptr -> {}); |
| disposeFinished.countDown(); |
| }); |
| threadB.start(); |
| |
| // Verify dispose does not finish while in-flight read lock is held. |
| assertThat(disposeFinished.await(100, TimeUnit.MILLISECONDS)).isFalse(); |
| assertThat(callCompleted.get()).isFalse(); |
| |
| allowInFlightToComplete.countDown(); |
| threadA.join(5000); |
| threadB.join(5000); |
| |
| assertThat(callCompleted.get()).isTrue(); |
| assertThat(disposeFinished.getCount()).isEqualTo(0); |
| assertThat(lifecycleLock.isDisposed()).isTrue(); |
| } |
| } |