diff options
| -rw-r--r-- | packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt | 5 | ||||
| -rw-r--r-- | tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt | 76 | ||||
| -rw-r--r-- | tests/testables/tests/Android.bp | 4 | ||||
| -rw-r--r-- | tests/testables/tests/AndroidManifest.xml | 4 | ||||
| -rw-r--r-- | tests/testables/tests/AndroidTest.xml | 51 | ||||
| -rw-r--r-- | tests/testables/tests/goldens/recordFilmstrip_withAnimator.png | bin | 0 -> 40500 bytes | |||
| -rw-r--r-- | tests/testables/tests/goldens/recordFilmstrip_withSpring.png | bin | 0 -> 32206 bytes | |||
| -rw-r--r-- | tests/testables/tests/goldens/recordTimeSeries_withAnimator.json (renamed from tests/testables/tests/goldens/recordMotion_withAnimator.json) | 2 | ||||
| -rw-r--r-- | tests/testables/tests/goldens/recordTimeSeries_withSpring.json (renamed from tests/testables/tests/goldens/recordMotion_withSpring.json) | 2 | ||||
| -rw-r--r-- | tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt | 188 |
10 files changed, 260 insertions, 72 deletions
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt index 071acfa44650..288ed4dc9d4f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt @@ -77,7 +77,10 @@ class TransitionAnimatorTest(val useSpring: Boolean) : SysuiTestCase() { @get:Rule(order = 2) val animatorTestRule = android.animation.AnimatorTestRule(this) @get:Rule(order = 3) val motionRule = - MotionTestRule(AnimatorTestRuleToolkit(animatorTestRule, kosmos.testScope), pathManager) + MotionTestRule( + AnimatorTestRuleToolkit(animatorTestRule, kosmos.testScope) { activityRule.scenario }, + pathManager, + ) @Test fun backgroundAnimation_whenLaunching() { diff --git a/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt b/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt index b27b8269575b..ded467993eef 100644 --- a/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt +++ b/tests/testables/src/android/animation/AnimatorTestRuleToolkit.kt @@ -17,7 +17,13 @@ package android.animation import android.animation.AnimatorTestRuleToolkit.Companion.TAG +import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.util.Log +import android.view.View +import androidx.core.graphics.drawable.toBitmap +import androidx.test.core.app.ActivityScenario +import java.util.concurrent.TimeUnit import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -36,13 +42,45 @@ import platform.test.motion.golden.FrameId import platform.test.motion.golden.TimeSeries import platform.test.motion.golden.TimeSeriesCaptureScope import platform.test.motion.golden.TimestampFrameId +import platform.test.screenshot.captureToBitmapAsync -class AnimatorTestRuleToolkit(val animatorTestRule: AnimatorTestRule, val testScope: TestScope) { +class AnimatorTestRuleToolkit( + internal val animatorTestRule: AnimatorTestRule, + internal val testScope: TestScope, + internal val currentActivityScenario: () -> ActivityScenario<*>, +) { internal companion object { const val TAG = "AnimatorRuleToolkit" } } +/** Capture utility to extract a [Bitmap] from a [drawable]. */ +fun captureDrawable(drawable: Drawable): Bitmap { + val width = drawable.bounds.right - drawable.bounds.left + val height = drawable.bounds.bottom - drawable.bounds.top + + // If either dimension is 0 this will fail, so we set it to 1 pixel instead. + return drawable.toBitmap( + width = + if (width > 0) { + width + } else { + 1 + }, + height = + if (height > 0) { + height + } else { + 1 + }, + ) +} + +/** Capture utility to extract a [Bitmap] from a [view]. */ +fun captureView(view: View): Bitmap { + return view.captureToBitmapAsync().get(10, TimeUnit.SECONDS) +} + /** * Controls the timing of the motion recording. * @@ -71,24 +109,39 @@ data class AnimatorRuleRecordingSpec<T>( /** Time interval between frame captures, in milliseconds. */ val frameDurationMs: Long = 16L, - /** Produces the time-series, invoked on each animation frame. */ + /** Whether a sequence of screenshots should also be recorded. */ + val visualCapture: ((captureRoot: T) -> Bitmap)? = null, + + /** Produces the time-series, invoked on each animation frame. */ val timeSeriesCapture: TimeSeriesCaptureScope<T>.() -> Unit, ) /** Records the time-series of the features specified in [recordingSpec]. */ fun <T> MotionTestRule<AnimatorTestRuleToolkit>.recordMotion( - recordingSpec: AnimatorRuleRecordingSpec<T>, + recordingSpec: AnimatorRuleRecordingSpec<T> ): RecordedMotion { with(toolkit.animatorTestRule) { + val activityScenario = toolkit.currentActivityScenario() val frameIdCollector = mutableListOf<FrameId>() val propertyCollector = mutableMapOf<String, MutableList<DataPoint<*>>>() + val screenshotCollector = + if (recordingSpec.visualCapture != null) { + mutableListOf<Bitmap>() + } else { + null + } fun recordFrame(frameId: FrameId) { Log.i(TAG, "recordFrame($frameId)") frameIdCollector.add(frameId) - recordingSpec.timeSeriesCapture.invoke( - TimeSeriesCaptureScope(recordingSpec.captureRoot, propertyCollector) - ) + activityScenario.onActivity { + recordingSpec.timeSeriesCapture.invoke( + TimeSeriesCaptureScope(recordingSpec.captureRoot, propertyCollector) + ) + } + + val bitmap = recordingSpec.visualCapture?.invoke(recordingSpec.captureRoot) + if (bitmap != null) screenshotCollector!!.add(bitmap) } val motionControl = @@ -101,10 +154,13 @@ fun <T> MotionTestRule<AnimatorTestRuleToolkit>.recordMotion( Log.i(TAG, "recordMotion() begin recording") - val startFrameTime = currentTime + var startFrameTime: Long? = null + toolkit.currentActivityScenario().onActivity { startFrameTime = currentTime } while (!motionControl.recordingEnded) { - recordFrame(TimestampFrameId(currentTime - startFrameTime)) - motionControl.nextFrame() + var time: Long? = null + toolkit.currentActivityScenario().onActivity { time = currentTime } + recordFrame(TimestampFrameId(time!! - startFrameTime!!)) + toolkit.currentActivityScenario().onActivity { motionControl.nextFrame() } } Log.i(TAG, "recordMotion() end recording") @@ -115,7 +171,7 @@ fun <T> MotionTestRule<AnimatorTestRuleToolkit>.recordMotion( propertyCollector.entries.map { entry -> Feature(entry.key, entry.value) }, ) - return create(timeSeries, null) + return create(timeSeries, screenshotCollector) } } diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp index 71105646a3d3..f0cda535b3aa 100644 --- a/tests/testables/tests/Android.bp +++ b/tests/testables/tests/Android.bp @@ -37,10 +37,11 @@ android_test { "androidx.core_core-ktx", "androidx.test.ext.junit", "androidx.test.rules", - "androidx.test.ext.junit", "hamcrest-library", "kotlinx_coroutines_test", "mockito-target-inline-minus-junit4", + "platform-screenshot-diff-core", + "platform-test-annotations", "testables", "truth", ], @@ -55,6 +56,7 @@ android_test { "android.test.mock.stubs.system", ], certificate: "platform", + test_config: "AndroidTest.xml", test_suites: [ "device-tests", "automotive-tests", diff --git a/tests/testables/tests/AndroidManifest.xml b/tests/testables/tests/AndroidManifest.xml index 2bfb04fdb765..6cba59872710 100644 --- a/tests/testables/tests/AndroidManifest.xml +++ b/tests/testables/tests/AndroidManifest.xml @@ -23,6 +23,10 @@ <application android:debuggable="true"> <uses-library android:name="android.test.runner" /> + <activity + android:name="platform.test.screenshot.ScreenshotActivity" + android:exported="true"> + </activity> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/tests/testables/tests/AndroidTest.xml b/tests/testables/tests/AndroidTest.xml new file mode 100644 index 000000000000..85f6e6257770 --- /dev/null +++ b/tests/testables/tests/AndroidTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<configuration description="Runs Tests for Testables."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="test-file-name" value="TestablesTests.apk" /> + <option name="install-arg" value="-t" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"> + <option name="force-root" value="true" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <option name="screen-always-on" value="on" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP" /> + <option name="run-command" value="wm dismiss-keyguard" /> + </target_preparer> + + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="framework-base-presubmit" /> + <option name="test-tag" value="TestableTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.testables" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="test-filter-dir" value="/data/data/com.android.testables" /> + <option name="hidden-api-checks" value="false"/> + </test> + + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/data/user/0/com.android.testables/files"/> + <option name="collect-on-run-ended-only" value="true"/> + <option name="clean-up" value="true"/> + </metrics_collector> +</configuration> diff --git a/tests/testables/tests/goldens/recordFilmstrip_withAnimator.png b/tests/testables/tests/goldens/recordFilmstrip_withAnimator.png Binary files differnew file mode 100644 index 000000000000..9aed2e970239 --- /dev/null +++ b/tests/testables/tests/goldens/recordFilmstrip_withAnimator.png diff --git a/tests/testables/tests/goldens/recordFilmstrip_withSpring.png b/tests/testables/tests/goldens/recordFilmstrip_withSpring.png Binary files differnew file mode 100644 index 000000000000..1d0c0c3c3393 --- /dev/null +++ b/tests/testables/tests/goldens/recordFilmstrip_withSpring.png diff --git a/tests/testables/tests/goldens/recordMotion_withAnimator.json b/tests/testables/tests/goldens/recordTimeSeries_withAnimator.json index 87fece5395e7..73eb6c74fee6 100644 --- a/tests/testables/tests/goldens/recordMotion_withAnimator.json +++ b/tests/testables/tests/goldens/recordTimeSeries_withAnimator.json @@ -29,7 +29,7 @@ ], "features": [ { - "name": "value", + "name": "alpha", "type": "float", "data_points": [ 1, diff --git a/tests/testables/tests/goldens/recordMotion_withSpring.json b/tests/testables/tests/goldens/recordTimeSeries_withSpring.json index e9fb5b4af869..2b97bad08e00 100644 --- a/tests/testables/tests/goldens/recordMotion_withSpring.json +++ b/tests/testables/tests/goldens/recordTimeSeries_withSpring.json @@ -21,7 +21,7 @@ ], "features": [ { - "name": "value", + "name": "alpha", "type": "float", "data_points": [ 1, diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt index fbef4899bca9..993c3fed9d59 100644 --- a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt +++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt @@ -16,10 +16,15 @@ package android.animation -import android.util.FloatProperty +import android.graphics.Color +import android.platform.test.annotations.MotionTest +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.dynamicanimation.animation.DynamicAnimation import com.android.internal.dynamicanimation.animation.SpringAnimation import com.android.internal.dynamicanimation.animation.SpringForce import kotlinx.coroutines.test.TestScope @@ -28,102 +33,169 @@ import org.junit.Test import org.junit.runner.RunWith import platform.test.motion.MotionTestRule import platform.test.motion.RecordedMotion -import platform.test.motion.golden.FeatureCapture -import platform.test.motion.golden.asDataPoint import platform.test.motion.testing.createGoldenPathManager +import platform.test.motion.view.ViewFeatureCaptures +import platform.test.screenshot.DeviceEmulationRule +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.DisplaySpec +import platform.test.screenshot.ScreenshotActivity +import platform.test.screenshot.ScreenshotTestRule @SmallTest +@MotionTest @RunWith(AndroidJUnit4::class) class AnimatorTestRuleToolkitTest { companion object { private val GOLDEN_PATH_MANAGER = createGoldenPathManager("frameworks/base/tests/testables/tests/goldens") - private val TEST_PROPERTY = - object : FloatProperty<TestState>("value") { - override fun get(state: TestState): Float { - return state.animatedValue - } - - override fun setValue(state: TestState, value: Float) { - state.animatedValue = value - } - } + private val EMULATION_SPEC = + DeviceEmulationSpec(DisplaySpec("phone", width = 320, height = 690, densityDpi = 160)) } - @get:Rule(order = 0) val animatorTestRule = AnimatorTestRule(this) - @get:Rule(order = 1) + @get:Rule(order = 0) val deviceEmulationRule = DeviceEmulationRule(EMULATION_SPEC) + @get:Rule(order = 1) val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java) + @get:Rule(order = 2) val animatorTestRule = AnimatorTestRule(this) + @get:Rule(order = 3) val screenshotRule = ScreenshotTestRule(GOLDEN_PATH_MANAGER) + @get:Rule(order = 4) val motionRule = - MotionTestRule(AnimatorTestRuleToolkit(animatorTestRule, TestScope()), GOLDEN_PATH_MANAGER) + MotionTestRule( + AnimatorTestRuleToolkit(animatorTestRule, TestScope()) { activityRule.scenario }, + GOLDEN_PATH_MANAGER, + bitmapDiffer = screenshotRule, + ) @Test - fun recordMotion_withAnimator() { - val state = TestState() - AnimatorSet().apply { - duration = 500 - play( - ValueAnimator.ofFloat(state.animatedValue, 0f).apply { - addUpdateListener { state.animatedValue = it.animatedValue as Float } - } + fun recordFilmstrip_withAnimator() { + val animatedBox = createScene() + createAnimator(animatedBox).apply { getInstrumentation().runOnMainSync { start() } } + + val recordedMotion = + record( + animatedBox, + MotionControl { awaitFrames(count = 26) }, + sampleIntervalMs = 20L, + recordScreenshots = true, + ) + + motionRule.assertThat(recordedMotion).filmstripMatchesGolden("recordFilmstrip_withAnimator") + } + + @Test + fun recordTimeSeries_withAnimator() { + val animatedBox = createScene() + createAnimator(animatedBox).apply { getInstrumentation().runOnMainSync { start() } } + + val recordedMotion = + record( + animatedBox, + MotionControl { awaitFrames(count = 26) }, + sampleIntervalMs = 20L, + recordScreenshots = false, ) + + motionRule + .assertThat(recordedMotion) + .timeSeriesMatchesGolden("recordTimeSeries_withAnimator") + } + + @Test + fun recordFilmstrip_withSpring() { + val animatedBox = createScene() + var isDone = false + createSpring(animatedBox).apply { + addEndListener { _, _, _, _ -> isDone = true } getInstrumentation().runOnMainSync { start() } } val recordedMotion = - record(state, MotionControl { awaitFrames(count = 26) }, sampleIntervalMs = 20L) + record( + animatedBox, + MotionControl { awaitCondition { isDone } }, + sampleIntervalMs = 16L, + recordScreenshots = true, + ) - motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden("recordMotion_withAnimator") + motionRule.assertThat(recordedMotion).filmstripMatchesGolden("recordFilmstrip_withSpring") } @Test - fun recordMotion_withSpring() { - val state = TestState() + fun recordTimeSeries_withSpring() { + val animatedBox = createScene() var isDone = false - SpringAnimation(state, TEST_PROPERTY).apply { + createSpring(animatedBox).apply { + addEndListener { _, _, _, _ -> isDone = true } + getInstrumentation().runOnMainSync { start() } + } + + val recordedMotion = + record( + animatedBox, + MotionControl { awaitCondition { isDone } }, + sampleIntervalMs = 16L, + recordScreenshots = false, + ) + + motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden("recordTimeSeries_withSpring") + } + + private fun createScene(): ViewGroup { + lateinit var sceneRoot: ViewGroup + activityRule.scenario.onActivity { activity -> + sceneRoot = FrameLayout(activity).apply { setBackgroundColor(Color.BLACK) } + activity.setContentView(sceneRoot) + } + getInstrumentation().waitForIdleSync() + return sceneRoot + } + + private fun createAnimator(animatedBox: ViewGroup): AnimatorSet { + return AnimatorSet().apply { + duration = 500 + play( + ValueAnimator.ofFloat(animatedBox.alpha, 0f).apply { + addUpdateListener { animatedBox.alpha = it.animatedValue as Float } + } + ) + } + } + + private fun createSpring(animatedBox: ViewGroup): SpringAnimation { + return SpringAnimation(animatedBox, DynamicAnimation.ALPHA).apply { spring = SpringForce(0f).apply { stiffness = 500f dampingRatio = 0.95f } - setStartValue(1f) + setStartValue(animatedBox.alpha) setMinValue(0f) setMaxValue(1f) minimumVisibleChange = 0.01f - - addEndListener { _, _, _, _ -> isDone = true } - getInstrumentation().runOnMainSync { start() } } - - val recordedMotion = - record(state, MotionControl { awaitCondition { isDone } }, sampleIntervalMs = 16L) - - motionRule.assertThat(recordedMotion).timeSeriesMatchesGolden("recordMotion_withSpring") } private fun record( - state: TestState, + container: ViewGroup, motionControl: MotionControl, sampleIntervalMs: Long, + recordScreenshots: Boolean, ): RecordedMotion { - var recordedMotion: RecordedMotion? = null - getInstrumentation().runOnMainSync { - recordedMotion = - motionRule.recordMotion( - AnimatorRuleRecordingSpec( - state, - motionControl, - sampleIntervalMs, - ) { - feature( - FeatureCapture("value") { state -> state.animatedValue.asDataPoint() }, - "value", - ) - } - ) - } - return recordedMotion!! + val visualCapture = + if (recordScreenshots) { + ::captureView + } else { + null + } + return motionRule.recordMotion( + AnimatorRuleRecordingSpec( + container, + motionControl, + sampleIntervalMs, + visualCapture, + ) { + feature(ViewFeatureCaptures.alpha, "alpha") + } + ) } - - data class TestState(var animatedValue: Float = 1f) } |