Skip to content

Commit

Permalink
refactor: Refactor data scratch-Android run (#1941)
Browse files Browse the repository at this point in the history
Fixes #1754

## Test Plan
> How do we know the code works?

- code is refactored according to the description provided in #1754 with a change 
    `interface Execute : (Config, Type) -> TestMatrix.Result` ->  `interface Execute : (Config, List<Type>) -> <TestMatrix.Data>`
- all runs of `flank firebase test android run ... ` work without any problems like previously

## Checklist

- [x] Unit tested
  • Loading branch information
piotradamczyk5 authored May 18, 2021
1 parent 569df1c commit 5a0acf2
Show file tree
Hide file tree
Showing 35 changed files with 496 additions and 457 deletions.
16 changes: 16 additions & 0 deletions test_runner/src/main/kotlin/ftl/adapter/GoogleTestMatrixAndroid.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ftl.adapter

import ftl.adapter.google.toApiModel
import ftl.api.TestMatrix
import ftl.api.TestMatrixAndroid
import ftl.client.google.run.android.executeAndroidTests
import kotlinx.coroutines.runBlocking
import com.google.testing.model.TestMatrix as GoogleTestMatrix

object GoogleTestMatrixAndroid :
TestMatrixAndroid.Execute,
(TestMatrixAndroid.Config, List<TestMatrixAndroid.Type>) -> List<TestMatrix.Data> by { config, types ->
runBlocking {
executeAndroidTests(config, types).map(GoogleTestMatrix::toApiModel)
}
}
70 changes: 70 additions & 0 deletions test_runner/src/main/kotlin/ftl/api/TestAndroidMatrix.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package ftl.api

import ftl.adapter.GoogleTestMatrixAndroid
import ftl.args.FlankRoboDirective
import ftl.config.Device

val executeTestMatrixAndroid: TestMatrixAndroid.Execute get() = GoogleTestMatrixAndroid

object TestMatrixAndroid {

data class Config(
// args
val clientDetails: Map<String, String>?,
val resultsBucket: String,
val autoGoogleLogin: Boolean,
val networkProfile: String?,
val directoriesToPull: List<String>,
val obbNames: List<String>,
val environmentVariables: Map<String, String>,
val autoGrantPermissions: Boolean,
val testTimeout: String,
val performanceMetrics: Boolean,
val recordVideo: Boolean,
val flakyTestAttempts: Int,
val failFast: Boolean,
val project: String,
val resultsHistoryName: String?,
val repeatTests: Int,

// build
val otherFiles: Map<String, String>,
val resultsDir: String,
val devices: List<Device>,
val additionalApkGcsPaths: List<String>,
val obbFiles: Map<String, String>,
)

sealed class Type {
data class Instrumentation(
val appApkGcsPath: String,
val testApkGcsPath: String,
val testRunnerClass: String?,
val orchestratorOption: String?,
// sharding
val disableSharding: Boolean,
val testShards: ShardChunks,
val numUniformShards: Int?,
val keepTestTargetsEmpty: Boolean,
val environmentVariables: Map<String, String> = emptyMap(),
val testTargetsForShard: ShardChunks
) : Type()

data class Robo(
val appApkGcsPath: String,
val flankRoboDirectives: List<FlankRoboDirective>?,
val roboScriptGcsPath: String?
) : Type()

data class GameLoop(
val appApkGcsPath: String,
val testRunnerClass: String?,
val scenarioNumbers: List<String>,
val scenarioLabels: List<String>
) : Type()
}

interface Execute : (Config, List<Type>) -> List<TestMatrix.Data>
}

typealias ShardChunks = List<List<String>>
1 change: 1 addition & 0 deletions test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ftl.args

import ftl.analytics.AnonymizeInStatistics
import ftl.analytics.IgnoreInStatistics
import ftl.api.ShardChunks
import ftl.args.yml.AppTestPair
import ftl.args.yml.Type
import ftl.run.ANDROID_SHARD_FILE
Expand Down
1 change: 1 addition & 0 deletions test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.google.cloud.storage.StorageOptions
import flank.common.defaultCredentialPath
import flank.common.isWindows
import flank.common.logLn
import ftl.api.ShardChunks
import ftl.api.downloadAsJunitXML
import ftl.args.IArgs.Companion.AVAILABLE_PHYSICAL_SHARD_COUNT_RANGE
import ftl.args.yml.YamlObjectMapper
Expand Down
3 changes: 0 additions & 3 deletions test_runner/src/main/kotlin/ftl/args/ShardChunks.kt

This file was deleted.

28 changes: 28 additions & 0 deletions test_runner/src/main/kotlin/ftl/client/google/run/Utils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ftl.client.google.run

import com.google.testing.model.ClientInfoDetail
import com.google.testing.model.FileReference
import com.google.testing.model.IosDeviceFile

internal fun List<String>.mapGcsPathsToFileReference(): List<FileReference> = map { it.toFileReference() }

fun String.toFileReference(): FileReference = FileReference().setGcsPath(this)

internal fun Map<String, String>.mapToIosDeviceFiles(): List<IosDeviceFile> = map { (testDevicePath, gcsFilePath) -> toIosDeviceFile(testDevicePath, gcsFilePath) }

internal fun toIosDeviceFile(testDevicePath: String, gcsFilePath: String = "") = IosDeviceFile().apply {
if (testDevicePath.contains(":")) {
val (bundleIdSeparated, pathSeparated) = testDevicePath.split(":")
bundleId = bundleIdSeparated
devicePath = pathSeparated
} else {
devicePath = testDevicePath
}
content = FileReference().setGcsPath(gcsFilePath)
}

internal fun Map<String, String>.toClientInfoDetailList() = map { (key, value) ->
ClientInfoDetail()
.setKey(key)
.setValue(value)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ftl.gc.android
package ftl.client.google.run.android

import com.google.testing.model.AndroidInstrumentationTest
import com.google.testing.model.FileReference
Expand All @@ -7,11 +7,11 @@ import com.google.testing.model.ShardingOption
import com.google.testing.model.TestTargetsForShard
import com.google.testing.model.UniformSharding
import flank.common.logLn
import ftl.args.ShardChunks
import ftl.run.platform.android.AndroidTestConfig
import ftl.api.ShardChunks
import ftl.api.TestMatrixAndroid

internal fun createAndroidInstrumentationTest(
config: AndroidTestConfig.Instrumentation
config: TestMatrixAndroid.Type.Instrumentation
) = AndroidInstrumentationTest().apply {
appApk = FileReference().setGcsPath(config.appApkGcsPath)
testApk = FileReference().setGcsPath(config.testApkGcsPath)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package ftl.gc.android
package ftl.client.google.run.android

import com.google.testing.model.AndroidTestLoop
import com.google.testing.model.FileReference
import ftl.run.platform.android.AndroidTestConfig
import ftl.api.TestMatrixAndroid

internal fun createGameLoopTest(config: AndroidTestConfig.GameLoop) = AndroidTestLoop().apply {
internal fun createGameLoopTest(config: TestMatrixAndroid.Type.GameLoop) = AndroidTestLoop().apply {
appApk = FileReference().setGcsPath(config.appApkGcsPath)
scenarioLabels = config.scenarioLabels
scenarios = config.scenarioNumbers.map { it.toInt() }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package ftl.gc.android
package ftl.client.google.run.android

import com.google.testing.model.AndroidRoboTest
import com.google.testing.model.FileReference
import com.google.testing.model.RoboDirective
import ftl.api.TestMatrixAndroid
import ftl.args.FlankRoboDirective
import ftl.run.platform.android.AndroidTestConfig

internal fun createAndroidRoboTest(
config: AndroidTestConfig.Robo
config: TestMatrixAndroid.Type.Robo
) = AndroidRoboTest().apply {
appApk = FileReference().setGcsPath(config.appApkGcsPath)
roboDirectives = config.flankRoboDirectives?.mapToApiRoboDirectives()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ftl.gc
package ftl.client.google.run.android

import com.google.testing.model.AndroidDevice
import com.google.testing.model.AndroidDeviceList
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package ftl.client.google.run.android

import com.google.common.annotations.VisibleForTesting
import com.google.testing.Testing
import com.google.testing.model.Account
import com.google.testing.model.Apk
import com.google.testing.model.ClientInfo
import com.google.testing.model.DeviceFile
import com.google.testing.model.EnvironmentMatrix
import com.google.testing.model.FileReference
import com.google.testing.model.GoogleAuto
import com.google.testing.model.GoogleCloudStorage
import com.google.testing.model.ObbFile
import com.google.testing.model.RegularFile
import com.google.testing.model.ResultStorage
import com.google.testing.model.TestMatrix
import com.google.testing.model.TestSetup
import com.google.testing.model.TestSpecification
import com.google.testing.model.ToolResultsHistory
import flank.common.join
import ftl.api.TestMatrixAndroid
import ftl.client.google.GcTesting
import ftl.client.google.run.toClientInfoDetailList
import ftl.client.google.run.toFileReference
import ftl.http.executeWithRetry
import ftl.run.exception.FlankGeneralError
import ftl.util.timeoutToSeconds
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope

suspend fun executeAndroidTests(
config: TestMatrixAndroid.Config,
testMatrixTypes: List<TestMatrixAndroid.Type>,
): List<TestMatrix> = testMatrixTypes
.foldIndexed(emptyList<Deferred<TestMatrix>>()) { testMatrixTypeIndex, testMatrices, testMatrixType ->
testMatrices + executeAndroidTestMatrix(testMatrixType, testMatrixTypeIndex, config)
}.awaitAll()

private suspend fun executeAndroidTestMatrix(
type: TestMatrixAndroid.Type,
typeIndex: Int,
config: TestMatrixAndroid.Config
): List<Deferred<TestMatrix>> = coroutineScope {
(0 until config.repeatTests).map { runIndex ->
async(Dispatchers.IO) {
createAndroidTestMatrix(type, config, typeIndex, runIndex).executeWithRetry()
}
}
}

private fun createAndroidTestMatrix(
testMatrixType: TestMatrixAndroid.Type,
config: TestMatrixAndroid.Config,
contextIndex: Int,
runIndex: Int
): Testing.Projects.TestMatrices.Create {

val testMatrix = TestMatrix()
.setClientInfo(config.clientInfo)
.setTestSpecification(getTestSpecification(testMatrixType, config))
.setResultStorage(config.resultsStorage(contextIndex, runIndex))
.setEnvironmentMatrix(config.environmentMatrix)
.setFlakyTestAttempts(config.flakyTestAttempts)
.setFailFast(config.failFast)

return runCatching {
GcTesting.get.projects().testMatrices().create(config.project, testMatrix)
}.getOrElse { e -> throw FlankGeneralError(e) }
}

// https://github.com/bootstraponline/studio-google-cloud-testing/blob/203ed2890c27a8078cd1b8f7ae12cf77527f426b/firebase-testing/src/com/google/gct/testing/launcher/CloudTestsLauncher.java#L120
private val TestMatrixAndroid.Config.clientInfo
get() = ClientInfo()
.setName("Flank")
.setClientInfoDetails(clientDetails?.toClientInfoDetailList())

private val TestMatrixAndroid.Config.environmentMatrix
get() = EnvironmentMatrix()
.setAndroidDeviceList(GcAndroidDevice.build(devices))

private fun getTestSpecification(
testMatrixType: TestMatrixAndroid.Type,
config: TestMatrixAndroid.Config
): TestSpecification = TestSpecification()
.setDisablePerformanceMetrics(!config.performanceMetrics)
.setDisableVideoRecording(!config.recordVideo)
.setTestTimeout("${timeoutToSeconds(config.testTimeout)}s")
.setTestSetup(getTestSetup(testMatrixType, config))
.setupAndroidTest(testMatrixType)

private fun getTestSetup(
testMatrixType: TestMatrixAndroid.Type,
config: TestMatrixAndroid.Config
): TestSetup = TestSetup()
.setAccount(config.account)
.setNetworkProfile(config.networkProfile)
.setDirectoriesToPull(config.directoriesToPull)
.setAdditionalApks(config.additionalApkGcsPaths.mapGcsPathsToApks())
.setFilesToPush(config.otherFiles.mapToDeviceFiles() + config.obbFiles.mapToDeviceObbFiles(config.obbNames))
.setDontAutograntPermissions(config.autoGrantPermissions.not())
.setEnvironmentVariables(config.environmentVariables, testMatrixType)

internal fun List<String>.mapGcsPathsToApks(): List<Apk>? = this
.map { gcsPath -> Apk().setLocation(gcsPath.toFileReference()) }
.takeIf { it.isNotEmpty() }

private fun Map<String, String>.mapToDeviceFiles(): List<DeviceFile> =
map { (devicePath: String, gcsFilePath: String) ->
DeviceFile().setRegularFile(
RegularFile()
.setDevicePath(devicePath)
.setContent(gcsFilePath.toFileReference())
)
}

private fun Map<String, String>.mapToDeviceObbFiles(obbnames: List<String>): List<DeviceFile> {
return values.mapIndexed { index, gcsFilePath ->
DeviceFile().setObbFile(
ObbFile().setObb(FileReference().setGcsPath(gcsFilePath)).setObbFileName(obbnames[index])
)
}
}

private fun TestMatrixAndroid.Config.resultsStorage(
contextIndex: Int,
runIndex: Int
) = ResultStorage()
.setGoogleCloudStorage(
GoogleCloudStorage().setGcsPath(join(resultsBucket, resultsDir.createGcsPath(contextIndex, runIndex)))
)
.setToolResultsHistory(
ToolResultsHistory().setHistoryId(resultsHistoryName).setProjectId(project)
)

@VisibleForTesting
internal fun String.createGcsPath(contextIndex: Int, runIndex: Int) =
if (runIndex == 0) "$this/matrix_$contextIndex/"
else "$this/matrix_${contextIndex}_$runIndex/"

private val TestMatrixAndroid.Config.account
get() = if (autoGoogleLogin) Account().setGoogleAuto(GoogleAuto()) else null
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ftl.client.google.run.android

import com.google.testing.model.TestSpecification
import ftl.api.TestMatrixAndroid

internal fun TestSpecification.setupAndroidTest(config: TestMatrixAndroid.Type) = apply {
when (config) {
is TestMatrixAndroid.Type.Instrumentation -> androidInstrumentationTest = createAndroidInstrumentationTest(config)
is TestMatrixAndroid.Type.Robo -> androidRoboTest = createAndroidRoboTest(config)
is TestMatrixAndroid.Type.GameLoop -> androidTestLoop = createGameLoopTest(config)
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package ftl.gc.android
package ftl.client.google.run.android

import com.google.common.annotations.VisibleForTesting
import com.google.testing.model.EnvironmentVariable
import com.google.testing.model.TestSetup
import ftl.args.AndroidArgs
import ftl.run.platform.android.AndroidTestConfig
import ftl.api.TestMatrixAndroid

@VisibleForTesting
internal fun TestSetup.setEnvironmentVariables(args: AndroidArgs, testConfig: AndroidTestConfig) = apply {
internal fun TestSetup.setEnvironmentVariables(args: Map<String, String>, testConfig: TestMatrixAndroid.Type) = apply {
environmentVariables = when (testConfig) {
is AndroidTestConfig.Instrumentation ->
testConfig.environmentVariables.map { it.toEnvironmentVariable() } + args.environmentVariables.map { it.toEnvironmentVariable() }
is AndroidTestConfig.Robo -> emptyList()
is AndroidTestConfig.GameLoop -> emptyList()
is TestMatrixAndroid.Type.Instrumentation ->
testConfig.environmentVariables.map { it.toEnvironmentVariable() } + args.map { it.toEnvironmentVariable() }
is TestMatrixAndroid.Type.Robo -> emptyList()
is TestMatrixAndroid.Type.GameLoop -> emptyList()
}.distinctBy { it.key }
}

Expand Down
2 changes: 1 addition & 1 deletion test_runner/src/main/kotlin/ftl/filter/TestFilters.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package ftl.filter

import com.linkedin.dex.parser.TestMethod
import flank.common.logLn
import ftl.args.ShardChunks
import ftl.api.ShardChunks
import ftl.config.FtlConstants
import ftl.run.exception.FlankConfigurationError
import ftl.run.exception.FlankGeneralError
Expand Down
Loading

0 comments on commit 5a0acf2

Please sign in to comment.