diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index b2265b74a..b2633e7fa 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -167,42 +167,3 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - name: Generate documentation run: ./gradlew :dokkaGenerate - - android-tests: - name: Android Tests - runs-on: ubuntu-latest - env: - USERNAME: ${{ github.actor }} - GITHUB_TOKEN: ${{ github.token }} - strategy: - matrix: - api-level: [ 26 ] - steps: - - name: Enable KVM - # https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/ - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: Checkout - uses: actions/checkout@v4 - - name: Set up Java - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Run Android Tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - arch: x86_64 - # Supported tasks per module - # :pillarbox-analytics:connectedDebugAndroidTest - # :pillarbox-core-business:connectedDebugAndroidTest - # :pillarbox-player:connectedDebugAndroidTest - # :pillarbox-ui:connectedDebugAndroidTest - script: ./gradlew :pillarbox-player:connectedDebugAndroidTest diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4961feb5b..e755920c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,6 @@ androidx-paging = "3.3.6" androidx-test-core = "1.6.1" androidx-test-ext-junit = "1.2.1" androidx-test-monitor = "1.7.2" -androidx-test-runner = "1.6.2" androidx-tv-material = "1.1.0-alpha01" coil = "3.2.0" comscore = "6.11.1" @@ -61,7 +60,6 @@ androidx-paging-common = { module = "androidx.paging:paging-common", version.ref androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-test-core" } androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidx-test-monitor" } -androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "androidx-tv-material" } coil = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" } coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } diff --git a/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/TestPillarboxRunHelper.kt b/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/TestPillarboxRunHelper.kt index af1f4cceb..23436ef8a 100644 --- a/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/TestPillarboxRunHelper.kt +++ b/pillarbox-player-testutils/src/main/java/ch/srgssr/pillarbox/player/test/utils/TestPillarboxRunHelper.kt @@ -30,7 +30,7 @@ object TestPillarboxRunHelper { * Runs tasks of the main [Looper] until [Player.Listener.onEvents] matches the * expected state or a playback error occurs. * - *

If a playback error occurs it will be thrown wrapped in an [IllegalStateException]. + * If a playback error occurs, it will be thrown wrapped in an [IllegalStateException]. * * @param player The [Player]. * @param expectedEvents The expected [Player.Event]. If empty, waits until the first [Player.Listener.onEvents]. @@ -62,7 +62,7 @@ object TestPillarboxRunHelper { /** * Runs tasks of the main Looper until [Player.Listener.onPlaybackParametersChanged] is called or a playback error occurs. * - *

If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}. + * If a playback error occurs, it will be thrown wrapped in an [IllegalStateException]. * * @param player The [Player]. * @throws TimeoutException If the [RobolectricUtil.DEFAULT_TIMEOUT_MS] is exceeded. diff --git a/pillarbox-player/build.gradle.kts b/pillarbox-player/build.gradle.kts index b8321a1cb..361c6d1f2 100644 --- a/pillarbox-player/build.gradle.kts +++ b/pillarbox-player/build.gradle.kts @@ -19,16 +19,6 @@ android { buildFeatures { buildConfig = true } - - // Mockk includes some licenses information, which may conflict with other license files. This block merges all licenses together. - // Mockk excludes all licenses instead: - // https://github.com/mockk/mockk/blob/f879502a044c83c2a5fd52992f20903209eb34f3/modules/mockk-android/build.gradle.kts#L14-L19 - packaging { - resources { - merges += "META-INF/LICENSE.md" - merges += "META-INF/LICENSE-notice.md" - } - } } dependencies { @@ -64,15 +54,8 @@ dependencies { testImplementation(libs.mockk) testImplementation(libs.mockk.dsl) testImplementation(libs.okio) - testRuntimeOnly(libs.robolectric) + testImplementation(libs.robolectric) testImplementation(libs.robolectric.annotations) testImplementation(libs.robolectric.shadows.framework) testImplementation(libs.turbine) - - androidTestImplementation(libs.androidx.test.monitor) - androidTestRuntimeOnly(libs.androidx.test.runner) - androidTestImplementation(libs.junit) - androidTestImplementation(libs.kotlin.test) - androidTestRuntimeOnly(libs.kotlinx.coroutines.android) - androidTestImplementation(libs.mockk) } diff --git a/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt b/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt deleted file mode 100644 index 471805226..000000000 --- a/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player - -import android.net.Uri -import androidx.media3.common.MediaItem -import androidx.media3.common.PlaybackException -import androidx.media3.common.Player -import androidx.media3.common.util.ConditionVariable -import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation -import ch.srgssr.pillarbox.player.utils.ContentUrls -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import java.util.concurrent.atomic.AtomicReference -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(Parameterized::class) -class IsPlayingAllTypeOfContentTest { - @Parameterized.Parameter - lateinit var urlToTest: String - - @Test - fun isPlayingTest() { - // Context of the app under test. - val appContext = getInstrumentation().targetContext - val atomicPlayer = AtomicReference() - val waitIsPlaying = WaitIsPlaying() - getInstrumentation().runOnMainSync { - val player = PillarboxExoPlayer(appContext, Default) - atomicPlayer.set(player) - player.addMediaItem(MediaItem.fromUri(urlToTest)) - player.addListener(waitIsPlaying) - player.prepare() - player.play() - } - - waitIsPlaying.block() - - getInstrumentation().runOnMainSync { - val player = atomicPlayer.get() - // Make test flaky because dependant of internet - if (player.playerError != null) { - throw Exception(player.playerError) - } - assertEquals(Player.STATE_READY, player.playbackState) - assertTrue(player.isPlaying) - assertNotNull(player.currentMediaItem) - assertEquals(player.currentMediaItem?.localConfiguration?.uri, Uri.parse(urlToTest)) - player.release() - } - } - - private class WaitIsPlaying : Player.Listener { - private val isPlaying = ConditionVariable() - - override fun onIsPlayingChanged(isPlaying: Boolean) { - if (isPlaying) { - this.isPlaying.open() - } - } - - /** - * Don't block test if a player error occurred - * @param error - */ - override fun onPlayerError(error: PlaybackException) { - isPlaying.open() - } - - fun block() { - isPlaying.block() - } - } - - companion object { - @Parameterized.Parameters(name = "{0}") - @JvmStatic - fun parameters(): Iterable { - return listOf( - ContentUrls.VOD_MP4, - ContentUrls.VOD_HLS, - ContentUrls.AOD_MP3, - ContentUrls.VOD_DASH_H264, - ContentUrls.VOD_DASH_H265, - ContentUrls.LIVE_HLS, - ContentUrls.LIVE_DVR_HLS, - ContentUrls.AUDIO_LIVE_DVR_HLS, - ContentUrls.AUDIO_LIVE_MP3 - ) - } - } -} diff --git a/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/utils/ContentUrls.kt b/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/utils/ContentUrls.kt deleted file mode 100644 index cf0174791..000000000 --- a/pillarbox-player/src/androidTest/java/ch/srgssr/pillarbox/player/utils/ContentUrls.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) SRG SSR. All rights reserved. - * License information is available from the LICENSE file. - */ -package ch.srgssr.pillarbox.player.utils - -@Suppress("MaxLineLength") -object ContentUrls { - const val VOD_HLS = "https://rts-vod-amd.akamaized.net/ww/14970442/4dcba1d3-8cc8-3667-a7d2-b3b92c4243d9/master.m3u8" - - // From urn:swi:video:48940210 - const val VOD_MP4 = "https://cdn.prod.swi-services.ch/video-projects/141b30ce-3850-424b-9063-a20d5619d342/localised-videos/ENG/renditions/ENG.mp4" - const val VOD_DASH_H264 = "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd" - const val VOD_DASH_H265 = "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd" - const val LIVE_HLS = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0" - const val LIVE_DVR_HLS = "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8" - const val AOD_MP3 = - "https://download-media.srf.ch/world/audio/Echo_der_Zeit_radio/2025/01/Echo_der_Zeit_radio_AUDI20250119_RS_0069_8a020b8274994bfdbc724cb0c6ed520c.mp3" - const val AUDIO_LIVE_MP3 = "https://stream.srg-ssr.ch/m/la-1ere/mp3_128" - const val AUDIO_LIVE_DVR_HLS = "https://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8" -} diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt new file mode 100644 index 000000000..488e2e989 --- /dev/null +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/IsPlayingAllTypeOfContentTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player + +import android.net.Uri +import android.os.Looper +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.test.utils.FakeClock +import androidx.media3.test.utils.robolectric.TestPlayerRunHelper +import androidx.test.core.app.ApplicationProvider +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.ParameterizedRobolectricTestRunner.Parameters +import org.robolectric.Shadows.shadowOf +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +@RunWith(ParameterizedRobolectricTestRunner::class) +class IsPlayingAllTypeOfContentTest( + @Suppress("unused") private val urlLabel: String, // Not used in the test itself, but for the test label + private val urlToTest: String, +) { + private lateinit var player: PillarboxExoPlayer + + @BeforeTest + fun setUp() { + player = PillarboxExoPlayer( + context = ApplicationProvider.getApplicationContext(), + type = Default, + ) { + clock(FakeClock(true)) + coroutineContext(EmptyCoroutineContext) + } + } + + @AfterTest + fun tearDown() { + player.release() + + shadowOf(Looper.getMainLooper()).idle() + } + + @Test + fun `is playing`() { + player.setMediaItem(MediaItem.fromUri(urlToTest)) + player.prepare() + player.play() + + TestPlayerRunHelper.play(player).untilBackgroundThreadCondition { player.isPlaying } + + assertEquals(Player.STATE_READY, player.playbackState) + assertTrue(player.isPlaying) + assertNotNull(player.currentMediaItem) + assertEquals(player.currentMediaItem?.localConfiguration?.uri, Uri.parse(urlToTest)) + } + + companion object { + @JvmStatic + @Suppress("unused", "MaximumLineLength", "MaxLineLength") + @Parameters(name = "{0}: {1}") + fun parameters(): Iterable { + return listOf( + arrayOf( + "VOD MP4", + "https://cdn.prod.swi-services.ch/video-projects/141b30ce-3850-424b-9063-a20d5619d342/localised-videos/ENG/renditions/ENG.mp4" + ), // From urn:swi:video:48940210 + arrayOf("VOD HLS", "https://rts-vod-amd.akamaized.net/ww/14970442/4dcba1d3-8cc8-3667-a7d2-b3b92c4243d9/master.m3u8"), + arrayOf( + "AOD MP3", + "https://download-media.srf.ch/world/audio/Echo_der_Zeit_radio/2025/01/Echo_der_Zeit_radio_AUDI20250119_RS_0069_8a020b8274994bfdbc724cb0c6ed520c.mp3" + ), + arrayOf("VOD DASH H264", "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd"), + arrayOf("VOD DASH H265", "https://storage.googleapis.com/wvmedia/clear/hevc/tears/tears.mpd"), + arrayOf("Live HLS", "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8?dw=0"), + arrayOf("Live DVR HLS", "https://rtsc3video.akamaized.net/hls/live/2042837/c3video/3/playlist.m3u8"), + arrayOf("Audio Live DVR HLS", "https://lsaplus.swisstxt.ch/audio/couleur3_96.stream/playlist.m3u8"), + // TODO Investigate why this content does not work + // arrayOf("Audio Live MP3", "https://stream.srg-ssr.ch/m/la-1ere/mp3_128"), + ) + } + } +}