Skip to content

Commit

Permalink
Merge branch 'refactoring-strategies&selectors' into 'master'
Browse files Browse the repository at this point in the history
Refactoring Strategies & Selectors (EAP-version)

See merge request nataniel.borges/droidmate!79
  • Loading branch information
Hotzkow committed Jul 24, 2019
2 parents 2925412 + 885ce38 commit 633ee48
Show file tree
Hide file tree
Showing 49 changed files with 1,000 additions and 700 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ wrapper{

allprojects {
group = "org.droidmate"
version = "1.3.4-SNAPSHOT"
version = "1.4.0-eap"

/* Merge all the build directories into one. */
// buildDir = rootProject.file('build') //TODO that means the apk/class paths are changing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import android.view.accessibility.AccessibilityNodeInfo
import kotlinx.coroutines.isActive
import org.droidmate.deviceInterface.communication.UiElementProperties
import org.droidmate.deviceInterface.exploration.Rectangle
import org.droidmate.deviceInterface.exploration.isActivated
import org.droidmate.deviceInterface.exploration.isEnabled
import org.droidmate.deviceInterface.exploration.visibleOuterBounds
import org.xmlpull.v1.XmlSerializer
import java.util.*
Expand Down Expand Up @@ -62,7 +62,7 @@ abstract class UiParser {
}
}

private val isClickableDescendant:(UiElementProperties)->Boolean = { it.hasClickableDescendant || it.clickable || it.selected.isActivated() }
private val isClickableDescendant:(UiElementProperties)->Boolean = { it.hasClickableDescendant || it.clickable || it.selected.isEnabled() }
private fun AccessibilityNodeInfo.createWidget(w: DisplayedWindow, xPath: String, children: List<UiElementProperties>,
img: Bitmap?, idHash: Int, parentH: Int, processedNodes: List<UiElementProperties>): UiElementProperties {
val nodeRect = Rect()
Expand Down Expand Up @@ -144,7 +144,7 @@ abstract class UiParser {
selected = selected, // ignore 'transparent' layouts
hasClickableDescendant = children.any(isClickableDescendant).let { hasClickableDescendant ->
// check if there are already 'selectable' items in the visible bounds of it, if so set clickable descendants to true
if (!hasClickableDescendant && selected.isActivated()) {
if (!hasClickableDescendant && selected.isEnabled()) {
// this visible area contains 'selectable/clickable' items therefore we want to mark this as having such descendants even if it is no direct parent but only an 'uncle' to these elements
processedNodes.any { visibleBounds.contains(it.visibleBounds) && isClickableDescendant(it) }
} else hasClickableDescendant
Expand Down
131 changes: 69 additions & 62 deletions project/pcComponents/core/src/main/kotlin/org/droidmate/app/Debug.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,26 @@ import org.droidmate.command.ExploreCommandBuilder
import org.droidmate.configuration.ConfigProperties
import org.droidmate.configuration.ConfigurationWrapper
import org.droidmate.device.android_sdk.Apk
import org.droidmate.deviceInterface.exploration.ExplorationAction
import org.droidmate.deviceInterface.exploration.isFetch
import org.droidmate.deviceInterface.exploration.isLaunchApp
import org.droidmate.deviceInterface.exploration.isPressBack
import org.droidmate.exploration.SelectorFunction
import org.droidmate.exploration.StrategySelector
import org.droidmate.exploration.strategy.AbstractStrategy
import org.droidmate.exploration.strategy.Back
import org.droidmate.exploration.strategy.Reset
import org.droidmate.exploration.strategy.Terminate
import org.droidmate.exploration.ExplorationContext
import org.droidmate.exploration.actions.closeAndReturn
import org.droidmate.exploration.actions.resetApp
import org.droidmate.exploration.actions.terminateApp
import org.droidmate.exploration.strategy.*
import org.droidmate.exploration.strategy.manual.Logging
import org.droidmate.exploration.strategy.manual.ManualExploration
import org.droidmate.exploration.strategy.manual.getLogger
import org.droidmate.exploration.strategy.widget.AllowRuntimePermission
import org.droidmate.explorationModel.factory.AbstractModel
import org.droidmate.explorationModel.factory.DefaultModelProvider
import org.droidmate.explorationModel.interaction.State
import org.droidmate.explorationModel.interaction.Widget
import org.droidmate.misc.FailableExploration
import java.util.*
import kotlin.collections.HashMap


@Suppress("SameParameterValue")
object Debug : Logging {
override val log by lazy { getLogger() }

Expand All @@ -38,15 +39,11 @@ object Debug : Logging {

private suspend fun manualExploration(cfg: ConfigurationWrapper){
val builder = ExploreCommandBuilder(
strategies = defaultStrategies.plus(
strategies = defaultStrategies(cfg).plus(
ManualExploration<Int>(resetOnStart = !cfg[org.droidmate.explorationModel.config.ConfigProperties.Output.debugMode])
).toMutableList(),
watcher = mutableListOf(),
selectors = mutableListOf(
StrategySelector(2, "allowRuntimePermission", StrategySelector.allowPermission),
StrategySelector(3, "leftApp", leftApp),
ManualExploration.selector( 42 )
)
selectors = mutableListOf()
)
ExplorationAPI.explore(
cfg = cfg,
Expand All @@ -55,38 +52,26 @@ object Debug : Logging {
).logResult()
}

private val defaultStrategies: Collection<AbstractStrategy> = listOf(Terminate, AllowRuntimePermission() )
@Suppress("unused")
private val defaultSelectors: (cfg: Configuration) -> Collection<StrategySelector> = { cfg ->
listOf(
private val defaultStrategies: (cfg: Configuration) -> MutableCollection<AExplorationStrategy> = { cfg ->
mutableListOf(
// timeLimit*60*1000 such that we can specify the limit in minutes instead of milliseconds
StrategySelector(
0,
"timeBasedTerminate",
StrategySelector.timeBasedTerminate,
bundle = arrayOf(cfg[ConfigProperties.Selectors.timeLimit]*60*1000)
),
StrategySelector(1, "stuckInLoop", isStuck),
StrategySelector(2, "allowRuntimePermission", allowPermission), // HAS To Be BEFORE 'leftApp' check since permission requests would be interpreted as out-of-app otherwise
StrategySelector(3, "leftApp", leftApp)
DefaultStrategies.timeBasedTerminate(0, cfg[ConfigProperties.Selectors.timeLimit] * 60 * 1000),
isStuck(1),
DefaultStrategies.allowPermission(2),
leftApp(3)
)
}

private var numPermissions = HashMap<UUID,Int>() // avoid some options to be misinterpreted as permission request to be infinitely triggered
private val allowPermission: SelectorFunction = { eContext, pool, _ ->
if (numPermissions.compute(eContext.getCurrentState().uid){ _,v -> v?.inc()?: 0 } ?: 0 < 5 && eContext.getCurrentState().isRequestRuntimePermissionDialogBox) {
pool.getFirstInstanceOf(AllowRuntimePermission::class.java)
}
else{
null
}
}
private fun isStuck(prio: Int) = object : AExplorationStrategy(){

override fun getPriority(): Int = prio
private var lastStuck = -1
var nextAction: ExplorationAction? = null

private var lastStuck = -1
private val isStuck: SelectorFunction = { eContext, strategyPool, _ ->
val reset by lazy{ strategyPool.getOrCreate("Reset") { Reset() } }
override suspend fun <M : AbstractModel<S, W>, S : State<W>, W : Widget> hasNext(eContext: ExplorationContext<M, S, W>): Boolean {
val reset by lazy{ eContext.resetApp() }
val lastActions by lazy{ eContext.explorationTrace.getActions().takeLast(20) }
when{
nextAction = when{
eContext.isEmpty() -> {
lastStuck = -1 // reset counter on each new exploration start
null
Expand All @@ -99,38 +84,60 @@ object Debug : Logging {
if( eContext.explorationTrace.size - lastStuck < 20){
log.warn(" We got stuck repeatedly within the last 20 actions! Check the app ${eContext.apk.packageName} for feasibility")
reset
// Terminate // TODO maybe we want instead Reset() and ignore this case?
// Terminate // TODO this leaded to early terminates even though 'unexplored' elements still existed
} else {
lastStuck = eContext.explorationTrace.size
reset
}
}
else ->null
}
return nextAction != null
}

override suspend fun <M : AbstractModel<S, W>, S : State<W>, W : Widget> nextAction(
eContext: ExplorationContext<M, S, W>
): ExplorationAction =
nextAction!!
}

private val leftApp: SelectorFunction = { eContext, strategyPool, _ ->
val currentState = eContext.getCurrentState()
val actions by lazy{ eContext.explorationTrace.getActions() }
val reset by lazy{ strategyPool.getOrCreate("Reset") { Reset() } }
when{
eContext.isEmpty() || (actions.size == 1 && actions.first().actionType.isFetch()) ->{
null
}
currentState.isAppHasStoppedDialogBox -> reset
!eContext.belongsToApp(currentState) -> // cannot check a single root-node since the first one may be some assistance window e.g. keyboard
if(actions.takeLast(3).any{ it.actionType.isPressBack() }) reset
else Back // the state does no longer belong to the AUT (happens by clicking browser links or advertisement)
currentState.isHomeScreen -> {
val recentRestart = actions.takeLast(10).count { it.actionType.isLaunchApp() }>2
if( recentRestart && actions.size>20){
log.error("Cannot start app: we are late in the exploration and already reset at least twice in the last actions.")
Terminate
} // we are late in the exploration and already reset at least twice in the last actions
else reset
private fun leftApp(prio: Int) = object : AExplorationStrategy(){
override fun getPriority(): Int = prio
var nextAction: ExplorationAction? = null

override suspend fun <M : AbstractModel<S, W>, S : State<W>, W : Widget> hasNext(eContext: ExplorationContext<M, S, W>): Boolean {
val currentState = eContext.getCurrentState()
val actions by lazy { eContext.explorationTrace.getActions() }
val reset by lazy { eContext.resetApp() }
nextAction = when {
eContext.isEmpty() || (actions.size == 1 && actions.first().actionType.isFetch()) -> { null /* NoOp */ }
currentState.isAppHasStoppedDialogBox -> {
reset
}
!eContext.belongsToApp(currentState) -> // cannot check a single root-node since the first one may be some assistance window e.g. keyboard
if (actions.takeLast(3).any { it.actionType.isPressBack() }) {
reset
} else { // the state does no longer belong to the AUT (happens by clicking browser links or advertisement)
ExplorationAction.closeAndReturn()
}
currentState.isHomeScreen -> {
val recentRestart = actions.takeLast(10).count { it.actionType.isLaunchApp() } > 2
if (recentRestart && actions.size > 20) {
log.error("Cannot start app: we are late in the exploration and already reset at least twice in the last actions.")
ExplorationAction.terminateApp()
} // we are late in the exploration and already reset at least twice in the last actions
else reset
}
else -> null
}
else -> null
return nextAction != null
}

override suspend fun <M : AbstractModel<S, W>, S : State<W>, W : Widget> nextAction(
eContext: ExplorationContext<M, S, W>
): ExplorationAction =
nextAction!!

}

private fun Map<Apk, FailableExploration>.logResult()= forEach { apk, (eContext, errors) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,16 @@ import org.droidmate.device.logcat.ApiLogcatMessage
import org.droidmate.device.logcat.ApiLogcatMessageListExtensions
import org.droidmate.deviceInterface.exploration.*
import org.droidmate.exploration.ExplorationContext
import org.droidmate.exploration.actions.resetApp
import org.droidmate.exploration.actions.launchApp
import org.droidmate.exploration.modelFeatures.ModelFeature
import org.droidmate.exploration.strategy.IExplorationStrategy
import org.droidmate.exploration.strategy.ExplorationStrategyPool
import org.droidmate.explorationModel.ModelFeatureI
import org.droidmate.explorationModel.config.ConfigProperties.ModelProperties.path.cleanDirs
import org.droidmate.explorationModel.config.ModelConfig
import org.droidmate.explorationModel.debugT
import org.droidmate.explorationModel.factory.AbstractModel
import org.droidmate.explorationModel.factory.ModelProvider
import org.droidmate.explorationModel.interaction.ActionResult
import org.droidmate.explorationModel.interaction.EmptyActionResult
import org.droidmate.explorationModel.interaction.State
import org.droidmate.explorationModel.interaction.Widget
import org.droidmate.logging.Markers
Expand All @@ -76,12 +75,12 @@ import java.time.LocalDateTime

open class ExploreCommand<M,S,W>(
private val cfg: ConfigurationWrapper,
private val apksProvider: IApksProvider,
private val deviceDeployer: IAndroidDeviceDeployer,
private val apkDeployer: IApkDeployer,
private val strategyProvider: (ExplorationContext<M,S,W>) -> IExplorationStrategy,
private var modelProvider: ModelProvider<M>,
val watcher: MutableList<ModelFeatureI> = mutableListOf()
private val apksProvider: IApksProvider,
private val deviceDeployer: IAndroidDeviceDeployer,
private val apkDeployer: IApkDeployer,
private val strategyProvider: ExplorationStrategyPool,
private var modelProvider: ModelProvider<M>,
private val watcher: MutableList<ModelFeatureI> = mutableListOf()
) where M: AbstractModel<S, W>, S: State<W>, W: Widget {
companion object {
@JvmStatic
Expand Down Expand Up @@ -270,19 +269,18 @@ open class ExploreCommand<M,S,W>(

// Construct initial action and execute it on the device to obtain initial result.
var action: ExplorationAction = EmptyAction
var result: ActionResult = EmptyActionResult
var result: ActionResult
var capturedPreviously = false

var isFirst = true

var strategy: IExplorationStrategy? = null
val strategyScheduler = strategyProvider.apply{ init(cfg, explorationContext) }
try {
strategy = strategyProvider.invoke(explorationContext)
// Execute the exploration loop proper, starting with the values of initial reset action and its result.
while (isFirst || !action.isTerminate()) {
try {
// decide for an action
action = strategy.decide(result) // check if we need to initialize timeProvider.getNow() here
action = strategyScheduler.nextAction(explorationContext) // check if we need to initialize timeProvider.getNow() here
// execute action
result = action.execute(app, device)

Expand Down Expand Up @@ -331,19 +329,19 @@ open class ExploreCommand<M,S,W>(
" ${e.localizedMessage}", e
)
explorationContext.exceptions.add(e)
explorationContext.resetApp().execute(app, device)
explorationContext.launchApp().execute(app, device)
}
} // end loop

explorationContext.explorationEndTime = LocalDateTime.now()
explorationContext.verify() // some result validation do this in the end of exploration for this app
// but within the catch block to NOT terminate other explorations and to NOT loose the derived context

} catch (e: Throwable) { // the loop handles internal error if possible, however if the resetApp after exception fails we end in this catch
} catch (e: Throwable) { // the loop handles internal error if possible, however if the launchApp after exception fails we end in this catch
// this means likely the uiAutomator is dead or we lost device connection
log.error("unhandled device exception \n ${e.localizedMessage}", e)
explorationContext.exceptions.add(e)
strategy?.close()
strategyScheduler.close()
} finally {
explorationContext.close()
}
Expand Down
Loading

0 comments on commit 633ee48

Please sign in to comment.