blob: 8042019ea5c2f551a21b98da413ea841ccf2c59d [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.camera.camera2.pipe.impl
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class TokenLockTest {
@Test
fun testTokenLockReportsNoAvailableCapacityWhenClosed() {
val tokenLock = TokenLockImpl(2)
assertThat(tokenLock.available).isEqualTo(2)
assertThat(tokenLock.size).isEqualTo(0)
tokenLock.close()
assertThat(tokenLock.available).isEqualTo(0)
assertThat(tokenLock.size).isEqualTo(2)
}
@Test
fun testTokenLockCanAcquireAndCloseTokens() = runBlocking {
val tokenLock = TokenLockImpl(2)
assertThat(tokenLock.available).isEqualTo(2)
assertThat(tokenLock.size).isEqualTo(0)
val token1 = tokenLock.acquire(1)
assertThat(tokenLock.available).isEqualTo(1)
assertThat(tokenLock.size).isEqualTo(1)
val token2 = tokenLock.acquire(1)
assertThat(tokenLock.available).isEqualTo(0)
assertThat(tokenLock.size).isEqualTo(2)
// Check to make sure that acquireOrNull returns null when all tokens have been allocated.
assertThat(tokenLock.acquireOrNull(1)).isNull()
// Close a token, freeing up a token slot
token2.close()
// Check to make sure acquireOrNull does *not* return null if the tokenLock has a free
// slot.
val token3 = tokenLock.acquireOrNull(1)
assertThat(token3).isNotNull()
// Close all outstanding tokens, and check to make sure the capacity is subsequently
// reported correctly.
token3?.close()
token1.close()
assertThat(tokenLock.available).isEqualTo(2)
assertThat(tokenLock.size).isEqualTo(0)
}
@Test
fun tokenLockHandlesRequestsThatTimeOut() = runBlocking {
val tokenLock = TokenLockImpl(2)
val token1 = tokenLock.acquire(1)
// This should suspend, and then cancel the request for the token.
val token2: TokenLock.Token? = withTimeoutOrNull(10) {
tokenLock.acquire(2)
}
assertThat(token2).isNull()
assertThat(tokenLock.available).isEqualTo(1)
// Make sure we can still acquire a token after a previous request timed out.
val token3 = tokenLock.acquire(1)
assertThat(tokenLock.available).isEqualTo(0)
token3.close()
token1.close()
assertThat(tokenLock.available).isEqualTo(2)
}
@Test
fun tokenLockSuspendsWithAsync() = runBlocking {
val tokenLock = TokenLockImpl(2)
val token1 = tokenLock.acquire(1)
val token2 = tokenLock.acquire(1)
// This launches a suspendable job that should resume when at least one of the other tokens
// is closed.
val token3Job = async { tokenLock.acquire(1) }
token1.close()
token2.close()
token3Job.await().close()
}
@Test
fun tokenLockIsFair() = runBlocking {
val tokenLock = TokenLockImpl(3)
val token1 = tokenLock.acquire(2)
assertThat(tokenLock.available).isEqualTo(1)
// Using CoroutineStart.UNDISPATCHED, while experimental, has a unique property: It will
// run synchronously until the coroutine suspends. For this test, this is exactly the
// behavior we want to test the token lock with because we want to make sure the coroutine
// has been placed into the internal request queue before we create the second pending job.
val token2Job = async(start = CoroutineStart.UNDISPATCHED) { tokenLock.acquire(2) }
val token3Job = async(start = CoroutineStart.UNDISPATCHED) { tokenLock.acquire(2) }
// Assert that the token lock is fully utilized, which happens when the next pending request
// is larger than the available capacity of the lock.
assertThat(tokenLock.available).isEqualTo(0)
// Assert that all of the async jobs are running, but not complete.
assertThat(token2Job.isCompleted).isFalse()
assertThat(token2Job.isCancelled).isFalse()
assertThat(token2Job.isActive).isTrue()
assertThat(token3Job.isCompleted).isFalse()
assertThat(token3Job.isCancelled).isFalse()
assertThat(token3Job.isActive).isTrue()
// Closing token1 releases enough capacity for the next job to acquire and resume.
token1.close()
// The token lock is well ordered, which means that token2Job is next in the queue and will
// always resume first.
val token2 = token2Job.await()
// token2 is large enough that token3 is still waiting
assertThat(token3Job.isCompleted).isFalse()
assertThat(token3Job.isCancelled).isFalse()
assertThat(token3Job.isActive).isTrue()
// Closing token2 releases enough capacity for the token3 job to acquire and resume.
token2.close()
val token3 = token3Job.await()
// There are no pending job, and token3 holds onto 2 / 3 of the values.
assertThat(tokenLock.available).isEqualTo(1)
// Closing the last token causes memory to be released.
token3.close()
assertThat(tokenLock.available).isEqualTo(3)
}
@Test
fun cancelingSuspendedJobReleasesPendingRequest() = runBlocking {
val tokenLock = TokenLockImpl(3)
val token1 = tokenLock.acquire(1)
val token2 = tokenLock.acquire(1)
val token3Job = async { tokenLock.acquire(2) }
delay(10)
assertThat(tokenLock.available).isEqualTo(0)
assertThat(token3Job.isCompleted).isFalse()
assertThat(token3Job.isCancelled).isFalse()
assertThat(token3Job.isActive).isTrue()
token3Job.cancel()
assertThat(tokenLock.available).isEqualTo(1)
token1.close()
token2.close()
assertThat(tokenLock.available).isEqualTo(3)
}
@Test
fun closingTokenLockCausesPendingRequestsToThrow() = runBlocking {
val tokenLock = TokenLockImpl(1)
val token1 = tokenLock.acquire(1)
val token2Job = async { tokenLock.acquire(1) }
// Close the tokenLock itself. This should inform all pending requests that they will
// never receive a token.
tokenLock.close()
try {
token2Job.await()
fail("Await should throw an exception if the token lock is closed.")
} catch (ex: CancellationException) {
// Expected
}
token1.close()
}
@Test
fun tokenLockAcquiresRange() = runBlocking {
val tokenLock = TokenLockImpl(10)
val token1 = tokenLock.acquire(8)
assertThat(tokenLock.available).isEqualTo(2)
val token2Job = async(start = CoroutineStart.UNDISPATCHED) { tokenLock.acquire(3, 8) }
val token3Job = async(start = CoroutineStart.UNDISPATCHED) { tokenLock.acquire(3) }
assertThat(tokenLock.available).isEqualTo(0)
// Closing the token causes the first request (Token 2) to greedily acquire it's max (8).
token1.close()
val token2 = token2Job.await()
assertThat(token2.value).isEqualTo(8)
assertThat(tokenLock.available).isEqualTo(0)
// Token 3 cannot be acquired until token2 is closed.
token2.close()
val token3 = token3Job.await()
assertThat(token3.value).isEqualTo(3)
token3.close()
assertThat(tokenLock.available).isEqualTo(10)
}
@Test
fun tokenLockFulfillsMultipleRequests() = runBlocking {
val tokenLock = TokenLockImpl(10)
val token1 = tokenLock.acquire(8)
assertThat(tokenLock.available).isEqualTo(2)
// Acquire using a range of values. Neither request can be fulfilled until token1 is
// closed.
val token2Job = async(start = CoroutineStart.UNDISPATCHED) { tokenLock.acquire(3, 4) }
val token3Job = async(start = CoroutineStart.UNDISPATCHED) { tokenLock.acquire(3, 8) }
assertThat(tokenLock.available).isEqualTo(0)
// When the token is closed, the first request is greedy and should acquire as much as the
// range allows (4). The second pending request (token 3) should also be greedy, but should
// be capped at (6) to limit it to the capacity of the TokenLock.
token1.close()
val token2 = token2Job.await()
val token3 = token3Job.await()
assertThat(token2.value).isEqualTo(4)
assertThat(token3.value).isEqualTo(6) // Not 8
}
@Test
fun tokensAreClosedWithUseKeyword() = runBlocking {
val tokenLock = TokenLockImpl(1)
tokenLock.acquire(1).use {
assertThat(tokenLock.size).isEqualTo(1)
}
assertThat(tokenLock.size).isEqualTo(0)
}
@Test
fun testWithTokenExtension() = runBlocking {
val tokenLock = TokenLockImpl(1)
tokenLock.withToken(1) {
assertThat(tokenLock.size).isEqualTo(1)
}
assertThat(tokenLock.size).isEqualTo(0)
}
@Test
fun testWithTokenRange() = runBlocking {
val tokenLock = TokenLockImpl(3)
val token1 = tokenLock.acquire(1)
tokenLock.withToken(1, 4) {
assertThat(tokenLock.size).isEqualTo(3)
assertThat(it.value).isEqualTo(2)
}
assertThat(tokenLock.size).isEqualTo(1)
token1.close()
assertThat(tokenLock.size).isEqualTo(0)
}
@Test
fun tokenReleasesOnlyOnce() = runBlocking {
val tokenLock = TokenLockImpl(2)
val token1 = tokenLock.acquire(1)
val token2 = tokenLock.acquire(1)
assertThat(token1.release()).isTrue()
assertThat(token1.release()).isFalse()
tokenLock.close()
// Token is not closed as a result of tokenLock.close().
assertThat(token2.release()).isTrue()
assertThat(token2.release()).isFalse()
// The size of the token lock is unaffected by tokens that are released after close.
assertThat(tokenLock.size).isEqualTo(2)
assertThat(tokenLock.available).isEqualTo(0)
}
@Test
fun tokenCloseIsTheSameAsRelease() = runBlocking {
val tokenLock = TokenLockImpl(2)
val token1 = tokenLock.acquire(1)
val token2 = tokenLock.acquire(1)
token1.close()
assertThat(token1.release()).isFalse()
tokenLock.close()
// Token is not closed as a result of tokenLock
token2.close()
assertThat(token2.release()).isFalse()
// The size of the token lock is unaffected by tokens that are released after close.
assertThat(tokenLock.size).isEqualTo(2)
assertThat(tokenLock.available).isEqualTo(0)
}
}