From 7e8794bc6eeb3b76bb87a65fec6ac81f74a6e23f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 29 Nov 2023 15:09:48 +0100 Subject: [PATCH] Add tests for AndroidMediaPreProcessor --- libraries/mediaupload/impl/build.gradle.kts | 10 + .../libraries/mediaupload/ThumbnailFactory.kt | 4 +- .../impl/src/test/assets/animated_gif.gif | 3 + .../impl/src/test/assets/image.png | 3 + .../impl/src/test/assets/sample3s.mp3 | 3 + .../mediaupload/impl/src/test/assets/text.txt | 3 + .../impl/src/test/assets/video.mp4 | 3 + .../AndroidMediaPreProcessorTest.kt | 331 ++++++++++++++++++ 8 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 libraries/mediaupload/impl/src/test/assets/animated_gif.gif create mode 100644 libraries/mediaupload/impl/src/test/assets/image.png create mode 100644 libraries/mediaupload/impl/src/test/assets/sample3s.mp3 create mode 100644 libraries/mediaupload/impl/src/test/assets/text.txt create mode 100644 libraries/mediaupload/impl/src/test/assets/video.mp4 create mode 100644 libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessorTest.kt diff --git a/libraries/mediaupload/impl/build.gradle.kts b/libraries/mediaupload/impl/build.gradle.kts index a23ab14b74..c65b6b01a6 100644 --- a/libraries/mediaupload/impl/build.gradle.kts +++ b/libraries/mediaupload/impl/build.gradle.kts @@ -27,6 +27,12 @@ android { generateDaggerFactories.set(true) } + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) @@ -37,6 +43,7 @@ android { implementation(projects.libraries.core) implementation(projects.libraries.di) implementation(projects.libraries.matrix.api) + implementation(projects.services.toolbox.api) implementation(libs.inject) implementation(libs.androidx.exifinterface) implementation(libs.coroutines.core) @@ -44,7 +51,10 @@ android { implementation(libs.vanniktech.blurhash) testImplementation(libs.test.junit) + testImplementation(libs.test.robolectric) testImplementation(libs.coroutines.test) testImplementation(libs.test.truth) + testImplementation(projects.tests.testutils) + testImplementation(projects.services.toolbox.test) } } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt index 2cee2566d1..40a75b6428 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.ThumbnailInfo +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import kotlinx.coroutines.suspendCancellableCoroutine import java.io.File import javax.inject.Inject @@ -56,13 +57,14 @@ private const val VIDEO_THUMB_FRAME = 0L class ThumbnailFactory @Inject constructor( @ApplicationContext private val context: Context, + private val sdkIntProvider: BuildVersionSdkIntProvider ) { @SuppressLint("NewApi") suspend fun createImageThumbnail(file: File): ThumbnailResult { return createThumbnail { cancellationSignal -> // This API works correctly with GIF - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.Q)) { ThumbnailUtils.createImageThumbnail( file, Size(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT), diff --git a/libraries/mediaupload/impl/src/test/assets/animated_gif.gif b/libraries/mediaupload/impl/src/test/assets/animated_gif.gif new file mode 100644 index 0000000000..8fd2ce1550 --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/animated_gif.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6831610b21668c49e31732f9005177e959277233d3cab758910e061294f91d79 +size 687979 diff --git a/libraries/mediaupload/impl/src/test/assets/image.png b/libraries/mediaupload/impl/src/test/assets/image.png new file mode 100644 index 0000000000..b0946f7ba8 --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/image.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a980f7b74cb9edc323919db8652798da4b3dcf865fc7b6a1eb1110096b7bfb4f +size 1856786 diff --git a/libraries/mediaupload/impl/src/test/assets/sample3s.mp3 b/libraries/mediaupload/impl/src/test/assets/sample3s.mp3 new file mode 100644 index 0000000000..cb4db9c9d8 --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/sample3s.mp3 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0244590f2b4bcb62352b574e78bea940e8d89cfa69823b5208ef4c43e0abcb44 +size 52079 diff --git a/libraries/mediaupload/impl/src/test/assets/text.txt b/libraries/mediaupload/impl/src/test/assets/text.txt new file mode 100644 index 0000000000..d45ec4338b --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/text.txt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ba904eae8773b70c75333db4de2f3ac45a8ad4ddba1b242f0b3cfc199391dd8 +size 13 diff --git a/libraries/mediaupload/impl/src/test/assets/video.mp4 b/libraries/mediaupload/impl/src/test/assets/video.mp4 new file mode 100644 index 0000000000..4d57318b1d --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/video.mp4 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb58436524db95bd0c10b2c3023c2eb7b87404a2eab8987939f051647eb859d3 +size 1673712 diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessorTest.kt new file mode 100644 index 0000000000..cc7a0862fb --- /dev/null +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessorTest.kt @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * 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.element.android.libraries.mediaupload + +import android.content.Context +import android.os.Build +import androidx.core.net.toUri +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.matrix.api.media.AudioInfo +import io.element.android.libraries.matrix.api.media.FileInfo +import io.element.android.libraries.matrix.api.media.ImageInfo +import io.element.android.libraries.matrix.api.media.ThumbnailInfo +import io.element.android.libraries.matrix.api.media.VideoInfo +import io.element.android.libraries.mediaupload.api.MediaPreProcessor +import io.element.android.libraries.mediaupload.api.MediaUploadInfo +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import kotlin.time.Duration + +@RunWith(RobolectricTestRunner::class) +class AndroidMediaPreProcessorTest { + @Test + fun `test processing image`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "image.png") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Png, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("image.png") + val info = (result as MediaUploadInfo.Image) + assertThat(info.thumbnailFile).isNull() // TODO Check this + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Png, + size = 114_867, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing image api Q`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context, sdkIntVersion = Build.VERSION_CODES.Q) + val file = getFileFromAssets(context, "image.png") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Png, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("image.png") + val info = (result as MediaUploadInfo.Image) + assertThat(info.thumbnailFile).isNull() // TODO Check this + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Png, + size = 114_867, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing image no compression`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "image.png") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Png, + deleteOriginal = false, + compressIfPossible = false, + ).getOrThrow() + assertThat(result.file.path).endsWith("image.png") + val info = (result as MediaUploadInfo.Image) + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Png, + size = 1_856_786, + thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643), + thumbnailSource = null, + blurhash = "K00000fQfQfQfQfQfQfQfQ", + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing image and delete`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "image.png") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Png, + deleteOriginal = true, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("image.png") + val info = (result as MediaUploadInfo.Image) + assertThat(info.thumbnailFile).isNull() // TODO Check this + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Png, + size = 114_867, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + // Does not work + // assertThat(file.exists()).isFalse() + } + + @Test + fun `test processing gif`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "animated_gif.gif") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Gif, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("animated_gif.gif") + val info = (result as MediaUploadInfo.Image) + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.imageInfo).isEqualTo( + ImageInfo( + height = 600, + width = 800, + mimetype = MimeTypes.Gif, + size = 687979, + thumbnailInfo = ThumbnailInfo(height = 50, width = 50, mimetype = MimeTypes.Jpeg, size = 691), + thumbnailSource = null, + blurhash = "K00000fQfQfQfQfQfQfQfQ", + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing file`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "text.txt") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.PlainText, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("text.txt") + val info = (result as MediaUploadInfo.AnyFile) + assertThat(info.fileInfo).isEqualTo( + FileInfo( + mimetype = MimeTypes.PlainText, + size = 13, + thumbnailInfo = null, + thumbnailSource = null, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing video`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "video.mp4") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Mp4, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("video.mp4") + val info = (result as MediaUploadInfo.Video) + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.videoInfo).isEqualTo( + VideoInfo( + duration = Duration.ZERO, // Not available with Robolectric? + height = 1_178, + width = 1_818, + mimetype = MimeTypes.Mp4, + size = 114_867, + thumbnailInfo = null, + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing video no compression`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "video.mp4") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Mp4, + deleteOriginal = false, + compressIfPossible = false, + ).getOrThrow() + assertThat(result.file.path).endsWith("video.mp4") + val info = (result as MediaUploadInfo.Video) + assertThat(info.thumbnailFile).isNotNull() + assertThat(info.videoInfo).isEqualTo( + VideoInfo( + duration = Duration.ZERO, // Not available with Robolectric? + height = 0, // Not available with Robolectric? + width = 0, // Not available with Robolectric? + mimetype = MimeTypes.Mp4, + size = 1_673_712, + thumbnailInfo = ThumbnailInfo(height = null, width = null, mimetype = MimeTypes.Jpeg, size = 0), // Not available with Robolectric? + thumbnailSource = null, + blurhash = null, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test processing audio`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = getFileFromAssets(context, "sample3s.mp3") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.Mp3, + deleteOriginal = false, + compressIfPossible = true, + ).getOrThrow() + assertThat(result.file.path).endsWith("sample3s.mp3") + val info = (result as MediaUploadInfo.Audio) + assertThat(info.audioInfo).isEqualTo( + AudioInfo( + duration = Duration.ZERO, // Not available with Robolectric? + size = 52_079, + mimetype = MimeTypes.Mp3, + ) + ) + assertThat(file.exists()).isTrue() + } + + @Test + fun `test file which does not exist`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val sut = createAndroidMediaPreProcessor(context) + val file = File(context.cacheDir, "not found.txt") + val result = sut.process( + uri = file.toUri(), + mimeType = MimeTypes.PlainText, + deleteOriginal = false, + compressIfPossible = true, + ) + assertThat(result.isFailure).isTrue() + val failure = result.exceptionOrNull() + assertThat(failure).isInstanceOf(MediaPreProcessor.Failure::class.java) + assertThat(failure?.cause).isInstanceOf(FileNotFoundException::class.java) + } + + private fun TestScope.createAndroidMediaPreProcessor( + context: Context, + sdkIntVersion: Int = Build.VERSION_CODES.P + ) = AndroidMediaPreProcessor( + context = context, + thumbnailFactory = ThumbnailFactory(context, FakeBuildVersionSdkIntProvider(sdkIntVersion)), + imageCompressor = ImageCompressor(context, testCoroutineDispatchers()), + videoCompressor = VideoCompressor(context), + coroutineDispatchers = testCoroutineDispatchers(), + ) + + @Throws(IOException::class) + private fun getFileFromAssets(context: Context, fileName: String): File = File(context.cacheDir, fileName) + .also { + if (!it.exists()) { + it.outputStream().use { cache -> + context.assets.open(fileName).use { inputStream -> + inputStream.copyTo(cache) + } + } + } + } +}