From c76f5afa4df467354ab0a53f2da6d3591f4b2e36 Mon Sep 17 00:00:00 2001 From: davidliu Date: Tue, 5 May 2026 18:37:44 +0900 Subject: [PATCH] Tests for withDeadline --- AGENTS.md | 10 ++- livekit-android-test/build.gradle | 22 +++++ .../livekit/android/util/WithDeadlineTest.kt | 84 +++++++++++++++++++ 3 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 livekit-android-test/src/test/java/io/livekit/android/util/WithDeadlineTest.kt diff --git a/AGENTS.md b/AGENTS.md index f4567e20..1d311dd7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,10 +83,8 @@ This library makes extensive use of Dagger to provide dependency injection throu ## FlowObservable -The SDK heavily relies on `@FlowObservable` class members, which allow them to be used as regular -variables, -while also allowing them to be observed as a `Flow`. This is especially useful for Android Compose -projects, +The SDK heavily relies on `@FlowObservable` class members, which allow them to be used as regular variables, +while also allowing them to be observed as a `Flow`. This is especially useful for Android Compose projects, as this allows them to be converted to `State` objects and update the UI appropriately. ```kotlin @@ -110,6 +108,10 @@ Unit tests are provided through the `livekit-android-test` module. - `io.livekit.android.test.mock` package - mocks and fakes - `MockE2ETest` - the base class for when testing `Room` behavior +`livekit-android-test` is setup as a friend module to `livekit-android-sdk`, so that internal +classes may be tested directly. However, avoid usage of internal methods for tests where possible +in tests. + ## Using Kotlin ### Concurrency and State diff --git a/livekit-android-test/build.gradle b/livekit-android-test/build.gradle index 855e2258..a83b3c96 100644 --- a/livekit-android-test/build.gradle +++ b/livekit-android-test/build.gradle @@ -41,6 +41,28 @@ android { } } +// Allow this module's Kotlin compilations to resolve `internal` APIs from :livekit-android-sdk +// https://kotlinlang.org/docs/visibility-modifiers.html#modules +// +// Use projectsEvaluated so :livekit-android-sdk Kotlin compile tasks exist before friendPaths +// references them. +gradle.projectsEvaluated { + def sdk = rootProject.project(':livekit-android-sdk') + project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { task -> + def variant + if (task.name.startsWith('compileDebug')) { + variant = 'Debug' + } else if (task.name.startsWith('compileRelease')) { + variant = 'Release' + } else { + return + } + // Point at the same `classes.jar` AGP puts on the compile classpath (not only `tmp/kotlin-classes`), + // or Kotlin will not treat the SDK as a friend module for `internal` visibility. + task.friendPaths.from(sdk.tasks.named("bundleLibCompileToJar${variant}")) + } +} + dokkaHtml { moduleName.set("livekit-android-test") dokkaSourceSets { diff --git a/livekit-android-test/src/test/java/io/livekit/android/util/WithDeadlineTest.kt b/livekit-android-test/src/test/java/io/livekit/android/util/WithDeadlineTest.kt new file mode 100644 index 00000000..2ff8f9f0 --- /dev/null +++ b/livekit-android-test/src/test/java/io/livekit/android/util/WithDeadlineTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2026 LiveKit, Inc. + * + * 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 io.livekit.android.util + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Test +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes + +@OptIn(ExperimentalCoroutinesApi::class) +class WithDeadlineTest { + + @Test + fun returnsWhenBlockFinishesBeforeDeadline() = runTest { + val deferred = async { + withDeadline(5000.milliseconds) { + delay(100) + 42 + } + } + advanceUntilIdle() + assertEquals(42, deferred.await()) + } + + @Test + fun exceedsDeadlineThrowsTimeoutExceptionWithTimeoutCancellationCause() = runTest { + val deferred = async { + try { + withDeadline(50.milliseconds) { + delay(1.hours) + } + null + } catch (e: TimeoutException) { + e + } + } + advanceUntilIdle() + val timeout = requireNotNull(deferred.await()) + assertTrue(timeout.cause is TimeoutCancellationException) + } + + @Test + fun externalCancellationPropagatesCancellationException() = runTest { + val deferred = async { + withDeadline(10.hours) { + delay(Long.MAX_VALUE) + } + } + advanceTimeBy(30.minutes.inWholeMilliseconds) + deferred.cancel() + advanceTimeBy(30.minutes.inWholeMilliseconds) + try { + deferred.await() + fail("expected CancellationException") + } catch (e: CancellationException) { + assertTrue(e !is TimeoutCancellationException) + } + } +}