Merge "Add AnimatorTestRule for use with platform animators" into udc-qpr-dev
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package android.animation;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.os.SystemClock;
|
||||
import android.os.SystemProperties;
|
||||
import android.util.ArrayMap;
|
||||
@@ -91,15 +92,30 @@ public class AnimationHandler {
|
||||
};
|
||||
|
||||
public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
|
||||
private static AnimationHandler sTestHandler = null;
|
||||
private boolean mListDirty = false;
|
||||
|
||||
public static AnimationHandler getInstance() {
|
||||
if (sTestHandler != null) {
|
||||
return sTestHandler;
|
||||
}
|
||||
if (sAnimatorHandler.get() == null) {
|
||||
sAnimatorHandler.set(new AnimationHandler());
|
||||
}
|
||||
return sAnimatorHandler.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an instance that will be returned by {@link #getInstance()} on every thread.
|
||||
* @return the previously active test handler, if any.
|
||||
* @hide
|
||||
*/
|
||||
public static @Nullable AnimationHandler setTestHandler(@Nullable AnimationHandler handler) {
|
||||
AnimationHandler oldHandler = sTestHandler;
|
||||
sTestHandler = handler;
|
||||
return oldHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* System property that controls the behavior of pausing infinite animators when an app
|
||||
* is moved to the background.
|
||||
@@ -369,7 +385,10 @@ public class AnimationHandler {
|
||||
* Return the number of callbacks that have registered for frame callbacks.
|
||||
*/
|
||||
public static int getAnimationCount() {
|
||||
AnimationHandler handler = sAnimatorHandler.get();
|
||||
AnimationHandler handler = sTestHandler;
|
||||
if (handler == null) {
|
||||
handler = sAnimatorHandler.get();
|
||||
}
|
||||
if (handler == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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.animation
|
||||
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper.RunWithLooper
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* This test class validates that two tests' animators are isolated from each other when using the
|
||||
* same animator test rule. This is a test to prevent future instances of b/275602127.
|
||||
*/
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@SmallTest
|
||||
@RunWithLooper(setAsMainLooper = true)
|
||||
class AnimatorTestRuleIsolationTest : SysuiTestCase() {
|
||||
|
||||
@get:Rule val animatorTestRule = AnimatorTestRule()
|
||||
|
||||
@Test
|
||||
fun testA() {
|
||||
// GIVEN global state is reset at the start of the test
|
||||
didTouchA = false
|
||||
didTouchB = false
|
||||
// WHEN starting 2 animations of different durations, and setting didTouchA at the end
|
||||
ObjectAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 100
|
||||
doOnEnd { didTouchA = true }
|
||||
start()
|
||||
}
|
||||
ObjectAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 150
|
||||
doOnEnd { didTouchA = true }
|
||||
start()
|
||||
}
|
||||
// WHEN when you advance time so that only one of the animations has ended
|
||||
animatorTestRule.advanceTimeBy(100)
|
||||
// VERIFY we did indeed end the current animation
|
||||
assertThat(didTouchA).isTrue()
|
||||
// VERIFY advancing the animator did NOT cause testB's animator to end
|
||||
assertThat(didTouchB).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testB() {
|
||||
// GIVEN global state is reset at the start of the test
|
||||
didTouchA = false
|
||||
didTouchB = false
|
||||
// WHEN starting 2 animations of different durations, and setting didTouchB at the end
|
||||
ObjectAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 100
|
||||
doOnEnd { didTouchB = true }
|
||||
start()
|
||||
}
|
||||
ObjectAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 150
|
||||
doOnEnd { didTouchB = true }
|
||||
start()
|
||||
}
|
||||
animatorTestRule.advanceTimeBy(100)
|
||||
// VERIFY advancing the animator did NOT cause testA's animator to end
|
||||
assertThat(didTouchA).isFalse()
|
||||
// VERIFY we did indeed end the current animation
|
||||
assertThat(didTouchB).isTrue()
|
||||
}
|
||||
|
||||
companion object {
|
||||
var didTouchA = false
|
||||
var didTouchB = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 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.animation
|
||||
|
||||
import android.testing.AndroidTestingRunner
|
||||
import android.testing.TestableLooper.RunWithLooper
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.app.animation.Interpolators
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@SmallTest
|
||||
@RunWithLooper(setAsMainLooper = true)
|
||||
class AnimatorTestRulePrecisionTest : SysuiTestCase() {
|
||||
|
||||
@get:Rule val animatorTestRule = AnimatorTestRule()
|
||||
|
||||
var value1: Float = -1f
|
||||
var value2: Float = -1f
|
||||
|
||||
private inline fun animateThis(
|
||||
propertyName: String,
|
||||
duration: Long,
|
||||
startDelay: Long = 0,
|
||||
crossinline onEndAction: (animator: Animator) -> Unit,
|
||||
) {
|
||||
ObjectAnimator.ofFloat(this, propertyName, 0f, 1f).also {
|
||||
it.interpolator = Interpolators.LINEAR
|
||||
it.duration = duration
|
||||
it.startDelay = startDelay
|
||||
it.doOnEnd(onEndAction)
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSingleAnimator() {
|
||||
var ended = false
|
||||
animateThis("value1", duration = 100) { ended = true }
|
||||
|
||||
assertThat(value1).isEqualTo(0f)
|
||||
assertThat(ended).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(50)
|
||||
assertThat(value1).isEqualTo(0.5f)
|
||||
assertThat(ended).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(49)
|
||||
assertThat(value1).isEqualTo(0.99f)
|
||||
assertThat(ended).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(1)
|
||||
assertThat(value1).isEqualTo(1f)
|
||||
assertThat(ended).isTrue()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDelayedAnimator() {
|
||||
var ended = false
|
||||
animateThis("value1", duration = 100, startDelay = 50) { ended = true }
|
||||
|
||||
assertThat(value1).isEqualTo(-1f)
|
||||
assertThat(ended).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(49)
|
||||
assertThat(value1).isEqualTo(-1f)
|
||||
assertThat(ended).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(1)
|
||||
assertThat(value1).isEqualTo(0f)
|
||||
assertThat(ended).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(99)
|
||||
assertThat(value1).isEqualTo(0.99f)
|
||||
assertThat(ended).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(1)
|
||||
assertThat(value1).isEqualTo(1f)
|
||||
assertThat(ended).isTrue()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTwoAnimators() {
|
||||
var ended1 = false
|
||||
var ended2 = false
|
||||
animateThis("value1", duration = 100) { ended1 = true }
|
||||
animateThis("value2", duration = 200) { ended2 = true }
|
||||
assertThat(value1).isEqualTo(0f)
|
||||
assertThat(value2).isEqualTo(0f)
|
||||
assertThat(ended1).isFalse()
|
||||
assertThat(ended2).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2)
|
||||
|
||||
animatorTestRule.advanceTimeBy(99)
|
||||
assertThat(value1).isEqualTo(0.99f)
|
||||
assertThat(value2).isEqualTo(0.495f)
|
||||
assertThat(ended1).isFalse()
|
||||
assertThat(ended2).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2)
|
||||
|
||||
animatorTestRule.advanceTimeBy(1)
|
||||
assertThat(value1).isEqualTo(1f)
|
||||
assertThat(value2).isEqualTo(0.5f)
|
||||
assertThat(ended1).isTrue()
|
||||
assertThat(ended2).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(99)
|
||||
assertThat(value1).isEqualTo(1f)
|
||||
assertThat(value2).isEqualTo(0.995f)
|
||||
assertThat(ended1).isTrue()
|
||||
assertThat(ended2).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(1)
|
||||
assertThat(value1).isEqualTo(1f)
|
||||
assertThat(value2).isEqualTo(1f)
|
||||
assertThat(ended1).isTrue()
|
||||
assertThat(ended2).isTrue()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChainedAnimators() {
|
||||
var ended1 = false
|
||||
var ended2 = false
|
||||
animateThis("value1", duration = 100) {
|
||||
ended1 = true
|
||||
animateThis("value2", duration = 100) { ended2 = true }
|
||||
}
|
||||
|
||||
assertThat(value1).isEqualTo(0f)
|
||||
assertThat(value2).isEqualTo(-1f)
|
||||
assertThat(ended1).isFalse()
|
||||
assertThat(ended2).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(99)
|
||||
assertThat(value1).isEqualTo(0.99f)
|
||||
assertThat(value2).isEqualTo(-1f)
|
||||
assertThat(ended1).isFalse()
|
||||
assertThat(ended2).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(1)
|
||||
assertThat(value1).isEqualTo(1f)
|
||||
assertThat(value2).isEqualTo(0f)
|
||||
assertThat(ended1).isTrue()
|
||||
assertThat(ended2).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(99)
|
||||
assertThat(value1).isEqualTo(1f)
|
||||
assertThat(value2).isEqualTo(0.99f)
|
||||
assertThat(ended1).isTrue()
|
||||
assertThat(ended2).isFalse()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
|
||||
|
||||
animatorTestRule.advanceTimeBy(1)
|
||||
assertThat(value1).isEqualTo(1f)
|
||||
assertThat(value2).isEqualTo(1f)
|
||||
assertThat(ended1).isTrue()
|
||||
assertThat(ended2).isTrue()
|
||||
assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
@@ -25,17 +25,23 @@ import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* This test class validates that two tests' animators are isolated from each other when using the
|
||||
* same animator test rule. This is a test to prevent future instances of b/275602127.
|
||||
*/
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
@SmallTest
|
||||
@RunWithLooper(setAsMainLooper = true)
|
||||
class AnimatorTestRuleTest : SysuiTestCase() {
|
||||
class AnimatorTestRuleIsolationTest : SysuiTestCase() {
|
||||
|
||||
@get:Rule val animatorTestRule = AnimatorTestRule()
|
||||
|
||||
@Test
|
||||
fun testA() {
|
||||
// GIVEN global state is reset at the start of the test
|
||||
didTouchA = false
|
||||
didTouchB = false
|
||||
// WHEN starting 2 animations of different durations, and setting didTouchA at the end
|
||||
ObjectAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 100
|
||||
doOnEnd { didTouchA = true }
|
||||
@@ -46,15 +52,20 @@ class AnimatorTestRuleTest : SysuiTestCase() {
|
||||
doOnEnd { didTouchA = true }
|
||||
start()
|
||||
}
|
||||
// WHEN when you advance time so that only one of the animations has ended
|
||||
animatorTestRule.advanceTimeBy(100)
|
||||
// VERIFY we did indeed end the current animation
|
||||
assertThat(didTouchA).isTrue()
|
||||
// VERIFY advancing the animator did NOT cause testB's animator to end
|
||||
assertThat(didTouchB).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testB() {
|
||||
// GIVEN global state is reset at the start of the test
|
||||
didTouchA = false
|
||||
didTouchB = false
|
||||
// WHEN starting 2 animations of different durations, and setting didTouchB at the end
|
||||
ObjectAnimator.ofFloat(0f, 1f).apply {
|
||||
duration = 100
|
||||
doOnEnd { didTouchB = true }
|
||||
@@ -66,7 +77,9 @@ class AnimatorTestRuleTest : SysuiTestCase() {
|
||||
start()
|
||||
}
|
||||
animatorTestRule.advanceTimeBy(100)
|
||||
// VERIFY advancing the animator did NOT cause testA's animator to end
|
||||
assertThat(didTouchA).isFalse()
|
||||
// VERIFY we did indeed end the current animation
|
||||
assertThat(didTouchB).isTrue()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.animation
|
||||
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.android.systemui.util.test.TestExceptionDeferrer
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/**
|
||||
* This rule is intended to be used by System UI tests that are otherwise blocked from using
|
||||
* animators because of [PlatformAnimatorIsolationRule]. It is preferred that test authors use
|
||||
* [AnimatorTestRule], as that rule allows test authors to step through animations and removes the
|
||||
* need for tests to handle multiple threads. However, many System UI tests were written before this
|
||||
* was conceivable, so this rule is intended to support those legacy tests.
|
||||
*/
|
||||
class AnimatorIsolationWorkaroundRule(
|
||||
private val requiredLooper: Looper? = Looper.getMainLooper(),
|
||||
) : TestRule {
|
||||
private inner class IsolationWorkaroundHandler(ruleThread: Thread) : AnimationHandler() {
|
||||
private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
|
||||
private val addedCallbacks = mutableSetOf<AnimationFrameCallback>()
|
||||
|
||||
fun tearDownAndThrowDeferred() {
|
||||
addedCallbacks.forEach { super.removeCallback(it) }
|
||||
exceptionDeferrer.throwDeferred()
|
||||
}
|
||||
|
||||
override fun addAnimationFrameCallback(callback: AnimationFrameCallback?, delay: Long) {
|
||||
checkLooper()
|
||||
if (callback != null) {
|
||||
addedCallbacks.add(callback)
|
||||
}
|
||||
super.addAnimationFrameCallback(callback, delay)
|
||||
}
|
||||
|
||||
override fun addOneShotCommitCallback(callback: AnimationFrameCallback?) {
|
||||
checkLooper()
|
||||
super.addOneShotCommitCallback(callback)
|
||||
}
|
||||
|
||||
override fun removeCallback(callback: AnimationFrameCallback?) {
|
||||
super.removeCallback(callback)
|
||||
}
|
||||
|
||||
override fun setProvider(provider: AnimationFrameCallbackProvider?) {
|
||||
checkLooper()
|
||||
super.setProvider(provider)
|
||||
}
|
||||
|
||||
override fun autoCancelBasedOn(objectAnimator: ObjectAnimator?) {
|
||||
checkLooper()
|
||||
super.autoCancelBasedOn(objectAnimator)
|
||||
}
|
||||
|
||||
private fun checkLooper() {
|
||||
exceptionDeferrer.check(requiredLooper == null || Looper.myLooper() == requiredLooper) {
|
||||
"Animations are being registered on a different looper than the expected one!" +
|
||||
" expected=$requiredLooper actual=${Looper.myLooper()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
val workaroundHandler = IsolationWorkaroundHandler(Thread.currentThread())
|
||||
val prevInstance = AnimationHandler.setTestHandler(workaroundHandler)
|
||||
check(PlatformAnimatorIsolationRule.isIsolatingHandler(prevInstance)) {
|
||||
"AnimatorIsolationWorkaroundRule must be used within " +
|
||||
"PlatformAnimatorIsolationRule, but test handler was $prevInstance"
|
||||
}
|
||||
try {
|
||||
base.evaluate()
|
||||
val count = AnimationHandler.getAnimationCount()
|
||||
if (count > 0) {
|
||||
Log.w(TAG, "Animations still running: $count")
|
||||
}
|
||||
} finally {
|
||||
val handlerAtEnd = AnimationHandler.setTestHandler(prevInstance)
|
||||
check(workaroundHandler == handlerAtEnd) {
|
||||
"Test handler was altered: expected=$workaroundHandler actual=$handlerAtEnd"
|
||||
}
|
||||
// Pass or fail, errors caught here should be the reason the test fails
|
||||
workaroundHandler.tearDownAndThrowDeferred()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val TAG = "AnimatorIsolationWorkaroundRule"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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.animation;
|
||||
|
||||
import android.animation.AnimationHandler.AnimationFrameCallback;
|
||||
import android.annotation.NonNull;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.util.AndroidRuntimeException;
|
||||
import android.view.Choreographer;
|
||||
|
||||
import com.android.internal.util.Preconditions;
|
||||
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JUnit {@link TestRule} that can be used to run {@link Animator}s without actually waiting for the
|
||||
* duration of the animation. This also helps the test to be written in a deterministic manner.
|
||||
*
|
||||
* Create an instance of {@code AnimatorTestRule} and specify it as a {@link org.junit.Rule}
|
||||
* of the test class. Use {@link #advanceTimeBy(long)} to advance animators that have been started.
|
||||
* Note that {@link #advanceTimeBy(long)} should be called from the same thread you have used to
|
||||
* start the animator.
|
||||
*
|
||||
* <pre>
|
||||
* {@literal @}SmallTest
|
||||
* {@literal @}RunWith(AndroidJUnit4.class)
|
||||
* public class SampleAnimatorTest {
|
||||
*
|
||||
* {@literal @}Rule
|
||||
* public AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule();
|
||||
*
|
||||
* {@literal @}UiThreadTest
|
||||
* {@literal @}Test
|
||||
* public void sample() {
|
||||
* final ValueAnimator animator = ValueAnimator.ofInt(0, 1000);
|
||||
* animator.setDuration(1000L);
|
||||
* assertThat(animator.getAnimatedValue(), is(0));
|
||||
* animator.start();
|
||||
* sAnimatorTestRule.advanceTimeBy(500L);
|
||||
* assertThat(animator.getAnimatedValue(), is(500));
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
public final class AnimatorTestRule implements TestRule {
|
||||
|
||||
private final Object mLock = new Object();
|
||||
private final TestHandler mTestHandler = new TestHandler();
|
||||
/**
|
||||
* initializing the start time with {@link SystemClock#uptimeMillis()} reduces the discrepancies
|
||||
* with various internals of classes like ValueAnimator which can sometimes read that clock via
|
||||
* {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
|
||||
*/
|
||||
private final long mStartTime = SystemClock.uptimeMillis();
|
||||
private long mTotalTimeDelta = 0;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Statement apply(@NonNull final Statement base, @NonNull Description description) {
|
||||
return new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
AnimationHandler objAtStart = AnimationHandler.setTestHandler(mTestHandler);
|
||||
try {
|
||||
base.evaluate();
|
||||
} finally {
|
||||
AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart);
|
||||
if (mTestHandler != objAtEnd) {
|
||||
// pass or fail, inner logic not restoring the handler needs to be reported.
|
||||
// noinspection ThrowFromFinallyBlock
|
||||
throw new IllegalStateException("Test handler was altered: expected="
|
||||
+ mTestHandler + " actual=" + objAtEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If any new {@link Animator}s have been registered since the last time the frame time was
|
||||
* advanced, initialize them with the current frame time. Failing to do this will result in the
|
||||
* animations beginning on the *next* advancement instead, so this is done automatically for
|
||||
* test authors inside of {@link #advanceTimeBy}. However this is exposed in case authors want
|
||||
* to validate operations performed by onStart listeners.
|
||||
* <p>
|
||||
* NOTE: This is only required of the platform ValueAnimator because its start() method calls
|
||||
* {@link AnimationHandler#addAnimationFrameCallback} BEFORE it calls startAnimation(), so this
|
||||
* rule can't synchronously trigger the callback at that time.
|
||||
*/
|
||||
public void initNewAnimators() {
|
||||
requireLooper("AnimationTestRule#initNewAnimators()");
|
||||
long currentTime = getCurrentTime();
|
||||
List<AnimationFrameCallback> newCallbacks = new ArrayList<>(mTestHandler.mNewCallbacks);
|
||||
mTestHandler.mNewCallbacks.clear();
|
||||
for (AnimationFrameCallback newCallback : newCallbacks) {
|
||||
newCallback.doAnimationFrame(currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the animation clock by the given amount of delta in milliseconds. This call will
|
||||
* produce an animation frame to all the ongoing animations. This method needs to be
|
||||
* called on the same thread as {@link Animator#start()}.
|
||||
*
|
||||
* @param timeDelta the amount of milliseconds to advance
|
||||
*/
|
||||
public void advanceTimeBy(long timeDelta) {
|
||||
Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative");
|
||||
requireLooper("AnimationTestRule#advanceTimeBy(long)");
|
||||
// before advancing time, start new animators with the current time
|
||||
initNewAnimators();
|
||||
synchronized (mLock) {
|
||||
// advance time
|
||||
mTotalTimeDelta += timeDelta;
|
||||
}
|
||||
// produce a frame
|
||||
mTestHandler.doFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a
|
||||
* different time than the time tracked by {@link SystemClock} This method needs to be called on
|
||||
* the same thread as {@link Animator#start()}.
|
||||
*/
|
||||
public long getCurrentTime() {
|
||||
requireLooper("AnimationTestRule#getCurrentTime()");
|
||||
synchronized (mLock) {
|
||||
return mStartTime + mTotalTimeDelta;
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireLooper(String method) {
|
||||
if (Looper.myLooper() == null) {
|
||||
throw new AndroidRuntimeException(method + " may only be called on Looper threads");
|
||||
}
|
||||
}
|
||||
|
||||
private class TestHandler extends AnimationHandler {
|
||||
public final TestProvider mTestProvider = new TestProvider();
|
||||
private final List<AnimationFrameCallback> mNewCallbacks = new ArrayList<>();
|
||||
|
||||
TestHandler() {
|
||||
setProvider(mTestProvider);
|
||||
}
|
||||
|
||||
public void doFrame() {
|
||||
mTestProvider.animateFrame();
|
||||
mTestProvider.commitFrame();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAnimationFrameCallback(AnimationFrameCallback callback, long delay) {
|
||||
// NOTE: using the delay is infeasible because the AnimationHandler uses
|
||||
// SystemClock.uptimeMillis(); -- If we fix this to use an overridable method, then we
|
||||
// could fix this for tests.
|
||||
super.addAnimationFrameCallback(callback, 0);
|
||||
if (delay <= 0) {
|
||||
mNewCallbacks.add(callback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeCallback(AnimationFrameCallback callback) {
|
||||
super.removeCallback(callback);
|
||||
mNewCallbacks.remove(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider {
|
||||
private long mFrameDelay = 10;
|
||||
private Choreographer.FrameCallback mFrameCallback = null;
|
||||
private final List<Runnable> mCommitCallbacks = new ArrayList<>();
|
||||
|
||||
public void animateFrame() {
|
||||
Choreographer.FrameCallback frameCallback = mFrameCallback;
|
||||
mFrameCallback = null;
|
||||
if (frameCallback != null) {
|
||||
frameCallback.doFrame(getFrameTime());
|
||||
}
|
||||
}
|
||||
|
||||
public void commitFrame() {
|
||||
List<Runnable> commitCallbacks = new ArrayList<>(mCommitCallbacks);
|
||||
mCommitCallbacks.clear();
|
||||
for (Runnable commitCallback : commitCallbacks) {
|
||||
commitCallback.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postFrameCallback(Choreographer.FrameCallback callback) {
|
||||
assert mFrameCallback == null;
|
||||
mFrameCallback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postCommitCallback(Runnable runnable) {
|
||||
mCommitCallbacks.add(runnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrameDelay(long delay) {
|
||||
mFrameDelay = delay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrameDelay() {
|
||||
return mFrameDelay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getFrameTime() {
|
||||
return getCurrentTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.animation
|
||||
|
||||
import com.android.systemui.util.test.TestExceptionDeferrer
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/**
|
||||
* This rule is used by [com.android.systemui.SysuiTestCase] to fail any test which attempts to
|
||||
* start a Platform [Animator] without using [android.animation.AnimatorTestRule].
|
||||
*
|
||||
* TODO(b/291645410): enable this; currently this causes hundreds of test failures.
|
||||
*/
|
||||
class PlatformAnimatorIsolationRule : TestRule {
|
||||
|
||||
private class IsolatingAnimationHandler(ruleThread: Thread) : AnimationHandler() {
|
||||
private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
|
||||
override fun addOneShotCommitCallback(callback: AnimationFrameCallback?) = onError()
|
||||
override fun removeCallback(callback: AnimationFrameCallback?) = onError()
|
||||
override fun setProvider(provider: AnimationFrameCallbackProvider?) = onError()
|
||||
override fun autoCancelBasedOn(objectAnimator: ObjectAnimator?) = onError()
|
||||
override fun addAnimationFrameCallback(callback: AnimationFrameCallback?, delay: Long) =
|
||||
onError()
|
||||
|
||||
private fun onError() =
|
||||
exceptionDeferrer.fail(
|
||||
"Test's animations are not isolated! " +
|
||||
"Did you forget to add an AnimatorTestRule to your test class?"
|
||||
)
|
||||
|
||||
fun throwDeferred() = exceptionDeferrer.throwDeferred()
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
val isolationHandler = IsolatingAnimationHandler(Thread.currentThread())
|
||||
val originalHandler = AnimationHandler.setTestHandler(isolationHandler)
|
||||
try {
|
||||
base.evaluate()
|
||||
} finally {
|
||||
val handlerAtEnd = AnimationHandler.setTestHandler(originalHandler)
|
||||
check(isolationHandler == handlerAtEnd) {
|
||||
"Test handler was altered: expected=$isolationHandler actual=$handlerAtEnd"
|
||||
}
|
||||
// Pass or fail, a deferred exception should be the failure reason
|
||||
isolationHandler.throwDeferred()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PlatformAnimatorIsolationRule"
|
||||
|
||||
fun isIsolatingHandler(handler: AnimationHandler?): Boolean =
|
||||
handler is IsolatingAnimationHandler
|
||||
}
|
||||
}
|
||||
@@ -16,40 +16,51 @@
|
||||
|
||||
package androidx.core.animation
|
||||
|
||||
import com.android.systemui.util.test.TestExceptionDeferrer
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/**
|
||||
* This rule is used by [com.android.systemui.SysuiTestCase] to fail any test which attempts to
|
||||
* start an AndroidX [Animator] without using [androidx.core.animation.AnimatorTestRule].
|
||||
*/
|
||||
class AndroidXAnimatorIsolationRule : TestRule {
|
||||
|
||||
private class TestAnimationHandler : AnimationHandler(null) {
|
||||
override fun addAnimationFrameCallback(callback: AnimationFrameCallback?) = doFail()
|
||||
override fun removeCallback(callback: AnimationFrameCallback?) = doFail()
|
||||
override fun onAnimationFrame(frameTime: Long) = doFail()
|
||||
override fun setFrameDelay(frameDelay: Long) = doFail()
|
||||
override fun getFrameDelay(): Long = doFail()
|
||||
private class IsolatingAnimationHandler(ruleThread: Thread) : AnimationHandler(null) {
|
||||
private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
|
||||
override fun addAnimationFrameCallback(callback: AnimationFrameCallback?) = onError()
|
||||
override fun removeCallback(callback: AnimationFrameCallback?) = onError()
|
||||
override fun onAnimationFrame(frameTime: Long) = onError()
|
||||
override fun setFrameDelay(frameDelay: Long) = onError()
|
||||
|
||||
private fun onError() =
|
||||
exceptionDeferrer.fail(
|
||||
"Test's animations are not isolated! " +
|
||||
"Did you forget to add an AnimatorTestRule to your test class?"
|
||||
)
|
||||
|
||||
fun throwDeferred() = exceptionDeferrer.throwDeferred()
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return object : Statement() {
|
||||
@Throws(Throwable::class)
|
||||
override fun evaluate() {
|
||||
AnimationHandler.setTestHandler(testHandler)
|
||||
val isolationHandler = IsolatingAnimationHandler(Thread.currentThread())
|
||||
AnimationHandler.setTestHandler(isolationHandler)
|
||||
try {
|
||||
base.evaluate()
|
||||
} finally {
|
||||
AnimationHandler.setTestHandler(null)
|
||||
// Pass or fail, a deferred exception should be the failure reason
|
||||
isolationHandler.throwDeferred()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val testHandler = TestAnimationHandler()
|
||||
private fun doFail(): Nothing =
|
||||
error(
|
||||
"Test's animations are not isolated! " +
|
||||
"Did you forget to add an AnimatorTestRule to your test class?"
|
||||
)
|
||||
private companion object {
|
||||
private const val TAG = "AndroidXAnimatorIsolationRule"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 com.android.systemui.util.test
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Helper class that intercepts test errors which may be occurring on the wrong thread, and saves
|
||||
* them so that they can be rethrown back on the correct thread.
|
||||
*/
|
||||
class TestExceptionDeferrer(private val tag: String, private val testThread: Thread) {
|
||||
private val deferredErrors = mutableListOf<IllegalStateException>()
|
||||
|
||||
/** Ensure the [value] is `true`; otherwise [fail] with the produced [message] */
|
||||
fun check(value: Boolean, message: () -> Any?) {
|
||||
if (value) return
|
||||
fail(message().toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* If the [Thread.currentThread] is the [testThread], then [error], otherwise [Log] and defer
|
||||
* the error until [throwDeferred] is called.
|
||||
*/
|
||||
fun fail(message: String) {
|
||||
if (testThread == Thread.currentThread()) {
|
||||
error(message)
|
||||
} else {
|
||||
val exception = IllegalStateException(message)
|
||||
Log.e(tag, "Deferring error: ", exception)
|
||||
deferredErrors.add(exception)
|
||||
}
|
||||
}
|
||||
|
||||
/** If any [fail] or failed [check] has happened, throw the first one. */
|
||||
fun throwDeferred() {
|
||||
deferredErrors.firstOrNull()?.let { firstError ->
|
||||
Log.e(tag, "Deferred errors: ${deferredErrors.size}")
|
||||
deferredErrors.clear()
|
||||
throw firstError
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user