Merge "Add AnimatorTestRule for use with platform animators" into udc-qpr-dev

This commit is contained in:
Jeff DeCew
2023-08-03 18:30:42 +00:00
committed by Android (Google) Code Review
9 changed files with 821 additions and 16 deletions

View File

@@ -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;
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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"
}
}

View File

@@ -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();
}
}
}

View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -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
}
}
}