Skip to content

Commit

Permalink
RUM-2708: Add tests for ResourceResolver
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Mar 12, 2024
1 parent ce43622 commit 737969c
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import com.datadog.android.sessionreplay.model.MobileSegment
import com.datadog.android.sessionreplay.utils.UniqueIdentifierGenerator
import java.util.Locale

// This should not have a callback but it should just create a placeholder for resourcesSerializer
// The resourcesSerializer dependency should be removed from here
// TODO: RUM-0000 Remove the resourcesSerializer dependency from here
// This should not have a callback but it should just create a placeholder for resourceResolver
// The resourceResolver dependency should be removed from here
// TODO: RUM-0000 Remove the resourceResolver dependency from here
internal class ImageWireframeHelper(
private val logger: InternalLogger,
private val resourceResolver: ResourceResolver,
Expand Down Expand Up @@ -115,7 +115,7 @@ internal class ImageWireframeHelper(
drawable = drawableProperties.drawable,
drawableWidth = width,
drawableHeight = height,
resourcesSerializerCallback = object : ResourcesSerializerCallback {
resourceResolverCallback = object : ResourceResolverCallback {
override fun onSuccess(resourceId: String) {
populateResourceIdInWireframe(resourceId, imageWireframe)
imageWireframeHelperCallback.onFinished()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
package com.datadog.android.sessionreplay.internal.recorder.resources

internal interface ResolveResourceCallback {
fun onResolved(resourceId: String, byteArray: ByteArray)
fun onResolved(resourceId: String, resourceData: ByteArray)
fun onFailed()
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ internal class ResourceItemCreationHandler(
@VisibleForTesting internal val resourceIdsSeen: MutableSet<String> =
Collections.synchronizedSet(HashSet<String>())

internal fun queueItem(resourceId: String, byteArray: ByteArray) {
internal fun queueItem(resourceId: String, resourceData: ByteArray) {
if (!resourceIdsSeen.contains(resourceId)) {
resourceIdsSeen.add(resourceId)

recordedDataQueueHandler.addResourceItem(
identifier = resourceId,
resourceData = byteArray,
resourceData = resourceData,
applicationId = applicationId
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ internal class ResourceResolver(
drawable: Drawable,
drawableWidth: Int,
drawableHeight: Int,
resourcesSerializerCallback: ResourcesSerializerCallback
resourceResolverCallback: ResourceResolverCallback
) {
bitmapCachesManager.registerCallbacks(applicationContext)

Expand All @@ -57,7 +57,7 @@ internal class ResourceResolver(
if (resourceId != null) {
// if we got here it means we saw the bitmap before,
// so we don't need to send the resource again
resourcesSerializerCallback.onSuccess(resourceId)
resourceResolverCallback.onSuccess(resourceId)
return
}

Expand All @@ -70,21 +70,21 @@ internal class ResourceResolver(
// do in the background
threadPoolExecutor.executeSafe("resolveResourceId", logger) {
@Suppress("ThreadSafety") // this runs inside an executor
createBitmapAsync(
createBitmap(
resources = resources,
drawable = drawable,
drawableWidth = drawableWidth,
drawableHeight = drawableHeight,
displayMetrics = displayMetrics,
bitmapFromDrawable = bitmapFromDrawable,
resolveResourceCallback = object : ResolveResourceCallback {
override fun onResolved(resourceId: String, byteArray: ByteArray) {
resourceItemCreationHandler.queueItem(resourceId, byteArray)
resourcesSerializerCallback.onSuccess(resourceId)
override fun onResolved(resourceId: String, resourceData: ByteArray) {
resourceItemCreationHandler.queueItem(resourceId, resourceData)
resourceResolverCallback.onSuccess(resourceId)
}

override fun onFailed() {
resourcesSerializerCallback.onFailure()
resourceResolverCallback.onFailure()
}
}
)
Expand All @@ -96,7 +96,7 @@ internal class ResourceResolver(
// region private

@WorkerThread
private fun createBitmapAsync(
private fun createBitmap(
resources: Resources,
drawable: Drawable,
drawableWidth: Int,
Expand All @@ -105,13 +105,14 @@ internal class ResourceResolver(
bitmapFromDrawable: Bitmap?,
resolveResourceCallback: ResolveResourceCallback
) {
var handledBitmap: Bitmap? = null
if (bitmapFromDrawable != null) {
handledBitmap = tryToGetBitmapFromBitmapDrawable(
val handledBitmap = if (bitmapFromDrawable != null) {
tryToGetBitmapFromBitmapDrawable(
drawable = drawable as BitmapDrawable,
bitmapFromDrawable = bitmapFromDrawable,
resolveResourceCallback = resolveResourceCallback
)
} else {
null
}

if (handledBitmap == null) {
Expand All @@ -131,18 +132,18 @@ internal class ResourceResolver(
private fun resolveResourceHash(
drawable: Drawable,
bitmap: Bitmap,
byteArray: ByteArray,
compressedBitmapBytes: ByteArray,
shouldCacheBitmap: Boolean,
resolveResourceCallback: ResolveResourceCallback
) {
// failed to get image data
if (byteArray.isEmpty()) {
if (compressedBitmapBytes.isEmpty()) {
// we are already logging the failure in webpImageCompression
resolveResourceCallback.onFailed()
return
}

val resourceId = md5HashGenerator.generate(byteArray)
val resourceId = md5HashGenerator.generate(compressedBitmapBytes)

// failed to resolve bitmap identifier
if (resourceId == null) {
Expand All @@ -158,7 +159,7 @@ internal class ResourceResolver(
drawable = drawable
)

resolveResourceCallback.onResolved(resourceId, byteArray)
resolveResourceCallback.onResolved(resourceId, compressedBitmapBytes)
}

private fun cacheIfNecessary(
Expand Down Expand Up @@ -191,13 +192,19 @@ internal class ResourceResolver(
displayMetrics = displayMetrics,
bitmapCreationCallback = object : BitmapCreationCallback {
override fun onReady(bitmap: Bitmap) {
val byteArray = webPImageCompression.compressBitmap(bitmap)
val compressedBitmapBytes = webPImageCompression.compressBitmap(bitmap)

// failed to compress bitmap
if (compressedBitmapBytes.isEmpty()) {
resolveResourceCallback.onFailed()
return
}

@Suppress("ThreadSafety") // this runs inside an executor
resolveResourceHash(
drawable = drawable,
bitmap = bitmap,
byteArray = byteArray,
compressedBitmapBytes = compressedBitmapBytes,
shouldCacheBitmap = true,
resolveResourceCallback = resolveResourceCallback
)
Expand All @@ -217,34 +224,37 @@ internal class ResourceResolver(
bitmapFromDrawable: Bitmap,
resolveResourceCallback: ResolveResourceCallback
): Bitmap? {
drawableUtils.createScaledBitmap(bitmapFromDrawable)?.let { scaledBitmap ->
val byteArray = webPImageCompression.compressBitmap(scaledBitmap)
val scaledBitmap = drawableUtils.createScaledBitmap(bitmapFromDrawable)
?: return null

// failed to get byteArray potentially because the bitmap was recycled before imageCompression
// we'll now failover to attempting to create a new bitmap from the drawable
if (byteArray.isEmpty() && scaledBitmap.isRecycled) {
return null
}
val compressedBitmapBytes = webPImageCompression.compressBitmap(scaledBitmap)

/**
* Check whether the scaled bitmap is the same as the original.
* Since Bitmap.createScaledBitmap will return the original bitmap if the
* requested dimensions match the dimensions of the original
*/
val shouldCacheBitmap = scaledBitmap != drawable.bitmap
// failed to get byteArray potentially because the bitmap was recycled before imageCompression
if (compressedBitmapBytes.isEmpty()) {
return null
}

resolveResourceHash(
drawable = drawable,
bitmap = scaledBitmap,
byteArray = byteArray,
shouldCacheBitmap = shouldCacheBitmap,
resolveResourceCallback = resolveResourceCallback
)
/**
* Check whether the scaled bitmap is the same as the original.
* Since Bitmap.createScaledBitmap will return the original bitmap if the
* requested dimensions match the dimensions of the original
* Add a specific check for isRecycled, because getting width/height from a recycled bitmap
* is undefined behavior
*/
val shouldCacheBitmap = !bitmapFromDrawable.isRecycled && (
scaledBitmap.width < bitmapFromDrawable.width ||
scaledBitmap.height < bitmapFromDrawable.height
)

return scaledBitmap
}
resolveResourceHash(
drawable = drawable,
bitmap = scaledBitmap,
compressedBitmapBytes = compressedBitmapBytes,
shouldCacheBitmap = shouldCacheBitmap,
resolveResourceCallback = resolveResourceCallback
)

return null
return scaledBitmap
}

private fun tryToGetResourceFromCache(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

package com.datadog.android.sessionreplay.internal.recorder.resources

internal interface ResourcesSerializerCallback {
internal interface ResourceResolverCallback {
fun onSuccess(resourceId: String)

fun onFailure()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ internal class ImageWireframeHelperTest {
any()
)
).thenAnswer {
val callback = it.arguments[6] as ResourcesSerializerCallback
val callback = it.arguments[6] as ResourceResolverCallback
callback.onSuccess(fakeResourceId)
}

Expand Down Expand Up @@ -461,7 +461,7 @@ internal class ImageWireframeHelperTest {
wireframes[0] as MobileSegment.Wireframe.ImageWireframe

// Then
val argumentCaptor = argumentCaptor<ResourcesSerializerCallback>()
val argumentCaptor = argumentCaptor<ResourceResolverCallback>()

verify(mockResourceResolver).resolveResourceId(
any(),
Expand Down Expand Up @@ -506,7 +506,7 @@ internal class ImageWireframeHelperTest {
wireframes[0] as MobileSegment.Wireframe.ImageWireframe

// Then
val argumentCaptor = argumentCaptor<ResourcesSerializerCallback>()
val argumentCaptor = argumentCaptor<ResourceResolverCallback>()
verify(mockResourceResolver, times(2)).resolveResourceId(
any(),
any(),
Expand Down Expand Up @@ -582,7 +582,7 @@ internal class ImageWireframeHelperTest {
drawable = any(),
drawableWidth = captor.capture(),
drawableHeight = captor.capture(),
resourcesSerializerCallback = any()
resourceResolverCallback = any()
)
assertThat(captor.allValues).containsExactly(fakeViewWidth, fakeViewHeight)
}
Expand Down Expand Up @@ -613,7 +613,7 @@ internal class ImageWireframeHelperTest {
drawable = any(),
drawableWidth = captor.capture(),
drawableHeight = captor.capture(),
resourcesSerializerCallback = any()
resourceResolverCallback = any()

)
assertThat(captor.allValues).containsExactly(fakeDrawableWidth, fakeDrawableHeight)
Expand Down Expand Up @@ -695,7 +695,7 @@ internal class ImageWireframeHelperTest {
drawable = any(),
drawableWidth = any(),
drawableHeight = any(),
resourcesSerializerCallback = any()
resourceResolverCallback = any()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.datadog.android.sessionreplay.internal.async.DataQueueHandler
import fr.xgouchet.elmyr.annotation.StringForgery
import fr.xgouchet.elmyr.junit5.ForgeConfiguration
import fr.xgouchet.elmyr.junit5.ForgeExtension
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
Expand Down Expand Up @@ -48,7 +49,7 @@ internal class ResourceItemCreationHandlerTest {
}

@Test
fun `M queue item W queueItemIfNotPreviouslySeen() { not previously seen }`() {
fun `M queue item W queueItem() { not previously seen }`() {
// Given
val fakeByteArray = fakeResourceId.toByteArray()

Expand All @@ -64,7 +65,7 @@ internal class ResourceItemCreationHandlerTest {
}

@Test
fun `M not queue item W queueItemIfNotPreviouslySeen() { previously seen }`() {
fun `M not queue item W queueItem() { previously seen }`() {
// Given
val fakeByteArray = fakeResourceId.toByteArray()

Expand All @@ -81,7 +82,7 @@ internal class ResourceItemCreationHandlerTest {
}

@Test
fun `M add unique resourceId only once W queueItemIfNotPreviouslySeen()`() {
fun `M add unique resourceId only once W queueItem()`() {
// Given
val fakeByteArray = fakeResourceId.toByteArray()

Expand All @@ -90,6 +91,6 @@ internal class ResourceItemCreationHandlerTest {
testedHandler.queueItem(fakeResourceId, fakeByteArray)

// Then
assert(testedHandler.resourceIdsSeen.size == 1)
assertThat(testedHandler.resourceIdsSeen).hasSize(1)
}
}

0 comments on commit 737969c

Please sign in to comment.