1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver.contentpreview 18 19 import android.graphics.Bitmap 20 import android.net.Uri 21 import com.google.common.truth.Truth.assertThat 22 import kotlin.math.ceil 23 import kotlin.math.roundToInt 24 import kotlin.time.Duration.Companion.milliseconds 25 import kotlinx.coroutines.ExperimentalCoroutinesApi 26 import kotlinx.coroutines.delay 27 import kotlinx.coroutines.test.StandardTestDispatcher 28 import kotlinx.coroutines.test.TestScope 29 import kotlinx.coroutines.test.advanceTimeBy 30 import kotlinx.coroutines.test.runCurrent 31 import kotlinx.coroutines.test.runTest 32 import org.junit.Test 33 34 @OptIn(ExperimentalCoroutinesApi::class) 35 class CachingImagePreviewImageLoaderTest { 36 37 private val testDispatcher = StandardTestDispatcher() 38 private val testScope = TestScope(testDispatcher) 39 private val testJobTime = 100.milliseconds 40 private val testCacheSize = 4 41 private val testMaxConcurrency = 2 42 private val testTimeToFillCache = 43 testJobTime * ceil((testCacheSize).toFloat() / testMaxConcurrency.toFloat()).roundToInt() 44 private val testUris = 45 List(5) { Uri.fromParts("TestScheme$it", "TestSsp$it", "TestFragment$it") } 46 private val testTimeToLoadAllUris = 47 testJobTime * ceil((testUris.size).toFloat() / testMaxConcurrency.toFloat()).roundToInt() 48 private val testBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8) 49 private val fakeThumbnailLoader = 50 FakeThumbnailLoader().apply { 51 testUris.forEach { 52 fakeInvoke[it] = { 53 delay(testJobTime) 54 testBitmap 55 } 56 } 57 } 58 59 private val imageLoader = 60 CachingImagePreviewImageLoader( 61 scope = testScope.backgroundScope, 62 bgDispatcher = testDispatcher, 63 thumbnailLoader = fakeThumbnailLoader, 64 cacheSize = testCacheSize, 65 maxConcurrency = testMaxConcurrency, 66 ) 67 68 @Test 69 fun loadImage_notCached_callsThumbnailLoader() = 70 testScope.runTest { 71 // Arrange 72 var result: Bitmap? = null 73 74 // Act 75 imageLoader.loadImage(testScope, testUris[0]) { result = it } 76 advanceTimeBy(testJobTime) 77 runCurrent() 78 79 // Assert 80 assertThat(fakeThumbnailLoader.invokeCalls).containsExactly(testUris[0]) 81 assertThat(result).isSameInstanceAs(testBitmap) 82 } 83 84 @Test 85 fun loadImage_cached_usesCachedValue() = 86 testScope.runTest { 87 // Arrange 88 imageLoader.loadImage(testScope, testUris[0]) {} 89 advanceTimeBy(testJobTime) 90 runCurrent() 91 fakeThumbnailLoader.invokeCalls.clear() 92 var result: Bitmap? = null 93 94 // Act 95 imageLoader.loadImage(testScope, testUris[0]) { result = it } 96 advanceTimeBy(testJobTime) 97 runCurrent() 98 99 // Assert 100 assertThat(fakeThumbnailLoader.invokeCalls).isEmpty() 101 assertThat(result).isSameInstanceAs(testBitmap) 102 } 103 104 @Test 105 fun loadImage_error_returnsNull() = 106 testScope.runTest { 107 // Arrange 108 fakeThumbnailLoader.fakeInvoke[testUris[0]] = { 109 delay(testJobTime) 110 throw RuntimeException("Test exception") 111 } 112 var result: Bitmap? = testBitmap 113 114 // Act 115 imageLoader.loadImage(testScope, testUris[0]) { result = it } 116 advanceTimeBy(testJobTime) 117 runCurrent() 118 119 // Assert 120 assertThat(fakeThumbnailLoader.invokeCalls).containsExactly(testUris[0]) 121 assertThat(result).isNull() 122 } 123 124 @Test 125 fun loadImage_uncached_limitsConcurrency() = 126 testScope.runTest { 127 // Arrange 128 val results = mutableListOf<Bitmap?>() 129 assertThat(testUris.size).isGreaterThan(testMaxConcurrency) 130 131 // Act 132 testUris.take(testMaxConcurrency + 1).forEach { uri -> 133 imageLoader.loadImage(testScope, uri) { results.add(it) } 134 } 135 136 // Assert 137 assertThat(results).isEmpty() 138 advanceTimeBy(testJobTime) 139 runCurrent() 140 assertThat(results).hasSize(testMaxConcurrency) 141 advanceTimeBy(testJobTime) 142 runCurrent() 143 assertThat(results).hasSize(testMaxConcurrency + 1) 144 assertThat(results) 145 .containsExactlyElementsIn(List(testMaxConcurrency + 1) { testBitmap }) 146 } 147 148 @Test 149 fun loadImage_cacheEvicted_cancelsLoadAndReturnsNull() = 150 testScope.runTest { 151 // Arrange 152 val results = MutableList<Bitmap?>(testUris.size) { null } 153 assertThat(testUris.size).isGreaterThan(testCacheSize) 154 155 // Act 156 imageLoader.loadImage(testScope, testUris[0]) { results[0] = it } 157 runCurrent() 158 testUris.indices.drop(1).take(testCacheSize).forEach { i -> 159 imageLoader.loadImage(testScope, testUris[i]) { results[i] = it } 160 } 161 advanceTimeBy(testTimeToFillCache) 162 runCurrent() 163 164 // Assert 165 assertThat(fakeThumbnailLoader.invokeCalls).containsExactlyElementsIn(testUris) 166 assertThat(results) 167 .containsExactlyElementsIn( 168 List(testUris.size) { index -> if (index == 0) null else testBitmap } 169 ) 170 .inOrder() 171 assertThat(fakeThumbnailLoader.unfinishedInvokeCount).isEqualTo(1) 172 } 173 174 @Test 175 fun prePopulate_fillsCache() = 176 testScope.runTest { 177 // Arrange 178 val fullCacheUris = testUris.take(testCacheSize) 179 assertThat(fullCacheUris).hasSize(testCacheSize) 180 181 // Act 182 imageLoader.prePopulate(fullCacheUris) 183 advanceTimeBy(testTimeToFillCache) 184 runCurrent() 185 186 // Assert 187 assertThat(fakeThumbnailLoader.invokeCalls).containsExactlyElementsIn(fullCacheUris) 188 189 // Act 190 fakeThumbnailLoader.invokeCalls.clear() 191 imageLoader.prePopulate(fullCacheUris) 192 advanceTimeBy(testTimeToFillCache) 193 runCurrent() 194 195 // Assert 196 assertThat(fakeThumbnailLoader.invokeCalls).isEmpty() 197 } 198 199 @Test 200 fun prePopulate_greaterThanCacheSize_fillsCacheThenDropsRemaining() = 201 testScope.runTest { 202 // Arrange 203 assertThat(testUris.size).isGreaterThan(testCacheSize) 204 205 // Act 206 imageLoader.prePopulate(testUris) 207 advanceTimeBy(testTimeToLoadAllUris) 208 runCurrent() 209 210 // Assert 211 assertThat(fakeThumbnailLoader.invokeCalls) 212 .containsExactlyElementsIn(testUris.take(testCacheSize)) 213 214 // Act 215 fakeThumbnailLoader.invokeCalls.clear() 216 imageLoader.prePopulate(testUris) 217 advanceTimeBy(testTimeToLoadAllUris) 218 runCurrent() 219 220 // Assert 221 assertThat(fakeThumbnailLoader.invokeCalls).isEmpty() 222 } 223 224 @Test 225 fun prePopulate_fewerThatCacheSize_loadsTheGiven() = 226 testScope.runTest { 227 // Arrange 228 val unfilledCacheUris = testUris.take(testMaxConcurrency) 229 assertThat(unfilledCacheUris.size).isLessThan(testCacheSize) 230 231 // Act 232 imageLoader.prePopulate(unfilledCacheUris) 233 advanceTimeBy(testJobTime) 234 runCurrent() 235 236 // Assert 237 assertThat(fakeThumbnailLoader.invokeCalls).containsExactlyElementsIn(unfilledCacheUris) 238 239 // Act 240 fakeThumbnailLoader.invokeCalls.clear() 241 imageLoader.prePopulate(unfilledCacheUris) 242 advanceTimeBy(testJobTime) 243 runCurrent() 244 245 // Assert 246 assertThat(fakeThumbnailLoader.invokeCalls).isEmpty() 247 } 248 249 @Test 250 fun invoke_uncached_alwaysCallsTheThumbnailLoader() = 251 testScope.runTest { 252 // Arrange 253 254 // Act 255 imageLoader.invoke(testUris[0], caching = false) 256 imageLoader.invoke(testUris[0], caching = false) 257 advanceTimeBy(testJobTime) 258 runCurrent() 259 260 // Assert 261 assertThat(fakeThumbnailLoader.invokeCalls).containsExactly(testUris[0], testUris[0]) 262 } 263 264 @Test 265 fun invoke_cached_usesTheCacheWhenPossible() = 266 testScope.runTest { 267 // Arrange 268 269 // Act 270 imageLoader.invoke(testUris[0], caching = true) 271 imageLoader.invoke(testUris[0], caching = true) 272 advanceTimeBy(testJobTime) 273 runCurrent() 274 275 // Assert 276 assertThat(fakeThumbnailLoader.invokeCalls).containsExactly(testUris[0]) 277 } 278 } 279