Skip to content

Commit

Permalink
Merge pull request #109 from fingerprintjs/fix/remove-nested-safe-wit…
Browse files Browse the repository at this point in the history
…h-timeout

Remove nested safe calls with timeouts, adjust timeouts & change dummy results to empty strings
  • Loading branch information
Sergey-Makarov authored Oct 6, 2023
2 parents 67212ef + b3e9bde commit dd670d3
Show file tree
Hide file tree
Showing 32 changed files with 415 additions and 255 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/blank.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
run: ./gradlew fingerprint:lint

- name: Test
run: ./gradlew fingerprint:test
run: ./gradlew fingerprint:test -PCItest="true"

- name: Build library
run: ./gradlew fingerprint:assembleRelease
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/instumented_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
touch emulator.log
chmod 777 emulator.log
adb logcat >> emulator.log &
./gradlew :fingerprint:connectedCheck
./gradlew :fingerprint:connectedCheck -PCItest="true"
- name: Save report if tests failed
if: always() && (steps.instrumented_tests.outcome == 'failure')
Expand Down
16 changes: 15 additions & 1 deletion fingerprint/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ android {
minSdk = 21
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")

buildConfigField("boolean", "CI_TEST", (project.properties.get("CItest") as? String) ?: "false")
}

lint {
Expand Down Expand Up @@ -82,6 +84,16 @@ android {
kotlinOptions {
jvmTarget = "1.8"
}

buildFeatures {
buildConfig = true
}
}

androidComponents {
onVariants {
it.androidTest?.packaging?.resources?.excludes?.add("META-INF/*")
}
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile::class.java) {
Expand All @@ -94,7 +106,9 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib:${Constants.kotlinVersion}")
implementation("androidx.appcompat:appcompat:1.6.1")
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.12.7")
testImplementation("io.mockk:mockk:1.12.8")
androidTestImplementation("io.mockk:mockk:1.12.8")
androidTestImplementation ("io.mockk:mockk-android:1.12.8")
androidTestImplementation("androidx.test.ext:junit-ktx:1.1.5")
androidTestImplementation("androidx.test:runner:1.5.2")
}
3 changes: 3 additions & 0 deletions fingerprint/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<manifest xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="io.mockk, io.mockk.proxy.android"/>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
package com.fingerprintjs.android.playground
package com.fingerprintjs.android.fingerprint

import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.fingerprintjs.android.fingerprint.Configuration
import com.fingerprintjs.android.fingerprint.Fingerprinter
import com.fingerprintjs.android.fingerprint.FingerprinterFactory
import com.fingerprintjs.android.fingerprint.signal_providers.StabilityLevel
import com.fingerprintjs.android.fingerprint.tools.FingerprintingLegacySchemeSupportExtensions.getDeviceStateSignals
import com.fingerprintjs.android.fingerprint.tools.FingerprintingLegacySchemeSupportExtensions.getHardwareSignals
import com.fingerprintjs.android.fingerprint.tools.FingerprintingLegacySchemeSupportExtensions.getInstalledAppsSignals
import com.fingerprintjs.android.fingerprint.tools.FingerprintingLegacySchemeSupportExtensions.getOsBuildSignals
import com.fingerprintjs.android.playground.utils.callbackToSync
import com.fingerprintjs.android.fingerprint.utils.callbackToSync
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class InstrumentedTests {
class ApiTests {

private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
package com.fingerprintjs.android.fingerprint

import android.content.Context
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.fingerprintjs.android.fingerprint.signal_providers.StabilityLevel
import com.fingerprintjs.android.fingerprint.tools.threading.createSharedExecutor
import com.fingerprintjs.android.fingerprint.tools.threading.runOnAnotherThread
import com.fingerprintjs.android.fingerprint.tools.threading.safe.ExecutionTimeoutException
import com.fingerprintjs.android.fingerprint.tools.threading.safe.Safe
import com.fingerprintjs.android.fingerprint.tools.threading.safe.safeWithTimeout
import com.fingerprintjs.android.fingerprint.tools.threading.sharedExecutor
import com.fingerprintjs.android.fingerprint.utils.callbackToSync
import com.fingerprintjs.android.fingerprint.utils.mockkObjectSupported
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.unmockkObject
import io.mockk.verify
import io.mockk.verifyOrder
import junit.framework.TestCase
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutionException

@RunWith(AndroidJUnit4::class)
class SafeTests {

private val context: Context
get() = InstrumentationRegistry.getInstrumentation().targetContext

@After
fun recreateExecutor() {
sharedExecutor = createSharedExecutor()
}

@Test
fun safeWithTimeoutValueReturned() {
val v = safeWithTimeout { 0 }
TestCase.assertEquals(v.getOrNull(), 0)
}

@Test
fun safeWithTimeoutErrorRetrievable() {
val errorId = "Hello"
val v = safeWithTimeout { throw Exception(errorId) }
val err = v.exceptionOrNull() as ExecutionException
val errCause = err.cause!!
TestCase.assertTrue(errCause is Exception && errCause.message == errorId)
}

@Test
fun safeWithTimeoutExecutionNeverStuck() {
val elapsedTime = elapsedTimeMs {
safeWithTimeout(timeoutMs = TimeConstants.t1) { Thread.sleep(TimeConstants.t4) }
}
TestCase.assertTrue(elapsedTime - TimeConstants.t1 < TimeConstants.epsilon)
}

@Test
fun safeWithTimeoutExecutionStuckThreadStackTraceReturned() {
val res = safeWithTimeout(timeoutMs = TimeConstants.t1) { Thread.sleep(TimeConstants.t4) }
val err = res.exceptionOrNull()!!
TestCase.assertTrue(
err is ExecutionTimeoutException
&& err.executionThreadStackTrace != null
&& err.executionThreadStackTrace.any { it.className == "java.lang.Thread" && it.methodName == "sleep" }
)
}

@Test
fun safeWithTimeoutFromMultipleThreadsIsNotBlocked() {
val countDownLatch = CountDownLatch(2)
val elapsedTime = elapsedTimeMs {
runOnAnotherThread { safeWithTimeout { Thread.sleep(TimeConstants.t1); countDownLatch.countDown() } }
runOnAnotherThread { safeWithTimeout { Thread.sleep(TimeConstants.t1); countDownLatch.countDown() } }
countDownLatch.await()
}
TestCase.assertTrue(elapsedTime - TimeConstants.t1 < TimeConstants.epsilon)
}

@Test
fun safeWithTimeoutThreadsAreReused() {
for (i in 0 until 4) {
safeWithTimeout { }
TestCase.assertEquals(1, sharedExecutor.poolSize)
Thread.sleep(TimeConstants.epsilon)
}
}

// this is a sad fact but we will leave it as it is
@Test
fun safeWithTimeoutThreadCountGrowsIfThreadsCantInterrupt() {
for (i in 1 until 5) {
safeWithTimeout(timeoutMs = TimeConstants.epsilon) { neverReturn() }
TestCase.assertEquals(i, sharedExecutor.poolSize)
Thread.sleep(TimeConstants.epsilon)
}
}

@Test
fun safeWithTimeoutOuterTimeoutDominatesOverInner() {
val elapsedTime = elapsedTimeMs {
safeWithTimeout(timeoutMs = TimeConstants.t1) {
safeWithTimeout(timeoutMs = TimeConstants.t2) {
Thread.sleep(TimeConstants.t3)
}
}
}
TestCase.assertTrue(elapsedTime - TimeConstants.t1 < TimeConstants.epsilon)
}

/**
* This test illustrates the behaviour when using one safe call inside the another.
* Such usage is prohibited, but we'd rather know the what-ifs.
*/
@Test
fun safeWithTimeoutNestedSafeInterruptedBehaviour() {
if (!mockkObjectSupported()) return
val errLvl1: Throwable?
var errLvl2: Throwable? = null
var errLvl3: Throwable? = null
val countDownLatch = CountDownLatch(2)
mockkObject(Safe)
every { Safe.logIllegalSafeWithTimeoutUsage() } answers {}

errLvl1 = safeWithTimeout(timeoutMs = TimeConstants.t1) {
errLvl2 = safeWithTimeout(timeoutMs = TimeConstants.t2) {
try {
Thread.sleep(TimeConstants.t3)
} catch (t: Throwable) {
errLvl3 = t
countDownLatch.countDown()
}
}.exceptionOrNull()
countDownLatch.countDown()
}.exceptionOrNull()
countDownLatch.await()

unmockkObject(Safe)
TestCase.assertTrue(errLvl1 is ExecutionTimeoutException)
TestCase.assertTrue(errLvl2 is InterruptedException)
TestCase.assertTrue(errLvl3 is InterruptedException)
}


/**
* Same motivation for the test as for the above.
*/
@Test
fun safeWithTimeoutNestedValueReturned() {
if (!mockkObjectSupported()) return
mockkObject(Safe)
every { Safe.logIllegalSafeWithTimeoutUsage() } answers { }

val v = safeWithTimeout { safeWithTimeout { 0 } }

unmockkObject(Safe)
TestCase.assertEquals(v.getOrNull()!!.getOrNull(), 0)
}

@Test
fun safeContextFlagUnsetWhenSafeBlockReturns() =
safeWithTimeoutContextFlagUnset(whenBlockThrows = false)

@Test
fun safeContextFlagUnsetWhenSafeBlockThrows() =
safeWithTimeoutContextFlagUnset(whenBlockThrows = true)

private fun safeWithTimeoutContextFlagUnset(whenBlockThrows: Boolean) {
if (!mockkObjectSupported()) return
mockkObject(Safe)
var clearThreadId: Long? = null
every { Safe.clearInsideSafeWithTimeout() } answers {
callOriginal().also { clearThreadId = Thread.currentThread().id }
}
var markThreadId: Long? = null
every { Safe.markInsideSafeWithTimeout() } answers {
callOriginal().also { markThreadId = Thread.currentThread().id }
}

safeWithTimeout {
if (whenBlockThrows)
throw Exception()
}

verify(exactly = 1) {
Safe.markInsideSafeWithTimeout()
Safe.clearInsideSafeWithTimeout()
}
verifyOrder {
Safe.markInsideSafeWithTimeout()
Safe.clearInsideSafeWithTimeout()
}

TestCase.assertEquals(markThreadId, clearThreadId)
unmockkObject(Safe)
}

@Test
fun safeWithTimeoutNestedUsageReported() {
if (!mockkObjectSupported()) return
var logCalled = false
mockkObject(Safe)
every { Safe.logIllegalSafeWithTimeoutUsage() } answers { logCalled = true }

safeWithTimeout { safeWithTimeout {} }

unmockkObject(Safe)
TestCase.assertEquals(true, logCalled)
}


@Test
fun nestedSafeCallNeverHappens() {
if (!mockkObjectSupported()) return

var logCalled = false
mockkObject(Safe)
every { Safe.logIllegalSafeWithTimeoutUsage() } answers { logCalled = true }

Fingerprinter.Version.values().forEach { version ->
val fingerprinter = FingerprinterFactory.create(context)
val deviceId = callbackToSync { fingerprinter.getDeviceId(version = version) { emit(it) } }
StabilityLevel.values().forEach { stabilityLevel ->
val fingerprint = callbackToSync { fingerprinter.getFingerprint(version, stabilityLevel) { emit(it) } }
}
val fingerprintingSignalsProvider = fingerprinter.getFingerprintingSignalsProvider()!!
}

TestCase.assertEquals(false, logCalled)
}
}

private object TimeConstants {
const val epsilon = 200L
const val t1 = epsilon * 3
const val t2 = t1 * 2
const val t3 = t1 * 3
const val t4 = t1 * 4
}

private inline fun elapsedTimeMs(block: () -> Unit): Long {
val currentTime = System.currentTimeMillis()
block()
return System.currentTimeMillis() - currentTime
}

@Suppress("ControlFlowWithEmptyBody")
private fun neverReturn() {
while (true);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.fingerprintjs.android.playground.utils
package com.fingerprintjs.android.fingerprint.utils

import java.util.concurrent.CountDownLatch

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.fingerprintjs.android.fingerprint.utils

internal fun mockkObjectSupported(): Boolean =
(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.fingerprintjs.android.fingerprint.tools.hashers.MurMur3x64x128Hasher
import com.fingerprintjs.android.fingerprint.tools.logs.Logger
import com.fingerprintjs.android.fingerprint.tools.logs.ePleaseReport
import com.fingerprintjs.android.fingerprint.tools.threading.runOnAnotherThread
import com.fingerprintjs.android.fingerprint.tools.threading.safe.Safe
import com.fingerprintjs.android.fingerprint.tools.threading.safe.safe


Expand All @@ -32,7 +31,7 @@ public class Fingerprinter internal constructor(
// does not ruin an entire operation.
// another option could be to not use timeout at all, since we have a lot of timeouts
// deep inside.
safe(timeoutMs = Safe.timeoutLong) { implFactory() }
safe { implFactory() }
}

/**
Expand Down
Loading

0 comments on commit dd670d3

Please sign in to comment.