Merge "Implement DevSdkIgnoreRule with SdkLevel"
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
new file mode 100644
index 0000000..f4a7d10
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTest.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2021 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.net.module.util
+
+import android.util.Log
+import com.android.testutils.tryTest
+import kotlin.test.assertFailsWith
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import kotlin.test.fail
+
+private val TAG = CleanupTest::class.toString()
+
+@RunWith(JUnit4::class)
+class CleanupTest {
+    class TestException1 : Exception()
+    class TestException2 : Exception()
+
+    @Test
+    fun testNotThrow() {
+        var x = 1
+        tryTest {
+            x = 2
+            Log.e(TAG, "Do nothing")
+        } cleanup {
+            assert(x == 2)
+            x = 3
+            Log.e(TAG, "Do nothing")
+        }
+        assert(x == 3)
+    }
+
+    @Test
+    fun testThrowTry() {
+        var x = 1
+        assertFailsWith<TestException1> {
+            tryTest {
+                x = 2
+                throw TestException1()
+                x = 4
+            } cleanup {
+                assert(x == 2)
+                x = 3
+                Log.e(TAG, "Do nothing")
+            }
+        }
+        assert(x == 3)
+    }
+
+    @Test
+    fun testThrowCleanup() {
+        var x = 1
+        assertFailsWith<TestException2> {
+            tryTest {
+                x = 2
+                Log.e(TAG, "Do nothing")
+            } cleanup {
+                assert(x == 2)
+                x = 3
+                throw TestException2()
+                x = 4
+            }
+        }
+        assert(x == 3)
+    }
+
+    @Test
+    fun testThrowBoth() {
+        var x = 1
+        try {
+            tryTest {
+                x = 2
+                throw TestException1()
+                x = 3
+            } cleanup {
+                assert(x == 2)
+                x = 4
+                throw TestException2()
+                x = 5
+            }
+            fail("Expected failure with TestException1")
+        } catch (e: TestException1) {
+            assert(e.suppressedExceptions[0] is TestException2)
+        }
+        assert(x == 4)
+    }
+}
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTestJava.java b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTestJava.java
new file mode 100644
index 0000000..ba4e679
--- /dev/null
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/CleanupTestJava.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 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.net.module.util;
+
+import static com.android.testutils.Cleanup.testAndCleanup;
+import static com.android.testutils.MiscAsserts.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CleanupTestJava {
+    private static final String TAG = CleanupTestJava.class.getSimpleName();
+    private static final class TestException1 extends Exception {}
+    private static final class TestException2 extends Exception {}
+
+    @Test
+    public void testNotThrow() {
+        final AtomicInteger x = new AtomicInteger(1);
+        testAndCleanup(() -> {
+            x.compareAndSet(1, 2);
+            Log.e(TAG, "Do nothing");
+        }, () -> {
+                x.compareAndSet(2, 3);
+                Log.e(TAG, "Do nothing");
+            });
+        assertEquals(3, x.get());
+    }
+
+    @Test
+    public void testThrowTry() {
+        final AtomicInteger x = new AtomicInteger(1);
+        assertThrows(TestException1.class, () ->
+                testAndCleanup(() -> {
+                    x.compareAndSet(1, 2);
+                    throw new TestException1();
+                    // Java refuses to call x.set(3) here because this line is unreachable
+                }, () -> {
+                        x.compareAndSet(2, 3);
+                        Log.e(TAG, "Do nothing");
+                    })
+        );
+        assertEquals(3, x.get());
+    }
+
+    @Test
+    public void testThrowCleanup() {
+        final AtomicInteger x = new AtomicInteger(1);
+        assertThrows(TestException2.class, () ->
+                testAndCleanup(() -> {
+                    x.compareAndSet(1, 2);
+                    Log.e(TAG, "Do nothing");
+                }, () -> {
+                        x.compareAndSet(2, 3);
+                        throw new TestException2();
+                        // Java refuses to call x.set(4) here because this line is unreachable
+                    })
+        );
+        assertEquals(3, x.get());
+    }
+
+    @Test
+    public void testThrowBoth() {
+        final AtomicInteger x = new AtomicInteger(1);
+        assertThrows(TestException1.class, () ->
+                testAndCleanup(() -> {
+                    x.compareAndSet(1, 2);
+                    throw new TestException1();
+                }, () -> {
+                        x.compareAndSet(2, 3);
+                        throw new TestException2();
+                    })
+        );
+        assertEquals(3, x.get());
+    }
+}
diff --git a/staticlibs/testutils/Android.bp b/staticlibs/testutils/Android.bp
index 89aa136..153285b 100644
--- a/staticlibs/testutils/Android.bp
+++ b/staticlibs/testutils/Android.bp
@@ -51,6 +51,9 @@
       "//frameworks/libs/net/common/tests:__subpackages__",
       "//frameworks/libs/net/client-libs/tests:__subpackages__",
   ],
+  libs: [
+      "jsr305",
+  ],
   static_libs: [
       "kotlin-test"
   ]
diff --git a/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
new file mode 100644
index 0000000..769d980
--- /dev/null
+++ b/staticlibs/testutils/hostdevice/com/android/testutils/Cleanup.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+@file:JvmName("Cleanup")
+
+package com.android.testutils
+
+import com.android.testutils.ExceptionUtils.ThrowingRunnable
+import javax.annotation.CheckReturnValue
+
+/**
+ * Utility to do cleanup in tests without replacing exceptions with those from a finally block.
+ *
+ * This utility is meant for tests that want to do cleanup after they execute their test
+ * logic, whether the test fails (and throws) or not.
+ *
+ * The usual way of doing this is to have a try{}finally{} block and put cleanup in finally{}.
+ * However, if any code in finally{} throws, the exception thrown in finally{} is thrown before
+ * any thrown in try{} ; that means errors reported from tests are from finally{} even if they
+ * have been caused by errors in try{}. This is unhelpful in tests, because it results in a
+ * stacktrace for a symptom rather than a stacktrace for a cause.
+ *
+ * To alleviate this, tests are encouraged to make sure the code in finally{} can't throw, or
+ * that the code in try{} can't cause it to fail. This is not always realistic ; not only does
+ * it require the developer thinks about complex interactions of code, test code often relies
+ * on bricks provided by other teams, not controlled by the team writing the test, which may
+ * start throwing with an update (see b/198998862 for an example).
+ *
+ * This utility allows a different approach : it offers a new construct, tryTest{}cleanup{} similar
+ * to try{}finally{}, but that will always throw the first exception that happens. In other words,
+ * if only tryTest{} throws or only cleanup{} throws, that exception will be thrown, but contrary
+ * to the standard try{}finally{}, if both throws, the construct throws the exception that happened
+ * in tryTest{} rather than the one that happened in cleanup{}.
+ *
+ * Kotlin usage is as try{}finally{} :
+ * tryTest {
+ *   testing code
+ * } cleanup {
+ *   cleanup code
+ * }
+ *
+ * Java doesn't allow this kind of syntax, so instead a function taking 2 lambdas is provided.
+ * testAndCleanup(() -> {
+ *   testing code
+ * }, () -> {
+ *   cleanup code
+ * });
+ */
+class ExceptionCleanupBlock(val originalException: Exception?) {
+    inline infix fun cleanup(block: () -> Unit) {
+        try {
+            block()
+            if (null != originalException) throw originalException
+        } catch (e: Exception) {
+            if (null == originalException) {
+                throw e
+            } else {
+                originalException.addSuppressed(e)
+                throw originalException
+            }
+        }
+    }
+}
+
+@CheckReturnValue
+inline fun tryTest(block: () -> Unit): ExceptionCleanupBlock {
+    try {
+        block()
+    } catch (e: Exception) {
+        return ExceptionCleanupBlock(e)
+    }
+    return ExceptionCleanupBlock(null)
+}
+
+// Java support
+fun testAndCleanup(tryBlock: ThrowingRunnable, cleanupBlock: ThrowingRunnable) {
+    tryTest {
+        tryBlock.run()
+    } cleanup {
+        cleanupBlock.run()
+    }
+}