1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.tools.flicker.junit 18 19 import android.os.Bundle 20 import android.tools.FLICKER_TAG 21 import android.tools.Scenario 22 import android.tools.flicker.FlickerConfig 23 import android.tools.flicker.FlickerService 24 import android.tools.flicker.FlickerServiceResultsCollector.Companion.FAAS_METRICS_PREFIX 25 import android.tools.flicker.IS_FAAS_ENABLED 26 import android.tools.flicker.annotation.FlickerConfigProvider 27 import android.tools.flicker.annotation.FlickerServiceCompatible 28 import android.tools.flicker.config.FlickerConfig 29 import android.tools.flicker.config.FlickerServiceConfig 30 import android.tools.flicker.config.ScenarioId 31 import android.tools.flicker.isShellTransitionsEnabled 32 import android.tools.traces.TRACE_CONFIG_REQUIRE_CHANGES 33 import android.tools.withTracing 34 import android.util.Log 35 import org.junit.runner.Description 36 import org.junit.runners.model.FrameworkMethod 37 import org.junit.runners.model.Statement 38 import org.junit.runners.model.TestClass 39 40 class LegacyFlickerServiceDecorator( 41 testClass: TestClass, 42 val scenario: Scenario?, 43 private val transitionRunner: ITransitionRunner, 44 private val skipNonBlocking: Boolean, 45 private val arguments: Bundle, 46 inner: IFlickerJUnitDecorator? 47 ) : AbstractFlickerRunnerDecorator(testClass, inner) { <lambda>null48 private val flickerService by lazy { FlickerService(getFlickerConfig()) } 49 getFlickerConfignull50 private fun getFlickerConfig(): FlickerConfig { 51 val annotatedMethods = testClass.getAnnotatedMethods(FlickerConfigProvider::class.java) 52 if (annotatedMethods.size == 0) { 53 return FlickerConfig().use(FlickerServiceConfig.DEFAULT) 54 } 55 56 val flickerConfigProviderProviderFunction = annotatedMethods.first() 57 return flickerConfigProviderProviderFunction.invokeExplosively(testClass) as FlickerConfig 58 } 59 60 private val isClassFlickerServiceCompatible: Boolean 61 get() = 62 testClass.annotations.filterIsInstance<FlickerServiceCompatible>().firstOrNull() != null 63 getChildDescriptionnull64 override fun getChildDescription(method: FrameworkMethod): Description { 65 requireNotNull(scenario) { "Expected to have a scenario to run" } 66 return if (isMethodHandledByDecorator(method)) { 67 Description.createTestDescription( 68 testClass.javaClass, 69 "${method.name}[${scenario.description}]", 70 *method.annotations 71 ) 72 } else { 73 inner?.getChildDescription(method) ?: error("Descriptor not found") 74 } 75 } 76 getTestMethodsnull77 override fun getTestMethods(test: Any): List<FrameworkMethod> { 78 val result = inner?.getTestMethods(test)?.toMutableList() ?: mutableListOf() 79 if (shouldComputeTestMethods()) { 80 withTracing("$FAAS_METRICS_PREFIX getTestMethods ${testClass.javaClass.simpleName}") { 81 requireNotNull(scenario) { "Expected to have a scenario to run" } 82 result.addAll(computeFlickerServiceTests(test, scenario)) 83 Log.d(FLICKER_TAG, "Computed ${result.size} flicker tests") 84 } 85 } 86 Log.d(LOG_TAG, "Computed ${result.size} methods") 87 result.forEach { Log.v(LOG_TAG, "Computed method - $it") } 88 return result 89 } 90 getMethodInvokernull91 override fun getMethodInvoker(method: FrameworkMethod, test: Any): Statement { 92 return object : Statement() { 93 @Throws(Throwable::class) 94 override fun evaluate() { 95 if (isMethodHandledByDecorator(method)) { 96 val description = getChildDescription(method) 97 (method as InjectedTestCase).execute(description) 98 } else { 99 inner?.getMethodInvoker(method, test)?.evaluate() 100 } 101 } 102 } 103 } 104 isMethodHandledByDecoratornull105 private fun isMethodHandledByDecorator(method: FrameworkMethod): Boolean { 106 return method is InjectedTestCase && method.injectedBy == this 107 } 108 shouldComputeTestMethodsnull109 private fun shouldComputeTestMethods(): Boolean { 110 // Don't compute when called from validateInstanceMethods since this will fail 111 // as the parameters will not be set. And AndroidLogOnlyBuilder is a non-executing runner 112 // used to run tests in dry-run mode, so we don't want to execute in flicker transition in 113 // that case either. 114 val stackTrace = Thread.currentThread().stackTrace 115 val isDryRun = 116 stackTrace.any { it.methodName == "validateInstanceMethods" } || 117 stackTrace.any { 118 it.className == "androidx.test.internal.runner.AndroidLogOnlyBuilder" 119 } || 120 stackTrace.any { 121 it.className == "androidx.test.internal.runner.NonExecutingRunner" 122 } 123 124 val filters = getFiltersFromArguments() 125 // a method is filtered out if there's a filter and the filter doesn't include it's class 126 // or if the filter includes its class, but it's not flicker as a service 127 val isFilteredOut = 128 filters.isNotEmpty() && !(filters[testClass.javaClass.simpleName] ?: false) 129 130 return IS_FAAS_ENABLED && 131 isShellTransitionsEnabled && 132 isClassFlickerServiceCompatible && 133 !isFilteredOut && 134 !isDryRun 135 } 136 getFiltersFromArgumentsnull137 private fun getFiltersFromArguments(): Map<String, Boolean> { 138 val testFilters = arguments.getString(OPTION_NAME) ?: return emptyMap() 139 val result = mutableMapOf<String, Boolean>() 140 141 // Test the display name against all filter arguments. 142 for (testFilter in testFilters.split(",")) { 143 val filterComponents = testFilter.split("#") 144 if (filterComponents.size != 2) { 145 Log.e( 146 LOG_TAG, 147 "Invalid filter-tests instrumentation argument supplied, $testFilter." 148 ) 149 continue 150 } 151 val methodName = filterComponents.drop(1).first() 152 val className = filterComponents.first() 153 result[className] = methodName.startsWith(FAAS_METRICS_PREFIX) 154 } 155 156 return result 157 } 158 159 /** 160 * Runs the flicker transition to collect the traces and run FaaS on them to get the FaaS 161 * results and then create functional test results for each of them. 162 */ computeFlickerServiceTestsnull163 private fun computeFlickerServiceTests( 164 test: Any, 165 testScenario: Scenario 166 ): Collection<InjectedTestCase> { 167 if (!android.tools.flicker.datastore.DataStore.containsResult(testScenario)) { 168 val description = 169 Description.createTestDescription( 170 this::class.java.simpleName, 171 "computeFlickerServiceTests" 172 ) 173 transitionRunner.runTransition(testScenario, test, description) 174 } 175 val reader = 176 android.tools.flicker.datastore.CachedResultReader( 177 testScenario, 178 TRACE_CONFIG_REQUIRE_CHANGES 179 ) 180 181 val expectedScenarios = 182 testClass.annotations 183 .filterIsInstance<FlickerServiceCompatible>() 184 .first() 185 .expectedCujs 186 .map { ScenarioId(it) } 187 .toSet() 188 189 return FlickerServiceDecorator.getFaasTestCases( 190 testScenario, 191 expectedScenarios, 192 "", 193 reader, 194 flickerService, 195 instrumentation, 196 this, 197 skipNonBlocking = skipNonBlocking, 198 ) 199 } 200 201 companion object { 202 private const val OPTION_NAME = "filter-tests" 203 private val LOG_TAG = LegacyFlickerServiceDecorator::class.java.simpleName 204 } 205 } 206