diff --git a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt new file mode 100644 index 0000000000..46a3588f9b --- /dev/null +++ b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2019 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.testutils + +import android.os.Handler +import android.os.HandlerThread +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +private const val ATTEMPTS = 50 // Causes testWaitForIdle to take about 150ms on aosp_crosshatch-eng +private const val TIMEOUT_MS = 200 + +@RunWith(JUnit4::class) +class HandlerUtilsTest { + @Test + fun testWaitForIdle() { + val handlerThread = HandlerThread("testHandler").apply { start() } + + // Tests that waitForIdle can be called many times without ill impact if the service is + // already idle. + repeat(ATTEMPTS) { + handlerThread.waitForIdle(TIMEOUT_MS) + } + + // Tests that calling waitForIdle waits for messages to be processed. Use both an + // inline runnable that's instantiated at each loop run and a runnable that's instantiated + // once for all. + val tempRunnable = object : Runnable { + // Use StringBuilder preferentially to StringBuffer because StringBuilder is NOT + // thread-safe. It's part of the point that both runnables run on the same thread + // so if anything is wrong in that space it's better to opportunistically use a class + // where things might go wrong, even if there is no guarantee of failure. + var memory = StringBuilder() + override fun run() { + memory.append("b") + } + } + repeat(ATTEMPTS) { i -> + handlerThread.threadHandler.post { tempRunnable.memory.append("a"); } + handlerThread.threadHandler.post(tempRunnable) + handlerThread.waitForIdle(TIMEOUT_MS) + assertEquals(tempRunnable.memory.toString(), "ab".repeat(i + 1)) + } + } + + // Statistical test : even if visibleOnHandlerThread doesn't work this is likely to succeed, + // but it will be at least flaky. + @Test + fun testVisibleOnHandlerThread() { + val handlerThread = HandlerThread("testHandler").apply { start() } + val handler = Handler(handlerThread.looper) + + repeat(ATTEMPTS) { attempt -> + var x = -10 + visibleOnHandlerThread(handler) { x = attempt } + assertEquals(attempt, x) + handler.post { assertEquals(attempt, x) } + } + } +} diff --git a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt index 861f45ee3c..68713490d2 100644 --- a/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt +++ b/staticlibs/testutils/devicetests/com/android/testutils/HandlerUtils.kt @@ -21,9 +21,14 @@ package com.android.testutils import android.os.ConditionVariable import android.os.Handler import android.os.HandlerThread +import android.util.Log +import com.android.testutils.FunctionalUtils.ThrowingRunnable +import java.lang.Exception import java.util.concurrent.Executor import kotlin.test.fail +private const val TAG = "HandlerUtils" + /** * Block until the specified Handler or HandlerThread becomes idle, or until timeoutMs has passed. */ @@ -48,3 +53,28 @@ fun waitForIdleSerialExecutor(executor: Executor, timeoutMs: Long) { fail("Executor did not become idle after ${timeoutMs}ms") } } + +/** + * Executes a block of code, making its side effects visible on the caller and the handler thread + * + * After this function returns, the side effects of the passed block of code are guaranteed to be + * observed both on the thread running the handler and on the thread running this method. + * To achieve this, this method runs the passed block on the handler and blocks this thread + * until it's executed, so keep in mind this method will block, (including, if the handler isn't + * running, blocking forever). + */ +fun visibleOnHandlerThread(handler: Handler, r: ThrowingRunnable) { + val cv = ConditionVariable() + handler.post { + try { + r.run() + } catch (exception: Exception) { + Log.e(TAG, "visibleOnHandlerThread caught exception", exception) + } + cv.open() + } + // After block() returns, the handler thread has seen the change (since it ran it) + // and this thread also has seen the change (since cv.open() happens-before cv.block() + // returns). + cv.block() +}