-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Release/2.1.0 #111
Release/2.1.0 #111
Changes from all commits
b17f399
bf3d4b0
f807c6d
67212ef
187dc1a
cff1d7f
bb46a59
9fba647
900efb0
b3e9bde
dd670d3
b175fbc
00b66b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -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" } | ||
Check warning Code scanning / detekt Line detected that is longer than the defined maximum line length in the code style. Warning
Line detected that is longer than the defined maximum line length in the code style.
|
||
) | ||
} | ||
|
||
@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() | ||
Check warning Code scanning / detekt Thrown exception is too generic. Prefer throwing project specific exceptions to handle error cases. Warning
Exception is a too generic Exception. Prefer throwing specific exceptions that indicate a specific error case.
|
||
} | ||
|
||
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); | ||
} |
Check warning
Code scanning / detekt
Thrown exception is too generic. Prefer throwing project specific exceptions to handle error cases. Warning