Create functional test results for FaaS
Allows us to have the results in ATH for triaging and tracking
Bug: 232515602
Test: Run CL and test using these changes through ABTD to check we get functional results for FaaS tests
Change-Id: I91904828cf9065d4f7544f342311f7e2c5dce21f
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt
index 594027d..88fe7fe 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt
@@ -19,21 +19,31 @@
import android.platform.test.util.TestFilter
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.annotation.FlickerServiceCompatible
import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
+import com.android.server.wm.flicker.service.FlickerFrameworkMethod
+import com.android.server.wm.flicker.service.FlickerTestCase
+import com.android.server.wm.flicker.service.assertors.AssertionResult
+import com.android.server.wm.flicker.service.config.AssertionInvocationGroup
+import java.lang.reflect.Modifier
+import org.junit.Test
import org.junit.internal.runners.statements.RunAfters
import org.junit.runner.notification.RunNotifier
+import org.junit.runners.Parameterized
+import org.junit.runners.model.FrameworkField
import org.junit.runners.model.FrameworkMethod
import org.junit.runners.model.Statement
+import org.junit.runners.model.TestClass
import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters
import org.junit.runners.parameterized.TestWithParameters
-import java.lang.reflect.Modifier
/**
* Implements the JUnit 4 standard test case class model, parsing from a flicker DSL.
*
* Supports both assertions in {@link org.junit.Test} and assertions defined in the DSL
*
- * When using this runnr the default `atest class#method` command doesn't work.
+ * When using this runner the default `atest class#method` command doesn't work.
* Instead use: -- --test-arg \
* com.android.tradefed.testtype.AndroidJUnitTest:instrumentation-arg:filter-tests:=<TEST_NAME>
*
@@ -47,9 +57,105 @@
test: TestWithParameters,
private val parameters: Array<Any> = test.parameters.toTypedArray(),
private val flickerTestParameter: FlickerTestParameter? =
- parameters.filterIsInstance(FlickerTestParameter::class.java).firstOrNull()
+ parameters.filterIsInstance<FlickerTestParameter>().firstOrNull()
) : BlockJUnit4ClassRunnerWithParameters(test) {
- private var flickerMethod: FrameworkMethod? = null
+ private var flickerBuilderProviderMethod: FrameworkMethod? = null
+
+ private val arguments = InstrumentationRegistry.getArguments()
+ // null parses to false (so defaults to running all FaaS tests)
+ private val isBlockingTest = arguments.getString("faas:blocking").toBoolean()
+
+ /**
+ * {@inheritDoc}
+ */
+ override fun validateInstanceMethods(errors: MutableList<Throwable>) {
+ validateFlickerObject(errors)
+ super.validateInstanceMethods(errors)
+ }
+
+ /**
+ * Returns the methods that run tests.
+ * Is ran after validateInstanceMethods, so flickerBuilderProviderMethod should be set.
+ */
+ override fun computeTestMethods(): List<FrameworkMethod> {
+ return computeTests()
+ }
+
+ private fun computeTests(): MutableList<FrameworkMethod> {
+ val tests = mutableListOf<FrameworkMethod>()
+ tests.addAll(super.computeTestMethods())
+
+ // 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
+ if (stackTrace.none { it.methodName == "validateInstanceMethods" } &&
+ stackTrace.none {
+ it.className == "androidx.test.internal.runner.AndroidLogOnlyBuilder"
+ }
+ ) {
+ require(flickerTestParameter != null) {
+ "Can't computeTests with null flickerTestParameter"
+ }
+
+ val hasFlickerServiceCompatibleAnnotation = TestClass(super.createTest()::class.java)
+ .annotations.filterIsInstance<FlickerServiceCompatible>().firstOrNull() != null
+
+ if (hasFlickerServiceCompatibleAnnotation && isShellTransitionsEnabled) {
+ if (!flickerTestParameter.isInitialized) {
+ Log.v(FLICKER_TAG, "Flicker object is not yet initialized")
+ val test = super.createTest()
+ injectFlickerOnTestParams(test)
+ }
+
+ tests.addAll(computeFlickerServiceTests(isBlockingTest))
+ }
+ }
+
+ return tests
+ }
+
+ /**
+ * Runs the flicker transition to collect the traces and run FaaS on them to get the FaaS
+ * results and then create functional test results for each of them.
+ */
+ private fun computeFlickerServiceTests(onlyBlockingAssertions: Boolean): List<FrameworkMethod> {
+ require(flickerTestParameter != null) {
+ "Can't computeFlickerServiceTests with null flickerTestParameter"
+ }
+
+ val flickerTestMethods = mutableListOf<FlickerFrameworkMethod>()
+
+ val flicker = flickerTestParameter.flicker
+ if (flicker.result == null) {
+ flicker.execute()
+ }
+
+ // TODO: Figure out how we can report this without aggregation to have more precise and
+ // granular data on the actual failure rate.
+ for (aggregatedResult in aggregateFaasResults(flicker.faas.assertionResults)
+ .entries.iterator()) {
+ val testName = aggregatedResult.key
+ var results = aggregatedResult.value
+ if (onlyBlockingAssertions) {
+ results = results.filter { it.invocationGroup == AssertionInvocationGroup.BLOCKING }
+ }
+ if (results.isEmpty()) {
+ continue
+ }
+
+ val injectedTestCase = FlickerTestCase(results)
+ val mockedTestMethod = TestClass(injectedTestCase.javaClass)
+ .getAnnotatedMethods(Test::class.java).first()
+ val mockedFrameworkMethod = FlickerFrameworkMethod(
+ mockedTestMethod.method, injectedTestCase, testName
+ )
+ flickerTestMethods.add(mockedFrameworkMethod)
+ }
+
+ return flickerTestMethods
+ }
/**
* {@inheritDoc}
@@ -87,14 +193,18 @@
val method = methods.first()
if (Modifier.isStatic(method.method.modifiers)) {
- errors.add(Exception("Method ${method.name}() show not be static"))
+ errors.add(Exception("Method ${method.name}() should not be static"))
}
if (!Modifier.isPublic(method.method.modifiers)) {
errors.add(Exception("Method ${method.name}() should be public"))
}
if (method.returnType != FlickerBuilder::class.java) {
- errors.add(Exception("Method ${method.name}() should return a " +
- "${FlickerBuilder::class.java.simpleName} object"))
+ errors.add(
+ Exception(
+ "Method ${method.name}() should return a " +
+ "${FlickerBuilder::class.java.simpleName} object"
+ )
+ )
}
if (method.method.parameterTypes.isNotEmpty()) {
errors.add(Exception("Method ${method.name} should have no parameters"))
@@ -102,7 +212,7 @@
}
if (errors.isEmpty()) {
- flickerMethod = methods.first()
+ flickerBuilderProviderMethod = methods.first()
}
}
@@ -115,6 +225,8 @@
Log.v(FLICKER_TAG, "Flicker object is not yet initialized")
injectFlickerOnTestParams(test)
}
+
+ val flicker = flickerTestParameter?.flicker
return test
}
@@ -123,30 +235,36 @@
*/
private fun injectFlickerOnTestParams(test: Any) {
val flickerTestParameter = flickerTestParameter
- val flickerMethod = flickerMethod
- if (flickerTestParameter != null && flickerMethod != null) {
- val testName = test::class.java.simpleName
- Log.v(FLICKER_TAG, "Creating flicker object for $testName and adding it into " +
- "test parameter")
- val builder = flickerMethod.invokeExplosively(test) as FlickerBuilder
+ val flickerBuilderProviderMethod = flickerBuilderProviderMethod
+ if (flickerTestParameter != null && flickerBuilderProviderMethod != null) {
+ val testClass = test::class.java
+ val testName = testClass.simpleName
+ Log.v(
+ FLICKER_TAG,
+ "Creating flicker object for $testName and adding it into " +
+ "test parameter"
+ )
+
+ val isFlickerServiceCompatible = TestClass(testClass).annotations
+ .filterIsInstance<FlickerServiceCompatible>().firstOrNull() != null
+ if (isFlickerServiceCompatible) {
+ flickerTestParameter.enableFaas()
+ }
+
+ val builder = flickerBuilderProviderMethod.invokeExplosively(test) as FlickerBuilder
flickerTestParameter.initialize(builder, testName)
} else {
- Log.v(FLICKER_TAG, "Missing flicker builder provider method " +
- "in ${test::class.java.simpleName}")
+ Log.v(
+ FLICKER_TAG,
+ "Missing flicker builder provider method " +
+ "in ${test::class.java.simpleName}"
+ )
}
}
/**
* {@inheritDoc}
*/
- override fun validateInstanceMethods(errors: MutableList<Throwable>) {
- validateFlickerObject(errors)
- super.validateInstanceMethods(errors)
- }
-
- /**
- * {@inheritDoc}
- */
override fun validateConstructor(errors: MutableList<Throwable>) {
super.validateConstructor(errors)
@@ -155,8 +273,12 @@
// validator will create an exception
val ctor = testClass.javaClass.constructors.first()
if (ctor.parameterTypes.none { it == FlickerTestParameter::class.java }) {
- errors.add(Exception("Constructor should have a parameter of type " +
- FlickerTestParameter::class.java.simpleName))
+ errors.add(
+ Exception(
+ "Constructor should have a parameter of type " +
+ FlickerTestParameter::class.java.simpleName
+ )
+ )
}
}
}
@@ -166,4 +288,34 @@
* necessary to release memory after a configuration is executed
*/
private fun getFlickerCleanUpMethod() = FlickerTestParameter::class.java.getMethod("clear")
+
+ private fun getAnnotatedFieldsByParameter(): List<FrameworkField?> {
+ return testClass.getAnnotatedFields(Parameterized.Parameter::class.java)
+ }
+
+ private fun getInjectionType(): String {
+ return if (fieldsAreAnnotated()) {
+ "FIELD"
+ } else {
+ "CONSTRUCTOR"
+ }
+ }
+
+ private fun fieldsAreAnnotated(): Boolean {
+ return !getAnnotatedFieldsByParameter().isEmpty()
+ }
+
+ private fun aggregateFaasResults(
+ assertionResults: MutableList<AssertionResult>
+ ): Map<String, List<AssertionResult>> {
+ val aggregatedResults = mutableMapOf<String, MutableList<AssertionResult>>()
+ for (result in assertionResults) {
+ val testName = "FaaS_${result.scenario.description}_${result.assertionName}"
+ if (!aggregatedResults.containsKey(testName)) {
+ aggregatedResults[testName] = mutableListOf()
+ }
+ aggregatedResults[testName]!!.add(result)
+ }
+ return aggregatedResults
+ }
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameter.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameter.kt
index ca050f0..0c8323c 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameter.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestParameter.kt
@@ -28,6 +28,7 @@
import com.android.server.wm.flicker.dsl.AssertionTag
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.SampleAppHelper
+import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.rules.ChangeDisplayOrientationRule
import com.android.server.wm.flicker.rules.LaunchAppRule
import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule
@@ -50,7 +51,7 @@
) {
private var internalFlicker: Flicker? = null
- private val flicker: Flicker get() = internalFlicker ?: error("Flicker not initialized")
+ internal val flicker: Flicker get() = internalFlicker ?: error("Flicker not initialized")
private val name: String get() = nameOverride ?: defaultName(this)
internal val isInitialized: Boolean get() = internalFlicker != null
@@ -109,6 +110,13 @@
config[IS_TABLET] = isTablet
}
+ val isFaasEnabled get() = config.getOrDefault(FAAS_ENABLED, false) as Boolean &&
+ isShellTransitionsEnabled
+
+ fun enableFaas() {
+ config[FAAS_ENABLED] = true
+ }
+
/**
* Clean the internal flicker reference (cache)
*/
@@ -123,7 +131,7 @@
internalFlicker = builder
.withTestName { "${testName}_$name" }
.repeat { config.getOrDefault(REPETITIONS, 1) as Int }
- .withFlickerAsAService { config.getOrDefault(FAAS_ENABLED, false) as Boolean }
+ .withFlickerAsAService { isFaasEnabled }
.build(TransitionRunnerWithRules(getTestSetupRules(builder.instrumentation)))
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/FlickerServiceCompatible.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/FlickerServiceCompatible.kt
new file mode 100644
index 0000000..c602395
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/FlickerServiceCompatible.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.wm.flicker.annotation
+
+/**
+ * Annotate your Flicker test class with this annotation to enable Flicker as a Service on the
+ * transition defined in the Flicker test class. It requires shell transitions to be enabled.
+ */
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FlickerServiceCompatible
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerFrameworkMethod.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerFrameworkMethod.kt
new file mode 100644
index 0000000..5d43cad
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerFrameworkMethod.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.wm.flicker.service
+
+import java.lang.reflect.Method
+import org.junit.runners.model.FrameworkMethod
+
+class FlickerFrameworkMethod(
+ method: Method?,
+ private val target: FlickerTestCase,
+ private val name: String
+) : FrameworkMethod(method) {
+ /**
+ * Returns the result of invoking this method on target with parameters params.
+ * Executes the test method on the supplied target (returned by the JUnitTestFactory)
+ * and not the instance generated by FrameworkMethod.
+ */
+ override fun invokeExplosively(target: Any?, vararg params: Any?): Any? {
+ return super.invokeExplosively(this.target, params)
+ }
+
+ /**
+ * Returns the method's name.
+ */
+ override fun getName(): String {
+ return name
+ }
+
+ /**
+ * We are reusing the same method with different parameters for each of our test results.
+ * So we can't use the parent definition of this which check for method equality, otherwise
+ * it would consider all FaaS test method as the same test which they are not.
+ */
+ override fun equals(other: Any?): Boolean {
+ return other is FlickerFrameworkMethod && name == other.name
+ }
+
+ /**
+ * @See equals
+ */
+ override fun hashCode(): Int {
+ return name.hashCode()
+ }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerServiceResultsCollector.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerServiceResultsCollector.kt
index 06872a1..155a632 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerServiceResultsCollector.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerServiceResultsCollector.kt
@@ -22,6 +22,7 @@
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import com.android.server.wm.flicker.service.assertors.AssertionResult
+import com.android.server.wm.flicker.service.config.AssertionInvocationGroup
import java.nio.file.Path
import org.junit.runner.Description
import org.junit.runner.Result
@@ -39,9 +40,6 @@
private var criticalUserJourneyName: String = UNDEFINED_CUJ
private var collectMetricsPerTest = true
- // The flicker service results (aka metrics) we want to upload
- private val metrics = mutableMapOf<String, Int>()
-
private val _executionErrors = mutableListOf<Throwable>()
val executionErrors: List<Throwable> get() = _executionErrors
@@ -61,7 +59,6 @@
errorReportingBlock {
Log.i(LOG_TAG, "onTestRunStart :: collectMetricsPerTest = $collectMetricsPerTest")
if (!collectMetricsPerTest) {
- require(metrics.isEmpty())
tracesCollector.start()
}
}
@@ -70,7 +67,6 @@
override fun onTestStart(testData: DataRecord, description: Description) {
errorReportingBlock {
Log.i(LOG_TAG, "onTestStart :: collectMetricsPerTest = $collectMetricsPerTest")
- require(metrics.isEmpty())
if (collectMetricsPerTest) {
tracesCollector.start()
}
@@ -97,19 +93,10 @@
Log.i(LOG_TAG, "onTestRunEnd :: collectMetricsPerTest = $collectMetricsPerTest")
if (!collectMetricsPerTest) {
stopTracingAndCollectFlickerMetrics(runData)
- collectMetrics(runData)
}
}
}
- fun postFlickerResultsForCollection(results: Map<String, Int>) {
- for ((key, res) in results) {
- require(res == 1 || res == 0)
- // If a failure is posted for key then we fail
- metrics[key] = (metrics[key] ?: 1) and res
- }
- }
-
private fun stopTracingAndCollectFlickerMetrics(dataRecord: DataRecord) {
tracesCollector.stop()
val collectedTraces = tracesCollector.getCollectedTraces()
@@ -118,36 +105,72 @@
collectedTraces.wmTrace,
collectedTraces.layersTrace, collectedTraces.transitionsTrace
)
- processFlickerResults(results)
- collectMetrics(dataRecord)
+ val aggregatedResults = processFlickerResults(results)
+ collectMetrics(dataRecord, aggregatedResults)
}
- private fun collectMetrics(data: DataRecord) {
- val it = metrics.entries.iterator()
- while (it.hasNext()) {
- val (key, value) = it.next()
- data.addStringMetric(key, value.toString())
- it.remove()
- }
- }
-
- /**
- * Convert the assertions generated by the Flicker Service to specific metric key pairs that
- * contain enough information to later further and analyze in dashboards.
- */
- private fun processFlickerResults(results: List<AssertionResult>) {
+ private fun processFlickerResults(
+ results: List<AssertionResult>
+ ): Map<String, AggregatedFlickerResult> {
+ val aggregatedResults = mutableMapOf<String, AggregatedFlickerResult>()
for (result in results) {
- // 0 for pass, 1 for failure
- val metric = if (result.failed) 1 else 0
-
- // Add information about the CUJ we are running the assertions on
- val assertionName = "${result.assertionGroup}#${result.assertionName}"
- val key = "$FASS_METRICS_PREFIX::$criticalUserJourneyName::$assertionName"
- if (metrics.containsKey(key)) {
- Log.w(LOG_TAG, "overriding metric $key")
+ val key = getKeyForAssertionResult(result)
+ if (!aggregatedResults.containsKey(key)) {
+ aggregatedResults[key] = AggregatedFlickerResult()
}
- metrics[key] = metric
+ aggregatedResults[key]!!.addResult(result)
}
+ return aggregatedResults
+ }
+
+ private fun collectMetrics(
+ data: DataRecord,
+ aggregatedResults: Map<String, AggregatedFlickerResult>
+ ) {
+ val it = aggregatedResults.entries.iterator()
+
+ while (it.hasNext()) {
+ val (key, result) = it.next()
+ val resultString = "${result.passes}/${result.passes + result.failures}"
+ var color = ANSI_RESET
+ if (result.failures > 0) {
+ color = ANSI_RED
+ }
+ if (result.failures == 0 && result.passes > 0) {
+ color = ANSI_GREEN
+ }
+
+ val errorString = StringBuilder()
+ if (result.errors.isNotEmpty()) {
+ errorString.append("\n\t$ANSI_RED_BOLD$key$ANSI_RESET\n")
+ for ((index, error) in result.errors.withIndex()) {
+ errorString.append(
+ "$ANSI_RED\t ${index + 1}) ${error.lines()[0]}" +
+ "${error.substring(error.indexOf('\n') + 1)
+ .prependIndent("\t ")}$ANSI_RESET\n"
+ )
+ }
+ }
+
+ var blockingStatus = ""
+ if (result.failures > 0) {
+ blockingStatus = if (result.invocationGroup == AssertionInvocationGroup.BLOCKING) {
+ "$ANSI_RED_BOLD(BLOCKING)$ANSI_RESET"
+ } else {
+ "$ANSI_WHITE$ANSI_LOW_INTENSITY(non blocking)$ANSI_RESET"
+ }
+ }
+
+ data.addStringMetric(
+ key,
+ "$color$resultString$ANSI_RESET $blockingStatus$errorString"
+ )
+ }
+ }
+
+ private fun getKeyForAssertionResult(result: AssertionResult): String {
+ val assertionName = "${result.scenario}#${result.assertionName}"
+ return "$FASS_METRICS_PREFIX::$criticalUserJourneyName::$assertionName"
}
fun setCriticalUserJourneyName(className: String?) {
@@ -159,5 +182,29 @@
private const val FASS_METRICS_PREFIX = "FASS"
private const val UNDEFINED_CUJ = "UndefinedCUJ"
private val LOG_TAG = "FlickerResultsCollector"
+
+ class AggregatedFlickerResult {
+ var failures = 0
+ var passes = 0
+ val errors = mutableListOf<String>()
+ var invocationGroup: AssertionInvocationGroup? = null
+
+ fun addResult(result: AssertionResult) {
+ if (result.failed) {
+ failures++
+ errors.add(result.assertionError?.message ?: "FAILURE WITHOUT ERROR MESSAGE...")
+ } else {
+ passes++
+ }
+
+ if (invocationGroup == null) {
+ invocationGroup = result.invocationGroup
+ }
+
+ if (invocationGroup != result.invocationGroup) {
+ error("Unexpected assertion group mismatch")
+ }
+ }
+ }
}
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerTestCase.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerTestCase.kt
new file mode 100644
index 0000000..9615dac
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerTestCase.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.wm.flicker.service
+
+import com.android.server.wm.flicker.service.assertors.AssertionResult
+import junit.framework.Assert
+import org.junit.Test
+
+class FlickerTestCase(val results: List<AssertionResult>) {
+
+ @Test
+ fun runTest(param: Any) {
+ if (containsFailures) {
+ Assert.fail(assertionMessage)
+ }
+ }
+
+ private val containsFailures: Boolean get() = results.any { it.failed }
+
+ private val assertionMessage: String get() {
+ if (!containsFailures) {
+ return "${results.size}/${results.size} PASSED"
+ }
+
+ if (results.size == 1) {
+ return results[0].assertionError!!.message
+ }
+
+ return buildString {
+ append("$failedCount/${results.size} FAILED\n")
+ for (result in results) {
+ if (result.failed) {
+ append("\n${result.assertionError!!.message.prependIndent(" ")}")
+ }
+ }
+ }
+ }
+
+ private val failedCount: Int get() = results.filter { it.failed }.size
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt
index 564b82e..795f774 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt
@@ -17,13 +17,13 @@
package com.android.server.wm.flicker.service.assertors
import com.android.server.wm.flicker.service.config.AssertionInvocationGroup
-import com.android.server.wm.flicker.service.config.FlickerServiceConfig.Companion.AssertionGroup
+import com.android.server.wm.flicker.service.config.FlickerServiceConfig.Companion.Scenario
/**
* Stores data for FASS assertions.
*/
data class AssertionData(
- val assertionGroup: AssertionGroup,
+ val scenario: Scenario,
val assertionBuilder: BaseAssertionBuilder,
val category: AssertionInvocationGroup
)
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionResult.kt
index c20fcf1..3bbe0b3 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionResult.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionResult.kt
@@ -17,7 +17,7 @@
package com.android.server.wm.flicker.service.assertors
import com.android.server.wm.flicker.service.config.AssertionInvocationGroup
-import com.android.server.wm.flicker.service.config.FlickerServiceConfig.Companion.AssertionGroup
+import com.android.server.wm.flicker.service.config.FlickerServiceConfig.Companion.Scenario
import com.android.server.wm.flicker.traces.FlickerSubjectException
import com.android.server.wm.flicker.traces.layers.LayerSubject
import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
@@ -34,7 +34,7 @@
*/
data class AssertionResult(
val assertionName: String,
- val assertionGroup: AssertionGroup,
+ val scenario: Scenario,
val invocationGroup: AssertionInvocationGroup,
val assertionError: FlickerSubjectException?
) {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertionBuilder.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertionBuilder.kt
index 2820058..0e3c805 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertionBuilder.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertionBuilder.kt
@@ -18,7 +18,7 @@
import com.android.server.wm.flicker.service.config.AssertionInvocationGroup
import com.android.server.wm.flicker.service.config.AssertionInvocationGroup.NON_BLOCKING
-import com.android.server.wm.flicker.service.config.FlickerServiceConfig.Companion.AssertionGroup
+import com.android.server.wm.flicker.service.config.FlickerServiceConfig.Companion.Scenario
import com.android.server.wm.flicker.traces.FlickerSubjectException
import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
@@ -31,7 +31,7 @@
internal var invocationGroup: AssertionInvocationGroup = NON_BLOCKING
// Assertion name
- val name: String = this::class.java.simpleName
+ open val name: String = this::class.java.simpleName
/**
* Evaluates assertions that require only WM traces.
@@ -67,7 +67,7 @@
transition: Transition,
wmSubject: WindowManagerTraceSubject?,
layerSubject: LayersTraceSubject?,
- assertionGroup: AssertionGroup
+ scenario: Scenario
): AssertionResult {
var assertionError: FlickerSubjectException? = null
try {
@@ -80,7 +80,7 @@
} catch (e: FlickerSubjectException) {
assertionError = e
}
- return AssertionResult(name, assertionGroup, invocationGroup, assertionError)
+ return AssertionResult(name, scenario, invocationGroup, assertionError)
}
infix fun runAs(invocationGroup: AssertionInvocationGroup): BaseAssertionBuilder {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/FaasData.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/FaasData.kt
new file mode 100644
index 0000000..e341095
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/FaasData.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2022 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 com.android.server.wm.flicker.service.assertors
+
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.prettyTimestamp
+import com.android.server.wm.traces.common.transition.Transition
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.google.common.truth.Fact
+
+data class FaasData(
+ val transition: Transition,
+ val entireWmTrace: WindowManagerTrace,
+ val entireLayersTrace: LayersTrace
+) {
+ fun toFacts(): Collection<Fact> {
+ val unclippedFirstTimestamp = entireWmTrace.firstOrNull()?.timestamp ?: 0L
+ val unclippedLastTimestamp = entireWmTrace.lastOrNull()?.timestamp ?: 0L
+ val unclippedTraceFirst = "${prettyTimestamp(unclippedFirstTimestamp)} " +
+ "(timestamp=$unclippedFirstTimestamp)"
+ val unclippedTraceLast = "${prettyTimestamp(unclippedLastTimestamp)} " +
+ "(timestamp=$unclippedLastTimestamp)"
+
+ return listOf(
+ Fact.fact("Transition type", transition.type),
+ Fact.fact(
+ "Transition start",
+ "${prettyTimestamp(transition.start)} (timestamp=${transition.start})"
+ ),
+ Fact.fact(
+ "Transition end",
+ "${prettyTimestamp(transition.end)} (timestamp=${transition.end})"
+ ),
+ Fact.fact("Transition type", transition.type),
+ Fact.fact(
+ "Transition changes",
+ transition.changes
+ .joinToString("\n -", "\n -") {
+ "${it.transitMode} ${it.windowName}"
+ }
+ ),
+ Fact.fact("Extracted from trace start", unclippedTraceFirst),
+ Fact.fact("Extracted from trace end", unclippedTraceLast)
+ )
+ }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAsserter.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAsserter.kt
index a5ef668..d0dacc2 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAsserter.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAsserter.kt
@@ -32,52 +32,88 @@
/** {@inheritDoc} */
fun analyze(
transition: Transition,
- _wmTrace: WindowManagerTrace?,
- _layersTrace: LayersTrace?
+ wmTrace: WindowManagerTrace,
+ layersTrace: LayersTrace
): List<AssertionResult> {
- var wmTrace = _wmTrace
- if (wmTrace != null && wmTrace.entries.isEmpty()) {
- // Empty trace, nothing to assert on the wmTrace
- logger("Passed an empty wmTrace")
- wmTrace = null
- }
-
- var layersTrace = _layersTrace
- if (layersTrace != null && layersTrace.entries.isEmpty()) {
- layersTrace = null
- }
-
- require(wmTrace != null || layersTrace != null)
+ val (wmTraceForTransition, layersTraceForTransition) =
+ splitTraces(transition, wmTrace, layersTrace)
+ require(wmTraceForTransition != null || layersTraceForTransition != null)
logger.invoke("Running assertions...")
- return runAssertionsOnSubjects(transition, wmTrace, layersTrace, assertions)
+ return runAssertionsOnSubjects(
+ transition, wmTraceForTransition, layersTraceForTransition,
+ wmTrace, layersTrace, assertions
+ )
}
private fun runAssertionsOnSubjects(
transition: Transition,
- wmTrace: WindowManagerTrace?,
- layersTrace: LayersTrace?,
+ wmTraceToAssert: WindowManagerTrace?,
+ layersTraceToAssert: LayersTrace?,
+ entireWmTrace: WindowManagerTrace,
+ entireLayersTrace: LayersTrace,
assertions: List<AssertionData>
): List<AssertionResult> {
val results: MutableList<AssertionResult> = mutableListOf()
assertions.forEach {
val assertion = it.assertionBuilder
- logger.invoke("Running assertion $assertion for ${it.assertionGroup}")
- val wmSubject = if (wmTrace != null) {
- WindowManagerTraceSubject.assertThat(wmTrace)
+ logger.invoke("Running assertion $assertion for ${it.scenario}")
+ val faasFacts = FaasData(transition, entireWmTrace, entireLayersTrace).toFacts()
+ val wmSubject = if (wmTraceToAssert != null) {
+ WindowManagerTraceSubject.assertThat(wmTraceToAssert, facts = faasFacts)
} else {
null
}
- val layersSubject = if (layersTrace != null) {
- LayersTraceSubject.assertThat(layersTrace)
+ val layersSubject = if (layersTraceToAssert != null) {
+ LayersTraceSubject.assertThat(layersTraceToAssert, facts = faasFacts)
} else {
null
}
- val result = assertion.evaluate(transition, wmSubject, layersSubject, it.assertionGroup)
+ val result = assertion.evaluate(transition, wmSubject, layersSubject, it.scenario)
results.add(result)
}
return results
}
+
+ /**
+ * Splits a [WindowManagerTrace] and a [LayersTrace] by a [Transition].
+ *
+ * @param tag a list with all [TransitionTag]s
+ * @param wmTrace Window Manager trace
+ * @param layersTrace Surface Flinger trace
+ * @return a list with [WindowManagerTrace] blocks
+ */
+ private fun splitTraces(
+ transition: Transition,
+ wmTrace: WindowManagerTrace,
+ layersTrace: LayersTrace
+ ): FilteredTraces {
+ var filteredWmTrace: WindowManagerTrace? = wmTrace.transitionSlice(transition)
+ var filteredLayersTrace: LayersTrace? = layersTrace.transitionSlice(transition)
+
+ if (filteredWmTrace?.entries?.isEmpty() == true) {
+ // Empty trace, nothing to assert on the wmTrace
+ logger("Got an empty wmTrace for transition $transition")
+ filteredWmTrace = null
+ }
+
+ if (filteredLayersTrace?.entries?.isEmpty() == true) {
+ // Empty trace, nothing to assert on the layers trace
+ logger("Got an empty surface trace for transition $transition")
+ filteredLayersTrace = null
+ }
+
+ return FilteredTraces(filteredWmTrace, filteredLayersTrace)
+ }
+
+ data class FilteredTraces(
+ val wmTrace: WindowManagerTrace?,
+ val layersTrace: LayersTrace?
+ )
+}
+
+private fun WindowManagerTrace.transitionSlice(transition: Transition): WindowManagerTrace {
+ return slice(transition.start, transition.end)
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/config/FlickerServiceConfig.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/config/FlickerServiceConfig.kt
index f64faa3..343c3f2 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/service/config/FlickerServiceConfig.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/service/config/FlickerServiceConfig.kt
@@ -16,7 +16,6 @@
package com.android.server.wm.flicker.service.config
-import android.view.WindowManager.TRANSIT_OPEN
import com.android.server.wm.flicker.service.assertors.AssertionData
import com.android.server.wm.flicker.service.assertors.BaseAssertionBuilder
import com.android.server.wm.flicker.service.assertors.Components
@@ -44,7 +43,7 @@
fun assertionsForTransition(transition: Transition): List<AssertionData> {
val assertions: MutableList<AssertionData> = mutableListOf()
- for (assertionGroup in AssertionGroup.values()) {
+ for (assertionGroup in Scenario.values()) {
assertionGroup.description
if (assertionGroup.executionCondition.shouldExecute(transition)) {
for (assertion in assertionGroup.assertions) {
@@ -59,7 +58,7 @@
}
companion object {
- enum class AssertionGroup(
+ enum class Scenario(
val description: String,
val executionCondition: AssertionExecutionCondition,
val assertions: List<BaseAssertionBuilder>
@@ -82,7 +81,7 @@
NEVER({ false }),
APP_LAUNCH({ t ->
t.type == Type.OPEN &&
- t.changes.any { it.transitMode == TRANSIT_OPEN }
+ t.changes.any { it.transitMode == Type.OPEN }
}),
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt
index 980d88e..9c6b914 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt
@@ -28,10 +28,10 @@
) : AssertionError(cause.message, if (cause is FlickerSubjectException) null else cause) {
internal val timestamp = subject.timestamp
private val prettyTimestamp =
- if (timestamp > 0) "${prettyTimestamp(timestamp)} (timestamp=$timestamp)" else ""
+ if (timestamp > 0) "${prettyTimestamp(timestamp)} (timestamp=$timestamp)" else ""
internal val errorType: String =
- if (cause is AssertionError) "Flicker assertion error" else "Unknown error"
+ if (cause is AssertionError) "Flicker assertion error" else "Unknown error"
internal val errorDescription = buildString {
appendLine("Where? $prettyTimestamp")
@@ -49,7 +49,7 @@
internal val subjectInformation = buildString {
appendLine("Facts:")
- subject.completeFacts.forEach { append("\t").appendLine(it) }
+ subject.completeFacts.forEach { appendLine(it.toString().prependIndent("\t")) }
}
override val message: String
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt
index 9f910f0..73dade4 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt
@@ -25,6 +25,7 @@
import com.android.server.wm.traces.common.layers.Layer
import com.android.server.wm.traces.common.layers.LayersTrace
import com.android.server.wm.traces.common.region.RegionTrace
+import com.google.common.truth.Fact
import com.google.common.truth.FailureMetadata
import com.google.common.truth.FailureStrategy
import com.google.common.truth.StandardSubjectBuilder
@@ -57,11 +58,17 @@
class LayersTraceSubject private constructor(
fm: FailureMetadata,
val trace: LayersTrace,
- override val parent: LayersTraceSubject?
+ override val parent: LayersTraceSubject?,
+ val facts: Collection<Fact>
) : FlickerTraceSubject<LayerTraceEntrySubject>(fm, trace),
ILayerSubject<LayersTraceSubject, RegionTraceSubject> {
- override val selfFacts
- get() = super.selfFacts.toMutableList()
+
+ override val selfFacts by lazy {
+ val allFacts = super.selfFacts.toMutableList()
+ allFacts.addAll(facts)
+ allFacts
+ }
+
override val subjects by lazy {
trace.entries.map { LayerTraceEntrySubject.assertThat(it, trace, this) }
}
@@ -308,8 +315,11 @@
/**
* Boilerplate Subject.Factory for LayersTraceSubject
*/
- private fun getFactory(parent: LayersTraceSubject?): Factory<Subject, LayersTrace> =
- Factory { fm, subject -> LayersTraceSubject(fm, subject, parent) }
+ private fun getFactory(
+ parent: LayersTraceSubject?,
+ facts: Collection<Fact> = emptyList()
+ ): Factory<Subject, LayersTrace> =
+ Factory { fm, subject -> LayersTraceSubject(fm, subject, parent, facts) }
/**
* Creates a [LayersTraceSubject] to representing a SurfaceFlinger trace,
@@ -321,11 +331,12 @@
@JvmOverloads
fun assertThat(
trace: LayersTrace,
- parent: LayersTraceSubject? = null
+ parent: LayersTraceSubject? = null,
+ facts: Collection<Fact> = emptyList()
): LayersTraceSubject {
val strategy = FlickerFailureStrategy()
val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
- .about(getFactory(parent))
+ .about(getFactory(parent, facts))
.that(trace) as LayersTraceSubject
strategy.init(subject)
return subject
@@ -335,8 +346,11 @@
* Static method for getting the subject factory (for use with assertAbout())
*/
@JvmStatic
- fun entries(parent: LayersTraceSubject?): Factory<Subject, LayersTrace> {
- return getFactory(parent)
+ fun entries(
+ parent: LayersTraceSubject?,
+ facts: Collection<Fact> = emptyList()
+ ): Factory<Subject, LayersTrace> {
+ return getFactory(parent, facts)
}
}
}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt
index 04b6a7c..3d88999 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt
@@ -26,8 +26,8 @@
import com.android.server.wm.traces.common.region.RegionTrace
import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
import com.android.server.wm.traces.common.windowmanager.windows.WindowState
+import com.google.common.truth.Fact
import com.google.common.truth.FailureMetadata
-import com.google.common.truth.FailureStrategy
import com.google.common.truth.StandardSubjectBuilder
import com.google.common.truth.Subject
import com.google.common.truth.Subject.Factory
@@ -58,11 +58,16 @@
class WindowManagerTraceSubject private constructor(
fm: FailureMetadata,
val trace: WindowManagerTrace,
- override val parent: WindowManagerTraceSubject?
+ override val parent: WindowManagerTraceSubject?,
+ private val facts: Collection<Fact>
) : FlickerTraceSubject<WindowManagerStateSubject>(fm, trace),
IWindowManagerSubject<WindowManagerTraceSubject, RegionTraceSubject> {
- override val selfFacts
- get() = super.selfFacts.toMutableList()
+
+ override val selfFacts by lazy {
+ val allFacts = super.selfFacts.toMutableList()
+ allFacts.addAll(facts)
+ allFacts
+ }
override val subjects by lazy {
trace.entries.map { WindowManagerStateSubject.assertThat(it, this, this) }
@@ -627,9 +632,10 @@
* Boilerplate Subject.Factory for WmTraceSubject
*/
private fun getFactory(
- parent: WindowManagerTraceSubject?
+ parent: WindowManagerTraceSubject?,
+ facts: Collection<Fact> = emptyList()
): Factory<Subject, WindowManagerTrace> =
- Factory { fm, subject -> WindowManagerTraceSubject(fm, subject, parent) }
+ Factory { fm, subject -> WindowManagerTraceSubject(fm, subject, parent, facts) }
/**
* Creates a [WindowManagerTraceSubject] representing a WindowManager trace,
@@ -641,11 +647,12 @@
@JvmOverloads
fun assertThat(
trace: WindowManagerTrace,
- parent: WindowManagerTraceSubject? = null
+ parent: WindowManagerTraceSubject? = null,
+ facts: Collection<Fact> = emptyList()
): WindowManagerTraceSubject {
val strategy = FlickerFailureStrategy()
val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy)
- .about(getFactory(parent))
+ .about(getFactory(parent, facts))
.that(trace) as WindowManagerTraceSubject
strategy.init(subject)
return subject
@@ -656,7 +663,8 @@
*/
@JvmStatic
fun entries(
- parent: WindowManagerTraceSubject?
- ): Factory<Subject, WindowManagerTrace> = getFactory(parent)
+ parent: WindowManagerTraceSubject?,
+ facts: Collection<Fact>
+ ): Factory<Subject, WindowManagerTrace> = getFactory(parent, facts)
}
}