blob: 534706ea8b499bbc5dad06a4afb299dc772335c2 [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.tools.device.flicker.junit
import android.app.Instrumentation
import android.device.collectors.util.SendToInstrumentation
import android.os.Bundle
import android.platform.test.rule.ArtifactSaver
import android.tools.common.Scenario
import android.tools.common.ScenarioBuilder
import android.tools.common.flicker.FlickerService
import android.tools.common.flicker.ScenarioInstance
import android.tools.common.flicker.annotation.ExpectedScenarios
import android.tools.common.flicker.assertions.ScenarioAssertion
import android.tools.common.flicker.config.FlickerConfig
import android.tools.common.flicker.config.ScenarioId
import android.tools.common.io.Reader
import android.tools.device.flicker.FlickerServiceResultsCollector.Companion.FLICKER_ASSERTIONS_COUNT_KEY
import android.tools.device.flicker.Utils.captureTrace
import android.tools.device.flicker.datastore.DataStore
import android.tools.device.traces.getDefaultFlickerOutputDir
import android.tools.device.traces.now
import com.google.common.truth.Truth
import java.lang.reflect.Method
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.Description
import org.junit.runners.model.FrameworkMethod
import org.junit.runners.model.Statement
import org.junit.runners.model.TestClass
class FlickerServiceDecorator(
testClass: TestClass,
val paramString: String?,
inner: IFlickerJUnitDecorator?
) : AbstractFlickerRunnerDecorator(testClass, inner) {
private val flickerService by lazy { FlickerService(getFlickerConfig()) }
private val testClassName =
ScenarioBuilder().forClass("${testClass.name}${paramString ?: ""}").build()
override fun getChildDescription(method: FrameworkMethod): Description {
return if (isMethodHandledByDecorator(method)) {
Description.createTestDescription(testClass.javaClass, method.name, *method.annotations)
} else {
inner?.getChildDescription(method) ?: error("No child descriptor found")
}
}
private val flickerServiceMethodsFor =
mutableMapOf<FrameworkMethod, Collection<InjectedTestCase>>()
private val innerMethodsResults = mutableMapOf<FrameworkMethod, Throwable?>()
override fun getTestMethods(test: Any): List<FrameworkMethod> {
val innerMethods =
inner?.getTestMethods(test)
?: error("FlickerServiceDecorator requires a non-null inner decorator")
val testMethods = innerMethods.toMutableList()
if (shouldComputeTestMethods()) {
for (method in innerMethods) {
if (!innerMethodsResults.containsKey(method)) {
var methodResult: Throwable? =
null // TODO: Maybe don't use null but wrap in another object
val reader =
captureTrace(testClassName, getDefaultFlickerOutputDir()) { writer ->
try {
val befores = testClass.getAnnotatedMethods(Before::class.java)
befores.forEach { it.invokeExplosively(test) }
writer.setTransitionStartTime(now())
method.invokeExplosively(test)
writer.setTransitionEndTime(now())
val afters = testClass.getAnnotatedMethods(After::class.java)
afters.forEach { it.invokeExplosively(test) }
} catch (e: Throwable) {
methodResult = e
} finally {
innerMethodsResults[method] = methodResult
}
}
if (methodResult == null) {
flickerServiceMethodsFor[method] =
computeFlickerServiceTests(reader, testClassName, method)
}
}
if (innerMethodsResults[method] == null) {
testMethods.addAll(flickerServiceMethodsFor[method]!!)
}
}
}
return testMethods
}
// TODO: Common with LegacyFlickerServiceDecorator, might be worth extracting this up
private fun shouldComputeTestMethods(): Boolean {
// Don't compute when called from validateInstanceMethods since this will fail
// as the parameters will not be set. And AndroidLogOnlyBuilder is a non-executing runner
// used to run tests in dry-run mode, so we don't want to execute in flicker transition in
// that case either.
val stackTrace = Thread.currentThread().stackTrace
val isDryRun =
stackTrace.any { it.methodName == "validateInstanceMethods" } ||
stackTrace.any {
it.className == "androidx.test.internal.runner.AndroidLogOnlyBuilder"
} ||
stackTrace.any {
it.className == "androidx.test.internal.runner.NonExecutingRunner"
}
return !isDryRun
}
override fun getMethodInvoker(method: FrameworkMethod, test: Any): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
val description = getChildDescription(method) ?: error("Missing description")
if (isMethodHandledByDecorator(method)) {
(method as InjectedTestCase).execute(description)
} else {
if (innerMethodsResults.containsKey(method)) {
innerMethodsResults[method]?.let {
ArtifactSaver.onError(description, it)
throw it
}
} else {
inner?.getMethodInvoker(method, test)?.evaluate()
}
}
}
}
}
override fun doValidateInstanceMethods(): List<Throwable> {
val errors = super.doValidateInstanceMethods().toMutableList()
val testMethods = testClass.getAnnotatedMethods(Test::class.java)
if (testMethods.size > 1) {
errors.add(IllegalArgumentException("Only one @Test annotated method is supported"))
}
// Validate Registry provider
val flickerConfigProviderProviderFunctions =
testClass
.getAnnotatedMethods(
android.tools.common.flicker.annotation.FlickerConfigProvider::class.java
)
.filter { it.isStatic && it.isPublic }
if (flickerConfigProviderProviderFunctions.isEmpty()) {
errors.add(
IllegalArgumentException(
"A public static function returning a " +
"${FlickerConfig::class.simpleName} annotated with " +
"@${android.tools.common.flicker.annotation
.FlickerConfigProvider::class.simpleName} should be provided."
)
)
} else if (flickerConfigProviderProviderFunctions.size > 1) {
errors.add(
IllegalArgumentException(
"Only one @${android.tools.common.flicker.annotation
.FlickerConfigProvider::class.simpleName} " +
"annotated method is supported."
)
)
} else if (
flickerConfigProviderProviderFunctions[0].returnType.name !=
FlickerConfig::class.qualifiedName
) {
errors.add(
IllegalArgumentException(
"Expected method annotated with " +
"@${FlickerConfig::class.simpleName} to return " +
"${FlickerConfig::class.qualifiedName} but was " +
"${flickerConfigProviderProviderFunctions[0].returnType.name} instead."
)
)
} else {
// Validate @ExpectedScenarios annotation
val expectedScenarioAnnotations =
testClass.getAnnotatedMethods(ExpectedScenarios::class.java).map {
it.getAnnotation(ExpectedScenarios::class.java)
}
val registeredScenarios = getFlickerConfig().getEntries().map { it.scenarioId.name }
for (expectedScenarioAnnotation in expectedScenarioAnnotations) {
for (expectedScenario in expectedScenarioAnnotation.expectedScenarios) {
val scenarioRegistered = registeredScenarios.contains(expectedScenario)
if (!scenarioRegistered) {
errors.add(
IllegalArgumentException(
"Provided scenarios that are not registered to " +
"@${ExpectedScenarios::class.simpleName} annotation. " +
"$expectedScenario is not registered in the " +
"${FlickerConfig::class.simpleName}. Available scenarios " +
"are [${registeredScenarios.joinToString()}]."
)
)
}
}
}
}
return errors
}
private fun getFlickerConfig(): FlickerConfig {
require(testClass.getAnnotatedMethods(ExpectedScenarios::class.java).size == 1) {
"@ExpectedScenarios missing. " +
"getFlickerConfig() maybe to have been called before validation."
}
val flickerConfigProviderProviderFunction =
testClass
.getAnnotatedMethods(
android.tools.common.flicker.annotation.FlickerConfigProvider::class.java
)[0]
// TODO: Pass the correct target
return flickerConfigProviderProviderFunction.invokeExplosively(testClass) as FlickerConfig
}
override fun shouldRunBeforeOn(method: FrameworkMethod): Boolean {
return false
}
override fun shouldRunAfterOn(method: FrameworkMethod): Boolean {
return false
}
private fun isMethodHandledByDecorator(method: FrameworkMethod): Boolean {
return method is InjectedTestCase && method.injectedBy == this
}
private fun computeFlickerServiceTests(
reader: Reader,
testScenario: Scenario,
method: FrameworkMethod
): Collection<InjectedTestCase> {
val expectedScenarios =
(method.annotations
.filterIsInstance<ExpectedScenarios>()
.firstOrNull()
?.expectedScenarios
?: emptyArray())
.map { ScenarioId(it) }
.toSet()
return getFaasTestCases(
testScenario,
expectedScenarios,
paramString ?: "",
reader,
flickerService,
instrumentation,
this
)
}
companion object {
private fun getDetectedScenarios(
testScenario: Scenario,
reader: Reader,
flickerService: FlickerService
): Collection<ScenarioId> {
val groupedAssertions = getGroupedAssertions(testScenario, reader, flickerService)
return groupedAssertions.keys.map { it.type }.distinct()
}
private fun getCachedResultMethod(): Method {
return InjectedTestCase::class.java.getMethod("execute", Description::class.java)
}
private fun getGroupedAssertions(
testScenario: Scenario,
reader: Reader,
flickerService: FlickerService,
): Map<ScenarioInstance, Collection<ScenarioAssertion>> {
if (!DataStore.containsFlickerServiceResult(testScenario)) {
val detectedScenarios = flickerService.detectScenarios(reader)
val groupedAssertions = detectedScenarios.associateWith { it.generateAssertions() }
DataStore.addFlickerServiceAssertions(testScenario, groupedAssertions)
}
return DataStore.getFlickerServiceAssertions(testScenario)
}
internal fun getFaasTestCases(
testScenario: Scenario,
expectedScenarios: Set<ScenarioId>,
paramString: String,
reader: Reader,
flickerService: FlickerService,
instrumentation: Instrumentation,
caller: IFlickerJUnitDecorator,
): Collection<InjectedTestCase> {
val groupedAssertions = getGroupedAssertions(testScenario, reader, flickerService)
val organizedScenarioInstances = groupedAssertions.keys.groupBy { it.type }
val faasTestCases = mutableListOf<FlickerServiceCachedTestCase>()
organizedScenarioInstances.values.forEachIndexed {
scenarioTypesIndex,
scenarioInstancesOfSameType ->
scenarioInstancesOfSameType.forEachIndexed { scenarioInstanceIndex, scenarioInstance
->
val assertionsForScenarioInstance = groupedAssertions[scenarioInstance]!!
assertionsForScenarioInstance.forEach {
faasTestCases.add(
FlickerServiceCachedTestCase(
assertion = it,
method = getCachedResultMethod(),
onlyBlocking = false,
isLast =
organizedScenarioInstances.values.size == scenarioTypesIndex &&
scenarioInstancesOfSameType.size == scenarioInstanceIndex,
injectedBy = caller,
paramString =
"${paramString}${
if (scenarioInstancesOfSameType.size > 1)
"_${scenarioInstanceIndex + 1}"
else
""}",
instrumentation = instrumentation,
)
)
}
}
}
val detectedScenarioTestCase =
AnonymousInjectedTestCase(
getCachedResultMethod(),
"FaaS_DetectedExpectedScenarios$paramString",
injectedBy = caller
) {
val metricBundle = Bundle()
metricBundle.putString(FLICKER_ASSERTIONS_COUNT_KEY, "${faasTestCases.size}")
SendToInstrumentation.sendBundle(instrumentation, metricBundle)
Truth.assertThat(getDetectedScenarios(testScenario, reader, flickerService))
.containsAtLeastElementsIn(expectedScenarios)
}
return faasTestCases + listOf(detectedScenarioTestCase)
}
}
}