Skip to content

Commit

Permalink
Support Robin uploads for Web apps
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitry-zaitsev committed Jan 10, 2025
1 parent 30f7363 commit 9df86f2
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 21 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ bin

# media assets
maestro-orchestra/src/test/resources/media/assets/*

# Local files
local/
69 changes: 50 additions & 19 deletions maestro-cli/src/main/java/maestro/cli/cloud/CloudInteractor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@ package maestro.cli.cloud

import maestro.cli.CliError
import maestro.cli.api.ApiClient
import maestro.cli.api.RobinUploadResponse
import maestro.cli.api.DeviceInfo
import maestro.cli.api.UploadStatus
import maestro.cli.api.DeviceConfiguration
import maestro.cli.api.DeviceInfo
import maestro.cli.api.MaestroCloudUploadResponse
import maestro.cli.api.RobinUploadResponse
import maestro.cli.api.UploadStatus
import maestro.cli.auth.Auth
import maestro.cli.device.Platform
import maestro.cli.model.RunningFlow
import maestro.cli.model.FlowStatus
import maestro.cli.model.RunningFlow
import maestro.cli.model.RunningFlows
import maestro.cli.model.TestExecutionSummary
import maestro.cli.report.ReportFormat
import maestro.cli.report.ReporterFactory
import maestro.cli.util.EnvUtils
import maestro.cli.util.FileUtils.isWebFlow
import maestro.cli.util.FileUtils.isZip
import maestro.cli.util.PrintUtils
import maestro.cli.util.TimeUtils
Expand All @@ -26,14 +27,14 @@ import maestro.cli.view.TestSuiteStatusView.TestSuiteViewModel.Companion.toViewM
import maestro.cli.view.TestSuiteStatusView.robinUploadUrl
import maestro.cli.view.TestSuiteStatusView.uploadUrl
import maestro.cli.view.box
import maestro.cli.web.WebInteractor
import maestro.utils.TemporaryDirectory
import okio.BufferedSink
import okio.buffer
import okio.sink
import org.rauschig.jarchivelib.ArchiveFormat
import org.rauschig.jarchivelib.ArchiverFactory
import java.io.File
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.io.path.absolute

Expand Down Expand Up @@ -72,7 +73,7 @@ class CloudInteractor(
deviceLocale: String? = null,
projectId: String? = null,
): Int {
if (appBinaryId == null && appFile == null) throw CliError("Missing required parameter for option '--app-file' or '--app-binary-id'")
if (appBinaryId == null && appFile == null && !flowFile.isWebFlow()) throw CliError("Missing required parameter for option '--app-file' or '--app-binary-id'")
if (!flowFile.exists()) throw CliError("File does not exist: ${flowFile.absolutePath}")
if (mapping?.exists() == false) throw CliError("File does not exist: ${mapping.absolutePath}")
if (async && reportFormat != ReportFormat.NOOP) throw CliError("Cannot use --format with --async")
Expand Down Expand Up @@ -104,6 +105,8 @@ class CloudInteractor(
@Suppress("RemoveRedundantSpreadOperator")
archiver.create(appFile.name + ".zip", tmpDir.toFile(), *arrayOf(appFile.absoluteFile))
}
} else if (flowFile.isWebFlow()) {
appFileToSend = WebInteractor.createManifestFromWorkspace(flowFile)
}

val response = client.upload(
Expand Down Expand Up @@ -137,7 +140,8 @@ class CloudInteractor(
val project = requireNotNull(projectId)
val appId = response.appId
val uploadUrl = robinUploadUrl(project, appId, response.uploadId, client.domain)
val deviceMessage = if (response.deviceConfiguration != null) printDeviceInfo(response.deviceConfiguration) else ""
val deviceMessage =
if (response.deviceConfiguration != null) printDeviceInfo(response.deviceConfiguration) else ""
return printMaestroCloudResponse(
async,
authToken,
Expand All @@ -153,6 +157,7 @@ class CloudInteractor(
projectId,
)
}

is MaestroCloudUploadResponse -> {
println()
val deviceInfo = response.deviceInfo
Expand All @@ -161,12 +166,13 @@ class CloudInteractor(
val uploadId = response.uploadId
val appBinaryIdResponse = response.appBinaryId
val uploadUrl = uploadUrl(uploadId, teamId, appId, client.domain)
val deviceInfoMessage = if (deviceInfo != null) printDeviceInfo(deviceInfo, iOSVersion, androidApiLevel) else ""
val deviceInfoMessage =
if (deviceInfo != null) printDeviceInfo(deviceInfo, iOSVersion, androidApiLevel) else ""
return printMaestroCloudResponse(
async = async,
authToken = authToken,
failOnCancellation = failOnCancellation,
reportFormat= reportFormat,
reportFormat = reportFormat,
reportOutput = reportOutput,
testSuiteName = testSuiteName,
uploadUrl = uploadUrl,
Expand Down Expand Up @@ -236,29 +242,35 @@ class CloudInteractor(
val platform = Platform.fromString(deviceInfo.platform)

val line1 = "Maestro Cloud device specs:\n* ${deviceInfo.displayInfo} - ${deviceInfo.deviceLocale}"
val line2 = "To change OS version use this option: ${if (platform == Platform.IOS) "--ios-version=<version>" else "--android-api-level=<version>"}"
val line2 =
"To change OS version use this option: ${if (platform == Platform.IOS) "--ios-version=<version>" else "--android-api-level=<version>"}"
val line3 = "To change device locale use this option: --device-locale=<device_locale>"

val version = when(platform) {
val version = when (platform) {
Platform.ANDROID -> "${androidApiLevel ?: 30}" // todo change with constant from DeviceConfigAndroid
Platform.IOS -> "${iOSVersion ?: 15}" // todo change with constant from DeviceConfigIos
else -> return ""
}

val line4 = "To create a similar device locally, run: `maestro start-device --platform=${platform.toString().lowercase()} --os-version=$version --device-locale=${deviceInfo.deviceLocale}`"
val line4 = "To create a similar device locally, run: `maestro start-device --platform=${
platform.toString().lowercase()
} --os-version=$version --device-locale=${deviceInfo.deviceLocale}`"
return "$line1\n\n$line2\n\n$line3\n\n$line4".box()
}

private fun printDeviceInfo(deviceConfiguration: DeviceConfiguration): String {
val platform = Platform.fromString(deviceConfiguration.platform)

val line1 = "Robin device specs:\n* ${deviceConfiguration.displayInfo} - ${deviceConfiguration.deviceLocale}"
val line2 = "To change OS version use this option: ${if (platform == Platform.IOS) "--ios-version=<version>" else "--android-api-level=<version>"}"
val line2 =
"To change OS version use this option: ${if (platform == Platform.IOS) "--ios-version=<version>" else "--android-api-level=<version>"}"
val line3 = "To change device locale use this option: --device-locale=<device_locale>"

val version = deviceConfiguration.osVersion

val line4 = "To create a similar device locally, run: `maestro start-device --platform=${platform.toString().lowercase()} --os-version=$version --device-locale=${deviceConfiguration.deviceLocale}`"
val line4 = "To create a similar device locally, run: `maestro start-device --platform=${
platform.toString().lowercase()
} --os-version=$version --device-locale=${deviceConfiguration.deviceLocale}`"
return "$line1\n\n$line2\n\n$line3\n\n$line4".box()
}

Expand Down Expand Up @@ -310,15 +322,21 @@ class CloudInteractor(
val runningFlow = runningFlows.flows.find { it.name == uploadFlowResult.name } ?: continue
runningFlow.status = uploadFlowResult.status
when (runningFlow.status) {
FlowStatus.PENDING -> { /* do nothing */ }
FlowStatus.PENDING -> { /* do nothing */
}

FlowStatus.RUNNING -> {
if (runningFlow.startTime == null) {
runningFlow.startTime = System.currentTimeMillis()
}
}

else -> {
if (runningFlow.duration == null) {
runningFlow.duration = TimeUtils.durationInSeconds(startTimeInMillis = runningFlow.startTime, endTimeInMillis = System.currentTimeMillis())
runningFlow.duration = TimeUtils.durationInSeconds(
startTimeInMillis = runningFlow.startTime,
endTimeInMillis = System.currentTimeMillis()
)
}
if (!runningFlow.reported) {
TestSuiteStatusView.showFlowCompletion(
Expand All @@ -331,7 +349,10 @@ class CloudInteractor(
}

if (upload.completed) {
runningFlows.duration = TimeUtils.durationInSeconds(startTimeInMillis = startTime, endTimeInMillis = System.currentTimeMillis())
runningFlows.duration = TimeUtils.durationInSeconds(
startTimeInMillis = startTime,
endTimeInMillis = System.currentTimeMillis()
)
return handleSyncUploadCompletion(
upload = upload,
runningFlows = runningFlows,
Expand Down Expand Up @@ -404,7 +425,13 @@ class CloudInteractor(
}

if (reportOutputSink != null) {
saveReport(reportFormat, !failed, createSuiteResult(!failed, upload, runningFlows), reportOutputSink, testSuiteName)
saveReport(
reportFormat,
!failed,
createSuiteResult(!failed, upload, runningFlows),
reportOutputSink,
testSuiteName
)
}


Expand Down Expand Up @@ -440,7 +467,11 @@ class CloudInteractor(
)
}

private fun createSuiteResult(passed: Boolean, upload: UploadStatus, runningFlows: RunningFlows): TestExecutionSummary.SuiteResult {
private fun createSuiteResult(
passed: Boolean,
upload: UploadStatus,
runningFlows: RunningFlows
): TestExecutionSummary.SuiteResult {
return TestExecutionSummary.SuiteResult(
passed = passed,
flows = upload.flows.map { uploadFlowResult ->
Expand Down
7 changes: 5 additions & 2 deletions maestro-cli/src/main/java/maestro/cli/command/CloudCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import maestro.cli.api.ApiClient
import maestro.cli.cloud.CloudInteractor
import maestro.cli.report.ReportFormat
import maestro.cli.report.TestDebugReporter
import maestro.cli.util.FileUtils.isWebFlow
import maestro.cli.util.PrintUtils
import maestro.orchestra.util.Env.withInjectedShellEnvVars
import maestro.orchestra.workspace.WorkspaceExecutionPlanner
Expand Down Expand Up @@ -174,7 +175,7 @@ class CloudCommand : Callable<Int> {
flattenDebugOutput = false,
printToConsole = parent?.verbose == true,
)

validateFiles()
validateWorkSpace()

Expand Down Expand Up @@ -258,8 +259,10 @@ class CloudCommand : Callable<Int> {
}
}

val hasApp = appFile != null || appBinaryId != null
val hasWorkspace = this::flowsFile.isInitialized
val hasApp = appFile != null
|| appBinaryId != null
|| (this::flowsFile.isInitialized && this::flowsFile.get().isWebFlow())

if (!hasApp && !hasWorkspace) {
throw CommandLine.MissingParameterException(spec!!.commandLine(), spec!!.findOption("--flows"), "Missing required parameters: '--app-file', " +
Expand Down
6 changes: 6 additions & 0 deletions maestro-cli/src/main/java/maestro/cli/util/FileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ object FileUtils {
}

fun File.isWebFlow(): Boolean {
if (isDirectory) {
return listFiles()
?.any { it.isWebFlow() }
?: false
}

val config = YamlCommandReader.readConfig(toPath())
return Regex("http(s?)://").containsMatchIn(config.appId)
}
Expand Down
38 changes: 38 additions & 0 deletions maestro-cli/src/main/java/maestro/cli/web/WebInteractor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package maestro.cli.web

import maestro.cli.util.FileUtils.isWebFlow
import maestro.orchestra.yaml.YamlCommandReader
import java.io.File

object WebInteractor {

fun createManifestFromWorkspace(workspaceFile: File): File? {
val appId = inferAppId(workspaceFile) ?: return null

val manifest = """
{
"url": "$appId"
}
""".trimIndent()

val manifestFile = File.createTempFile("manifest", ".json")
manifestFile.writeText(manifest)
return manifestFile
}

private fun inferAppId(file: File): String? {
if (file.isDirectory) {
return file.listFiles()
?.firstNotNullOfOrNull { inferAppId(it) }
}

if (!file.isWebFlow()) {
return null
}

return file.readText()
.let { YamlCommandReader.readConfig(file.toPath()) }
.appId
}

}

0 comments on commit 9df86f2

Please sign in to comment.