Merge "Create onTeardown and use it." into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt
new file mode 100644
index 0000000..8635bb0
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/OnTeardownRuleTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2024 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
+
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.JUnitCore
+
+@Suppress("JUnitMalformedDeclaration")
+@SmallTest
+class OnTeardownRuleTest : SysuiTestCase() {
+    // None of these inner classes should be run except as part of this utilities-testing test
+    class HasTeardown {
+        @get:Rule val teardownRule = OnTeardownRule()
+
+        @Before
+        fun setUp() {
+            teardownWasRun = false
+            teardownRule.onTeardown { teardownWasRun = true }
+        }
+
+        @Test fun doTest() {}
+
+        companion object {
+            var teardownWasRun = false
+        }
+    }
+
+    @Test
+    fun teardownRuns() {
+        val result = JUnitCore().run(HasTeardown::class.java)
+        assertThat(result.failures).isEmpty()
+        assertThat(HasTeardown.teardownWasRun).isTrue()
+    }
+
+    class FirstTeardownFails {
+        @get:Rule val teardownRule = OnTeardownRule()
+
+        @Before
+        fun setUp() {
+            teardownWasRun = false
+            teardownRule.onTeardown { fail("One fails") }
+            teardownRule.onTeardown { teardownWasRun = true }
+        }
+
+        @Test fun doTest() {}
+
+        companion object {
+            var teardownWasRun = false
+        }
+    }
+
+    @Test
+    fun allTeardownsRun() {
+        val result = JUnitCore().run(FirstTeardownFails::class.java)
+        assertThat(result.failures.map { it.message }).isEqualTo(listOf("One fails"))
+        assertThat(FirstTeardownFails.teardownWasRun).isTrue()
+    }
+
+    class ThreeTeardowns {
+        @get:Rule val teardownRule = OnTeardownRule()
+
+        @Before
+        fun setUp() {
+            messages.clear()
+        }
+
+        @Test
+        fun doTest() {
+            teardownRule.onTeardown { messages.add("A") }
+            teardownRule.onTeardown { messages.add("B") }
+            teardownRule.onTeardown { messages.add("C") }
+        }
+
+        companion object {
+            val messages = mutableListOf<String>()
+        }
+    }
+
+    @Test
+    fun reverseOrder() {
+        val result = JUnitCore().run(ThreeTeardowns::class.java)
+        assertThat(result.failures).isEmpty()
+        assertThat(ThreeTeardowns.messages).isEqualTo(listOf("C", "B", "A"))
+    }
+
+    class TryToDoABadThing {
+        @get:Rule val teardownRule = OnTeardownRule()
+
+        @Test
+        fun doTest() {
+            teardownRule.onTeardown {
+                teardownRule.onTeardown {
+                    // do nothing
+                }
+            }
+        }
+    }
+
+    @Test
+    fun prohibitTeardownDuringTeardown() {
+        val result = JUnitCore().run(TryToDoABadThing::class.java)
+        assertThat(result.failures.map { it.message })
+            .isEqualTo(listOf("Cannot add new teardown routines after test complete."))
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
index e0d9fac..110dec6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerViaListenerTest.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
 import com.android.systemui.log.logcatLogBuffer
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
@@ -58,7 +57,6 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
-import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -144,6 +142,7 @@
         controller.start()
         controller.addCallback(mockOngoingCallListener)
         controller.setChipView(chipView)
+        onTeardown { controller.tearDownChipView() }
 
         val collectionListenerCaptor = ArgumentCaptor.forClass(NotifCollectionListener::class.java)
         verify(notificationCollection).addCollectionListener(collectionListenerCaptor.capture())
@@ -153,11 +152,6 @@
             .thenReturn(PROC_STATE_INVISIBLE)
     }
 
-    @After
-    fun tearDown() {
-        controller.tearDownChipView()
-    }
-
     @Test
     fun onEntryUpdated_isOngoingCallNotif_listenerAndRepoNotified() {
         val notification = NotificationEntryBuilder(createOngoingCallNotifEntry())
@@ -224,7 +218,7 @@
         notifCollectionListener.onEntryUpdated(notification.build())
         chipView.measure(
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
         )
 
         assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
@@ -241,7 +235,7 @@
         notifCollectionListener.onEntryUpdated(notification.build())
         chipView.measure(
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
         )
 
         assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
@@ -257,7 +251,7 @@
         notifCollectionListener.onEntryUpdated(notification.build())
         chipView.measure(
             View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
-            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
+            View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
         )
 
         assertThat(chipView.findViewById<View>(R.id.ongoing_activity_chip_time)?.measuredWidth)
@@ -668,7 +662,7 @@
 
     private fun createCallNotifEntry(
         callStyle: Notification.CallStyle,
-        nullContentIntent: Boolean = false
+        nullContentIntent: Boolean = false,
     ): NotificationEntry {
         val notificationEntryBuilder = NotificationEntryBuilder()
         notificationEntryBuilder.modifyNotification(context).style = callStyle
diff --git a/packages/SystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java b/packages/SystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java
index a95735e..82cfab6 100644
--- a/packages/SystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java
+++ b/packages/SystemUI/tests/src/com/android/AAAPlusPlusVerifySysuiRequiredTestPropertiesTest.java
@@ -56,7 +56,6 @@
 @RunWith(AndroidTestingRunner.class)
 @SmallTest
 public class AAAPlusPlusVerifySysuiRequiredTestPropertiesTest extends SysuiTestCase {
-
     private static final String TAG = "AAA++VerifyTest";
 
     private static final Class[] BASE_CLS_TO_INCLUDE = {
@@ -149,6 +148,9 @@
      */
     private boolean isTestClass(Class<?> loadedClass) {
         try {
+            if (loadedClass.getAnnotation(SkipSysuiVerification.class) != null) {
+                return false;
+            }
             if (Modifier.isAbstract(loadedClass.getModifiers())) {
                 logDebug(String.format("Skipping abstract class %s: not a test",
                         loadedClass.getName()));
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/OnTeardownRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/OnTeardownRule.kt
new file mode 100644
index 0000000..778614b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/OnTeardownRule.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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
+
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.junit.runners.model.MultipleFailureException
+
+/**
+ * Rule that allows teardown steps to be added right next to the places where it becomes clear they
+ * are needed. This can avoid the need for complicated or conditional logic in a single teardown
+ * method. Examples:
+ * ```
+ * @get:Rule teardownRule = OnTeardownRule()
+ *
+ * // setup and teardown right next to each other
+ * @Before
+ * fun setUp() {
+ *   val oldTimeout = getGlobalTimeout()
+ *   teardownRule.onTeardown { setGlobalTimeout(oldTimeout) }
+ *   overrideGlobalTimeout(5000)
+ * }
+ *
+ * // add teardown logic for fixtures that aren't used in every test
+ * fun addCustomer() {
+ *   val id = globalDatabase.addCustomer(TEST_NAME, TEST_ADDRESS, ...)
+ *   teardownRule.onTeardown { globalDatabase.deleteCustomer(id) }
+ * }
+ * ```
+ */
+class OnTeardownRule : TestWatcher() {
+    private var canAdd = true
+    private val teardowns = mutableListOf<() -> Unit>()
+
+    fun onTeardown(teardownRunnable: () -> Unit) {
+        if (!canAdd) {
+            throw IllegalStateException("Cannot add new teardown routines after test complete.")
+        }
+        teardowns.add(teardownRunnable)
+    }
+
+    fun onTeardown(teardownRunnable: Runnable) {
+        if (!canAdd) {
+            throw IllegalStateException("Cannot add new teardown routines after test complete.")
+        }
+        teardowns.add { teardownRunnable.run() }
+    }
+
+    override fun finished(description: Description?) {
+        canAdd = false
+        val errors = mutableListOf<Throwable>()
+        teardowns.reversed().forEach {
+            try {
+                it()
+            } catch (e: Throwable) {
+                errors.add(e)
+            }
+        }
+        MultipleFailureException.assertEmpty(errors)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
index 27a2cab..153a8be 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
@@ -56,6 +56,8 @@
 
 import java.io.FileInputStream;
 import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Future;
@@ -69,6 +71,17 @@
 // background on Ravenwood is available at go/ravenwood-docs
 @DisabledOnRavenwood
 public abstract class SysuiTestCase {
+    /**
+     * Especially when self-testing test utilities, we may have classes that look like test
+     * classes, but we don't expect to ever actually run as a top-level test.
+     * For example, {@link com.android.systemui.TryToDoABadThing}.
+     * Verifying properties on these as a part of structural tests like
+     * AAAPlusPlusVerifySysuiRequiredTestPropertiesTest is a waste of our time, and makes things
+     * look more confusing, so this lets us skip when appropriate.
+     */
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface SkipSysuiVerification {
+    }
 
     private static final String TAG = "SysuiTestCase";
 
@@ -172,6 +185,15 @@
     public final DexmakerShareClassLoaderRule mDexmakerShareClassLoaderRule =
             new DexmakerShareClassLoaderRule();
 
+    @Rule public final OnTeardownRule mTearDownRule = new OnTeardownRule();
+
+    /**
+     * Schedule a cleanup routine to happen when the test state is torn down.
+     */
+    protected void onTeardown(Runnable tearDownRunnable) {
+        mTearDownRule.onTeardown(tearDownRunnable);
+    }
+
     // set the highest order so it's the innermost rule
     @Rule(order = Integer.MAX_VALUE)
     public TestWithLooperRule mlooperRule = new TestWithLooperRule();