diff options
Diffstat (limited to 'firebase/testlab/testlab-gradle-plugin/src/main/java/com/android/tools/firebase/testlab/gradle/services/TestLabBuildService.kt')
-rw-r--r-- | firebase/testlab/testlab-gradle-plugin/src/main/java/com/android/tools/firebase/testlab/gradle/services/TestLabBuildService.kt | 312 |
1 files changed, 251 insertions, 61 deletions
diff --git a/firebase/testlab/testlab-gradle-plugin/src/main/java/com/android/tools/firebase/testlab/gradle/services/TestLabBuildService.kt b/firebase/testlab/testlab-gradle-plugin/src/main/java/com/android/tools/firebase/testlab/gradle/services/TestLabBuildService.kt index cd5515a052..e3a138d014 100644 --- a/firebase/testlab/testlab-gradle-plugin/src/main/java/com/android/tools/firebase/testlab/gradle/services/TestLabBuildService.kt +++ b/firebase/testlab/testlab-gradle-plugin/src/main/java/com/android/tools/firebase/testlab/gradle/services/TestLabBuildService.kt @@ -18,6 +18,8 @@ package com.android.tools.firebase.testlab.gradle.services import com.android.build.api.instrumentation.StaticTestData import com.android.builder.testing.api.DeviceConfigProvider +import com.android.tools.firebase.testlab.gradle.FixtureImpl +import com.android.tools.firebase.testlab.gradle.ManagedDeviceImpl import com.android.tools.firebase.testlab.gradle.UtpTestSuiteResultMerger import com.android.tools.utp.plugins.host.device.info.proto.AndroidTestDeviceInfoProto import com.google.api.client.googleapis.auth.oauth2.GoogleCredential @@ -26,6 +28,7 @@ import com.google.api.client.googleapis.util.Utils import com.google.api.client.http.GenericUrl import com.google.api.client.http.HttpRequestFactory import com.google.api.client.http.HttpRequestInitializer +import com.google.api.client.http.HttpTransport import com.google.api.client.http.InputStreamContent import com.google.api.client.json.GenericJson import com.google.api.client.json.JsonObjectParser @@ -40,15 +43,19 @@ import com.google.api.services.testing.model.AndroidDeviceList import com.google.api.services.testing.model.AndroidInstrumentationTest import com.google.api.services.testing.model.AndroidModel import com.google.api.services.testing.model.ClientInfo +import com.google.api.services.testing.model.DeviceFile import com.google.api.services.testing.model.EnvironmentMatrix import com.google.api.services.testing.model.GoogleCloudStorage +import com.google.api.services.testing.model.RegularFile import com.google.api.services.testing.model.ResultStorage import com.google.api.services.testing.model.TestExecution import com.google.api.services.testing.model.TestMatrix +import com.google.api.services.testing.model.TestSetup import com.google.api.services.testing.model.TestSpecification +import com.google.api.services.testing.model.ToolResultsHistory import com.google.api.services.toolresults.ToolResults +import com.google.api.services.toolresults.model.History import com.google.api.services.toolresults.model.StackTrace -import com.google.firebase.testlab.gradle.Orientation import com.google.firebase.testlab.gradle.TestLabGradlePluginExtension import com.google.testing.platform.proto.api.core.ErrorProto.Error import com.google.testing.platform.proto.api.core.IssueProto.Issue @@ -61,14 +68,16 @@ import com.google.testing.platform.proto.api.core.TestResultProto.TestResult import com.google.testing.platform.proto.api.core.TestStatusProto.TestStatus import com.google.testing.platform.proto.api.core.TestSuiteResultProto.TestSuiteMetaData import com.google.testing.platform.proto.api.core.TestSuiteResultProto.TestSuiteResult +import org.gradle.api.Project import org.gradle.api.file.RegularFileProperty import org.gradle.api.logging.Logging +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.provider.Provider import org.gradle.api.provider.ProviderFactory import org.gradle.api.services.BuildService import org.gradle.api.services.BuildServiceParameters -import org.gradle.api.services.BuildServiceRegistry import org.w3c.dom.Element import org.w3c.dom.Node import java.io.File @@ -77,6 +86,8 @@ import java.io.FileOutputStream import java.nio.charset.StandardCharsets import java.util.Locale import java.util.UUID +import java.util.logging.Level +import java.util.logging.Logger import javax.xml.parsers.DocumentBuilderFactory import javax.xml.transform.TransformerFactory import javax.xml.transform.dom.DOMSource @@ -87,12 +98,20 @@ import javax.xml.transform.stream.StreamResult */ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters> { + init { + Logger.getLogger("com.google.api.client").level = Level.WARNING + } + companion object { const val TEST_RESULT_PB_FILE_NAME = "test-result.pb" const val clientApplicationName: String = "Firebase TestLab Gradle Plugin" const val xGoogUserProjectHeaderKey: String = "X-Goog-User-Project" val cloudStorageUrlRegex = Regex("""gs://(.*?)/(.*)""") + const val INSTRUMENTATION_TEST_SHARD_FIELD = "shardingOption" + const val TEST_MATRIX_FLAKY_TEST_ATTEMPTS_FIELD = "flakyTestAttempts" + const val TEST_MATRIX_FAIL_FAST_FIELD = "failFast" + const val CHECK_TEST_STATE_WAIT_MS = 10 * 1000L; val oauthScope = listOf( @@ -133,6 +152,14 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters class FileReference: GenericJson() { @Key var fileUri: String? = null } + + class UniformSharding: GenericJson() { + @Key var numShards: Int? = null + } + + class ShardingOption: GenericJson() { + @Key var uniformSharding: UniformSharding? = null + } } private val logger = Logging.getLogger(this.javaClass) @@ -143,10 +170,24 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters interface Parameters : BuildServiceParameters { val quotaProjectName: Property<String> val credentialFile: RegularFileProperty + val cloudStorageBucket: Property<String> + val timeoutMinutes: Property<Int> + val maxTestReruns: Property<Int> + val failFast: Property<Boolean> + val numUniformShards: Property<Int> + val grantedPermissions: Property<String> + val extraDeviceFiles: MapProperty<String, String> + val networkProfile: Property<String> + val resultsHistoryName: Property<String> + val directoriesToPull: ListProperty<String> + val recordVideo: Property<Boolean> + val performanceMetrics: Property<Boolean> } - private val credential = parameters.credentialFile.get().asFile.inputStream().use { - GoogleCredential.fromStream(it).createScoped(oauthScope) + internal open val credential: GoogleCredential by lazy { + parameters.credentialFile.get().asFile.inputStream().use { + GoogleCredential.fromStream(it).createScoped(oauthScope) + } } private val httpRequestInitializer: HttpRequestInitializer = HttpRequestInitializer { request -> @@ -157,12 +198,17 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters private val jacksonFactory: JacksonFactory get() = JacksonFactory.getDefaultInstance() + internal open val httpTransport: HttpTransport + get() = GoogleNetHttpTransport.newTrustedTransport() + val numUniformShards: Int + get() = parameters.numUniformShards.getOrNull() ?: 0 + fun runTestsOnDevice( deviceName: String, deviceId: String, deviceApiLevel: Int, deviceLocale: Locale, - deviceOrientation: Orientation, + deviceOrientation: ManagedDeviceImpl.Orientation, ftlDeviceModel: AndroidModel, testData: StaticTestData, resultsOutDir: File, @@ -176,21 +222,23 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters } val projectName = parameters.quotaProjectName.get() - val requestId = UUID.randomUUID().toString() + val runRequestId = UUID.randomUUID().toString() val toolResultsClient = ToolResults.Builder( - GoogleNetHttpTransport.newTrustedTransport(), + httpTransport, jacksonFactory, httpRequestInitializer ).apply { applicationName = clientApplicationName }.build() - val defaultBucketName = toolResultsClient.projects().initializeSettings(projectName) - .execute().defaultBucket + val initSettingsResult = + toolResultsClient.projects().initializeSettings(projectName).execute() + val bucketName = parameters.cloudStorageBucket.orNull?.ifBlank { null } + ?: initSettingsResult.defaultBucket val storageClient = Storage.Builder( - GoogleNetHttpTransport.newTrustedTransport(), + httpTransport, jacksonFactory, httpRequestInitializer ).apply { @@ -198,7 +246,7 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters }.build() val testApkStorageObject = uploadToCloudStorage( - testData.testApk, requestId, storageClient, defaultBucketName + testData.testApk, runRequestId, storageClient, bucketName ) val configProvider = createConfigProvider( @@ -206,13 +254,13 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters ) val appApkStorageObject = uploadToCloudStorage( testData.testedApkFinder(configProvider).first(), - requestId, + runRequestId, storageClient, - defaultBucketName + bucketName ) val testingClient = Testing.Builder( - GoogleNetHttpTransport.newTrustedTransport(), + httpTransport, jacksonFactory, httpRequestInitializer ).apply { @@ -221,22 +269,61 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters val testMatricesClient = testingClient.projects().testMatrices() + val testHistoryName = parameters.resultsHistoryName.getOrElse("").ifBlank { + testData.testedApplicationId ?: testData.applicationId + } + val historyId = getOrCreateHistory(toolResultsClient, projectName, testHistoryName) + val testMatrix = TestMatrix().apply { projectId = projectName clientInfo = ClientInfo().apply { name = clientApplicationName } testSpecification = TestSpecification().apply { + testSetup = TestSetup().apply { + set("dontAutograntPermissions", parameters.grantedPermissions.orNull == + FixtureImpl.GrantedPermissions.NONE.name) + if(parameters.networkProfile.getOrElse("").isNotBlank()) { + networkProfile = parameters.networkProfile.get() + } + filesToPush = mutableListOf() + parameters.extraDeviceFiles.get().forEach { (onDevicePath, filePath) -> + val gcsFilePath = if (filePath.startsWith("gs://")) { + filePath + } else { + val file = File(filePath) + check(file.exists()) { "$filePath doesn't exist." } + check(file.isFile) { "$filePath must be file." } + val storageObject = uploadToCloudStorage( + file, runRequestId, storageClient, bucketName) + "gs://$bucketName/${storageObject.name}" + } + filesToPush.add(DeviceFile().apply { + regularFile = RegularFile().apply { + content = com.google.api.services.testing.model.FileReference().apply { + gcsPath = gcsFilePath + } + devicePath = onDevicePath + } + }) + } + + directoriesToPull = parameters.directoriesToPull.get() + } androidInstrumentationTest = AndroidInstrumentationTest().apply { testApk = com.google.api.services.testing.model.FileReference().apply { - gcsPath = "gs://$defaultBucketName/${testApkStorageObject.name}" + gcsPath = "gs://$bucketName/${testApkStorageObject.name}" } appApk = com.google.api.services.testing.model.FileReference().apply { - gcsPath = "gs://$defaultBucketName/${appApkStorageObject.name}" + gcsPath = "gs://$bucketName/${appApkStorageObject.name}" } appPackageId = testData.testedApplicationId testPackageId = testData.applicationId testRunnerClass = testData.instrumentationRunner + + createShardingOption()?.also { sharding -> + this.set(INSTRUMENTATION_TEST_SHARD_FIELD, sharding) + } } environmentMatrix = EnvironmentMatrix().apply { androidDeviceList = AndroidDeviceList().apply { @@ -252,17 +339,27 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters } resultStorage = ResultStorage().apply { googleCloudStorage = GoogleCloudStorage().apply { - gcsPath = "gs://$defaultBucketName/$requestId/results" + gcsPath = "gs://$bucketName/$runRequestId/results" + } + toolResultsHistory = ToolResultsHistory().apply { + projectId = projectName + this.historyId = historyId } } + testTimeout = "${parameters.timeoutMinutes.get() * 60}s" + disablePerformanceMetrics = !parameters.performanceMetrics.get() + disableVideoRecording = !parameters.recordVideo.get() } + set(TEST_MATRIX_FLAKY_TEST_ATTEMPTS_FIELD, parameters.maxTestReruns.get()) + set(TEST_MATRIX_FAIL_FAST_FIELD, parameters.failFast.get()) } val updatedTestMatrix = testMatricesClient.create(projectName, testMatrix).apply { - this.requestId = requestId + this.requestId = runRequestId }.execute() lateinit var resultTestMatrix: TestMatrix var previousTestMatrixState = "" + var printResultsUrl = true while (true) { val latestTestMatrix = testMatricesClient.get( projectName, updatedTestMatrix.testMatrixId).execute() @@ -270,6 +367,15 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters previousTestMatrixState = latestTestMatrix.state logger.lifecycle("Firebase TestLab Test execution state: $previousTestMatrixState") } + if (printResultsUrl) { + val resultsUrl = latestTestMatrix.resultStorage?.get("resultsUrl") as String? + if (!resultsUrl.isNullOrBlank()) { + logger.lifecycle( + "Test request for device $deviceName has been submitted to " + + "Firebase TestLab: $resultsUrl") + printResultsUrl = false + } + } val testFinished = when (latestTestMatrix.state) { "VALIDATING", "PENDING", "RUNNING" -> false else -> true @@ -296,7 +402,7 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters testExecution.toolResultsStep.stepId ).execute() executionStep.testExecutionStep.testSuiteOverviews?.forEach { suiteOverview -> - downloadFromCloudStorage(storageClient, suiteOverview.xmlSource.fileUri) { + downloadFromCloudStorage(storageClient, suiteOverview.xmlSource.fileUri, runRequestId) { File(resultsOutDir, "TEST-${it.replace("/", "_")}") }?.also { updateTestResultXmlFile(it, deviceName, projectPath, variantName) @@ -310,6 +416,7 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters deviceInfoFile, storageClient, resultsOutDir, + runRequestId, ) val testSuitePassed = testSuiteResult.testStatus.isPassedOrSkipped() @@ -334,9 +441,27 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters return ftlTestRunResults } + fun getOrCreateHistory( + toolResultsClient: ToolResults, + projectId: String, historyName: String): String { + val historyList = toolResultsClient.projects().histories().list(projectId).apply { + filterByName = historyName + }.execute() + historyList?.histories?.firstOrNull()?.historyId?.let { return it } + + return toolResultsClient.projects().histories().create( + projectId, + History().apply { + name = historyName + displayName = historyName + }).apply { + requestId = UUID.randomUUID().toString() + }.execute().historyId + } + fun catalog(): AndroidDeviceCatalog { val testingClient = Testing.Builder( - GoogleNetHttpTransport.newTrustedTransport(), + httpTransport, jacksonFactory, httpRequestInitializer, ).apply { @@ -350,6 +475,17 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters return catalog.androidDeviceCatalog } + private fun createShardingOption(): ShardingOption? { + if (numUniformShards == 0) { + return null + } + return ShardingOption().apply { + uniformSharding = UniformSharding().apply { + numShards = numUniformShards + } + } + } + private fun getTestSuiteResult( toolResultsClient: ToolResults, testMatrix: TestMatrix, @@ -357,6 +493,7 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters deviceInfoFile: File, storageClient: Storage, resultsOutDir: File, + runRequestId: String, ): TestSuiteResult { val testSuiteResult = TestSuiteResult.newBuilder() @@ -377,7 +514,7 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters testSuiteMetaData = TestSuiteMetaData.newBuilder().apply { testSuiteName = step.name var scheduledTestCount = 0 - for (testSuiteOverview in step.testExecutionStep.testSuiteOverviews) { + step.testExecutionStep.testSuiteOverviews?.forEach { testSuiteOverview -> scheduledTestCount += testSuiteOverview.totalCount } scheduledTestCaseCount = scheduledTestCount @@ -389,21 +526,25 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters "skipped" -> TestStatus.SKIPPED else -> TestStatus.TEST_STATUS_UNSPECIFIED } - addOutputArtifact( - Artifact.newBuilder().apply { - label = Label.newBuilder().apply { - label = "firebase.xmlSource" - namespace = "android" - }.build() - sourcePath = Path.newBuilder().apply { - path = step.testExecutionStep.testSuiteOverviews[0].xmlSource.fileUri - }.build() - type = ArtifactType.TEST_DATA - }.build() - ) + val testResultXmlFilePath = + step.testExecutionStep.testSuiteOverviews?.get(0)?.xmlSource?.fileUri.orEmpty() + if (testResultXmlFilePath.isNotBlank()) { + addOutputArtifact( + Artifact.newBuilder().apply { + label = Label.newBuilder().apply { + label = "firebase.xmlSource" + namespace = "android" + }.build() + sourcePath = Path.newBuilder().apply { + path = testResultXmlFilePath + }.build() + type = ArtifactType.TEST_DATA + }.build() + ) + } } - for (log in step.testExecutionStep.toolExecution.toolLogs) { + step.testExecutionStep.toolExecution?.toolLogs?.forEach { log -> testSuiteResult.apply { addOutputArtifact(Artifact.newBuilder().apply { label = Label.newBuilder().apply { @@ -418,7 +559,8 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters }.build()) } } - for (toolOutput in step.testExecutionStep.toolExecution.toolOutputs) { + + step.testExecutionStep.toolExecution?.toolOutputs?.forEach { toolOutput -> val outputArtifact = Artifact.newBuilder().apply { label = Label.newBuilder().apply { label = "firebase.toolOutput" @@ -462,7 +604,7 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters } } - for (testIssue in step.testExecutionStep.testIssues) { + step.testExecutionStep?.testIssues?.forEach { testIssue -> testSuiteResult.apply { addIssue(Issue.newBuilder().apply { message = testIssue.errorMessage @@ -483,10 +625,35 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters } } - // Need latest version of google-api-client to use + step.testExecutionStep?.toolExecution?.toolOutputs?.forEach { toolOutput -> + if (toolOutput?.output?.fileUri != null) { + val fileUri = requireNotNull(toolOutput.output.fileUri) + val download = parameters.directoriesToPull.get().any { directoriesToPull -> + fileUri.contains(directoriesToPull) + } + if (download) { + val downloadedFile = downloadFromCloudStorage( + storageClient, fileUri, runRequestId) { + File(resultsOutDir, it) + } ?: return@forEach + testSuiteResult.addOutputArtifactBuilder().apply { + labelBuilder.apply { + label = "firebase.toolOutput" + namespace = "android" + } + sourcePathBuilder.apply { + path = downloadedFile.path + }.build() + type = ArtifactType.TEST_DATA + } + } + } + } + + // Need the latest version of google-api-client to use // toolResultsClient.projects().histories().executions().steps().testCases().list(). // Manually calling this API until this is available. - val httpRequestFactory: HttpRequestFactory = GoogleNetHttpTransport.newTrustedTransport().createRequestFactory(httpRequestInitializer) + val httpRequestFactory: HttpRequestFactory = httpTransport.createRequestFactory(httpRequestInitializer) val url = "https://toolresults.googleapis.com/toolresults/v1beta3/projects/$projectId/histories/$historyId/executions/$executionId/steps/$stepId/testCases" val request = httpRequestFactory.buildGetRequest(GenericUrl(url)) val parser = JsonObjectParser(Utils.getDefaultJsonFactory()) @@ -497,7 +664,7 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters it, StandardCharsets.UTF_8, TestCases::class.java ) - for (case in testCaseContents["testCases"] as List<TestCase>) { + (testCaseContents["testCases"] as? List<TestCase>)?.forEach { case -> testSuiteResult.apply { addTestResult(TestResult.newBuilder().apply { testCase = TestCaseProto.TestCase.newBuilder().apply { @@ -536,7 +703,8 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters for (toolOutput in (case.toolOutputs as List<ToolOutputReference>)) { if (toolOutput.output!!.fileUri!!.endsWith("logcat")) { val logcatFile = downloadFromCloudStorage( - storageClient, toolOutput.output!!.fileUri!!) { + storageClient, toolOutput.output!!.fileUri!!, + runRequestId) { File(resultsOutDir, it) } if (logcatFile != null) { @@ -551,17 +719,6 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters type = ArtifactType.TEST_DATA } } - } else { - addOutputArtifactBuilder().apply { - labelBuilder.apply { - label = "firebase.toolOutput" - namespace = "android" - } - sourcePathBuilder.apply { - path = toolOutput.output!!.fileUri - }.build() - type = ArtifactType.TEST_DATA - } } } } @@ -691,10 +848,10 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters */ private fun downloadFromCloudStorage( storageClient: Storage, fileUri: String, - destination: (objectName: String) -> File): File? { + runId: String, destination: (objectName: String) -> File): File? { val matchResult = cloudStorageUrlRegex.find(fileUri) ?: return null val (bucketName, objectName) = matchResult.destructured - return destination(objectName).apply { + return destination(objectName.removePrefix("$runId/")).apply { parentFile.mkdirs() outputStream().use { storageClient.objects() @@ -800,13 +957,13 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters * Copied from * com.android.build.gradle.internal.services.BuildServicesKt.getBuildServiceName. */ - private fun getBuildServiceName(type: Class<*>): String { - return type.name + "_" + perClassLoaderConstant + private fun getBuildServiceName(type: Class<*>, project: Project): String { + return type.name + "_" + perClassLoaderConstant + "_" + project.path } - fun getBuildService(registry: BuildServiceRegistry): Provider<TestLabBuildService> { - val serviceName = getBuildServiceName(TestLabBuildService::class.java) - return registry.registerIfAbsent( + fun getBuildService(project: Project): Provider<TestLabBuildService> { + val serviceName = getBuildServiceName(TestLabBuildService::class.java, project) + return project.gradle.sharedServices.registerIfAbsent( serviceName, TestLabBuildService::class.java, ) { @@ -879,9 +1036,9 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters /** * Register [TestLabBuildService] to a registry if absent. */ - fun registerIfAbsent(registry: BuildServiceRegistry): Provider<TestLabBuildService> { - return registry.registerIfAbsent( - getBuildServiceName(TestLabBuildService::class.java), + fun registerIfAbsent(project: Project): Provider<TestLabBuildService> { + return project.gradle.sharedServices.registerIfAbsent( + getBuildServiceName(TestLabBuildService::class.java, project), TestLabBuildService::class.java, ) { buildServiceSpec -> configure(buildServiceSpec.parameters) @@ -899,6 +1056,39 @@ abstract class TestLabBuildService : BuildService<TestLabBuildService.Parameters params.quotaProjectName.set(params.credentialFile.map { getQuotaProjectName(it.asFile) }) + params.cloudStorageBucket.set(providerFactory.provider { + testLabExtension.testOptions.results.cloudStorageBucket + }) + + params.timeoutMinutes.set(providerFactory.provider { + testLabExtension.testOptions.execution.timeoutMinutes + }) + params.maxTestReruns.set(providerFactory.provider { + testLabExtension.testOptions.execution.maxTestReruns + }) + params.failFast.set(providerFactory.provider { + testLabExtension.testOptions.execution.failFast + }) + params.numUniformShards.set( providerFactory.provider { + testLabExtension.testOptions.execution.numUniformShards + }) + params.grantedPermissions.set(providerFactory.provider { + testLabExtension.testOptions.fixture.grantedPermissions + }) + params.extraDeviceFiles.set(testLabExtension.testOptions.fixture.extraDeviceFiles) + params.networkProfile.set(providerFactory.provider { + testLabExtension.testOptions.fixture.networkProfile + }) + params.resultsHistoryName.set(providerFactory.provider { + testLabExtension.testOptions.results.resultsHistoryName + }) + params.directoriesToPull.set(testLabExtension.testOptions.results.directoriesToPull) + params.recordVideo.set(providerFactory.provider { + testLabExtension.testOptions.results.recordVideo + }) + params.performanceMetrics.set(providerFactory.provider { + testLabExtension.testOptions.results.performanceMetrics + }) } } } |