blob: 2fed5c6d274a730af0a384dd4409df082c027278 [file] [log] [blame]
/*
* 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.tools.idea.run
import com.android.ddmlib.AndroidDebugBridge
import com.android.ddmlib.IDevice
import com.android.ddmlib.internal.DeviceImpl
import com.android.ddmlib.internal.FakeAdbTestRule
import com.android.sdklib.AndroidVersion
import com.android.testutils.MockitoCleanerRule
import com.android.testutils.MockitoKt.any
import com.android.testutils.MockitoKt.eq
import com.android.testutils.MockitoKt.whenever
import com.android.tools.analytics.UsageTrackerRule
import com.android.tools.deployer.Deployer
import com.android.tools.deployer.DeployerException
import com.android.tools.idea.editors.liveedit.LiveEditService
import com.android.tools.idea.editors.liveedit.LiveEditServiceImpl
import com.android.tools.idea.execution.common.AndroidExecutionException
import com.android.tools.idea.execution.common.AndroidExecutionTarget
import com.android.tools.idea.execution.common.AndroidSessionInfo
import com.android.tools.idea.execution.common.ApplicationDeployer
import com.android.tools.idea.execution.common.DeployOptions
import com.android.tools.idea.execution.common.assertTaskPresentedInStats
import com.android.tools.idea.execution.common.processhandler.AndroidProcessHandler
import com.android.tools.idea.execution.common.processhandler.AndroidRemoteDebugProcessHandler
import com.android.tools.idea.execution.common.stats.RunStats
import com.android.tools.idea.execution.common.stats.RunStatsService
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.idea.gradle.project.sync.snapshots.AndroidCoreTestProject
import com.android.tools.idea.projectsystem.ApplicationProjectContext
import com.android.tools.idea.projectsystem.applicationProjectContextForTests
import com.android.tools.idea.run.AndroidRunConfiguration.Companion.DO_NOTHING
import com.android.tools.idea.run.activity.launch.EmptyTestConsoleView
import com.android.tools.idea.run.configuration.execution.createApp
import com.android.tools.idea.run.deployment.liveedit.LiveEditApp
import com.android.tools.idea.testing.AndroidProjectRule
import com.android.tools.idea.testing.executeMakeBeforeRunStepInTest
import com.android.tools.idea.testing.flags.override
import com.google.common.truth.Truth.assertThat
import com.intellij.execution.DefaultExecutionResult
import com.intellij.execution.ExecutionException
import com.intellij.execution.ExecutionManager
import com.intellij.execution.Executor
import com.intellij.execution.RunManager
import com.intellij.execution.RunnerAndConfigurationSettings
import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.impl.ExecutionManagerImpl
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ExecutionEnvironmentBuilder
import com.intellij.execution.runners.showRunContent
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.execution.ui.RunContentManager
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.replaceService
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.ui.content.Content
import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.Mockito.spy
import org.mockito.Spy
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.test.fail
/**
* Unit test for [AndroidRunConfigurationExecutor].
*/
class AndroidRunConfigurationExecutorTest {
companion object {
val APPLICATION_ID = "google.simpleapplication"
val ACTIVITY_NAME = "google.simpleapplication.MyActivity"
}
val fakeAdb: FakeAdbTestRule = FakeAdbTestRule()
val projectRule = AndroidProjectRule.testProject(AndroidCoreTestProject.SIMPLE_APPLICATION)
val cleaner = MockitoCleanerRule()
val usageTrackerRule = UsageTrackerRule()
@get:Rule
val chain = RuleChain.outerRule(cleaner).around(usageTrackerRule).around(projectRule).around(fakeAdb)
@Test
fun runSucceeded() {
val deviceState = fakeAdb.connectAndWaitForDevice()
val latch = CountDownLatch(1)
deviceState.setActivityManager { args, _ ->
val command = args.joinToString(" ")
if (command == "start -n google.simpleapplication/google.simpleapplication.MyActivity -a android.intent.action.MAIN -c android.intent.category.LAUNCHER") {
deviceState.startClient(1234, 1235, APPLICATION_ID, false)
latch.countDown()
}
}
val device = AndroidDebugBridge.getBridge()!!.devices.single()
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val stats = RunStatsService.get(projectRule.project).create()
val env = getExecutionEnvironment(listOf(device)).apply {
putUserData(RunStats.KEY, stats)
}
val configuration = env.runProfile as AndroidRunConfiguration
configuration.CLEAR_APP_STORAGE = true
configuration.CLEAR_LOGCAT = true
configuration.executeMakeBeforeRunStepInTest(device)
var logcatCleared = false
projectRule.project.messageBus.connect(projectRule.testRootDisposable)
.subscribe(ClearLogcatListener.TOPIC, ClearLogcatListener { logcatCleared = true })
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
applicationDeployer = testApplicationDeployer(device, ApplicationDeployer::fullDeploy.name)
)
val runContentDescriptor = ProgressManager.getInstance()
.runProcess(Computable { runner.run(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
stats.success()
assertTaskPresentedInStats(usageTrackerRule.usages, "CLEAR_APP_STORAGE_TASK")
assertTaskPresentedInStats(usageTrackerRule.usages, "DEFAULT_ACTIVITY")
val processHandler = runContentDescriptor.processHandler!!
processHandler.startNotify()
assertThat(logcatCleared).isTrue() // comes from [com.android.tools.idea.run.tasks.ClearAppStorageTaskKt.clearAppStorage]
assertThat(deviceState.pmLogs).contains("list packages google.simpleapplication")
assertThat(processHandler).isInstanceOf(AndroidProcessHandler::class.java)
assertThat((processHandler as AndroidProcessHandler).targetApplicationId).isEqualTo(APPLICATION_ID)
assertThat(processHandler.autoTerminate).isEqualTo(true)
assertThat(processHandler.isAssociated(device)).isEqualTo(true)
assertThat(AndroidSessionInfo.from(processHandler)).isNotNull()
if (!latch.await(10, TimeUnit.SECONDS)) {
fail("Activity is not started")
}
deviceState.stopClient(1234) // TODO: flaky test b/273744887
//if (!processHandler.waitFor(5000)) {
// fail("Process handler didn't stop when debug process terminated")
//}
processHandler.destroyProcess()
}
@Test
fun debugSucceeded() { //TODO: write handler in fakeAdb for "am capabilities --protobuf"
StudioFlags.DEBUG_ATTEMPT_SUSPENDED_START.override(false, projectRule.testRootDisposable)
val deviceState = fakeAdb.connectAndWaitForDevice()
var startInvocation = 0
deviceState.setActivityManager { args, output ->
val command = args.joinToString(" ")
if (command == "start -n google.simpleapplication/google.simpleapplication.MyActivity -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -D") {
deviceState.startClient(1234, 1235, APPLICATION_ID, true)
startInvocation++
}
}
val device = AndroidDebugBridge.getBridge()!!.devices.single()
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val stats = RunStatsService.get(projectRule.project).create()
val env = getExecutionEnvironment(listOf(device), isDebug = true).apply {
putUserData(RunStats.KEY, stats)
}
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
configuration.setLaunchActivity(ACTIVITY_NAME)
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
applicationDeployer = testApplicationDeployer(device, ApplicationDeployer::fullDeploy.name)
)
val processHandler = (ProgressManager.getInstance()
.runProcess(
Computable { runner.debug(ProgressManager.getInstance().progressIndicator) },
EmptyProgressIndicator()
)).processHandler as AndroidRemoteDebugProcessHandler
stats.success()
assertTaskPresentedInStats(usageTrackerRule.usages, "waitForProcessTermination")
assertTaskPresentedInStats(usageTrackerRule.usages, "SPECIFIC_ACTIVITY")
assertTaskPresentedInStats(usageTrackerRule.usages, "startDebuggerSession")
assertThat(!processHandler.isProcessTerminating || !processHandler.isProcessTerminated)
deviceState.stopClient(1234)
if (!processHandler.waitFor(5000)) {
fail("Process handler didn't stop when debug process terminated")
}
assertThat(startInvocation).isEqualTo(1)
}
@Test
fun applyChangesSucceeded() {
val device = DeviceImpl(null, "serial_number", IDevice.DeviceState.ONLINE)
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val env = getExecutionEnvironment(listOf(device))
val runningDescriptor = setSwapInfo(env, device)
val runningProcessHandler = runningDescriptor.processHandler as AndroidProcessHandler
var liveEditServiceNotified = false
val liveEditServiceImpl = LiveEditServiceImpl(projectRule.project).apply { Disposer.register(projectRule.testRootDisposable, this) }
val liveEditService = object : LiveEditService by liveEditServiceImpl {
override fun notifyAppDeploy(
runProfile: RunProfile, executor: Executor, packageName: String, device: IDevice, app: LiveEditApp
): Boolean {
liveEditServiceNotified = true
return true
}
}
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
liveEditService,
testApplicationDeployer(device, ApplicationDeployer::applyChangesDeploy.name)
)
val runContentDescriptor = ProgressManager.getInstance()
.runProcess(Computable { runner.applyChanges(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
assertThat(runContentDescriptor.isHiddenContent).isEqualTo(true)
assertThat(liveEditServiceNotified).isEqualTo(false) // Live Edit doesn't need to know if AC was performed.
val processHandler = runContentDescriptor.processHandler
assertThat(processHandler).isEqualTo(runningProcessHandler)
assertThat(runContentDescriptor.executionConsole).isEqualTo(runningDescriptor.executionConsole)
val printedMessage = (runContentDescriptor.executionConsole as EmptyTestConsoleView).printedMessages.map { it.first }.first()
assertThat(printedMessage).endsWith("Applying changes to app on 'TestTarget'.\n")
assertThat((processHandler as AndroidProcessHandler).targetApplicationId).isEqualTo(APPLICATION_ID)
assertThat(processHandler.autoTerminate).isEqualTo(true)
assertThat(processHandler.isAssociated(device)).isEqualTo(true)
assertThat(processHandler.isProcessTerminated).isEqualTo(false)
assertThat(processHandler.isProcessTerminating).isEqualTo(false)
}
@Test
fun applyCodeChangesSucceeded() {
val device = DeviceImpl(null, "serial_number", IDevice.DeviceState.ONLINE)
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val env = getExecutionEnvironment(listOf(device))
val runningDescriptor = setSwapInfo(env, device)
val runningProcessHandler = runningDescriptor.processHandler as AndroidProcessHandler
runningProcessHandler.addTargetDevice(device)
var liveEditServiceNotified = false
val liveEditServiceImpl = LiveEditServiceImpl(projectRule.project).apply { Disposer.register(projectRule.testRootDisposable, this) }
val liveEditService = object : LiveEditService by liveEditServiceImpl {
override fun notifyAppDeploy(
runProfile: RunProfile, executor: Executor, packageName: String, device: IDevice, app: LiveEditApp
): Boolean {
liveEditServiceNotified = true
return true
}
}
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
liveEditService,
applicationDeployer = testApplicationDeployer(device, ApplicationDeployer::applyCodeChangesDeploy.name)
)
val runContentDescriptor =
ProgressManager.getInstance().runProcess(Computable { runner.applyCodeChanges(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
assertThat(runContentDescriptor.isHiddenContent).isEqualTo(true)
assertThat(liveEditServiceNotified).isEqualTo(false) // Live Edit doesn't need to know if AC was performed.
val processHandler = runContentDescriptor.processHandler
assertThat(processHandler).isEqualTo(runningProcessHandler)
assertThat(runContentDescriptor.executionConsole).isEqualTo(runningDescriptor.executionConsole)
val printedMessage = (runContentDescriptor.executionConsole as EmptyTestConsoleView).printedMessages.map { it.first }.first()
assertThat(printedMessage).endsWith("Applying code changes to app on 'TestTarget'.\n")
assertThat((processHandler as AndroidProcessHandler).targetApplicationId).isEqualTo(APPLICATION_ID)
assertThat(processHandler.autoTerminate).isEqualTo(true)
assertThat(processHandler.isAssociated(device)).isEqualTo(true)
assertThat(processHandler.isProcessTerminated).isEqualTo(false)
assertThat(processHandler.isProcessTerminating).isEqualTo(false)
}
@Test
fun runFailedApkProvisionException() {
val device = DeviceImpl(null, "serial_number", IDevice.DeviceState.ONLINE)
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val env = getExecutionEnvironment(listOf(device))
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
var liveEditServiceNotified = false
val liveEditServiceImpl = LiveEditServiceImpl(projectRule.project).apply { Disposer.register(projectRule.testRootDisposable, this) }
val liveEditService = object : LiveEditService by liveEditServiceImpl {
override fun notifyAppDeploy(
runProfile: RunProfile, executor: Executor, packageName: String, device: IDevice, app: LiveEditApp
): Boolean {
liveEditServiceNotified = true
return true
}
}
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
apkProvider = { throw ApkProvisionException("ApkProvisionException") },
liveEditService
)
val thrown = assertThrows(ExecutionException::class.java) {
ProgressManager.getInstance()
.runProcess(Computable { runner.run(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
}
assertThat(thrown).hasMessageThat().contains("ApkProvisionException")
assertThat(liveEditServiceNotified).isEqualTo(false)
}
@Test
fun runGetApplicationIdException() {
val device = DeviceImpl(null, "serial_number", IDevice.DeviceState.ONLINE)
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val env = getExecutionEnvironment(listOf(device))
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
val runner = AndroidRunConfigurationExecutor(
applicationIdProvider = object : ApplicationIdProvider{
override fun getPackageName(): String {
throw ApkProvisionException("AndroidExecutionException packageName")
}
override fun getTestPackageName(): String? {
throw ApkProvisionException("AndroidExecutionException testPackageName")
}
},
applicationContext = object: ApplicationProjectContext {
override val applicationId: String
get() = error("Not supposed to be invoked")
},
env,
deviceFutures,
apkProvider = { throw ApkProvisionException("ApkProvisionException") })
val thrown = assertThrows(AndroidExecutionException::class.java) {
ProgressManager.getInstance()
.runProcess(Computable { runner.run(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
}
assertThat(thrown).hasMessageThat().contains("AndroidExecutionException packageName")
}
@Test
fun runNoLaunchOptions() {
val deviceState = fakeAdb.connectAndWaitForDevice()
val latch = CountDownLatch(1)
deviceState.setActivityManager { args, _ ->
val command = args.joinToString(" ")
if (command == "start -n google.simpleapplication/google.simpleapplication.MyActivity" +
" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER") {
deviceState.startClient(1234, 1235, APPLICATION_ID, false)
latch.countDown()
}
}
val device = AndroidDebugBridge.getBridge()!!.devices.single()
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val stats = RunStatsService.get(projectRule.project).create()
val env = getExecutionEnvironment(listOf(device)).apply {
putUserData(RunStats.KEY, stats)
}
val configuration = env.runProfile as AndroidRunConfiguration
configuration.CLEAR_APP_STORAGE = true
configuration.CLEAR_LOGCAT = true
configuration.MODE = DO_NOTHING
configuration.executeMakeBeforeRunStepInTest(device)
var logcatCleared = false
projectRule.project.messageBus.connect(projectRule.testRootDisposable)
.subscribe(ClearLogcatListener.TOPIC, ClearLogcatListener { logcatCleared = true })
var liveEditServiceNotified = false
val liveEditServiceImpl = LiveEditServiceImpl(projectRule.project).apply { Disposer.register(projectRule.testRootDisposable, this) }
val liveEditService = object : LiveEditService by liveEditServiceImpl {
override fun notifyAppDeploy(
runProfile: RunProfile, executor: Executor, packageName: String, device: IDevice, app: LiveEditApp
): Boolean {
liveEditServiceNotified = true
return true
}
}
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
applicationDeployer = testApplicationDeployer(device, ApplicationDeployer::fullDeploy.name),
liveEditService = liveEditService
)
val runContentDescriptor = ProgressManager.getInstance()
.runProcess(Computable { runner.run(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
stats.success()
assertTaskPresentedInStats(usageTrackerRule.usages, "CLEAR_APP_STORAGE_TASK")
assertTaskPresentedInStats(usageTrackerRule.usages, "NO_LAUNCH")
val processHandler = runContentDescriptor.processHandler!!
processHandler.startNotify()
assertThat(logcatCleared).isTrue() // comes from [com.android.tools.idea.run.tasks.ClearAppStorageTaskKt.clearAppStorage]
assertThat(deviceState.pmLogs).contains("list packages google.simpleapplication")
assertThat(processHandler).isInstanceOf(AndroidProcessHandler::class.java)
assertThat((processHandler as AndroidProcessHandler).targetApplicationId).isEqualTo(APPLICATION_ID)
assertThat(processHandler.autoTerminate).isEqualTo(true)
assertThat(processHandler.isAssociated(device)).isEqualTo(true)
assertThat(AndroidSessionInfo.from(processHandler)).isNotNull()
assertThat(liveEditServiceNotified).isEqualTo(false) // Live Edit doesn't need to know if AC was performed.
deviceState.stopClient(1234) // TODO: flaky test b/273744887
//if (!processHandler.waitFor(5000)) {
// fail("Process handler didn't stop when debug process terminated")
//}
processHandler.destroyProcess()
}
@Test
fun runFailedDeployException() {
val device = DeviceImpl(null, "serial_number", IDevice.DeviceState.ONLINE)
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val env = getExecutionEnvironment(listOf(device))
var liveEditServiceNotified = false
val liveEditServiceImpl = LiveEditServiceImpl(projectRule.project).apply { Disposer.register(projectRule.testRootDisposable, this) }
val liveEditService = object : LiveEditService by liveEditServiceImpl {
override fun notifyAppDeploy(
runProfile: RunProfile, executor: Executor, packageName: String, device: IDevice, app: LiveEditApp
): Boolean {
liveEditServiceNotified = true
return true
}
}
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
liveEditService,
applicationDeployer = object : ApplicationDeployer {
override fun fullDeploy(
device: IDevice,
app: ApkInfo,
deployOptions: DeployOptions,
indicator: ProgressIndicator
): Deployer.Result {
throw DeployerException.pmFlagsNotSupported()
}
override fun applyChangesDeploy(
device: IDevice,
app: ApkInfo,
deployOptions: DeployOptions,
indicator: ProgressIndicator
): Deployer.Result {
throw DeployerException.pmFlagsNotSupported()
}
override fun applyCodeChangesDeploy(
device: IDevice,
app: ApkInfo,
deployOptions: DeployOptions,
indicator: ProgressIndicator
): Deployer.Result {
throw DeployerException.pmFlagsNotSupported()
}
}
)
val thrown = assertThrows(ExecutionException::class.java) {
ProgressManager.getInstance()
.runProcess(Computable { runner.run(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
}
assertThat(thrown).hasMessageThat().contains(DeployerException.pmFlagsNotSupported().message)
assertThat(liveEditServiceNotified).isEqualTo(false)
}
@Test
fun swapRunFailedButProcessHandlerShouldNotBeDetached() {
val device = DeviceImpl(null, "serial_number", IDevice.DeviceState.ONLINE)
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val env = getExecutionEnvironment(listOf(device))
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
val runningDescriptor = setSwapInfo(env, device)
val runningProcessHandler = runningDescriptor.processHandler as AndroidProcessHandler
runningProcessHandler.addTargetDevice(device)
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env, deviceFutures, { throw ApkProvisionException("Exception") })
assertThrows(ExecutionException::class.java) {
ProgressManager.getInstance()
.runProcess(Computable { runner.applyChanges(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
}
assertThat(runningProcessHandler.isAssociated(device)).isEqualTo(true)
assertThat(runningProcessHandler.isProcessTerminated).isEqualTo(false)
assertThat(runningProcessHandler.isProcessTerminating).isEqualTo(false)
}
@Test
fun applyChangesNeedsRestart() {
val deviceState = fakeAdb.connectAndWaitForDevice()
val restartHappened = CountDownLatch(1)
deviceState.setActivityManager { args, _ ->
val command = args.joinToString(" ")
if (command == "start -n google.simpleapplication/google.simpleapplication.MyActivity -a android.intent.action.MAIN -c android.intent.category.LAUNCHER") {
deviceState.startClient(1234, 1235, APPLICATION_ID, false)
restartHappened.countDown()
}
}
val device = AndroidDebugBridge.getBridge()!!.devices.single()
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val env = getExecutionEnvironment(listOf(device))
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
val runningDescriptor = setSwapInfo(env, device)
val runningProcessHandler = runningDescriptor.processHandler as AndroidProcessHandler
runningProcessHandler.addTargetDevice(device)
val result =
Deployer.Result(false, /*needsRestart */ true, false, createApp(device, APPLICATION_ID, activitiesName = listOf(ACTIVITY_NAME)))
val applicationDeployer = testApplicationDeployer(device, ApplicationDeployer::applyChangesDeploy.name, result)
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
applicationDeployer = applicationDeployer
)
val newProcessHandler = ProgressManager.getInstance()
.runProcess(Computable { runner.applyChanges(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator()).processHandler
if (!restartHappened.await(10, TimeUnit.SECONDS)) {
fail("Activity is not restarted")
}
// New process handler should be created if we restarted Activity
assertThat(newProcessHandler).isNotEqualTo(runningProcessHandler)
assertThat((newProcessHandler as AndroidProcessHandler).isAssociated(device)).isEqualTo(true)
}
@Test
fun applyCodeChangesNeedsRestartForDebug() {
//TODO: write handler in fakeAdb for "am capabilities --protobuf"
StudioFlags.DEBUG_ATTEMPT_SUSPENDED_START.override(false, projectRule.testRootDisposable)
val deviceState = fakeAdb.connectAndWaitForDevice()
val restartHappened = CountDownLatch(1)
deviceState.setActivityManager { args, output ->
val command = args.joinToString(" ")
if (command == "start -n google.simpleapplication/google.simpleapplication.MyActivity -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -D") {
deviceState.startClient(1234, 1235, APPLICATION_ID, true)
restartHappened.countDown()
}
}
val device = AndroidDebugBridge.getBridge()!!.devices.single()
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val env = getExecutionEnvironment(listOf(device))
val configuration = env.runProfile as AndroidRunConfiguration
configuration.executeMakeBeforeRunStepInTest(device)
val runningDescriptor = setSwapInfo(env, device)
val runningProcessHandler = runningDescriptor.processHandler as AndroidProcessHandler
runningProcessHandler.addTargetDevice(device)
val result =
Deployer.Result(false, /*needsRestart */ true, false, createApp(device, APPLICATION_ID, activitiesName = listOf(ACTIVITY_NAME)))
val applicationDeployer = testApplicationDeployer(device, ApplicationDeployer::applyCodeChangesDeploy.name, result)
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
applicationDeployer = applicationDeployer
)
val newProcessHandler =
ProgressManager.getInstance()
.runProcess(Computable { runner.applyCodeChanges(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator()).processHandler
if (!restartHappened.await(10, TimeUnit.SECONDS)) {
fail("Activity is not restarted")
}
// New process handler should be created if we restarted Activity
assertThat(newProcessHandler).isNotEqualTo(runningProcessHandler)
assertThat(newProcessHandler!!).isInstanceOf(AndroidRemoteDebugProcessHandler::class.java)
deviceState.stopClient(1234)
if (!newProcessHandler.waitFor(5000)) {
fail("Process handler didn't stop when debug process terminated")
}
}
private fun testApplicationDeployer(
device: IDevice,
expectedMethod: String,
result: Deployer.Result = Deployer.Result(
false,
false,
false,
createApp(device, APPLICATION_ID, activitiesName = listOf(ACTIVITY_NAME))
)
) = object : ApplicationDeployer {
override fun fullDeploy(
deviceToInstall: IDevice, app: ApkInfo, deployOptions: DeployOptions, indicator: ProgressIndicator
): Deployer.Result {
if (expectedMethod != ::fullDeploy.name) {
throw RuntimeException("Method invocation is not expected")
}
if (deviceToInstall == device) {
return result
}
throw RuntimeException("Unexpected device")
}
override fun applyChangesDeploy(
deviceToInstall: IDevice, app: ApkInfo, deployOptions: DeployOptions, indicator: ProgressIndicator
): Deployer.Result {
if (expectedMethod != ::applyChangesDeploy.name) {
throw RuntimeException("Method invocation is not expected")
}
if (deviceToInstall == device) {
return result
}
throw RuntimeException("Unexpected device")
}
override fun applyCodeChangesDeploy(
deviceToInstall: IDevice, app: ApkInfo, deployOptions: DeployOptions, indicator: ProgressIndicator
): Deployer.Result {
if (expectedMethod != ::applyCodeChangesDeploy.name) {
throw RuntimeException("Method invocation is not expected")
}
if (deviceToInstall == device) {
return result
}
throw RuntimeException("Unexpected device")
}
}
private fun getExecutionEnvironment(
devices: List<IDevice>, isDebug: Boolean = false
): ExecutionEnvironment {
val configSettings = RunManager.getInstance(projectRule.project).allSettings.single { it.configuration is AndroidRunConfiguration }
val executor = if (isDebug) DefaultRunExecutor.getRunExecutorInstance() else DefaultDebugExecutor.getDebugExecutorInstance()
val executionEnvironment =
ExecutionEnvironmentBuilder(projectRule.project, executor).runnerAndSettings(DefaultStudioProgramRunner(), configSettings)
.target(object : AndroidExecutionTarget() {
override fun getId() = "TestTarget"
override fun getDisplayName() = "TestTarget"
override fun getIcon() = null
override fun getAvailableDeviceCount() = devices.size
override fun getRunningDevices() = devices
}).build()
return executionEnvironment
}
private fun setSwapInfo(env: ExecutionEnvironment, device: IDevice): RunContentDescriptor {
val processHandlerForSwap = AndroidProcessHandler(APPLICATION_ID).apply { addTargetDevice(device) }
processHandlerForSwap.startNotify()
Disposer.register(projectRule.project) {
processHandlerForSwap.detachProcess()
}
var runContentDescriptor: RunContentDescriptor? = null
runInEdtAndWait {
runContentDescriptor = showRunContent(DefaultExecutionResult(EmptyTestConsoleView(), processHandlerForSwap), env)!!.apply {
setAttachedContent(mock(Content::class.java))
}
val mockRunContentManager = mock(RunContentManager::class.java)
whenever(mockRunContentManager.findContentDescriptor(eq(env.executor), eq(processHandlerForSwap))).thenReturn(runContentDescriptor)
projectRule.project.replaceService(RunContentManager::class.java, mockRunContentManager, projectRule.testRootDisposable)
val mockExecutionManager = mock(ExecutionManagerImpl::class.java)
whenever(mockExecutionManager.getRunningDescriptors(any())).thenReturn(listOf(runContentDescriptor!!))
projectRule.project.replaceService(ExecutionManager::class.java, mockExecutionManager, projectRule.testRootDisposable)
}
AndroidSessionInfo.create(processHandlerForSwap, listOf(device), APPLICATION_ID)
return runContentDescriptor!!
}
@Test
fun runAPI33() {
println("Starting runAPI33")
val deviceState = fakeAdb.connectAndWaitForDevice()
val latch = CountDownLatch(1)
deviceState.setActivityManager { args, _ ->
val command = args.joinToString(" ")
if (command == "start -n google.simpleapplication/google.simpleapplication.MyActivity -a android.intent.action.MAIN -c android.intent.category.LAUNCHER --splashscreen-show-icon") {
deviceState.startClient(1234, 1235, APPLICATION_ID, false)
latch.countDown()
}
}
val device = spy(AndroidDebugBridge.getBridge()!!.devices.single())
Mockito.`when`(device.version).thenReturn(AndroidVersion(33))
Mockito.`when`(device.forceStop(any())).thenThrow(RuntimeException("Should not kill"))
val deviceFutures = DeviceFutures.forDevices(listOf(device))
val stats = RunStatsService.get(projectRule.project).create()
val env = getExecutionEnvironment(listOf(device)).apply {
putUserData(RunStats.KEY, stats)
}
val configuration = env.runProfile as AndroidRunConfiguration
configuration.CLEAR_APP_STORAGE = true
configuration.CLEAR_LOGCAT = true
configuration.executeMakeBeforeRunStepInTest(device)
var logcatCleared = false
projectRule.project.messageBus.connect(projectRule.testRootDisposable)
.subscribe(ClearLogcatListener.TOPIC, ClearLogcatListener { logcatCleared = true })
val runner = AndroidRunConfigurationExecutor(
configuration.applicationIdProvider!!,
configuration.applicationProjectContextForTests,
env,
deviceFutures,
configuration.apkProvider!!,
applicationDeployer = testApplicationDeployer(device, ApplicationDeployer::fullDeploy.name)
)
val runContentDescriptor = ProgressManager.getInstance()
.runProcess(Computable { runner.run(ProgressManager.getInstance().progressIndicator) }, EmptyProgressIndicator())
stats.success()
assertTaskPresentedInStats(usageTrackerRule.usages, "CLEAR_APP_STORAGE_TASK")
assertTaskPresentedInStats(usageTrackerRule.usages, "DEFAULT_ACTIVITY")
val processHandler = runContentDescriptor.processHandler!!
processHandler.startNotify()
assertThat(logcatCleared).isTrue() // comes from [com.android.tools.idea.run.tasks.ClearAppStorageTaskKt.clearAppStorage]
assertThat(deviceState.pmLogs).contains("list packages google.simpleapplication")
assertThat(processHandler).isInstanceOf(AndroidProcessHandler::class.java)
assertThat((processHandler as AndroidProcessHandler).targetApplicationId).isEqualTo(APPLICATION_ID)
assertThat(processHandler.autoTerminate).isEqualTo(true)
assertThat(processHandler.isAssociated(device)).isEqualTo(true)
assertThat(AndroidSessionInfo.from(processHandler)).isNotNull()
if (!latch.await(10, TimeUnit.SECONDS)) {
fail("Activity is not started")
}
deviceState.stopClient(1234) // TODO: flaky test b/273744887
//if (!processHandler.waitFor(5000)) {
// fail("Process handler didn't stop when debug process terminated")
//}
processHandler.destroyProcess()
println("Finished runAPI33")
}
}