Skip to content

Commit

Permalink
feat: add retry functionality (#2168)
Browse files Browse the repository at this point in the history
  • Loading branch information
amanjeetsingh150 authored Dec 4, 2024
1 parent 367dc6e commit 62c2486
Show file tree
Hide file tree
Showing 7 changed files with 185 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,41 @@ data class RepeatCommand(

}

data class RetryCommand(
val maxRetries: String? = null,
val commands: List<MaestroCommand>,
val config: MaestroConfig?,
override val label: String? = null,
override val optional: Boolean = false,
) : CompositeCommand {

override fun subCommands(): List<MaestroCommand> {
return commands
}

override fun config(): MaestroConfig? {
return null
}

override fun description(): String {
val maxAttempts = maxRetries?.toIntOrNull() ?: 1

return when {
label != null -> {
label
}
else -> "Retry $maxAttempts times"
}
}

override fun evaluateScripts(jsEngine: JsEngine): Command {
return copy(
maxRetries = maxRetries?.evaluateScripts(jsEngine),
)
}

}

data class DefineVariablesCommand(
val env: Map<String, String>,
override val label: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ data class MaestroCommand(
val addMediaCommand: AddMediaCommand? = null,
val setAirplaneModeCommand: SetAirplaneModeCommand? = null,
val toggleAirplaneModeCommand: ToggleAirplaneModeCommand? = null,
val retryCommand: RetryCommand? = null,
) {

constructor(command: Command) : this(
Expand Down Expand Up @@ -109,6 +110,7 @@ data class MaestroCommand(
addMediaCommand = command as? AddMediaCommand,
setAirplaneModeCommand = command as? SetAirplaneModeCommand,
toggleAirplaneModeCommand = command as? ToggleAirplaneModeCommand,
retryCommand = command as? RetryCommand
)

fun asCommand(): Command? = when {
Expand Down Expand Up @@ -151,6 +153,7 @@ data class MaestroCommand(
addMediaCommand != null -> addMediaCommand
setAirplaneModeCommand != null -> setAirplaneModeCommand
toggleAirplaneModeCommand != null -> toggleAirplaneModeCommand
retryCommand != null -> retryCommand
else -> null
}

Expand Down
40 changes: 32 additions & 8 deletions maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import okio.Buffer
import okio.Sink
import okio.buffer
import okio.sink
import org.slf4j.LoggerFactory
import java.io.File
import java.lang.Long.max

Expand Down Expand Up @@ -298,6 +299,7 @@ class Orchestra(
is AddMediaCommand -> addMediaCommand(command.mediaPaths)
is SetAirplaneModeCommand -> setAirplaneMode(command)
is ToggleAirplaneModeCommand -> toggleAirplaneMode()
is RetryCommand -> retryCommand(command, config)
else -> true
}.also { mutating ->
if (mutating) {
Expand Down Expand Up @@ -560,6 +562,29 @@ class Orchestra(
return mutatiing
}

private fun retryCommand(command: RetryCommand, config: MaestroConfig?): Boolean {
val maxRetries = (command.maxRetries?.toIntOrNull() ?: 1).coerceAtMost(MAX_RETRIES_ALLOWED)

var attempt = 0
while(attempt <= maxRetries) {
try {
return runSubFlow(command.commands, config, command.config)
} catch (exception: Throwable) {
if (attempt == maxRetries) {
logger.error("Max retries ($maxRetries) reached. Commands failed.", exception)
break
}

val message = "Retrying the commands due to an error: ${exception.message} while execution (Attempt ${attempt + 1})"
logger.error("Attempt ${attempt +1} failed for retry command", exception)
insights.report(Insight(message = message, Insight.Level.WARNING))
}
attempt++
}

return false
}

private fun updateMetadata(rawCommand: MaestroCommand, metadata: CommandMetadata) {
rawCommandToMetadata[rawCommand] = metadata
onCommandMetadataUpdate(rawCommand, metadata)
Expand Down Expand Up @@ -719,14 +744,14 @@ class Orchestra(
private fun runSubFlow(
commands: List<MaestroCommand>,
config: MaestroConfig?,
subflowConfig: MaestroConfig?
subflowConfig: MaestroConfig?,
): Boolean {
executeDefineVariablesCommands(commands, config)
// filter out DefineVariablesCommand to not execute it twice
val filteredCommands = commands.filter { it.asCommand() !is DefineVariablesCommand }

var exception: Throwable? = null
var flowSuccess = false
val onCompleteSuccess: Boolean
try {
val onStartSuccess = subflowConfig?.onFlowStart?.commands?.let {
executeSubflowCommands(it, config)
Expand All @@ -736,16 +761,13 @@ class Orchestra(
flowSuccess = executeSubflowCommands(filteredCommands, config)
}
} catch (e: Throwable) {
exception = e
throw e
} finally {
val onCompleteSuccess = subflowConfig?.onFlowComplete?.commands?.let {
onCompleteSuccess = subflowConfig?.onFlowComplete?.commands?.let {
executeSubflowCommands(it, config)
} ?: true

exception?.let { throw it }

return onCompleteSuccess && flowSuccess
}
return onCompleteSuccess && flowSuccess
}

private fun takeScreenshotCommand(command: TakeScreenshotCommand): Boolean {
Expand Down Expand Up @@ -1219,5 +1241,7 @@ class Orchestra(
val REGEX_OPTIONS = setOf(RegexOption.IGNORE_CASE, RegexOption.DOT_MATCHES_ALL, RegexOption.MULTILINE)

private const val MAX_ERASE_CHARACTERS = 50
private const val MAX_RETRIES_ALLOWED = 3
private val logger = LoggerFactory.getLogger(Orchestra::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ data class YamlFluentCommand(
val addMedia: YamlAddMedia? = null,
val setAirplaneMode: YamlSetAirplaneMode? = null,
val toggleAirplaneMode: YamlToggleAirplaneMode? = null,
val retry: YamlRetryCommand? = null,
) {

@SuppressWarnings("ComplexMethod")
Expand Down Expand Up @@ -209,6 +210,9 @@ data class YamlFluentCommand(
repeat != null -> listOf(
repeatCommand(repeat, flowPath, appId)
)
retry != null -> listOf(
retryCommand(retry, flowPath, appId)
)
copyTextFrom != null -> listOf(copyTextFromCommand(copyTextFrom))
runScript != null -> listOf(
MaestroCommand(
Expand Down Expand Up @@ -315,6 +319,40 @@ data class YamlFluentCommand(
)
}

private fun retryCommand(retry: YamlRetryCommand, flowPath: Path, appId: String): MaestroCommand {
if (retry.file == null && retry.commands == null) {
throw SyntaxError("Invalid retry command: No file or commands provided")
}

if (retry.file != null && retry.commands != null) {
throw SyntaxError("Invalid retry command: Can't provide both file and commands at the same time")
}

val commands = retry.commands
?.flatMap {
it.toCommands(flowPath, appId)
.withEnv(retry.env)
}
?: retry(flowPath, retry)

val config = retry.file?.let {
readConfig(flowPath, retry.file)
}


val maxRetries = retry.maxRetries ?: "1"

return MaestroCommand(
RetryCommand(
maxRetries = maxRetries,
commands = commands,
label = retry.label,
optional = retry.optional,
config = config
)
)
}

private fun travelCommand(command: YamlTravelCommand): MaestroCommand {
return MaestroCommand(
TravelCommand(
Expand Down Expand Up @@ -386,6 +424,16 @@ data class YamlFluentCommand(
.withEnv(command.env)
}

private fun retry(flowPath: Path, command: YamlRetryCommand): List<MaestroCommand> {
if (command.file == null) {
error("Invalid runFlow command: No file or commands provided")
}

val retryFlowPath = resolvePath(flowPath, command.file)
return YamlCommandReader.readCommands(retryFlowPath)
.withEnv(command.env)
}

private fun readConfig(flowPath: Path, commandFile: String): MaestroConfig? {
val runFlowPath = resolvePath(flowPath, commandFile)
return YamlCommandReader.readConfig(runFlowPath).toCommand(runFlowPath).applyConfigurationCommand?.config
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package maestro.orchestra.yaml

data class YamlRetryCommand(
val maxRetries: String? = null,
val file: String? = null,
val commands: List<YamlFluentCommand>? = null,
val env: Map<String, String> = emptyMap(),
val label: String? = null,
val optional: Boolean = false,
)
47 changes: 47 additions & 0 deletions maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package maestro.test

import com.google.common.truth.Truth.assertThat
import com.oracle.truffle.js.nodes.function.EvalNode
import maestro.KeyCode
import maestro.Maestro
import maestro.MaestroException
Expand Down Expand Up @@ -3171,6 +3172,52 @@ class IntegrationTest {
)
}

@Test
fun `Case 119 - Retry set of commands with n attempts`() {
// Given
val commands = readCommands("119_retry_commands")

var counter = 0
val driver = driver {
val indicator = element {
text = counter.toString()
bounds = Bounds(0, 100, 100, 200)
}

element {
text = "Button"
bounds = Bounds(0, 0, 100, 100)
onClick = {
counter++
if (counter == 1) {
throw RuntimeException("Exception for the first time")
}
indicator.text = counter.toString()
}
}

}

// When
Maestro(driver).use {
orchestra(it).runFlow(commands)
}

// Then
// No test failure
driver.assertEvents(
listOf(
Event.Scroll,
Event.TakeScreenshot,
/**----after retry----**/
Event.Scroll,
Event.TakeScreenshot,
Event.Tap(Point(50, 50)),
Event.Scroll,
)
)
}

private fun orchestra(
maestro: Maestro,
) = Orchestra(
Expand Down
10 changes: 10 additions & 0 deletions maestro-test/src/test/resources/119_retry_commands.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
appId: com.other.app
---
- retry:
maxRetries: 3
commands:
- scroll
- tapOn:
text: Button
waitToSettleTimeoutMs: 40
- scroll

0 comments on commit 62c2486

Please sign in to comment.