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