diff --git a/app/build.gradle b/app/build.gradle index 501595fda..0ccc38f60 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,6 +93,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.1" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.1" testImplementation 'com.facebook.soloader:soloader:0.9.0' + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2" // Android testing androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION" diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt index c0282c92c..49a1f61c3 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedDao.kt @@ -1,9 +1,7 @@ package fr.free.nrw.commons.customselector.database import androidx.room.* -import kotlinx.coroutines.runBlocking import java.util.* -import kotlinx.coroutines.* /** * UploadedStatusDao for Custom Selector. @@ -29,58 +27,30 @@ abstract class UploadedStatusDao { @Delete abstract suspend fun delete(uploadedStatus: UploadedStatus) - /** - * Get All entries from the uploaded status table. - */ - @Query("SELECT * FROM uploaded_table") - abstract suspend fun getAll() : List - /** * Query uploaded status with image sha1. */ @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") - abstract suspend fun getFromImageSHA1(imageSHA1 : String) : UploadedStatus + abstract suspend fun getFromImageSHA1(imageSHA1 : String) : UploadedStatus? /** * Query uploaded status with modified image sha1. */ @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") - abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus + abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus? /** * Asynchronous insert into uploaded status table. */ suspend fun insertUploaded(uploadedStatus: UploadedStatus) { - uploadedStatus.lastUpdated = Calendar.getInstance().time as Date? + uploadedStatus.lastUpdated = Calendar.getInstance().time insert(uploadedStatus) } - /** - * Asynchronous delete from uploaded status table. - */ - suspend fun deleteUploaded(uploadedStatus: UploadedStatus) { - delete(uploadedStatus) - } - - /** - * Asynchronous update entry in uploaded status table. - */ - suspend fun updateUploaded(uploadedStatus: UploadedStatus) { - update(uploadedStatus) - } - /** * Asynchronous image sha1 query. */ - suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus { + suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? { return getFromImageSHA1(imageSHA1) } - - /** - * Asynchronous modified image sha1 query. - */ - suspend fun getUploadedFromModifiedImageSHA1(modifiedImageSHA1: String):UploadedStatus { - return getFromModifiedImageSHA1(modifiedImageSHA1) - } - } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index a617b2d2a..3b5254f86 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -62,9 +62,9 @@ class ImageLoader @Inject constructor( /** * Coroutine Dispatchers and Scope. */ - private var defaultDispatcher = Dispatchers.Default - private var ioDispatcher = Dispatchers.IO - private val scope = MainScope() + private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default + private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO + private val scope : CoroutineScope = MainScope() /** * Query image and setUp the view. @@ -129,7 +129,7 @@ class ImageLoader @Inject constructor( * @return Query result. */ - private suspend fun querySHA1(SHA1: String): Result { + suspend fun querySHA1(SHA1: String): Result { return withContext(ioDispatcher) { mapResult[SHA1]?.let { return@withContext it @@ -157,7 +157,7 @@ class ImageLoader @Inject constructor( * * @return sha1 of the image */ - private suspend fun getSHA1(image: Image): String { + suspend fun getSHA1(image: Image): String { mapModifiedImageSHA1[image]?.let{ return it } @@ -169,14 +169,14 @@ class ImageLoader @Inject constructor( /** * Get the uploaded status entry from the database. */ - private suspend fun getFromUploaded(imageSha1:String): UploadedStatus?{ + suspend fun getFromUploaded(imageSha1:String): UploadedStatus? { return uploadedStatusDao.getUploadedFromImageSHA1(imageSha1) } /** * Insert into uploaded status table. */ - private suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ + suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ uploadedStatusDao.insertUploaded( UploadedStatus( imageSha1, @@ -190,7 +190,7 @@ class ImageLoader @Inject constructor( /** * Get image sha1 from uri, used to retrieve the original image sha1. */ - private suspend fun getImageSHA1(uri: Uri): String { + suspend fun getImageSHA1(uri: Uri): String { return withContext(ioDispatcher) { mapImageSHA1[uri]?.let{ return@withContext it @@ -204,7 +204,7 @@ class ImageLoader @Inject constructor( /** * Get result data from database. */ - private fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result { + fun getResultFromUploadedStatus(uploadedStatus: UploadedStatus): Result { if (uploadedStatus.imageResult || uploadedStatus.modifiedImageResult) { return Result.TRUE } else { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt index cb7cf3a50..fe26921e5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/customselector/ui/selector/ImageLoaderTest.kt @@ -3,8 +3,7 @@ package fr.free.nrw.commons.customselector.ui.selector import android.content.ContentResolver import android.content.Context import android.net.Uri -import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.whenever +import com.nhaarman.mockitokotlin2.* import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.customselector.database.UploadedStatus import fr.free.nrw.commons.customselector.database.UploadedStatusDao @@ -17,6 +16,10 @@ import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileUtilsWrapper import io.reactivex.Single import junit.framework.Assert +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.* +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -28,7 +31,6 @@ import org.powermock.reflect.Whitebox import org.robolectric.annotation.Config import java.io.File import java.io.FileInputStream -import java.lang.Exception import java.util.* import kotlin.collections.HashMap @@ -38,6 +40,7 @@ import kotlin.collections.HashMap @RunWith(PowerMockRunner::class) @PrepareForTest(PickedFiles::class) @Config(sdk = [21], application = TestCommonsApplication::class) +@ExperimentalCoroutinesApi class ImageLoaderTest { @Mock @@ -73,146 +76,145 @@ class ImageLoaderTest { @Mock private lateinit var contentResolver: ContentResolver - @Mock - private lateinit var image: Image; + @ExperimentalCoroutinesApi + private val testDispacher = TestCoroutineDispatcher() private lateinit var imageLoader: ImageLoader; - private var mapImageSHA1: HashMap = HashMap() + private var mapImageSHA1: HashMap = HashMap() private var mapHolderImage : HashMap = HashMap() private var mapResult: HashMap = HashMap() + private var mapModifiedImageSHA1: HashMap = HashMap() + private lateinit var image: Image; + private lateinit var uploadedStatus: UploadedStatus; /** * Setup before test. */ @Before + @ExperimentalCoroutinesApi fun setup() { + Dispatchers.setMain(testDispacher) MockitoAnnotations.initMocks(this) + imageLoader = ImageLoader(mediaClient, fileProcessor, fileUtilsWrapper, uploadedStatusDao, context) + uploadedStatus= UploadedStatus( + "testSha1", + "testSha1", + false, + false, + Calendar.getInstance().time + ) + image = Image(1, "test", uri, "test", 0, "test") Whitebox.setInternalState(imageLoader, "mapImageSHA1", mapImageSHA1); Whitebox.setInternalState(imageLoader, "mapHolderImage", mapHolderImage); + Whitebox.setInternalState(imageLoader, "mapModifiedImageSHA1", mapModifiedImageSHA1); Whitebox.setInternalState(imageLoader, "mapResult", mapResult); Whitebox.setInternalState(imageLoader, "context", context) + Whitebox.setInternalState(imageLoader, "ioDispatcher", testDispacher) + Whitebox.setInternalState(imageLoader, "defaultDispatcher", testDispacher) + + whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream) + whenever(context.contentResolver).thenReturn(contentResolver) + whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") } /** - * Test queryAndSetView. + * Reset Dispatchers. + */ + @After + @ExperimentalCoroutinesApi + fun tearDown() { + Dispatchers.resetMain() + testDispacher.cleanupTestCoroutines() + } + + /** + * Test queryAndSetView with upload Status as null. */ @Test - fun testQueryAndSetView(){ - // TODO - imageLoader.queryAndSetView(holder,image) + fun testQueryAndSetViewUploadedStatusNull() = testDispacher.runBlockingTest { + whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null) + mapModifiedImageSHA1[image] = "testSha1" + mapImageSHA1[uri] = "testSha1" + + mapResult["testSha1"] = ImageLoader.Result.TRUE + imageLoader.queryAndSetView(holder, image) + + mapResult["testSha1"] = ImageLoader.Result.FALSE + imageLoader.queryAndSetView(holder, image) + } + + /** + * Test queryAndSetView with upload Status not null (ie retrieved from table) + */ + @Test + fun testQueryAndSetViewUploadedStatusNotNull() = testDispacher.runBlockingTest { + whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus) + imageLoader.queryAndSetView(holder, image) } /** * Test querySha1 */ @Test - fun testQuerySha1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "querySHA1", - String::class.java - ) - func.isAccessible = true + fun testQuerySha1() = testDispacher.runBlockingTest { - Mockito.`when`(single.blockingGet()).thenReturn(true) - Mockito.`when`(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single) - Mockito.`when`(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1") + whenever(single.blockingGet()).thenReturn(true) + whenever(mediaClient.checkFileExistsUsingSha("testSha1")).thenReturn(single) + whenever(fileUtilsWrapper.getSHA1(any())).thenReturn("testSha1") - // test without saving in map. - func.invoke(imageLoader, "testSha1"); - - // test with map save. - mapResult["testSha1"] = ImageLoader.Result.FALSE - func.invoke(imageLoader, "testSha1"); + imageLoader.querySHA1("testSha1") } /** * Test getSha1 */ @Test - @Throws (Exception::class) - fun testGetSha1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "getSHA1", - Image::class.java - ) - func.isAccessible = true + @ExperimentalCoroutinesApi + fun testGetSha1() = testDispacher.runBlockingTest { - PowerMockito.mockStatic(PickedFiles::class.java); + PowerMockito.mockStatic(PickedFiles::class.java) BDDMockito.given(PickedFiles.pickedExistingPicture(context, image.uri)) - .willReturn(UploadableFile(uri, File("ABC"))); + .willReturn(UploadableFile(uri, File("ABC"))) + whenever(fileUtilsWrapper.getFileInputStream("ABC")).thenReturn(inputStream) whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") - Assert.assertEquals("testSha1", func.invoke(imageLoader, image)); - whenever(PickedFiles.pickedExistingPicture(context,Uri.parse("test"))).thenReturn(uploadableFile) + Assert.assertEquals("testSha1", imageLoader.getSHA1(image)); + whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn( + uploadableFile + ) - mapImageSHA1[image] = "testSha2" - Assert.assertEquals("testSha2", func.invoke(imageLoader, image)); - } - - /** - * Test insertIntoUploaded Function. - */ - @Test - @Throws (Exception::class) - fun testInsertIntoUploaded() { - val func = imageLoader.javaClass.getDeclaredMethod( - "insertIntoUploaded", - String::class.java, - String::class.java, - Boolean::class.java, - Boolean::class.java) - func.isAccessible = true - - func.invoke(imageLoader, "", "", true, true) - } - - /** - * Test getImageSha1. - */ - @Test - @Throws (Exception::class) - fun testGetImageSHA1() { - val func = imageLoader.javaClass.getDeclaredMethod( - "getImageSHA1", - Uri::class.java) - func.isAccessible = true - - whenever(contentResolver.openInputStream(uri)).thenReturn(inputStream) - whenever(context.contentResolver).thenReturn(contentResolver) - whenever(fileUtilsWrapper.getSHA1(inputStream)).thenReturn("testSha1") - - Assert.assertEquals("testSha1", func.invoke(imageLoader,uri)) + mapModifiedImageSHA1[image] = "testSha2" + Assert.assertEquals("testSha2", imageLoader.getSHA1(image)); } /** * Test getResultFromUploadedStatus. */ @Test - @Throws (Exception::class) fun testGetResultFromUploadedStatus() { val func = imageLoader.javaClass.getDeclaredMethod( "getResultFromUploadedStatus", UploadedStatus::class.java) func.isAccessible = true - // test Result.TRUE - Assert.assertEquals(ImageLoader.Result.TRUE, - func.invoke(imageLoader, - UploadedStatus("", "", true, true))) - - // test Result.FALSE - Assert.assertEquals(ImageLoader.Result.FALSE, - func.invoke(imageLoader, - UploadedStatus("", "", false, false, Calendar.getInstance().time))) - // test Result.INVALID + uploadedStatus.lastUpdated = Date(0); Assert.assertEquals(ImageLoader.Result.INVALID, - func.invoke(imageLoader, UploadedStatus("", "", false, false, Date(0)))) + imageLoader.getResultFromUploadedStatus(uploadedStatus)) + // test Result.TRUE + uploadedStatus.imageResult = true; + Assert.assertEquals(ImageLoader.Result.TRUE, + imageLoader.getResultFromUploadedStatus(uploadedStatus)) + } + + @Test + fun testCleanUP() { + imageLoader.cleanUP() } } \ No newline at end of file