Skip to content

Commit

Permalink
Merge pull request #111 from fingerprintjs/release/2.1.0
Browse files Browse the repository at this point in the history
Release/2.1.0
  • Loading branch information
Sergey-Makarov committed Oct 9, 2023
2 parents 563fbbc + 00b66b8 commit 0df2a9b
Show file tree
Hide file tree
Showing 35 changed files with 428 additions and 267 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 @@ -50,7 +50,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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Add this to a `build.gradle` of a module.
```gradle
dependencies {
...
implementation "com.github.fingerprintjs:fingerprint-android:2.0.2"
implementation "com.github.fingerprintjs:fingerprint-android:2.1.0"
}
```

Expand Down Expand Up @@ -171,7 +171,7 @@ Check out [Migration to V2](docs/migration_to_v2.md) for migration steps and the

## Fingerprint Android Demo App

Try the library features in the [Fingerprint Android Demo App](https://github.com/fingerprintjs/fingerprintjs-android/releases/download/2.0.2/Playground-release-2.0.2.apk).
Try the library features in the [Fingerprint Android Demo App](https://github.com/fingerprintjs/fingerprintjs-android/releases/download/2.1.0/Playground-release-2.1.0.apk).

## Android API support

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 Expand Up @@ -144,10 +141,10 @@ class InstrumentedTests {
}
val fp2 = fingerprinter.getFingerprint(
fingerprintingSignals = fingerprinter.getFingerprintingSignalsProvider()
.getSignalsMatching(
?.getSignalsMatching(
version = version,
stabilityLevel = stabilityLevel
)
).orEmpty()
)
if (version >= Fingerprinter.Version.fingerprintingFlattenedSignalsFirstVersion) {
assertEquals(fp1, fp2)
Expand All @@ -165,16 +162,17 @@ class InstrumentedTests {
.forEach { version ->
StabilityLevel.values().forEach { stabilityLevel ->
val expectedLegacySignalsInfos = listOf(
fingerprintingSignalsProvider.getDeviceStateSignals(version, stabilityLevel),
fingerprintingSignalsProvider.getHardwareSignals(version, stabilityLevel),
fingerprintingSignalsProvider.getOsBuildSignals(version, stabilityLevel),
fingerprintingSignalsProvider.getInstalledAppsSignals(version, stabilityLevel),
fingerprintingSignalsProvider?.getDeviceStateSignals(version, stabilityLevel).orEmpty(),
fingerprintingSignalsProvider?.getHardwareSignals(version, stabilityLevel).orEmpty(),
fingerprintingSignalsProvider?.getOsBuildSignals(version, stabilityLevel).orEmpty(),
fingerprintingSignalsProvider?.getInstalledAppsSignals(version, stabilityLevel).orEmpty(),
)
.flatten()
.map { it.info }
.toSet()
val matchingSignalsInfos = fingerprintingSignalsProvider
.getSignalsMatching(version, stabilityLevel)
?.getSignalsMatching(version, stabilityLevel)
.orEmpty()
.map { it.info }
.toSet()
assertEquals(expectedLegacySignalsInfos, matchingSignalsInfos)
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);
}
Loading

0 comments on commit 0df2a9b

Please sign in to comment.