@onekeyfe/react-native-image 3.0.105 → 3.0.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ package com.margelo.nitro.onekeyimage
2
+
3
+ import androidx.core.util.Pools
4
+ import com.bumptech.glide.Priority
5
+ import com.bumptech.glide.disklrucache.DiskLruCache
6
+ import com.bumptech.glide.load.DataSource
7
+ import com.bumptech.glide.load.EncodeStrategy
8
+ import com.bumptech.glide.load.Options
9
+ import com.bumptech.glide.load.data.DataFetcher
10
+ import com.bumptech.glide.load.engine.DiskCacheStrategy
11
+ import com.bumptech.glide.load.model.MultiModelLoaderFactory
12
+ import org.junit.Assert.assertArrayEquals
13
+ import org.junit.Assert.assertEquals
14
+ import org.junit.Assert.assertFalse
15
+ import org.junit.Assert.assertNotEquals
16
+ import org.junit.Assert.assertNull
17
+ import org.junit.Assert.assertTrue
18
+ import org.junit.Test
19
+ import java.io.IOException
20
+ import java.io.File
21
+ import java.nio.file.Files
22
+ import java.nio.ByteBuffer
23
+ import java.security.MessageDigest
24
+ import java.util.concurrent.CountDownLatch
25
+ import java.util.concurrent.Executors
26
+ import java.util.concurrent.TimeUnit
27
+ import java.util.concurrent.atomic.AtomicInteger
28
+
29
+ class OneKeyBlockieAvatarLoaderTest {
30
+ private val uri = "onekey-avatar://blockie/v1/0x1234"
31
+
32
+ private class Callback : DataFetcher.DataCallback<ByteBuffer> {
33
+ var data: ByteBuffer? = null
34
+ var error: Exception? = null
35
+ override fun onDataReady(data: ByteBuffer?) { this.data = data }
36
+ override fun onLoadFailed(error: Exception) { this.error = error }
37
+ }
38
+
39
+ @Test
40
+ fun memoryDiskAvatarRequestsCacheTheOriginalLocalPngAcrossSizes() {
41
+ val strategy = oneKeyImageMemoryDiskStrategy(uri)
42
+ assertTrue(strategy.isDataCacheable(DataSource.LOCAL))
43
+ assertTrue(strategy.decodeCachedData())
44
+ assertFalse(strategy.decodeCachedResource())
45
+ assertFalse(strategy.isResourceCacheable(false, DataSource.LOCAL, EncodeStrategy.TRANSFORMED))
46
+ assertFalse(strategy.isDataCacheable(DataSource.DATA_DISK_CACHE))
47
+ assertFalse(DiskCacheStrategy.ALL.isDataCacheable(DataSource.LOCAL))
48
+ assertEquals(DiskCacheStrategy.AUTOMATIC, oneKeyImageMemoryDiskStrategy("https://example.com/image.png"))
49
+ assertEquals(DiskCacheStrategy.AUTOMATIC, oneKeyImageMemoryDiskStrategy("data:image/png;base64,AA=="))
50
+ assertFalse(DiskCacheStrategy.AUTOMATIC.isDataCacheable(DataSource.LOCAL))
51
+ }
52
+
53
+ @Test
54
+ fun originalPngCacheKeyIsStableAcrossRequestedDecodeDimensions() {
55
+ val factory = MultiModelLoaderFactory(Pools.SynchronizedPool<List<Throwable>>(1))
56
+ val loader = OneKeyBlockieAvatarLoaderFactory().build(factory)
57
+ val first = loader.buildLoadData(OneKeyBlockieAvatarModel(uri), 96, 96, Options())!!
58
+ val second = loader.buildLoadData(OneKeyBlockieAvatarModel(uri), 128, 128, Options())!!
59
+ val other = loader.buildLoadData(OneKeyBlockieAvatarModel(uri + "0"), 96, 96, Options())!!
60
+ assertEquals(first.sourceKey, second.sourceKey)
61
+ assertNotEquals(first.sourceKey, other.sourceKey)
62
+ val firstDigest = MessageDigest.getInstance("SHA-256").also(first.sourceKey::updateDiskCacheKey).digest()
63
+ val secondDigest = MessageDigest.getInstance("SHA-256").also(second.sourceKey::updateDiskCacheKey).digest()
64
+ assertArrayEquals(firstDigest, secondDigest)
65
+ }
66
+
67
+ private fun cacheFetcher(file: File): DataFetcher<ByteBuffer> {
68
+ val factory = MultiModelLoaderFactory(Pools.SynchronizedPool<List<Throwable>>(1))
69
+ val loader = OneKeyAvatarCacheFileLoaderFactory().build(factory)
70
+ return loader.buildLoadData(file, 96, 96, Options().set(oneKeyAvatarCacheFileOption, true))!!.fetcher
71
+ }
72
+
73
+ @Test
74
+ fun cacheValidationIsNotRegisteredForOrdinaryImageRequests() {
75
+ val factory = MultiModelLoaderFactory(Pools.SynchronizedPool<List<Throwable>>(1))
76
+ val loader = OneKeyAvatarCacheFileLoaderFactory().build(factory)
77
+ assertNull(loader.buildLoadData(File("ordinary-network-image"), 96, 96, Options()))
78
+ }
79
+
80
+ @Test
81
+ fun validCachedSourceIsReturnedWithoutRemovingOrRewritingIt() {
82
+ val directory = Files.createTempDirectory("avatar-cache-valid").toFile()
83
+ val file = File(directory, "avatar.0")
84
+ val bytes = OneKeyBlockieAvatar.png(uri)
85
+ try {
86
+ file.writeBytes(bytes)
87
+ val modified = file.lastModified()
88
+ val fetcher = cacheFetcher(file)
89
+ val callback = Callback()
90
+ fetcher.loadData(Priority.NORMAL, callback)
91
+ fetcher.cleanup()
92
+ assertNull(callback.error)
93
+ assertArrayEquals(bytes, callback.data!!.array())
94
+ assertEquals(modified, file.lastModified())
95
+ assertArrayEquals(bytes, file.readBytes())
96
+ } finally {
97
+ directory.deleteRecursively()
98
+ }
99
+ }
100
+
101
+ @Test
102
+ fun corruptEntryRemovalAllowsRealGlideJournalRewriteAndRestartReuse() {
103
+ val directory = Files.createTempDirectory("avatar-cache-journal").toFile()
104
+ val bytes = OneKeyBlockieAvatar.png(uri)
105
+ try {
106
+ DiskLruCache.open(directory, 1, 1, 1024L * 1024).use { disk ->
107
+ disk.edit("avatar").also { editor ->
108
+ editor.getFile(0).writeBytes(bytes.copyOf(bytes.size - 1))
109
+ editor.commit()
110
+ }
111
+ val file = disk.get("avatar")!!.getFile(0)
112
+ val fetcher = cacheFetcher(file)
113
+ val callback = Callback()
114
+ fetcher.loadData(Priority.NORMAL, callback)
115
+ fetcher.cleanup()
116
+ assertTrue(callback.error is IOException)
117
+ assertNull(callback.data)
118
+ assertFalse(file.exists())
119
+ // DiskLruCacheWrapper.put uses this same journal lookup before deciding to skip a write.
120
+ assertNull(disk.get("avatar"))
121
+ disk.edit("avatar").also { editor ->
122
+ editor.getFile(0).writeBytes(bytes)
123
+ editor.commit()
124
+ }
125
+ assertArrayEquals(bytes, disk.get("avatar")!!.getFile(0).readBytes())
126
+ }
127
+ DiskLruCache.open(directory, 1, 1, 1024L * 1024).use { reopened ->
128
+ val fetcher = cacheFetcher(reopened.get("avatar")!!.getFile(0))
129
+ val callback = Callback()
130
+ fetcher.loadData(Priority.NORMAL, callback)
131
+ fetcher.cleanup()
132
+ assertNull(callback.error)
133
+ assertArrayEquals(bytes, callback.data!!.array())
134
+ }
135
+ } finally {
136
+ directory.deleteRecursively()
137
+ }
138
+ }
139
+
140
+ @Test
141
+ fun oversizedCacheIsRejectedAndRemovedBeforeUnboundedRead() {
142
+ val directory = Files.createTempDirectory("avatar-cache-large").toFile()
143
+ val file = File(directory, "avatar.0")
144
+ try {
145
+ file.writeBytes(ByteArray(OneKeyBlockieAvatar.MAX_PNG_BYTES + 1))
146
+ val fetcher = cacheFetcher(file)
147
+ val callback = Callback()
148
+ fetcher.loadData(Priority.NORMAL, callback)
149
+ fetcher.cleanup()
150
+ assertTrue(callback.error is IOException)
151
+ assertFalse(file.exists())
152
+ } finally {
153
+ directory.deleteRecursively()
154
+ }
155
+ }
156
+
157
+ @Test
158
+ fun cancelledCacheReadDoesNotDeleteTheFileOrCallBack() {
159
+ val directory = Files.createTempDirectory("avatar-cache-cancel").toFile()
160
+ val file = File(directory, "avatar.0")
161
+ try {
162
+ file.writeBytes(byteArrayOf(1, 2, 3))
163
+ val fetcher = cacheFetcher(file)
164
+ val callback = Callback()
165
+ fetcher.cancel()
166
+ fetcher.loadData(Priority.NORMAL, callback)
167
+ fetcher.cleanup()
168
+ assertNull(callback.data)
169
+ assertNull(callback.error)
170
+ assertTrue(file.exists())
171
+ } finally {
172
+ directory.deleteRecursively()
173
+ }
174
+ }
175
+
176
+ @Test
177
+ fun modelDispatchDoesNotDecodeTheUriOrParseHeadersOnMain() {
178
+ val model = OneKeyImageModel.build("onekey-avatar://blockie/v1/%INVALID", "invalid JSON")
179
+ assertEquals(OneKeyBlockieAvatarModel("onekey-avatar://blockie/v1/%INVALID"), model)
180
+ }
181
+
182
+ @Test
183
+ fun defaultFetcherGeneratesAValidatedPngAndReportsLocalData() {
184
+ val fetcher = OneKeyBlockieAvatarFetcher(uri)
185
+ val callback = Callback()
186
+ try {
187
+ fetcher.loadData(Priority.NORMAL, callback)
188
+ assertNull(callback.error)
189
+ assertEquals(DataSource.LOCAL, fetcher.dataSource)
190
+ assertArrayEquals(OneKeyBlockieAvatar.png(uri), callback.data!!.array())
191
+ } finally {
192
+ fetcher.cleanup()
193
+ }
194
+ }
195
+
196
+ @Test
197
+ fun malformedUriReportsFailureWithoutReturningImageData() {
198
+ val fetcher = OneKeyBlockieAvatarFetcher("onekey-avatar://blockie/v2/seed")
199
+ val callback = Callback()
200
+ try {
201
+ fetcher.loadData(Priority.NORMAL, callback)
202
+ assertNull(callback.data)
203
+ assertTrue(callback.error is IllegalArgumentException)
204
+ } finally {
205
+ fetcher.cleanup()
206
+ }
207
+ }
208
+
209
+ @Test
210
+ fun cancelledBeforeLoadingDoesNotGenerateOrCallBack() {
211
+ val generated = AtomicInteger()
212
+ val fetcher = OneKeyBlockieAvatarFetcher(uri, OneKeyAvatarInFlight { _, _ ->
213
+ generated.incrementAndGet()
214
+ byteArrayOf(1)
215
+ })
216
+ val callback = Callback()
217
+ fetcher.cancel()
218
+ fetcher.loadData(Priority.NORMAL, callback)
219
+ fetcher.cleanup()
220
+ assertEquals(0, generated.get())
221
+ assertNull(callback.data)
222
+ assertNull(callback.error)
223
+ }
224
+
225
+ @Test
226
+ fun cancellationSuppressesLateCallbacksAndReleasesTheWorkForAFreshRequest() {
227
+ val started = CountDownLatch(1)
228
+ val complete = CountDownLatch(1)
229
+ val generated = AtomicInteger()
230
+ val requests = OneKeyAvatarInFlight { _, cancelled ->
231
+ if (generated.incrementAndGet() == 1) {
232
+ started.countDown()
233
+ check(complete.await(3, TimeUnit.SECONDS))
234
+ assertTrue(cancelled())
235
+ }
236
+ byteArrayOf(1)
237
+ }
238
+ val fetcher = OneKeyBlockieAvatarFetcher(uri, requests)
239
+ val callback = Callback()
240
+ val executor = Executors.newSingleThreadExecutor()
241
+ try {
242
+ val loaded = executor.submit { fetcher.loadData(Priority.NORMAL, callback) }
243
+ assertTrue(started.await(3, TimeUnit.SECONDS))
244
+ fetcher.cancel()
245
+ fetcher.cleanup()
246
+ complete.countDown()
247
+ loaded.get(3, TimeUnit.SECONDS)
248
+ assertNull(callback.data)
249
+ assertNull(callback.error)
250
+ val fresh = OneKeyBlockieAvatarFetcher(uri, requests)
251
+ val next = Callback()
252
+ fresh.loadData(Priority.NORMAL, next)
253
+ fresh.cleanup()
254
+ assertArrayEquals(byteArrayOf(1), next.data!!.array())
255
+ assertEquals(2, generated.get())
256
+ } finally {
257
+ fetcher.cleanup()
258
+ complete.countDown()
259
+ executor.shutdownNow()
260
+ }
261
+ }
262
+
263
+ @Test
264
+ fun cancellationNeverWaitsForAGlideCallbackHoldingItsOwnLock() {
265
+ val callbackStarted = CountDownLatch(1)
266
+ val callbackComplete = CountDownLatch(1)
267
+ val fetcher = OneKeyBlockieAvatarFetcher(uri, OneKeyAvatarInFlight { _, _ -> byteArrayOf(1) })
268
+ val executor = Executors.newFixedThreadPool(2)
269
+ val callback = object : DataFetcher.DataCallback<ByteBuffer> {
270
+ override fun onDataReady(data: ByteBuffer?) {
271
+ callbackStarted.countDown()
272
+ check(callbackComplete.await(3, TimeUnit.SECONDS))
273
+ }
274
+ override fun onLoadFailed(error: Exception) { throw error }
275
+ }
276
+ try {
277
+ val loaded = executor.submit { fetcher.loadData(Priority.NORMAL, callback) }
278
+ assertTrue(callbackStarted.await(3, TimeUnit.SECONDS))
279
+ executor.submit { fetcher.cancel() }.get(1, TimeUnit.SECONDS)
280
+ callbackComplete.countDown()
281
+ loaded.get(3, TimeUnit.SECONDS)
282
+ } finally {
283
+ callbackComplete.countDown()
284
+ fetcher.cleanup()
285
+ executor.shutdownNow()
286
+ }
287
+ }
288
+
289
+ @Test
290
+ fun glideCallbackFailureIsNotReportedAsASecondGenerationFailure() {
291
+ val failures = AtomicInteger()
292
+ val expected = IllegalStateException("Synthetic callback failure")
293
+ val fetcher = OneKeyBlockieAvatarFetcher(uri, OneKeyAvatarInFlight { _, _ -> byteArrayOf(1) })
294
+ val callback = object : DataFetcher.DataCallback<ByteBuffer> {
295
+ override fun onDataReady(data: ByteBuffer?) { throw expected }
296
+ override fun onLoadFailed(error: Exception) { failures.incrementAndGet() }
297
+ }
298
+ try {
299
+ fetcher.loadData(Priority.NORMAL, callback)
300
+ org.junit.Assert.fail("Callback exception must propagate")
301
+ } catch (error: IllegalStateException) {
302
+ assertEquals(expected, error)
303
+ assertEquals(0, failures.get())
304
+ } finally {
305
+ fetcher.cleanup()
306
+ }
307
+ }
308
+
309
+ @Test
310
+ fun failedGenerationIsReleasedSoRetryCanSucceed() {
311
+ val generated = AtomicInteger()
312
+ val requests = OneKeyAvatarInFlight { _, _ ->
313
+ if (generated.incrementAndGet() == 1) throw IOException("Synthetic generation failure")
314
+ byteArrayOf(1)
315
+ }
316
+ val failed = OneKeyBlockieAvatarFetcher(uri, requests)
317
+ val first = Callback()
318
+ failed.loadData(Priority.NORMAL, first)
319
+ failed.cleanup()
320
+ assertTrue(first.error is IOException)
321
+ val retried = OneKeyBlockieAvatarFetcher(uri, requests)
322
+ val second = Callback()
323
+ retried.loadData(Priority.NORMAL, second)
324
+ retried.cleanup()
325
+ assertArrayEquals(byteArrayOf(1), second.data!!.array())
326
+ }
327
+ }
@@ -0,0 +1,214 @@
1
+ package com.margelo.nitro.onekeyimage
2
+
3
+ import org.junit.Assert.assertArrayEquals
4
+ import org.junit.Assert.assertEquals
5
+ import org.junit.Assert.assertFalse
6
+ import org.junit.Assert.assertTrue
7
+ import org.junit.Assert.fail
8
+ import org.junit.Test
9
+ import java.io.ByteArrayOutputStream
10
+ import java.io.DataOutputStream
11
+ import java.nio.ByteBuffer
12
+ import java.util.zip.CRC32
13
+ import java.util.zip.DeflaterOutputStream
14
+ import java.util.concurrent.CancellationException
15
+ import java.util.concurrent.CountDownLatch
16
+ import java.util.concurrent.Executors
17
+ import java.util.concurrent.TimeUnit
18
+ import java.util.concurrent.atomic.AtomicInteger
19
+
20
+ class OneKeyBlockieAvatarTest {
21
+ @Test
22
+ fun percentDecodePreservesJavaScriptNormalizedUtf16WithoutRecasing() {
23
+ assertEquals("i\u0307中🙂/#+", OneKeyBlockieAvatar.decodeSeed(
24
+ "onekey-avatar://blockie/v1/i%CC%87%E4%B8%AD%F0%9F%99%82%2F%23%2B",
25
+ ))
26
+ assertEquals("İ", OneKeyBlockieAvatar.decodeSeed("onekey-avatar://blockie/v1/%C4%B0"))
27
+ assertEquals("\u0000", OneKeyBlockieAvatar.decodeSeed("onekey-avatar://blockie/v1/%00"))
28
+ }
29
+
30
+ @Test
31
+ fun invalidProtocolPercentEncodingAndUtf8AreRejected() {
32
+ listOf(
33
+ "onekey-avatar://blockie/v2/seed", "onekey-avatar://blockie/v1/",
34
+ "onekey-avatar://blockie/v1/%", "onekey-avatar://blockie/v1/%GG",
35
+ "onekey-avatar://blockie/v1/%C0%AF", "onekey-avatar://blockie/v1/%ED%A0%80",
36
+ "onekey-avatar://blockie/v1/seed?query", "onekey-avatar://blockie/v1/a/b",
37
+ "onekey-avatar://blockie/v1/a+b", "onekey-avatar://blockie/v1/中",
38
+ ).forEach { uri ->
39
+ try {
40
+ OneKeyBlockieAvatar.decodeSeed(uri)
41
+ fail("Invalid avatar URI was accepted")
42
+ } catch (_: Exception) { }
43
+ }
44
+ }
45
+
46
+ @Test
47
+ fun generationIsDeterministicAndEmits128pxIndexedPng() {
48
+ val uri = "onekey-avatar://blockie/v1/0x1234"
49
+ val first = OneKeyBlockieAvatar.png(uri)
50
+ assertArrayEquals(first, OneKeyBlockieAvatar.png(uri))
51
+ assertArrayEquals(byteArrayOf(-119, 80, 78, 71, 13, 10, 26, 10), first.copyOfRange(0, 8))
52
+ assertArrayEquals(byteArrayOf(0, 0, 0, -128, 0, 0, 0, -128), first.copyOfRange(16, 24))
53
+ assertTrue(first.size < 1024)
54
+ }
55
+
56
+ @Test(expected = CancellationException::class)
57
+ fun cancellationStopsBeforeGeneration() {
58
+ OneKeyBlockieAvatar.png("onekey-avatar://blockie/v1/seed") { true }
59
+ }
60
+
61
+ @Test(expected = CancellationException::class)
62
+ fun cancellationAlsoStopsLargeSeedDecoding() {
63
+ val checks = AtomicInteger()
64
+ OneKeyBlockieAvatar.png("onekey-avatar://blockie/v1/" + "a".repeat(8192)) {
65
+ checks.incrementAndGet() > 3
66
+ }
67
+ }
68
+
69
+ private fun replaceChunk(png: ByteArray, type: String, data: ByteArray): ByteArray {
70
+ val input = ByteBuffer.wrap(png)
71
+ input.position(8)
72
+ while (input.hasRemaining()) {
73
+ val start = input.position()
74
+ val length = input.int
75
+ val chunkType = ByteArray(4).also(input::get).toString(Charsets.US_ASCII)
76
+ val end = input.position() + length + 4
77
+ if (chunkType == type) {
78
+ val chunk = ByteArrayOutputStream()
79
+ DataOutputStream(chunk).use {
80
+ val name = type.toByteArray(Charsets.US_ASCII)
81
+ it.writeInt(data.size)
82
+ it.write(name)
83
+ it.write(data)
84
+ it.writeInt(CRC32().apply { update(name); update(data) }.value.toInt())
85
+ }
86
+ return png.copyOfRange(0, start) + chunk.toByteArray() + png.copyOfRange(end, png.size)
87
+ }
88
+ input.position(end)
89
+ }
90
+ throw IllegalArgumentException("Test PNG chunk missing")
91
+ }
92
+
93
+ private fun compressedPixels(size: Int, invalidFilter: Boolean = false): ByteArray {
94
+ val result = ByteArrayOutputStream()
95
+ DeflaterOutputStream(result).use { output ->
96
+ repeat(size) { index -> output.write(if (invalidFilter && index == 0) 1 else 0) }
97
+ }
98
+ return result.toByteArray()
99
+ }
100
+
101
+ @Test
102
+ fun completeFixedFormatPngPassesIntegrityValidation() {
103
+ listOf("seed", "0x1234", "%E4%B8%AD%F0%9F%99%82").forEach {
104
+ assertTrue(OneKeyBlockieAvatar.isValidPng(OneKeyBlockieAvatar.png(OneKeyBlockieAvatar.URI_PREFIX + it)))
105
+ }
106
+ }
107
+
108
+ @Test
109
+ fun corruptedOrTruncatedChunksAndTrailingBytesAreRejected() {
110
+ val png = OneKeyBlockieAvatar.png(OneKeyBlockieAvatar.URI_PREFIX + "seed")
111
+ assertFalse(OneKeyBlockieAvatar.isValidPng(png.copyOf(png.size - 1)))
112
+ assertFalse(OneKeyBlockieAvatar.isValidPng(png + byteArrayOf(0)))
113
+ assertFalse(OneKeyBlockieAvatar.isValidPng(png.copyOf().also { it[45] = (it[45].toInt() xor 1).toByte() }))
114
+ assertFalse(OneKeyBlockieAvatar.isValidPng(png.copyOf().also { ByteBuffer.wrap(it).putInt(8, Int.MAX_VALUE) }))
115
+ }
116
+
117
+ @Test
118
+ fun validCrcCannotHideWrongDimensionsOrMalformedCompressedData() {
119
+ val png = OneKeyBlockieAvatar.png(OneKeyBlockieAvatar.URI_PREFIX + "seed")
120
+ val header = png.copyOfRange(16, 29).also { it[3] = 96 }
121
+ assertFalse(OneKeyBlockieAvatar.isValidPng(replaceChunk(png, "IHDR", header)))
122
+ assertFalse(OneKeyBlockieAvatar.isValidPng(replaceChunk(png, "IDAT", byteArrayOf(1, 2, 3))))
123
+ assertFalse(OneKeyBlockieAvatar.isValidPng(replaceChunk(png, "IDAT", compressedPixels(128 * 33 - 1))))
124
+ assertFalse(OneKeyBlockieAvatar.isValidPng(replaceChunk(png, "IDAT", compressedPixels(128 * 33, true))))
125
+ }
126
+
127
+ @Test
128
+ fun decompressionIsBoundedEvenWhenEveryChunkCrcIsValid() {
129
+ val png = OneKeyBlockieAvatar.png(OneKeyBlockieAvatar.URI_PREFIX + "seed")
130
+ val bomb = replaceChunk(png, "IDAT", compressedPixels(1024 * 1024))
131
+ assertTrue(bomb.size < OneKeyBlockieAvatar.MAX_PNG_BYTES)
132
+ assertFalse(OneKeyBlockieAvatar.isValidPng(bomb))
133
+ }
134
+
135
+ @Test(expected = CancellationException::class)
136
+ fun cancellationStopsCachedPngValidation() {
137
+ OneKeyBlockieAvatar.isValidPng(OneKeyBlockieAvatar.png(OneKeyBlockieAvatar.URI_PREFIX + "seed")) { true }
138
+ }
139
+
140
+ @Test
141
+ fun overlappingSizeRequestsShareOneGenerationAndDoNotRetainCompletedImages() {
142
+ val generated = AtomicInteger()
143
+ val started = CountDownLatch(1)
144
+ val complete = CountDownLatch(1)
145
+ val requests = OneKeyAvatarInFlight { _, _ ->
146
+ generated.incrementAndGet()
147
+ started.countDown()
148
+ check(complete.await(3, TimeUnit.SECONDS))
149
+ byteArrayOf(1, 2, 3)
150
+ }
151
+ val leases = List(8) { requests.acquire("uri") }
152
+ val executor = Executors.newFixedThreadPool(8)
153
+ try {
154
+ val values = leases.map { lease -> executor.submit<ByteArray> { lease.bytes() } }
155
+ assertTrue(started.await(3, TimeUnit.SECONDS))
156
+ complete.countDown()
157
+ values.forEach { assertArrayEquals(byteArrayOf(1, 2, 3), it.get(3, TimeUnit.SECONDS)) }
158
+ assertEquals(1, generated.get())
159
+ leases.forEach { it.release() }
160
+ val fresh = requests.acquire("uri")
161
+ assertArrayEquals(byteArrayOf(1, 2, 3), fresh.bytes())
162
+ fresh.release()
163
+ assertEquals(2, generated.get())
164
+ } finally {
165
+ complete.countDown()
166
+ leases.forEach { it.release() }
167
+ executor.shutdownNow()
168
+ }
169
+ }
170
+
171
+ @Test
172
+ fun cancellingOneConsumerKeepsTheOtherConsumerAlive() {
173
+ val requests = OneKeyAvatarInFlight { _, cancelled ->
174
+ assertFalse(cancelled())
175
+ byteArrayOf(7)
176
+ }
177
+ val cancelled = requests.acquire("uri")
178
+ val active = requests.acquire("uri")
179
+ cancelled.release()
180
+ cancelled.release()
181
+ assertArrayEquals(byteArrayOf(7), active.bytes())
182
+ active.release()
183
+ }
184
+
185
+ @Test
186
+ fun cancellingEveryConsumerStopsWorkAndAllowsAFreshRequest() {
187
+ val started = CountDownLatch(1)
188
+ val stopped = CountDownLatch(1)
189
+ val generated = AtomicInteger()
190
+ val requests = OneKeyAvatarInFlight { _, cancelled ->
191
+ if (generated.incrementAndGet() == 1) {
192
+ started.countDown()
193
+ while (!cancelled()) Thread.yield()
194
+ stopped.countDown()
195
+ throw CancellationException("Cancelled")
196
+ }
197
+ byteArrayOf(9)
198
+ }
199
+ val lease = requests.acquire("uri")
200
+ val executor = Executors.newSingleThreadExecutor()
201
+ try {
202
+ executor.submit { try { lease.bytes() } catch (_: CancellationException) { } }
203
+ assertTrue(started.await(3, TimeUnit.SECONDS))
204
+ lease.release()
205
+ assertTrue(stopped.await(3, TimeUnit.SECONDS))
206
+ val fresh = requests.acquire("uri")
207
+ assertArrayEquals(byteArrayOf(9), fresh.bytes())
208
+ fresh.release()
209
+ } finally {
210
+ lease.release()
211
+ executor.shutdownNow()
212
+ }
213
+ }
214
+ }