From b063c9ffa6d70f33176ebafb5726fc052f7eb7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Fri, 5 Jan 2024 17:46:49 +0100 Subject: [PATCH] POC for testing a player --- gradle/libs.versions.toml | 6 + pillarbox-ui/build.gradle.kts | 10 ++ .../ui/TestSimpleProgressTrackerState.kt | 134 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 pillarbox-ui/src/androidTest/java/ch/srgssr/pillarbox/ui/TestSimpleProgressTrackerState.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2be54ebc6..e94127294 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,8 @@ androidx-media = "1.7.0" androidx-media3 = "1.2.0" androidx-navigation = "2.7.6" androidx-paging = "3.2.1" +androidx-test = "1.5.0" +androidx-test-espresso = "3.5.1" androidx-test-ext-junit = "1.1.5" androidx-test-monitor = "1.6.1" androidx-test-runner = "1.5.2" @@ -86,6 +88,7 @@ androidx-media3-ui-leanback = { group = "androidx.media3", name = "media3-ui-lea androidx-media3-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "androidx-media3" } androidx-media3-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "androidx-media3" } androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "androidx-media3" } +androidx-media3-test-utils = { module = "androidx.media3:media3-test-utils", version.ref = "androidx-media3" } androidx-media = { group = "androidx.media", name = "media", version.ref = "androidx-media" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } @@ -112,6 +115,9 @@ androidx-compose-material-icons-extended = { module = "androidx.compose.material androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" } leanback = { group = "androidx.leanback", name = "leanback", version.ref = "androidx-leanback" } +androidx-test = { module = "androidx.test:core", version.ref = "androidx-test" } +androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-test-espresso" } +androidx-test-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "androidx-test-espresso" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } guava = { module = "com.google.guava:guava", version.ref = "guava" } diff --git a/pillarbox-ui/build.gradle.kts b/pillarbox-ui/build.gradle.kts index bbcbe3d84..5de94e885 100644 --- a/pillarbox-ui/build.gradle.kts +++ b/pillarbox-ui/build.gradle.kts @@ -71,6 +71,16 @@ dependencies { implementation(libs.kotlinx.coroutines.core) debugImplementation(libs.androidx.compose.ui.tooling) + + androidTestImplementation(libs.androidx.media3.exoplayer) + androidTestImplementation(libs.androidx.media3.test.utils) + androidTestImplementation(libs.androidx.test) + androidTestImplementation(libs.androidx.test.espresso.core) + androidTestImplementation(libs.androidx.test.espresso.idling.resource) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestRuntimeOnly(libs.androidx.test.runner) + androidTestImplementation(libs.junit) + androidTestImplementation(libs.kotlinx.coroutines.test) } publishing { diff --git a/pillarbox-ui/src/androidTest/java/ch/srgssr/pillarbox/ui/TestSimpleProgressTrackerState.kt b/pillarbox-ui/src/androidTest/java/ch/srgssr/pillarbox/ui/TestSimpleProgressTrackerState.kt new file mode 100644 index 000000000..aca1c8ada --- /dev/null +++ b/pillarbox-ui/src/androidTest/java/ch/srgssr/pillarbox/ui/TestSimpleProgressTrackerState.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.ui + +import android.os.Looper +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.Player.Listener +import androidx.media3.test.utils.FakeClock +import androidx.media3.test.utils.TestExoPlayerBuilder +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.IdlingResource +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExternalResource +import org.junit.runner.RunWith +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +// TODO Move this in "pillarbox-player-testutils" +private class PlayerIdlingResource( + @Player.State private val expectedPlaybackState: Int +) : IdlingResource { + private var callback: IdlingResource.ResourceCallback? = null + + @Player.State + var playbackState: Int = Player.STATE_IDLE + set(value) { + field = value + + if (playbackState == expectedPlaybackState) { + callback?.onTransitionToIdle() + } + } + + override fun getName(): String { + return "PlayerIdlingResource" + } + + override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) { + this.callback = callback + } + + override fun isIdleNow(): Boolean { + return playbackState == expectedPlaybackState + } +} + +// TODO Move this in "pillarbox-player-testutils" +class ExoPlayerRule( + private val mediaUri: String, + @Player.State private val waitForPlaybackState: Int +) : ExternalResource() { + private val playerIdlingResource = PlayerIdlingResource(waitForPlaybackState) + + lateinit var clock: FakeClock + private set + + lateinit var player: Player + private set + + override fun before() { + Looper.prepare() + IdlingRegistry.getInstance().register(playerIdlingResource) + + setupClock() + setupPlayer() + + Espresso.onIdle() + } + + override fun after() { + player.release() + + IdlingRegistry.getInstance().unregister(playerIdlingResource) + Looper.myLooper()?.quit() + } + + private fun setupClock() { + clock = FakeClock(true) + } + + private fun setupPlayer() { + player = TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) + .setClock(clock) + .build() + player.addListener( + object : Listener { + override fun onPlaybackStateChanged(playbackState: Int) { + playerIdlingResource.playbackState = playbackState + } + } + ) + player.setMediaItem(MediaItem.fromUri(mediaUri)) + player.prepare() + player.play() + } +} + +@RunWith(AndroidJUnit4::class) +class TestSimpleProgressTrackerState { + @get:Rule + val playerRule = ExoPlayerRule( + mediaUri = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", + waitForPlaybackState = Player.STATE_READY + ) + + @Test + fun progressWithoutManualChanges() = runTest { + val progressTrackerState = SimpleProgressTrackerState(playerRule.player, this) + + val playerPositions = (0L..50L step 5L).toList() + playerPositions.forEach { playerPosition -> + playerRule.clock.advanceTime(playerPosition) + } + + val actualProgress = mutableListOf() + launch(coroutineContext) { + progressTrackerState.progress + .toList(actualProgress) + }.join() + + assertEquals(playerPositions.map { it.milliseconds }, actualProgress) + } +}