Skip to content

Commit

Permalink
Merge pull request wix#1858 from wix/rn62-timers-idle-res
Browse files Browse the repository at this point in the history
Introduce a flexible impl strategy for TimersIdlingResource, paving way for RN 62
  • Loading branch information
d4vidi authored Jan 20, 2020
2 parents 99a3aea + aab5f91 commit da70b11
Show file tree
Hide file tree
Showing 11 changed files with 569 additions and 375 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.wix.detox.common

import kotlin.annotation.AnnotationTarget.*

/**
* Source-annotation, indicating that some changes need to be made once the associated RN version
* (or higher) becomes the one minimally supported by Detox Android.
*/
@Target(FUNCTION, CLASS, CONSTRUCTOR, PROPERTY_GETTER, PROPERTY_SETTER, PROPERTY, FIELD, FILE)
@Retention(AnnotationRetention.SOURCE)
annotation class RNDropSupportTodo(val rnMajorVersion: Int, val message: String)
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.base.IdlingResourceRegistry
import com.facebook.react.bridge.ReactContext
import com.wix.detox.reactnative.idlingresources.*
import com.wix.detox.reactnative.idlingresources.timers.TimersIdlingResource
import com.wix.detox.reactnative.idlingresources.timers.getInterrogationStrategy
import org.joor.Reflect
import org.joor.ReflectException
import java.util.Set
Expand Down Expand Up @@ -108,7 +110,7 @@ class ReactNativeIdlingResources constructor(

private fun setupCustomRNIdlingResources() {
rnBridgeIdlingResource = BridgeIdlingResource(reactContext)
timersIdlingResource = TimersIdlingResource(reactContext)
timersIdlingResource = TimersIdlingResource(getInterrogationStrategy(reactContext)!!)
uiModuleIdlingResource = UIModuleIdlingResource(reactContext)
animIdlingResource = AnimatedModuleIdlingResource(reactContext)

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
@file:RNDropSupportTodo(62, "Remove all of this; Use DelegatedIdleInterrogationStrategy, instead.")

package com.wix.detox.reactnative.idlingresources.timers

import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactContext
import com.wix.detox.common.RNDropSupportTodo
import org.joor.Reflect
import java.util.*

private const val BUSY_WINDOW_THRESHOLD = 1500

private class TimerReflected(timer: Any) {
private var reflected = Reflect.on(timer)

val isRepeating: Boolean
get() = reflected.field("mRepeat").get()
val interval: Int
get() = reflected.field("mInterval").get()
val targetTime: Long
get() = reflected.field("mTargetTime").get()
}

private class TimingModuleReflected(private val nativeModule: NativeModule) {
val timersQueue: PriorityQueue<Any>
get() = Reflect.on(nativeModule).field("mTimers").get()
val timersLock: Any
get() = Reflect.on(nativeModule).field("mTimerGuard").get()

operator fun component1() = timersQueue
operator fun component2() = timersLock
}

class DefaultIdleInterrogationStrategy
internal constructor(private val timersModule: NativeModule)
: IdleInterrogationStrategy {

override fun isIdleNow(): Boolean {
val (timersQueue, timersLock) = TimingModuleReflected(timersModule)
synchronized(timersLock) {
val nextTimer = timersQueue.peek()
nextTimer?.let {
return !isTimerInBusyWindow(it) && !hasBusyTimers(timersQueue)
}
return true
}
}

private fun isTimerInBusyWindow(timer: Any): Boolean {
val timerReflected = TimerReflected(timer)
return when {
timerReflected.isRepeating -> false
timerReflected.interval > BUSY_WINDOW_THRESHOLD -> false
else -> true
}
}

private fun hasBusyTimers(timersQueue: PriorityQueue<Any>): Boolean {
timersQueue.forEach {
if (isTimerInBusyWindow(it)) {
return true
}
}
return false
}

companion object {
fun createIfSupported(reactContext: ReactContext): DefaultIdleInterrogationStrategy? =
try {
val timingClass: Class<NativeModule> = Class.forName("com.facebook.react.modules.core.Timing") as Class<NativeModule>
DefaultIdleInterrogationStrategy(reactContext.getNativeModule(timingClass))
} catch (ex: Exception) {
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.wix.detox.reactnative.idlingresources.timers

import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactContext
import com.wix.detox.common.RNDropSupportTodo
import org.joor.Reflect

private const val BUSY_WINDOW_THRESHOLD = 1500L

private class RN62TimingModuleReflected(private val timingModule: NativeModule) {
fun hasActiveTimers(): Boolean = Reflect.on(timingModule).call("hasActiveTimersInRange", BUSY_WINDOW_THRESHOLD).get()
}

/**
* Delegates the interrogation to the native module itself, added
* [here](https://github.com/facebook/react-native/pull/27539) in the context
* of RN v0.62 (followed by a previous refactor and rename of the class).
*/
@RNDropSupportTodo(62, """
When min RN version supported by Detox is 0.62.x (or higher),
| can (and should) remove any usage of reflection here. That
| includes the unit test's stub being used for that reason in particular.
""")
class DelegatedIdleInterrogationStrategy(timingModule: NativeModule): IdleInterrogationStrategy {
private val timingModuleReflected = RN62TimingModuleReflected(timingModule)

override fun isIdleNow(): Boolean = timingModuleReflected.hasActiveTimers()

companion object {
fun createIfSupported(reactContext: ReactContext): DelegatedIdleInterrogationStrategy? {
val moduleClass: Class<NativeModule>?
try {
moduleClass = Class.forName("com.facebook.react.modules.core.TimingModule") as Class<NativeModule>
} catch (ex: Exception) {
return null
}

if (!reactContext.hasNativeModule(moduleClass)) {
return null
}

try {
moduleClass.getDeclaredMethod("hasActiveTimersInRange", Long::class.java)
} catch (ex: Exception) {
return null
}

return DelegatedIdleInterrogationStrategy(reactContext.getNativeModule(moduleClass))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.wix.detox.reactnative.idlingresources.timers

import android.util.Log
import com.facebook.react.bridge.ReactContext

interface IdleInterrogationStrategy {
fun isIdleNow(): Boolean
}

fun getInterrogationStrategy(reactContext: ReactContext): IdleInterrogationStrategy? {
DelegatedIdleInterrogationStrategy.createIfSupported(reactContext)?.let {
return it
}

DefaultIdleInterrogationStrategy.createIfSupported(reactContext)?.let {
return it
}

Log.e(LOG_TAG, "Failed to determine proper implementation-strategy for timers idling resource")
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.wix.detox.reactnative.idlingresources.timers

import android.view.Choreographer
import androidx.test.espresso.IdlingResource
import java.util.concurrent.atomic.AtomicBoolean

const val LOG_TAG = "TimersIdlingResource"

class TimersIdlingResource @JvmOverloads constructor(
private val interrogationStrategy: IdleInterrogationStrategy,
private val getChoreographer: () -> Choreographer = { Choreographer.getInstance() }
) : IdlingResource, Choreographer.FrameCallback {

private var callback: IdlingResource.ResourceCallback? = null
private var paused = AtomicBoolean(false)

override fun getName(): String = this.javaClass.name

override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
getChoreographer().postFrameCallback(this)
}

override fun isIdleNow(): Boolean {
if (paused.get()) {
return true
}

return checkIdle().also { result ->
if (result) {
callback?.onTransitionToIdle()
} else {
getChoreographer().postFrameCallback(this@TimersIdlingResource)
}
}
}

override fun doFrame(frameTimeNanos: Long) {
callback?.let {
isIdleNow
}
}

fun pause() {
paused.set(true)
callback?.onTransitionToIdle()
}

fun resume() {
paused.set(false)
}

private fun checkIdle() = interrogationStrategy.isIdleNow()
}
Loading

0 comments on commit da70b11

Please sign in to comment.