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

Commit fc0eed37 authored by Massimo Carli's avatar Massimo Carli Committed by Android (Google) Code Review
Browse files

Merge "[12/n] Implement tests for LetterboxControllers implementations" into main

parents 15b8a499 5dcd5e72
Loading
Loading
Loading
Loading
+142 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.wm.shell.compatui.letterbox

import android.content.Context
import android.graphics.Rect
import android.view.SurfaceControl
import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
import com.android.wm.shell.compatui.letterbox.LetterboxMatchers.asAnyMode
import org.mockito.kotlin.any
import org.mockito.kotlin.clearInvocations
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

/**
 * Robot to test [LetterboxController] implementations.
 */
open class LetterboxControllerRobotTest(
    ctx: Context,
    controllerBuilder: (LetterboxSurfaceBuilder) -> LetterboxController
) {

    companion object {
        @JvmStatic
        private val DISPLAY_ID = 1

        @JvmStatic
        private val TASK_ID = 20
    }

    private val letterboxConfiguration: LetterboxConfiguration
    private val surfaceBuilder: LetterboxSurfaceBuilder
    private val letterboxController: LetterboxController
    private val transaction: SurfaceControl.Transaction
    private val parentLeash: SurfaceControl

    init {
        letterboxConfiguration = LetterboxConfiguration(ctx)
        surfaceBuilder = LetterboxSurfaceBuilder(letterboxConfiguration)
        letterboxController = controllerBuilder(surfaceBuilder)
        transaction = getTransactionMock()
        parentLeash = mock<SurfaceControl>()
        spyOn(surfaceBuilder)
    }

    fun sendCreateSurfaceRequest(
        displayId: Int = DISPLAY_ID,
        taskId: Int = TASK_ID
    ) = letterboxController.createLetterboxSurface(
        key = LetterboxKey(displayId, taskId),
        transaction = transaction,
        parentLeash = parentLeash
    )

    fun sendDestroySurfaceRequest(
        displayId: Int = DISPLAY_ID,
        taskId: Int = TASK_ID
    ) = letterboxController.destroyLetterboxSurface(
        key = LetterboxKey(displayId, taskId),
        transaction = transaction
    )

    fun sendUpdateSurfaceVisibilityRequest(
        displayId: Int = DISPLAY_ID,
        taskId: Int = TASK_ID,
        visible: Boolean
    ) = letterboxController.updateLetterboxSurfaceVisibility(
        key = LetterboxKey(displayId, taskId),
        transaction = transaction,
        visible = visible
    )

    fun sendUpdateSurfaceBoundsRequest(
        displayId: Int = DISPLAY_ID,
        taskId: Int = TASK_ID,
        taskBounds: Rect,
        activityBounds: Rect
    ) = letterboxController.updateLetterboxSurfaceBounds(
        key = LetterboxKey(displayId, taskId),
        transaction = transaction,
        taskBounds = taskBounds,
        activityBounds = activityBounds
    )

    fun checkSurfaceBuilderInvoked(times: Int = 1, name: String = "", callSite: String = "") {
        verify(surfaceBuilder, times(times)).createSurface(
            eq(transaction),
            eq(parentLeash),
            name.asAnyMode(),
            callSite.asAnyMode(),
            any()
        )
    }

    fun checkTransactionRemovedInvoked(times: Int = 1) {
        verify(transaction, times(times)).remove(any())
    }

    fun checkVisibilityUpdated(times: Int = 1, expectedVisibility: Boolean) {
        verify(transaction, times(times)).setVisibility(any(), eq(expectedVisibility))
    }

    fun checkSurfacePositionUpdated(
        times: Int = 1,
        expectedX: Float = -1f,
        expectedY: Float = -1f
    ) {
        verify(transaction, times(times)).setPosition(
            any(),
            expectedX.asAnyMode(),
            expectedY.asAnyMode()
        )
    }

    fun checkSurfaceSizeUpdated(times: Int = 1, expectedWidth: Int = -1, expectedHeight: Int = -1) {
        verify(transaction, times(times)).setWindowCrop(
            any(),
            expectedWidth.asAnyMode(),
            expectedHeight.asAnyMode()
        )
    }

    fun resetTransitionTest() {
        clearInvocations(transaction)
    }
}
+1 −4
Original line number Diff line number Diff line
@@ -28,7 +28,6 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.verify
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.times

@@ -85,9 +84,7 @@ class LetterboxSurfaceBuilderTest : ShellTestCase() {
        init {
            letterboxConfiguration = LetterboxConfiguration(ctx)
            letterboxSurfaceBuilder = LetterboxSurfaceBuilder(letterboxConfiguration)
            tx = org.mockito.kotlin.mock<SurfaceControl.Transaction>()
            doReturn(tx).`when`(tx).setLayer(anyOrNull(), anyOrNull())
            doReturn(tx).`when`(tx).setColorSpaceAgnostic(anyOrNull(), anyOrNull())
            tx = getTransactionMock()
            parentLeash = org.mockito.kotlin.mock<SurfaceControl>()
            surfaceBuilder = SurfaceControl.Builder()
            spyOn(surfaceBuilder)
+27 −0
Original line number Diff line number Diff line
@@ -16,9 +16,36 @@

package com.android.wm.shell.compatui.letterbox

import android.view.SurfaceControl
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.verification.VerificationMode

/**
 * @return A [SurfaceControl.Transaction] mock supporting chaining for some operations. Please
 *         add other operations if needed.
 */
fun getTransactionMock(): SurfaceControl.Transaction = mock<SurfaceControl.Transaction>().apply {
    doReturn(this).`when`(this).setLayer(anyOrNull(), anyOrNull())
    doReturn(this).`when`(this).setColorSpaceAgnostic(anyOrNull(), anyOrNull())
    doReturn(this).`when`(this).setPosition(anyOrNull(), any(), any())
    doReturn(this).`when`(this).setWindowCrop(anyOrNull(), any(), any())
}

// Utility to make verification mode depending on a [Boolean].
fun Boolean.asMode(): VerificationMode = if (this) times(1) else never()

// Utility matchers to use for the main types as Mockito [VerificationMode].
object LetterboxMatchers {
    fun Int.asAnyMode() = asAnyMode { this < 0 }
    fun Float.asAnyMode() = asAnyMode { this < 0f }
    fun String.asAnyMode() = asAnyMode { this.isEmpty() }
}

private inline fun <reified T : Any> T.asAnyMode(condition: () -> Boolean) =
    (if (condition()) any() else eq(this))
+155 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.wm.shell.compatui.letterbox

import android.graphics.Rect
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import java.util.function.Consumer
import org.junit.Test
import org.junit.runner.RunWith

/**
 * Tests for [MultiSurfaceLetterboxController].
 *
 * Build/Install/Run:
 *  atest WMShellUnitTests:MultiSurfaceLetterboxControllerTest
 */
@RunWith(AndroidTestingRunner::class)
@SmallTest
class MultiSurfaceLetterboxControllerTest : ShellTestCase() {

    @Test
    fun `When creation is requested the surfaces are created if not present`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()

            r.checkSurfaceBuilderInvoked(times = 4)
        }
    }

    @Test
    fun `When creation is requested multiple times the surfaces are created once`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()

            r.checkSurfaceBuilderInvoked(times = 4)
        }
    }

    @Test
    fun `Different surfaces are created for every key`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest(displayId = 2)
            r.sendCreateSurfaceRequest(displayId = 2, taskId = 2)
            r.sendCreateSurfaceRequest(displayId = 2)
            r.sendCreateSurfaceRequest(displayId = 2, taskId = 2)

            r.checkSurfaceBuilderInvoked(times = 12)
        }
    }

    @Test
    fun `Created surface are removed once`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()
            r.checkSurfaceBuilderInvoked(times = 4)

            r.sendDestroySurfaceRequest()
            r.sendDestroySurfaceRequest()
            r.sendDestroySurfaceRequest()

            r.checkTransactionRemovedInvoked(times = 4)
        }
    }

    @Test
    fun `Only existing surfaces receive visibility update`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()
            r.sendUpdateSurfaceVisibilityRequest(visible = true)
            r.sendUpdateSurfaceVisibilityRequest(visible = true, displayId = 20)

            r.checkVisibilityUpdated(times = 4, expectedVisibility = true)
        }
    }

    @Test
    fun `Only existing surfaces receive taskBounds update`() {
        runTestScenario { r ->
            r.sendUpdateSurfaceBoundsRequest(
                taskBounds = Rect(0, 0, 2000, 1000),
                activityBounds = Rect(500, 0, 1500, 1000)
            )

            r.checkSurfacePositionUpdated(times = 0)
            r.checkSurfaceSizeUpdated(times = 0)

            r.sendCreateSurfaceRequest()

            // Pillarbox.
            r.sendUpdateSurfaceBoundsRequest(
                taskBounds = Rect(0, 0, 2000, 1000),
                activityBounds = Rect(500, 0, 1500, 1000)
            )
            // The Left and Top surfaces.
            r.checkSurfacePositionUpdated(times = 2, expectedX = 0f, expectedY = 0f)
            // The Right surface.
            r.checkSurfacePositionUpdated(times = 1, expectedX = 1500f, expectedY = 0f)
            // The Bottom surface.
            r.checkSurfacePositionUpdated(times = 1, expectedX = 0f, expectedY = 1000f)
            // Left and Right surface.
            r.checkSurfaceSizeUpdated(times = 2, expectedWidth = 500, expectedHeight = 1000)
            // Top and Button.
            r.checkSurfaceSizeUpdated(times = 2, expectedWidth = 2000, expectedHeight = 0)

            r.resetTransitionTest()

            // Letterbox.
            r.sendUpdateSurfaceBoundsRequest(
                taskBounds = Rect(0, 0, 1000, 2000),
                activityBounds = Rect(0, 500, 1000, 1500)
            )
            // Top and Left surfaces.
            r.checkSurfacePositionUpdated(times = 2, expectedX = 0f, expectedY = 0f)
            // Bottom surface.
            r.checkSurfacePositionUpdated(times = 1, expectedX = 0f, expectedY = 1500f)
            // Right surface.
            r.checkSurfacePositionUpdated(times = 1, expectedX = 1000f, expectedY = 0f)

            // Left and Right surfaces.
            r.checkSurfaceSizeUpdated(times = 2, expectedWidth = 0, expectedHeight = 2000)
            // Top and Button surfaces,
            r.checkSurfaceSizeUpdated(times = 2, expectedWidth = 1000, expectedHeight = 500)
        }
    }

    /**
     * Runs a test scenario providing a Robot.
     */
    fun runTestScenario(consumer: Consumer<LetterboxControllerRobotTest>) {
        val robot =
            LetterboxControllerRobotTest(mContext, { sb -> MultiSurfaceLetterboxController(sb) })
        consumer.accept(robot)
    }
}
+128 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.wm.shell.compatui.letterbox

import android.graphics.Rect
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import java.util.function.Consumer
import org.junit.Test
import org.junit.runner.RunWith

/**
 * Tests for [SingleSurfaceLetterboxController].
 *
 * Build/Install/Run:
 *  atest WMShellUnitTests:SingleSurfaceLetterboxControllerTest
 */
@RunWith(AndroidTestingRunner::class)
@SmallTest
class SingleSurfaceLetterboxControllerTest : ShellTestCase() {

    @Test
    fun `When creation is requested the surface is created if not present`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()

            r.checkSurfaceBuilderInvoked()
        }
    }

    @Test
    fun `When creation is requested multiple times the surface is created once`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()

            r.checkSurfaceBuilderInvoked(times = 1)
        }
    }

    @Test
    fun `A different surface is created for every key`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest()
            r.sendCreateSurfaceRequest(displayId = 2)
            r.sendCreateSurfaceRequest(displayId = 2, taskId = 2)
            r.sendCreateSurfaceRequest(displayId = 2)
            r.sendCreateSurfaceRequest(displayId = 2, taskId = 2)

            r.checkSurfaceBuilderInvoked(times = 3)
        }
    }

    @Test
    fun `Created surface is removed once`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()
            r.checkSurfaceBuilderInvoked()

            r.sendDestroySurfaceRequest()
            r.sendDestroySurfaceRequest()
            r.sendDestroySurfaceRequest()

            r.checkTransactionRemovedInvoked()
        }
    }

    @Test
    fun `Only existing surfaces receive visibility update`() {
        runTestScenario { r ->
            r.sendCreateSurfaceRequest()
            r.sendUpdateSurfaceVisibilityRequest(visible = true)
            r.sendUpdateSurfaceVisibilityRequest(visible = true, displayId = 20)

            r.checkVisibilityUpdated(expectedVisibility = true)
        }
    }

    @Test
    fun `Only existing surfaces receive taskBounds update`() {
        runTestScenario { r ->
            r.sendUpdateSurfaceBoundsRequest(
                taskBounds = Rect(0, 0, 2000, 1000),
                activityBounds = Rect(500, 0, 1500, 1000)
            )

            r.checkSurfacePositionUpdated(times = 0)
            r.checkSurfaceSizeUpdated(times = 0)

            r.resetTransitionTest()

            r.sendCreateSurfaceRequest()
            r.sendUpdateSurfaceBoundsRequest(
                taskBounds = Rect(0, 0, 2000, 1000),
                activityBounds = Rect(500, 0, 1500, 1000)
            )
            r.checkSurfacePositionUpdated(times = 1, expectedX = 0f, expectedY = 0f)
            r.checkSurfaceSizeUpdated(times = 1, expectedWidth = 2000, expectedHeight = 1000)
        }
    }

    /**
     * Runs a test scenario providing a Robot.
     */
    fun runTestScenario(consumer: Consumer<LetterboxControllerRobotTest>) {
        val robot =
            LetterboxControllerRobotTest(mContext, { sb -> SingleSurfaceLetterboxController(sb) })
        consumer.accept(robot)
    }
}