blob: fe0f4cfc03211dd7ce9b8d4b48bf154e5d6068cb [file] [log] [blame]
/*
* Copyright (C) 2022 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.sdklib.deviceprovisioner
import com.android.adblib.deviceProperties
import com.android.adblib.testing.FakeAdbSession
import com.android.adblib.testingutils.CoroutineTestUtils.runBlockingWithTimeout
import com.android.adblib.testingutils.CoroutineTestUtils.yieldUntil
import com.android.sdklib.AndroidVersion
import com.android.sdklib.SystemImageTags
import com.android.sdklib.deviceprovisioner.DeviceState.Connected
import com.android.sdklib.deviceprovisioner.DeviceState.Disconnected
import com.android.sdklib.devices.Abi
import com.android.sdklib.internal.avd.AvdInfo
import com.android.sdklib.internal.avd.AvdInfo.AvdStatus
import com.android.sdklib.internal.avd.AvdManager
import com.google.common.truth.Truth.assertThat
import com.google.wireless.android.sdk.stats.DeviceInfo
import com.google.wireless.android.sdk.stats.DeviceInfo.ApplicationBinaryInterface
import com.google.wireless.android.sdk.stats.DeviceInfo.MdnsConnectionType
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.atomic.AtomicInteger
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch
import org.junit.After
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class LocalEmulatorProvisionerPluginTest {
@get:Rule val temporaryFolder = TemporaryFolder()
val session = FakeAdbSession()
private val deviceIcons =
DeviceIcons(EmptyIcon.DEFAULT, EmptyIcon.DEFAULT, EmptyIcon.DEFAULT, EmptyIcon.DEFAULT)
private lateinit var avdsPath: Path
private lateinit var avdManager: FakeAvdManager
private lateinit var plugin: LocalEmulatorProvisionerPlugin
private lateinit var provisioner: DeviceProvisioner
@Before
fun setUp() {
avdsPath = temporaryFolder.newFolder("avds").toPath()
avdManager = FakeAvdManager(session, avdsPath)
plugin =
LocalEmulatorProvisionerPlugin(
session.scope,
session,
avdManager,
deviceIcons,
TestDefaultDeviceActionPresentation,
Dispatchers.IO,
Duration.ofMillis(100),
)
provisioner = DeviceProvisioner.create(session, listOf(plugin), deviceIcons)
}
@After
fun tearDown() {
avdManager.close()
session.close()
}
@Test
fun propertiesEquality() {
val props = buildProperties(avdManager.makeAvdInfo(1, AndroidVersion(29), hasPlayStore = true))
val builder = props.toBuilder()
val newProps = builder.build()
assertThat(newProps).isEqualTo(props)
assertThat(newProps).isNotSameAs(props)
}
@Test
fun genericPropertiesUpdate() {
val avdProps =
buildProperties(avdManager.makeAvdInfo(1, AndroidVersion(29), hasPlayStore = true))
val baseProps =
DeviceProperties.build {
populateDeviceInfoProto("Test", null, emptyMap(), "1")
model = "Phone 6"
icon = EmptyIcon.DEFAULT
}
val props = listOf(avdProps, baseProps)
val updatedProps = props.map { it.toBuilder().apply { manufacturer = "XYZ" }.build() }
assertThat(updatedProps[0]).isInstanceOf(LocalEmulatorProperties::class.java)
assertThat((updatedProps[0] as LocalEmulatorProperties).avdPath).isEqualTo(avdProps.avdPath)
assertThat(updatedProps[1]).isInstanceOf(BaseDeviceProperties::class.java)
updatedProps.forEach { assertThat(it.manufacturer).isEqualTo("XYZ") }
}
@Test
fun offlineDevices(): Unit = runBlockingWithTimeout {
avdManager.createAvd()
avdManager.createAvd()
yieldUntil { provisioner.devices.value.size == 2 }
val devices = provisioner.devices.value
assertThat(devices.map { it.state.properties.title })
.containsExactly("Fake Device 1", "Fake Device 2")
checkProperties(devices[0].state.properties as LocalEmulatorProperties)
}
@Test
fun removeOfflineDevice(): Unit = runBlockingWithTimeout {
val n = 20
repeat(n) { avdManager.createAvd() }
val avds = avdManager.rescanAvds()
yieldUntil { provisioner.devices.value.size == n }
avdManager.deleteAvd(avds[0])
yieldUntil { provisioner.devices.value.size == n - 1 }
val displayNames = provisioner.devices.value.map { it.state.properties.title }
assertThat(displayNames).doesNotContain(avds[0].displayName)
assertThat(displayNames).contains(avds[1].displayName)
}
@Test
fun removedOfflineDeviceScopeIsCancelled(): Unit = runBlockingWithTimeout {
avdManager.createAvd()
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
val job = handle.scope.launch { handle.stateFlow.collect {} }
avdManager.deleteAvd(avdManager.avds[0])
yieldUntil { provisioner.devices.value.isEmpty() }
job.join()
}
@Test
fun startAndStopDevice(): Unit = runBlockingWithTimeout {
avdManager.createAvd()
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
handle.activationAction?.activate()
assertThat(handle.state.connectedDevice).isNotNull()
handle.awaitReady()
assertThat(provisioner.devices.value.map { it.state.properties.title })
.containsExactly("Fake Device 1")
val properties = provisioner.devices.value[0].state.properties as LocalEmulatorProperties
checkProperties(properties)
assertThat(properties.androidRelease).isEqualTo(RELEASE)
handle.deactivationAction?.deactivate()
assertThat(handle.state.connectedDevice).isNull()
assertThat(handle.state).isInstanceOf(Disconnected::class.java)
assertThat(provisioner.devices.value.map { it.state.properties.title })
.containsExactly("Fake Device 1")
}
@Test
fun coldBootDevice(): Unit = runBlockingWithTimeout {
avdManager.createAvd()
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
handle.coldBootAction?.activate()
val connectedDevice = checkNotNull(handle.state.connectedDevice)
assertThat(handle.state.isReady).isFalse()
avdManager.finishBoot(connectedDevice)
handle.awaitReady()
assertThat(handle.state.isReady).isTrue()
assertThat(connectedDevice.deviceProperties().allReadonly()["ro.test.coldboot"]).isEqualTo("1")
assertThat(handle.id.pluginId).isEqualTo(LocalEmulatorProvisionerPlugin.PLUGIN_ID)
}
@Test
fun bootSnapshot(): Unit = runBlockingWithTimeout {
val avdInfo = checkNotNull(avdManager.createAvd())
val snapshotPath = avdInfo.dataFolderPath.resolve("snapshots").resolve("snap1")
createNormalSnapshot(snapshotPath)
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
val snapshots = runBlockingWithTimeout { handle.bootSnapshotAction!!.snapshots() }
handle.bootSnapshotAction?.activate(snapshot = snapshots[0])
val connectedDevice = checkNotNull(handle.state.connectedDevice)
handle.awaitReady()
assertThat(connectedDevice.deviceProperties().allReadonly()["ro.test.snapshot"])
.isEqualTo(snapshotPath.toString())
}
@Test
fun bootFailure(): Unit = runBlockingWithTimeout {
val avdInfo =
avdManager.makeAvdInfo(1).let {
it.copy(properties = it.properties + (LAUNCH_EXCEPTION_MESSAGE to "AVD is broken"))
}
avdManager.createAvd(avdInfo)
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
try {
handle.activationAction?.activate()
fail("Expected DeviceActionException")
} catch (expected: DeviceActionException) {
assertThat(expected.message).isEqualTo("AVD is broken")
}
// This flag is reset immediately after the DeviceActionException is thrown, but we still need
// to wait for it to happen
yieldUntil { !handle.state.isTransitioning }
assertThat(handle.state.isOnline()).isFalse()
}
private fun buildProperties(info: AvdInfo) =
LocalEmulatorProperties.build(info) {
icon = EmptyIcon.DEFAULT
populateDeviceInfoProto("Test", null, emptyMap(), "")
}
@Test
fun isPairable() {
val api29WithPlay = avdManager.makeAvdInfo(1, AndroidVersion(29), hasPlayStore = true)
val api31NoPlay = avdManager.makeAvdInfo(2, AndroidVersion(29), hasPlayStore = false)
val api30WithPlay = avdManager.makeAvdInfo(3, AndroidVersion(30), hasPlayStore = true)
assertThat(buildProperties(api29WithPlay).wearPairingId).isNull()
assertThat(buildProperties(api31NoPlay).wearPairingId).isNull()
assertThat(buildProperties(api30WithPlay).wearPairingId).isNotNull()
}
@Test
fun isDebuggable() {
val withPlay = avdManager.makeAvdInfo(1, AndroidVersion(29), hasPlayStore = true)
val noPlay = avdManager.makeAvdInfo(2, AndroidVersion(29), hasPlayStore = false)
assertThat(buildProperties(withPlay).isDebuggable).isFalse()
assertThat(buildProperties(noPlay).isDebuggable).isTrue()
}
@Test
fun id() = runBlockingWithTimeout {
val avdInfo = avdManager.createAvd()
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
handle.id.apply {
assertThat(pluginId).isEqualTo(LocalEmulatorProvisionerPlugin.PLUGIN_ID)
assertThat(isTemplate).isFalse()
assertThat(identifier).isEqualTo("path=${avdInfo.dataFolderPath}")
}
// Editing the device adds "Edited" to its name. Verify that updating display name doesn't
// affect ID equality.
val id1 = handle.id
handle.editAction?.edit()
assertThat(id1).isEqualTo(handle.id)
}
@Test
fun isActivatable() = runBlockingWithTimeout {
avdManager.createAvd()
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
val activationAction = handle.activationAction!!
activationAction.presentation.takeWhile { !it.enabled }.collect()
avdManager.avds[0] = avdManager.makeAvdInfo(1, avdStatus = AvdStatus.ERROR_IMAGE_MISSING)
// The action should become disabled.
yieldUntil { activationAction.presentation.value.enabled == false }
}
@Test
fun repair() = runBlockingWithTimeout {
avdManager.createAvd(avdManager.makeAvdInfo(1, avdStatus = AvdStatus.ERROR_IMAGE_MISSING))
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
assertThat(handle.state.error).isNotNull()
val activationAction = handle.activationAction!!
val repairAction = handle.repairDeviceAction!!
repairAction.presentation.takeWhile { !it.enabled }.collect()
repairAction.repair()
// Should become possible to activate the device
activationAction.presentation.takeWhile { !it.enabled }.collect()
repairAction.presentation.takeWhile { it.enabled }.collect()
assertThat(handle.state.error).isNull()
}
@Test
fun tvDeviceType() = runBlockingWithTimeout {
avdManager.createAvd(avdManager.makeAvdInfo(1, tag = SystemImageTags.GOOGLE_TV_TAG))
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
assertThat(handle.state.properties.deviceType).isEqualTo(DeviceType.TV)
handle.activationAction?.activate()
handle.awaitReady()
assertThat(handle.state.properties.deviceType).isEqualTo(DeviceType.TV)
}
@Test
fun editDevice() = runBlockingWithTimeout {
avdManager.createAvd(avdManager.makeAvdInfo(1, tag = SystemImageTags.GOOGLE_TV_TAG))
val channel = Channel<DeviceState>()
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
val name = handle.state.properties.title
val job = launch { handle.stateFlow.collect { channel.send(it) } }
// Editing the device adds "Edited" to its name
handle.editAction?.edit()
channel.receiveUntilPassing { newState ->
assertThat(newState.properties.title).isEqualTo("$name Edited")
}
// Editing the device while it's online puts it in AvdChangedError state
handle.activationAction?.activate()
channel.receiveUntilPassing { newState ->
assertThat(newState).isInstanceOf(Connected::class.java)
}
handle.editAction?.edit()
channel.receiveUntilPassing { newState ->
assertThat(newState.error).isEqualTo(AvdChangedError)
assertThat(newState.properties.title).isEqualTo("$name Edited")
}
// Deactivating the device causes the prior edit to take effect
handle.deactivationAction?.deactivate()
channel.receiveUntilPassing { newState ->
assertThat(newState.error).isNull()
assertThat(newState.properties.title).isEqualTo("$name Edited Edited")
}
job.cancel()
}
// Some of the data for an AVD can be edited and updated while running.
// Check that it's fine to edit these properties while the AVD is offline or online.
@Test
fun editMetadata() = runBlockingWithTimeout {
val info = avdManager.makeAvdInfo(1, tag = SystemImageTags.GOOGLE_TV_TAG)
avdManager.createAvd(info)
val channel = Channel<DeviceState>()
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
val job = launch { handle.stateFlow.collect { channel.send(it) } }
// Check if editing the AVD name works while offline.
val originalName = info.name
avdManager.avdEditor = { avdInfo: AvdInfo -> avdInfo.copy("New $originalName") }
handle.editAction?.edit()
channel.receiveUntilPassing { newState ->
assertThat((newState.properties as LocalEmulatorProperties).avdName)
.isEqualTo("New $originalName")
}
handle.activationAction?.activate()
channel.receiveUntilPassing { newState ->
assertThat(newState).isInstanceOf(Connected::class.java)
}
// Editing metadata while running shouldn't have problems.
avdManager.avdEditor = { avdInfo: AvdInfo -> avdInfo.copy("Final AVD name") }
handle.editAction?.edit()
channel.receiveUntilPassing { newState ->
assertThat((newState.properties as LocalEmulatorProperties).avdName)
.isEqualTo("Final AVD name")
assertThat(newState.error).isNull()
}
job.cancel()
}
/** Verify that the device updates the expected number of times. */
@Test
fun updateCount() = runBlockingWithTimeout {
avdManager.createAvd()
yieldUntil { provisioner.devices.value.size == 1 }
val handle = provisioner.devices.value[0]
val updateCount = AtomicInteger(0)
handle.scope.launch { handle.stateFlow.collect { updateCount.incrementAndGet() } }
delay(1.seconds)
// Nothing changed, so the periodic AvdInfo rescans (every 100 ms) should not update it.
assertThat(updateCount.get()).isEqualTo(1)
}
private fun checkProperties(properties: LocalEmulatorProperties) {
assertThat(properties.manufacturer).isEqualTo(MANUFACTURER)
assertThat(properties.avdConfigProperties[AvdManager.AVD_INI_DEVICE_MANUFACTURER])
.isEqualTo(MANUFACTURER)
assertThat(properties.model).isEqualTo(MODEL)
assertThat(properties.androidVersion).isEqualTo(API_LEVEL)
assertThat(properties.primaryAbi).isEqualTo(ABI)
assertThat(properties.avdName).startsWith("fake_avd_")
assertThat(properties.displayName).startsWith("Fake Device")
assertThat(Path.of(properties.wearPairingId!!).parent).isEqualTo(avdsPath)
properties.deviceInfoProto.let {
assertThat(it.deviceType).isEqualTo(DeviceInfo.DeviceType.LOCAL_EMULATOR)
assertThat(it.cpuAbi).isEqualTo(ApplicationBinaryInterface.ARM64_V8A_ABI)
assertThat(it.manufacturer).isEqualTo(MANUFACTURER)
assertThat(it.model).isEqualTo(MODEL)
assertThat(it.buildVersionRelease).isEqualTo(RELEASE)
assertThat(it.buildApiLevelFull).isEqualTo(API_LEVEL.apiStringWithExtension)
assertThat(it.mdnsConnectionType).isEqualTo(MdnsConnectionType.UNKNOWN_MDNS_CONNECTION_TYPE)
assertThat(it.deviceProvisionerId).isEqualTo(LocalEmulatorProvisionerPlugin.PLUGIN_ID)
}
}
companion object {
const val MANUFACTURER = "Google"
const val MODEL = "Pixel 6"
val API_LEVEL = AndroidVersion(31)
val ABI = Abi.ARM64_V8A
const val RELEASE = "12.0"
}
}