Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f792c8cf authored by Fabián Kozynski's avatar Fabián Kozynski
Browse files

Add support for removeCallbacks in MockExecutorHandler

If the handler is constructed with a DelayableExecutor, it adds support
for cancelling runnables scheduled with postDelayed and postAtTime. It
has the same behavior as in Handler: calling it with a Runnable will
remove all pending instances of that runnable.

Test: atest MockExecutorHandlerTest
Bug: 339290820
Flag: NA

Change-Id: I47d68931ac9225efd3f1b6a211fe6475fcddaf06
parent b464f153
Loading
Loading
Loading
Loading
+82 −0
Original line number Diff line number Diff line
@@ -141,6 +141,88 @@ class MockExecutorHandlerTest : SysuiTestCase() {
        assertEquals(3, runnable.mRunCount)
    }

    @Test
    fun testRemoveCallback_postDelayed() {
        val clock = FakeSystemClock()
        val fakeExecutor = FakeExecutor(clock)
        val handler = mockExecutorHandler(fakeExecutor)
        val runnable = RunnableImpl()

        handler.postDelayed(runnable, 50)
        handler.postDelayed(runnable, 150)
        fakeExecutor.advanceClockToNext()
        fakeExecutor.runAllReady()

        assertEquals(1, runnable.mRunCount)
        assertEquals(1, fakeExecutor.numPending())

        handler.removeCallbacks(runnable)
        assertEquals(0, fakeExecutor.numPending())

        assertEquals(1, runnable.mRunCount)
    }

    @Test
    fun testRemoveCallback_postAtTime() {
        val clock = FakeSystemClock()
        val fakeExecutor = FakeExecutor(clock)
        val handler = mockExecutorHandler(fakeExecutor)
        val runnable = RunnableImpl()
        assertEquals(10000, clock.uptimeMillis())

        handler.postAtTime(runnable, 10050)
        handler.postAtTime(runnable, 10150)
        fakeExecutor.advanceClockToNext()
        fakeExecutor.runAllReady()

        assertEquals(1, runnable.mRunCount)
        assertEquals(1, fakeExecutor.numPending())

        handler.removeCallbacks(runnable)
        assertEquals(0, fakeExecutor.numPending())

        assertEquals(1, runnable.mRunCount)
    }

    @Test
    fun testRemoveCallback_mixed_allRemoved() {
        val clock = FakeSystemClock()
        val fakeExecutor = FakeExecutor(clock)
        val handler = mockExecutorHandler(fakeExecutor)
        val runnable = RunnableImpl()
        assertEquals(10000, clock.uptimeMillis())

        handler.postAtTime(runnable, 10050)
        handler.postDelayed(runnable, 150)

        handler.removeCallbacks(runnable)
        assertEquals(0, fakeExecutor.numPending())

        fakeExecutor.advanceClockToLast()
        fakeExecutor.runAllReady()
        assertEquals(0, runnable.mRunCount)
    }

    @Test
    fun testRemoveCallback_differentRunnables_onlyMatchingRemoved() {
        val clock = FakeSystemClock()
        val fakeExecutor = FakeExecutor(clock)
        val handler = mockExecutorHandler(fakeExecutor)
        val runnable1 = RunnableImpl()
        val runnable2 = RunnableImpl()

        handler.postDelayed(runnable1, 50)
        handler.postDelayed(runnable2, 150)

        handler.removeCallbacks(runnable1)
        assertEquals(1, fakeExecutor.numPending())

        fakeExecutor.advanceClockToLast()
        fakeExecutor.runAllReady()
        assertEquals(0, runnable1.mRunCount)
        assertEquals(1, runnable2.mRunCount)
    }

    /**
     * Verifies that `Handler.removeMessages`, which doesn't make sense with executor backing,
     * causes an error in the test (rather than failing silently like most mocks).
+42 −2
Original line number Diff line number Diff line
@@ -17,6 +17,8 @@
package com.android.systemui.util.concurrency

import android.os.Handler
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.Executor
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyLong
@@ -29,9 +31,14 @@ import org.mockito.stubbing.Answer
 * Wrap an [Executor] in a mock [Handler] that execute when [Handler.post] is called, and throws an
 * exception otherwise. This is useful when a class requires a Handler only because Handlers are
 * used by ContentObserver, and no other methods are used.
 *
 * If the [executor] is a [DelayableExecutor], it also supports:
 * * [Handler.postDelayed] with a Runnable parameter
 * * [Handler.postAtTime] with a RunnableParameter
 */
fun mockExecutorHandler(executor: Executor): Handler {
    val handlerMock = Mockito.mock(Handler::class.java, RuntimeExceptionAnswer())
    val cancellations = ConcurrentHashMap<Runnable, MutableList<Cancellation>>()
    doAnswer { invocation: InvocationOnMock ->
            executor.execute(invocation.getArgument(0))
            true
@@ -42,7 +49,19 @@ fun mockExecutorHandler(executor: Executor): Handler {
        doAnswer { invocation: InvocationOnMock ->
                val runnable = invocation.getArgument<Runnable>(0)
                val uptimeMillis = invocation.getArgument<Long>(1)
                executor.executeAtTime(runnable, uptimeMillis)
                val token = Any()
                val canceller =
                    executor.executeAtTime(
                        {
                            cancellations.get(runnable)?.removeIf { it.token == token }
                            cancellations.remove(runnable, emptyList())
                            runnable.run()
                        },
                        uptimeMillis
                    )
                cancellations
                    .getOrPut(runnable) { CopyOnWriteArrayList() }
                    .add(Cancellation(token, canceller))
                true
            }
            .`when`(handlerMock)
@@ -50,15 +69,36 @@ fun mockExecutorHandler(executor: Executor): Handler {
        doAnswer { invocation: InvocationOnMock ->
                val runnable = invocation.getArgument<Runnable>(0)
                val delayInMillis = invocation.getArgument<Long>(1)
                executor.executeDelayed(runnable, delayInMillis)
                val token = Any()
                val canceller =
                    executor.executeDelayed(
                        {
                            cancellations.get(runnable)?.removeIf { it.token == token }
                            cancellations.remove(runnable, emptyList())
                            runnable.run()
                        },
                        delayInMillis
                    )
                cancellations
                    .getOrPut(runnable) { CopyOnWriteArrayList() }
                    .add(Cancellation(token, canceller))
                true
            }
            .`when`(handlerMock)
            .postDelayed(any(), anyLong())
        doAnswer { invocation: InvocationOnMock ->
                val runnable = invocation.getArgument<Runnable>(0)
                cancellations.remove(runnable)?.forEach(Runnable::run)
                Unit
            }
            .`when`(handlerMock)
            .removeCallbacks(any())
    }
    return handlerMock
}

private class Cancellation(val token: Any, canceller: Runnable) : Runnable by canceller

private class RuntimeExceptionAnswer : Answer<Any> {
    override fun answer(invocation: InvocationOnMock): Any {
        throw RuntimeException(invocation.method.name + " is not stubbed")