1 /*
<lambda>null2  * 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.app.Instrumentation
20 import android.os.Bundle
21 import android.platform.test.util.TestFilter
22 import android.tools.FLICKER_TAG
23 import android.tools.Scenario
24 import android.tools.flicker.legacy.FlickerBuilder
25 import android.tools.flicker.legacy.runner.TransitionRunner
26 import android.tools.withTracing
27 import android.util.Log
28 import androidx.test.platform.app.InstrumentationRegistry
29 import com.google.common.annotations.VisibleForTesting
30 import java.util.Collections
31 import java.util.concurrent.locks.Lock
32 import java.util.concurrent.locks.ReentrantLock
33 import org.junit.FixMethodOrder
34 import org.junit.Ignore
35 import org.junit.internal.AssumptionViolatedException
36 import org.junit.internal.runners.model.EachTestNotifier
37 import org.junit.runner.Description
38 import org.junit.runner.manipulation.Filter
39 import org.junit.runner.manipulation.InvalidOrderingException
40 import org.junit.runner.manipulation.NoTestsRemainException
41 import org.junit.runner.manipulation.Orderable
42 import org.junit.runner.manipulation.Orderer
43 import org.junit.runner.manipulation.Sorter
44 import org.junit.runner.notification.RunNotifier
45 import org.junit.runner.notification.StoppedByUserException
46 import org.junit.runners.model.FrameworkMethod
47 import org.junit.runners.model.InvalidTestClassError
48 import org.junit.runners.model.RunnerScheduler
49 import org.junit.runners.model.Statement
50 import org.junit.runners.model.TestClass
51 import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters
52 import org.junit.runners.parameterized.TestWithParameters
53 
54 /**
55  * Implements the JUnit 4 standard test case class model, parsing from a flicker DSL.
56  *
57  * Supports both assertions in {@link org.junit.Test} and assertions defined in the DSL
58  *
59  * When using this runner the default `atest class#method` command doesn't work. Instead use: --
60  * --test-arg \
61  *
62  * ```
63  *     com.android.tradefed.testtype.AndroidJUnitTest:instrumentation-arg:filter-tests:=<TEST_NAME>
64  * ```
65  *
66  * For example: `atest FlickerTests -- \
67  *
68  * ```
69  *     --test-arg com.android.tradefed.testtype.AndroidJUnitTest:instrumentation-arg:filter-tests\
70  *     :=com.android.server.wm.flicker.close.\
71  *     CloseAppBackButtonTest#launcherWindowBecomesVisible[ROTATION_90_GESTURAL_NAV]`
72  * ```
73  */
74 class LegacyFlickerJUnit4ClassRunner(
75     test: TestWithParameters?,
76     private val scenario: Scenario?,
77     private val arguments: Bundle = InstrumentationRegistry.getArguments()
78 ) : BlockJUnit4ClassRunnerWithParameters(test), IFlickerJUnitDecorator {
79     private val onlyBlocking
80         get() =
81             scenario?.getConfigValue<Boolean>(Scenario.FAAS_BLOCKING)
82                 ?: arguments.getString(Scenario.FAAS_BLOCKING)?.toBoolean() ?: true
83 
84     @VisibleForTesting
85     val transitionRunner =
86         object : ITransitionRunner {
87             private val instrumentation: Instrumentation =
88                 InstrumentationRegistry.getInstrumentation()
89 
90             override fun runTransition(scenario: Scenario, test: Any, description: Description?) {
91                 withTracing("LegacyFlickerJUnit4ClassRunner#runTransition") {
92                     Log.v(FLICKER_TAG, "Creating flicker object for $scenario")
93                     val builder = getFlickerBuilder(test)
94                     Log.v(FLICKER_TAG, "Creating flicker object for $scenario")
95                     val flicker = builder.build()
96                     val runner = TransitionRunner(scenario, instrumentation)
97                     Log.v(FLICKER_TAG, "Running transition for $scenario")
98                     runner.execute(flicker, description)
99                 }
100             }
101 
102             private val providerMethod: FrameworkMethod
103                 get() =
104                     getCandidateProviderMethods(testClass).firstOrNull()
105                         ?: error("Provider method not found")
106 
107             private fun getFlickerBuilder(test: Any): FlickerBuilder {
108                 Log.v(FLICKER_TAG, "Obtaining flicker builder for $testClass")
109                 return providerMethod.invokeExplosively(test) as FlickerBuilder
110             }
111         }
112 
113     private fun getCandidateProviderMethods(testClass: TestClass): List<FrameworkMethod> =
114         testClass.getAnnotatedMethods(FlickerBuilderProvider::class.java) ?: emptyList()
115 
116     @VisibleForTesting
117     val flickerDecorator: LegacyFlickerServiceDecorator =
118         LegacyFlickerServiceDecorator(
119             this.testClass,
120             scenario,
121             transitionRunner,
122             arguments = arguments,
123             skipNonBlocking = onlyBlocking,
124             inner = LegacyFlickerDecorator(this.testClass, scenario, transitionRunner, inner = this)
125         )
126 
127     init {
128         val errors = mutableListOf<Throwable>()
129         flickerDecorator.doValidateInstanceMethods().let { errors.addAll(it) }
130         flickerDecorator.doValidateConstructor().let { errors.addAll(it) }
131 
132         if (errors.isNotEmpty()) {
133             throw InvalidTestClassError(testClass.javaClass, errors)
134         }
135     }
136 
137     override fun run(notifier: RunNotifier) {
138         val testNotifier = EachTestNotifier(notifier, description)
139         testNotifier.fireTestSuiteStarted()
140         try {
141             val statement = classBlock(notifier)
142             statement.evaluate()
143         } catch (e: AssumptionViolatedException) {
144             testNotifier.addFailedAssumption(e)
145         } catch (e: StoppedByUserException) {
146             throw e
147         } catch (e: Throwable) {
148             testNotifier.addFailure(e)
149         } finally {
150             testNotifier.fireTestSuiteFinished()
151         }
152     }
153 
154     /**
155      * Implementation of Filterable and Sortable Based on JUnit's ParentRunner implementation but
156      * with a minor modification to ensure injected FaaS tests are not filtered out.
157      */
158     @Throws(NoTestsRemainException::class)
159     override fun filter(filter: Filter) {
160         childrenLock.lock()
161         try {
162             val children: MutableList<FrameworkMethod> = getFilteredChildren().toMutableList()
163             val iter: MutableIterator<FrameworkMethod> = children.iterator()
164             while (iter.hasNext()) {
165                 val each: FrameworkMethod = iter.next()
166                 if (isInjectedFaasTest(each)) {
167                     // Don't filter out injected FaaS tests
168                     continue
169                 }
170                 if (shouldRun(filter, each)) {
171                     try {
172                         filter.apply(each)
173                     } catch (e: NoTestsRemainException) {
174                         iter.remove()
175                     }
176                 } else {
177                     iter.remove()
178                 }
179             }
180             filteredChildren = Collections.unmodifiableList(children)
181             val filteredChildren = filteredChildren
182             if (filteredChildren!!.isEmpty()) {
183                 throw NoTestsRemainException()
184             }
185         } finally {
186             childrenLock.unlock()
187         }
188     }
189 
190     private fun isInjectedFaasTest(method: FrameworkMethod): Boolean {
191         return method is FlickerServiceCachedTestCase
192     }
193 
194     override fun isIgnored(child: FrameworkMethod): Boolean {
195         return child.getAnnotation(Ignore::class.java) != null
196     }
197 
198     /**
199      * Returns the methods that run tests. Is ran after validateInstanceMethods, so
200      * flickerBuilderProviderMethod should be set.
201      */
202     public override fun computeTestMethods(): List<FrameworkMethod> {
203         val result = mutableListOf<FrameworkMethod>()
204         if (scenario != null) {
205             val testInstance = createTest()
206             result.addAll(flickerDecorator.getTestMethods(testInstance))
207         } else {
208             result.addAll(getTestMethods({} /* placeholder param */))
209         }
210         return result
211     }
212 
213     override fun describeChild(method: FrameworkMethod): Description {
214         return if (scenario != null) {
215             flickerDecorator.getChildDescription(method)
216         } else {
217             getChildDescription(method)
218         }
219     }
220 
221     /** {@inheritDoc} */
222     override fun getChildren(): MutableList<FrameworkMethod> {
223         val validChildren =
224             super.getChildren().filter {
225                 val childDescription = describeChild(it)
226                 TestFilter.isFilteredOrUnspecified(arguments, childDescription)
227             }
228         return validChildren.toMutableList()
229     }
230 
231     override fun methodInvoker(method: FrameworkMethod, test: Any): Statement {
232         return flickerDecorator.getMethodInvoker(method, test)
233     }
234 
235     /** IFlickerJunitDecorator implementation */
236     override fun getTestMethods(test: Any): List<FrameworkMethod> {
237         val tests = mutableListOf<FrameworkMethod>()
238         tests.addAll(super.computeTestMethods())
239         return tests
240     }
241 
242     override fun doValidateInstanceMethods(): List<Throwable> {
243         // NOOP - handled in init of this class otherwise called on initialization of parent class
244         // which means this class is not initialized yet leading to issues
245         val errors = emptyList<Throwable>()
246         super.validateInstanceMethods(errors)
247         return errors
248     }
249 
250     override fun doValidateConstructor(): List<Throwable> {
251         // NOOP - handled in init of this class otherwise called on initialization of parent class
252         // which means this class is not initialized yet leading to issues
253         val errors = emptyList<Throwable>()
254         super.validateInstanceMethods(errors)
255         return errors
256     }
257 
258     override fun getChildDescription(method: FrameworkMethod): Description {
259         return super.describeChild(method)
260     }
261 
262     override fun getMethodInvoker(method: FrameworkMethod, test: Any): Statement {
263         return super.methodInvoker(method, test)
264     }
265 
266     override fun shouldRunBeforeOn(method: FrameworkMethod): Boolean = true
267 
268     override fun shouldRunAfterOn(method: FrameworkMethod): Boolean = true
269 
270     /**
271      * ********************************************************************************************
272      * START of code copied from ParentRunner to have local access to filteredChildren to ensure
273      * FaaS injected tests are not filtered out.
274      */
275 
276     // Guarded by childrenLock
277     @Volatile private var filteredChildren: List<FrameworkMethod>? = null
278     private val childrenLock: Lock = ReentrantLock()
279 
280     @Volatile
281     private var scheduler: RunnerScheduler =
282         object : RunnerScheduler {
283             override fun schedule(childStatement: Runnable) {
284                 childStatement.run()
285             }
286 
287             override fun finished() {
288                 // do nothing
289             }
290         }
291 
292     /**
293      * Sets a scheduler that determines the order and parallelization of children. Highly
294      * experimental feature that may change.
295      */
296     override fun setScheduler(scheduler: RunnerScheduler) {
297         this.scheduler = scheduler
298     }
299 
300     private fun shouldRun(filter: Filter, each: FrameworkMethod): Boolean {
301         return filter.shouldRun(describeChild(each))
302     }
303 
304     override fun sort(sorter: Sorter) {
305         if (shouldNotReorder()) {
306             return
307         }
308         childrenLock.lock()
309         filteredChildren =
310             try {
311                 for (each in getFilteredChildren()) {
312                     sorter.apply(each)
313                 }
314                 val sortedChildren: List<FrameworkMethod> =
315                     ArrayList<FrameworkMethod>(getFilteredChildren())
316                 Collections.sort(sortedChildren, comparator(sorter))
317                 Collections.unmodifiableList(sortedChildren)
318             } finally {
319                 childrenLock.unlock()
320             }
321     }
322 
323     /**
324      * Implementation of [Orderable.order].
325      *
326      * @since 4.13
327      */
328     @Throws(InvalidOrderingException::class)
329     override fun order(orderer: Orderer) {
330         if (shouldNotReorder()) {
331             return
332         }
333         childrenLock.lock()
334         try {
335             var children: List<FrameworkMethod> = getFilteredChildren()
336             // In theory, we could have duplicate Descriptions. De-dup them before ordering,
337             // and add them back at the end.
338             val childMap: MutableMap<Description, MutableList<FrameworkMethod>> =
339                 LinkedHashMap(children.size)
340             for (child in children) {
341                 val description = describeChild(child)
342                 var childrenWithDescription: MutableList<FrameworkMethod>? = childMap[description]
343                 if (childrenWithDescription == null) {
344                     childrenWithDescription = ArrayList<FrameworkMethod>(1)
345                     childMap[description] = childrenWithDescription
346                 }
347                 childrenWithDescription.add(child)
348                 orderer.apply(child)
349             }
350             val inOrder = orderer.order(childMap.keys)
351             children = ArrayList(children.size)
352             for (description in inOrder) {
353                 children.addAll(childMap[description]!!)
354             }
355             filteredChildren = Collections.unmodifiableList(children)
356         } finally {
357             childrenLock.unlock()
358         }
359     }
360 
361     private fun shouldNotReorder(): Boolean {
362         // If the test specifies a specific order, do not reorder.
363         return description.getAnnotation(FixMethodOrder::class.java) != null
364     }
365 
366     private fun getFilteredChildren(): List<FrameworkMethod> {
367         childrenLock.lock()
368         val filteredChildren =
369             try {
370                 if (filteredChildren != null) {
371                     filteredChildren!!
372                 } else {
373                     Collections.unmodifiableList(ArrayList<FrameworkMethod>(children))
374                 }
375             } finally {
376                 childrenLock.unlock()
377             }
378         return filteredChildren
379     }
380 
381     override fun getDescription(): Description {
382         val clazz = testClass.javaClass
383         // if subclass overrides `getName()` then we should use it
384         // to maintain backwards compatibility with JUnit 4.12
385         val description: Description =
386             if (clazz == null || clazz.name != name) {
387                 Description.createSuiteDescription(name, *runnerAnnotations)
388             } else {
389                 Description.createSuiteDescription(clazz, *runnerAnnotations)
390             }
391         for (child in getFilteredChildren()) {
392             description.addChild(describeChild(child))
393         }
394         return description
395     }
396 
397     /**
398      * Returns a [Statement]: Call [.runChild] on each object returned by [.getChildren] (subject to
399      * any imposed filter and sort)
400      */
401     override fun childrenInvoker(notifier: RunNotifier): Statement {
402         return object : Statement() {
403             override fun evaluate() {
404                 runChildren(notifier)
405             }
406         }
407     }
408 
409     private fun runChildren(notifier: RunNotifier) {
410         val currentScheduler = scheduler
411         try {
412             for (each in getFilteredChildren()) {
413                 currentScheduler.schedule { this.runChild(each, notifier) }
414             }
415         } finally {
416             currentScheduler.finished()
417         }
418     }
419 
420     private fun comparator(sorter: Sorter): Comparator<in FrameworkMethod> {
421         return Comparator { o1, o2 -> sorter.compare(describeChild(o1), describeChild(o2)) }
422     }
423 
424     /**
425      * END of code copied from ParentRunner to have local access to filteredChildren to ensure FaaS
426      * injected tests are not filtered out.
427      */
428 }
429