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