Skip to content

Commit

Permalink
Extract safecall to utils
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanmos committed Aug 8, 2023
1 parent ba3545f commit 227afe9
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 56 deletions.
1 change: 0 additions & 1 deletion features/dd-sdk-android-session-replay/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ dependencies {
}
testImplementation(libs.bundles.jUnit5)
testImplementation(libs.bundles.testTools)
unmock(libs.robolectric)

// TODO MTG-12 detekt(project(":tools:detekt"))
// TODO MTG-12 detekt(libs.detektCli)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import android.util.LruCache
import androidx.annotation.VisibleForTesting
import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.internal.utils.CacheUtils
import com.datadog.android.sessionreplay.internal.utils.InvocationUtils
import java.util.concurrent.atomic.AtomicInteger

@Suppress("TooManyFunctions")
Expand All @@ -28,9 +29,8 @@ internal object BitmapPool : Cache<String, Bitmap>, ComponentCallbacks2 {
private var bitmapsBySize = HashMap<String, HashSet<Bitmap>>()
private var usedBitmaps = HashSet<Bitmap>()
private val logger = InternalLogger.UNBOUND

@VisibleForTesting
internal var bitmapIndex = AtomicInteger(0)
private val invocationUtils = InvocationUtils()
private var bitmapIndex = AtomicInteger(0)

private var cache: LruCache<String, Bitmap> = object :
LruCache<String, Bitmap>(MAX_CACHE_MEMORY_SIZE_BYTES) {
Expand All @@ -50,10 +50,14 @@ internal object BitmapPool : Cache<String, Bitmap>, ComponentCallbacks2 {
val dimensionsKey = generateKey(oldValue)
val bitmapGroup = bitmapsBySize[dimensionsKey] ?: HashSet()

safeCall {
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
bitmapGroup.remove(oldValue)
}
invocationUtils.safeCallWithErrorLogging(
logger = logger,
call = {
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
bitmapGroup.remove(oldValue)
},
failureMessage = BITMAP_OPERATION_FAILED
)
markBitmapAsFree(oldValue)
oldValue.recycle()
}
Expand Down Expand Up @@ -84,10 +88,14 @@ internal object BitmapPool : Cache<String, Bitmap>, ComponentCallbacks2 {

val key = generateKey(value)

val bitmapExistsInPool = safeCallWithBoolean {
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
bitmapsBySize[key]?.contains(value) ?: false
}
val bitmapExistsInPool = invocationUtils.safeCallWithErrorLogging(
logger = logger,
call = {
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
bitmapsBySize[key]?.contains(value) ?: false
},
failureMessage = BITMAP_OPERATION_FAILED
) ?: false

if (!bitmapExistsInPool) {
addBitmapToPool(key, value)
Expand All @@ -107,7 +115,11 @@ internal object BitmapPool : Cache<String, Bitmap>, ComponentCallbacks2 {

// find the first unused bitmap, mark it as used and return it
return bitmapsWithReqDimensions.find {
safeCallWithBoolean { !usedBitmaps.contains(it) }
invocationUtils.safeCallWithErrorLogging(
logger = logger,
call = { !usedBitmaps.contains(it) },
failureMessage = BITMAP_OPERATION_FAILED
) ?: false
}?.apply { markBitmapAsUsed(this) }
}

Expand All @@ -117,16 +129,24 @@ internal object BitmapPool : Cache<String, Bitmap>, ComponentCallbacks2 {
}

private fun markBitmapAsFree(bitmap: Bitmap) {
safeCall {
usedBitmaps.remove(bitmap)
}
invocationUtils.safeCallWithErrorLogging(
logger = logger,
call = {
usedBitmaps.remove(bitmap)
},
failureMessage = BITMAP_OPERATION_FAILED
)
}

private fun markBitmapAsUsed(bitmap: Bitmap) {
safeCall {
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
usedBitmaps.add(bitmap)
}
invocationUtils.safeCallWithErrorLogging(
logger = logger,
call = {
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
usedBitmaps.add(bitmap)
},
failureMessage = BITMAP_OPERATION_FAILED
)
}

private fun addBitmapToPool(key: String, bitmap: Bitmap) {
Expand All @@ -136,12 +156,20 @@ internal object BitmapPool : Cache<String, Bitmap>, ComponentCallbacks2 {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
safeCall { bitmapsBySize.putIfAbsent(key, HashSet()) }
invocationUtils.safeCallWithErrorLogging(
logger = logger,
call = { bitmapsBySize.putIfAbsent(key, HashSet()) },
failureMessage = BITMAP_OPERATION_FAILED
)
} else {
if (bitmapsBySize[key] == null) bitmapsBySize[key] = HashSet()
}
@Suppress("UnsafeThirdPartyFunctionCall") // Called within a try/catch block
safeCall { bitmapsBySize[key]?.add(bitmap) }
invocationUtils.safeCallWithErrorLogging(
logger = logger,
call = { bitmapsBySize[key]?.add(bitmap) },
failureMessage = BITMAP_OPERATION_FAILED
)
}

private fun generateKey(bitmap: Bitmap) =
Expand All @@ -150,40 +178,6 @@ internal object BitmapPool : Cache<String, Bitmap>, ComponentCallbacks2 {
private fun generateKey(width: Int, height: Int, config: Config) =
"$width-$height-$config"

@Suppress("SwallowedException", "TooGenericExceptionCaught")
private inline fun<R> safeCall(
call: () -> R
) {
try {
call()
} catch (e: Exception) {
// TODO: REPLAY-1364 Add logs here once the sdkLogger is added
logger.log(
level = InternalLogger.Level.WARN,
target = InternalLogger.Target.MAINTAINER,
{ BITMAP_OPERATION_FAILED }
)
}
}

@Suppress("SwallowedException", "TooGenericExceptionCaught")
private inline fun safeCallWithBoolean(
call: () -> Boolean
): Boolean {
try {
return call()
} catch (e: Exception) {
// TODO: REPLAY-1364 Add logs here once the sdkLogger is added
logger.log(
level = InternalLogger.Level.WARN,
target = InternalLogger.Target.MAINTAINER,
{ BITMAP_OPERATION_FAILED }
)
}

return false
}

override fun onConfigurationChanged(newConfig: Configuration) {}

override fun onLowMemory() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.internal.utils

import com.datadog.android.api.InternalLogger

internal class InvocationUtils {
@Suppress("SwallowedException", "TooGenericExceptionCaught")
inline fun<R> safeCallWithErrorLogging(
logger: InternalLogger,
call: () -> R,
failureMessage: String,
level: InternalLogger.Level = InternalLogger.Level.WARN,
target: InternalLogger.Target = InternalLogger.Target.MAINTAINER
): R? {
try {
return call()
} catch (e: Exception) {
// TODO: REPLAY-1364 Add logs here once the sdkLogger is added
logger.log(
level,
target,
{ failureMessage }
)
}

return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/

package com.datadog.android.sessionreplay.internal.utils

import com.datadog.android.api.InternalLogger
import com.datadog.android.sessionreplay.forge.ForgeConfigurator
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
import org.junit.jupiter.api.extension.Extensions
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.junit.jupiter.MockitoExtension
import org.mockito.junit.jupiter.MockitoSettings
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
import java.lang.Exception

@Extensions(
ExtendWith(MockitoExtension::class),
ExtendWith(ForgeExtension::class)
)
@MockitoSettings(strictness = Strictness.LENIENT)
@ForgeConfiguration(ForgeConfigurator::class)
internal class InvocationUtilsTest {
private lateinit var testedInvocationUtils: InvocationUtils

@Mock
lateinit var mockLogger: InternalLogger

@BeforeEach
fun setup() {
testedInvocationUtils = InvocationUtils()
}

@Test
fun `M call function W safeCallWithErrorLogging()`() {
// Given
val mockFunc = mock<() -> Unit>()

// When
testedInvocationUtils.safeCallWithErrorLogging(
logger = mockLogger,
call = { mockFunc() },
failureMessage = "someMessage"
)

// Then
verify(mockFunc, times(1))()
}

@Test
fun `M return function result W safeCallWithErrorLogging()`() {
// Given
val mockFunc = mock<() -> Boolean>()
whenever(mockFunc()).thenReturn(true)

// When
val result = testedInvocationUtils.safeCallWithErrorLogging(
logger = mockLogger,
call = { mockFunc() },
failureMessage = "someMessage"
)

// Then
assertThat(result).isTrue
}

@Test
fun `M log failureMessage W safeCallWithErrorLogging() { failure }`(
@StringForgery fakeMessage: String
) {
// Given
val mockFunc = mock<() -> Boolean>()
val mockException: Exception = mock()
doThrow(mockException).`when`(mockFunc)

val captor = argumentCaptor<() -> String>()

// When
testedInvocationUtils.safeCallWithErrorLogging(
logger = mockLogger,
call = { mockFunc() },
failureMessage = fakeMessage
)

// Then
verify(mockLogger).log(
level = any(),
target = any(),
captor.capture(),
anyOrNull(),
anyOrNull()
)
assertThat(captor.firstValue()).isEqualTo(fakeMessage)
}
}

0 comments on commit 227afe9

Please sign in to comment.