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