blob: 7283e5314608b541c34fbe3b296f9dfa8da23607 [file] [log] [blame]
/*
* Copyright 2016 The Bazel Authors. All rights reserved.
*
* 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.google.idea.blaze.android.run.test;
import com.android.ddmlib.IDevice;
import com.android.tools.idea.run.ConsolePrinter;
import com.android.tools.idea.run.tasks.LaunchTask;
import com.android.tools.idea.run.tasks.LaunchTaskDurations;
import com.android.tools.idea.run.util.LaunchStatus;
import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.idea.blaze.base.async.executor.BlazeExecutor;
import com.google.idea.blaze.base.async.process.ExternalTask;
import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
import com.google.idea.blaze.base.command.BlazeCommand;
import com.google.idea.blaze.base.command.BlazeCommandName;
import com.google.idea.blaze.base.command.BlazeFlags;
import com.google.idea.blaze.base.filecache.FileCaches;
import com.google.idea.blaze.base.model.primitives.Label;
import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
import com.google.idea.blaze.base.projectview.ProjectViewManager;
import com.google.idea.blaze.base.projectview.ProjectViewSet;
import com.google.idea.blaze.base.scope.BlazeContext;
import com.google.idea.blaze.base.scope.Scope;
import com.google.idea.blaze.base.scope.ScopedFunction;
import com.google.idea.blaze.base.scope.output.IssueOutput;
import com.google.idea.blaze.base.settings.Blaze;
import com.google.idea.blaze.base.util.SaveUtil;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.ide.PooledThreadExecutor;
/**
* An Android application launcher that invokes `blaze test` on an android_test target, and sets up
* process handling and debugging for the test run.
*/
class BlazeAndroidTestLaunchTask implements LaunchTask {
// Uses a local device/emulator attached to adb to run an android_test.
public static final String TEST_LOCAL_DEVICE =
BlazeFlags.TEST_ARG + "--device_broker_type=LOCAL_ADB_SERVER";
// Uses a local device/emulator attached to adb to run an android_test.
public static final String TEST_DEBUG = BlazeFlags.TEST_ARG + "--enable_debug";
// Specifies the serial number for a local test device.
private static final String TEST_DEVICE_SERIAL = "--device_serial_number=";
private static final Logger LOG = Logger.getInstance(BlazeAndroidTestLaunchTask.class);
private final Project project;
private final Label target;
private final List<String> buildFlags;
private final BlazeAndroidTestFilter testFilter;
private ListenableFuture<Boolean> blazeResult;
private final BlazeAndroidTestRunContext runContext;
private final boolean debug;
public BlazeAndroidTestLaunchTask(
Project project,
Label target,
List<String> buildFlags,
BlazeAndroidTestFilter testFilter,
BlazeAndroidTestRunContext runContext,
boolean debug) {
this.project = project;
this.target = target;
this.buildFlags = buildFlags;
this.testFilter = testFilter;
this.runContext = runContext;
this.debug = debug;
}
@NotNull
@Override
public String getDescription() {
return String.format("Running %s tests", Blaze.buildSystemName(project));
}
@Override
public int getDuration() {
return LaunchTaskDurations.LAUNCH_ACTIVITY;
}
@Override
public boolean perform(
@NotNull IDevice device,
@NotNull LaunchStatus launchStatus,
@NotNull ConsolePrinter printer) {
BlazeExecutor executor = BlazeExecutor.getInstance();
ProcessHandlerLaunchStatus processHandlerLaunchStatus =
(ProcessHandlerLaunchStatus) launchStatus;
final ProcessHandler processHandler = processHandlerLaunchStatus.getProcessHandler();
blazeResult =
executor.submit(
new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return Scope.root(
new ScopedFunction<Boolean>() {
@Override
public Boolean execute(@NotNull BlazeContext context) {
ProjectViewSet projectViewSet =
ProjectViewManager.getInstance(project).getProjectViewSet();
if (projectViewSet == null) {
IssueOutput.error("Could not load project view. Please resync project.")
.submit(context);
return false;
}
BlazeCommand.Builder commandBuilder =
BlazeCommand.builder(
Blaze.getBuildSystem(project), BlazeCommandName.TEST)
.addTargets(target);
// Build flags must match BlazeBeforeRunTask.
commandBuilder.addBlazeFlags(buildFlags);
// Run the test on the selected local device/emulator.
commandBuilder
.addBlazeFlags(TEST_LOCAL_DEVICE, BlazeFlags.TEST_OUTPUT_STREAMED)
.addBlazeFlags(testDeviceSerialFlags(device.getSerialNumber()))
.addBlazeFlags(testFilter.getBlazeFlags());
if (debug) {
commandBuilder.addBlazeFlags(
TEST_DEBUG, BlazeFlags.NO_CACHE_TEST_RESULTS);
}
BlazeCommand command = commandBuilder.build();
printer.stdout(
String.format("Starting %s test...\n", Blaze.buildSystemName(project)));
printer.stdout(command + "\n");
LineProcessingOutputStream.LineProcessor stdoutLineProcessor =
line -> {
printer.stdout(line);
return true;
};
LineProcessingOutputStream.LineProcessor stderrLineProcessor =
line -> {
printer.stderr(line);
return true;
};
SaveUtil.saveAllFiles();
int retVal =
ExternalTask.builder(WorkspaceRoot.fromProject(project))
.addBlazeCommand(command)
.context(context)
.stdout(LineProcessingOutputStream.of(stdoutLineProcessor))
.stderr(LineProcessingOutputStream.of(stderrLineProcessor))
.build()
.run();
FileCaches.refresh(project);
if (retVal != 0) {
context.setHasError();
}
return !context.hasErrors();
}
});
}
});
blazeResult.addListener(runContext::onLaunchTaskComplete, PooledThreadExecutor.INSTANCE);
// The debug case is set up in ConnectBlazeTestDebuggerTask
if (!debug) {
waitAndSetUpForKillingBlazeOnStop(processHandler, launchStatus);
}
return true;
}
/**
* Hooks up the Blaze process to be killed if the user hits the 'Stop' button, then waits for the
* Blaze process to stop. In non-debug mode, we wait for test execution to finish before returning
* from launch() (this matches the behavior of the stock ddmlib runner).
*/
private void waitAndSetUpForKillingBlazeOnStop(
@NotNull final ProcessHandler processHandler, @NotNull LaunchStatus launchStatus) {
processHandler.addProcessListener(
new ProcessAdapter() {
@Override
public void processWillTerminate(ProcessEvent event, boolean willBeDestroyed) {
blazeResult.cancel(true /* mayInterruptIfRunning */);
launchStatus.terminateLaunch("Test run stopped.\n");
}
});
try {
blazeResult.get();
launchStatus.terminateLaunch("Tests ran to completion.\n");
} catch (CancellationException e) {
// The user has canceled the test.
launchStatus.terminateLaunch("Test run stopped.\n");
} catch (InterruptedException e) {
// We've been interrupted - cancel the underlying Blaze process.
blazeResult.cancel(true /* mayInterruptIfRunning */);
launchStatus.terminateLaunch("Test run stopped.\n");
} catch (ExecutionException e) {
LOG.error(e);
launchStatus.terminateLaunch(
"Test run stopped due to internal exception. Please file a bug report.\n");
}
}
@NotNull
private static String testDeviceSerialFlags(@NotNull String serial) {
return BlazeFlags.TEST_ARG + TEST_DEVICE_SERIAL + serial;
}
}