Skip to content
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

Merged
merged 13 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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) }

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.
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);
}
Loading
Loading