blob: f4a76d2a09691e68b89bc827bc4fb792c3361de1 [file]
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.env;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.RunManager;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.execution.executors.DefaultRunExecutor;
import com.intellij.execution.impl.ConsoleViewImpl;
import com.intellij.execution.impl.RunManagerImpl;
import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessListener;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ExecutionEnvironmentBuilder;
import com.intellij.execution.runners.ProgramRunner;
import com.intellij.execution.ui.RunContentDescriptor;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.project.Project;
import com.jetbrains.python.run.AbstractPythonRunConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import javax.swing.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Runner to run python configurations. You only need to provide {@link PyConfigurationProducerForRunner} (see it's inheritors for useful defaults)
* Some methods are overridible to allow customization.
* <p/>
* This class allows configuration to <strong>rerun</strong> (using {@link #shouldRunAgain()},
* you need to implement {@link #getEnvironmentToRerun(RunContentDescriptor)}, accept last run descriptor and return
* {@link ExecutionEnvironment} to rerun (probably obtained from descriptor).
* <p/>
* It also has {@link #getAvailableRunnersForLastRun()} with list of runners that represents runner ids available for last run.
*
* @param <CONF_T> configuration class this runner supports
* @author Ilya.Kazakevich
*/
public abstract class ConfigurationBasedProcessRunner<CONF_T extends AbstractPythonRunConfiguration<?>>
extends ProcessWithConsoleRunner {
@NotNull
private final Class<CONF_T> myExpectedConfigurationType;
@NotNull
private final List<ProgramRunner<?>> myAvailableRunnersForLastRun = new ArrayList<>();
@NotNull
private final PyConfigurationProducerForRunner<CONF_T> myConfigurationProducer;
/**
* Environment to be used to run instead of factory. Used to rerun
*/
private ExecutionEnvironment myRerunExecutionEnvironment;
/**
* Process descriptor of last run
*/
protected RunContentDescriptor myLastProcessDescriptor;
protected ConfigurationBasedProcessRunner(
@NotNull Class<CONF_T> expectedConfigurationType,
@NotNull final PyConfigurationProducerForRunner<CONF_T> configurationProducer) {
myExpectedConfigurationType = expectedConfigurationType;
myConfigurationProducer = configurationProducer;
}
@Override
final void runProcess(@NotNull final String sdkPath,
@NotNull final Project project,
@NotNull final ProcessListener processListener,
@NotNull final String tempWorkingPath)
throws ExecutionException {
ensureConsoleOk(myConsole);
// Do not create new environment from factory, if child provided environment to rerun
final ExecutionEnvironment executionEnvironment =
// TODO: RENAME
(myRerunExecutionEnvironment != null ? myRerunExecutionEnvironment : createExecutionEnvironment
(sdkPath, project, tempWorkingPath));
// Engine to be run after process end to post process console
ProcessListener consolePostprocessor = new ProcessAdapter() {
@Override
public void processTerminated(@NotNull final ProcessEvent event) {
super.processTerminated(event);
ApplicationManager.getApplication().invokeAndWait(() -> prepareConsoleAfterProcessEnd(), ModalityState.NON_MODAL);
}
};
/// Find all available runners to report them to the test
myAvailableRunnersForLastRun.clear();
for (ProgramRunner<?> runner : ProgramRunner.PROGRAM_RUNNER_EP.getExtensions()) {
for (Executor executor : Executor.EXECUTOR_EXTENSION_NAME.getExtensions()) {
if (runner.canRun(executor.getId(), executionEnvironment.getRunProfile())) {
myAvailableRunnersForLastRun.add(runner);
}
}
}
executionEnvironment.setCallback(new ProgramRunner.Callback() {
@Override
public void processStarted(final RunContentDescriptor descriptor) {
final ProcessHandler handler = descriptor.getProcessHandler();
assert handler != null : "No process handler";
handler.addProcessListener(consolePostprocessor);
handler.addProcessListener(processListener);
myConsole = null;
fetchConsoleAndSetToField(descriptor);
assert myConsole != null : "fetchConsoleAndSetToField did not set console!";
final JComponent component = myConsole.getComponent(); // Console does not work with out of this method
assert component != null;
myLastProcessDescriptor = descriptor;
}
});
executionEnvironment.getRunner().execute(executionEnvironment);
}
/**
* {@link PyProcessWithConsoleTestTask#createProcessRunner()} should always return new runner.
* But some buggy code returns runner from prev. rerun with stale project in console.
* This code checks it.
*/
private static void ensureConsoleOk(@Nullable final ConsoleViewImpl console) {
if (console == null) { // Not set yet
return;
}
if (console.getProject().isDisposed()) {
throw new AssertionError(
"=== Console is stale. Make sure you did not cache and reuse runner from prev. run. See this method doc for more info === ");
}
}
/**
* @return descriptor of last run
*/
@Nullable
public RunContentDescriptor getLastProcessDescriptor() {
return myLastProcessDescriptor;
}
@NotNull
private ExecutionEnvironment createExecutionEnvironment(@NotNull final String sdkPath,
@NotNull final Project project,
@NotNull final String workingDir)
throws ExecutionException {
CONF_T configuration = myConfigurationProducer.createConfiguration(project, myExpectedConfigurationType);
RunnerAndConfigurationSettings settings =
new RunnerAndConfigurationSettingsImpl(RunManagerImpl.getInstanceImpl(project), configuration);
assert myExpectedConfigurationType.isInstance(configuration) :
String.format("Expected configuration %s, but got %s", myExpectedConfigurationType, configuration.getClass());
configuration.setSdkHome(sdkPath);
configuration.setWorkingDirectory(workingDir);
try {
WriteAction.run(() -> {
configurationCreatedAndWillLaunch(configuration);
RunManager runManager = RunManager.getInstance(project);
runManager.addConfiguration(settings);
runManager.setSelectedConfiguration(settings);
Assert.assertSame(settings, runManager.getSelectedConfiguration());
});
}
catch (IOException e) {
throw new ExecutionException(e);
}
// Execute
final Executor executor = DefaultRunExecutor.getRunExecutorInstance();
return ExecutionEnvironmentBuilder.create(executor, settings).build();
}
/**
* Prepares console to be tested after process end. Always call super if override.
*/
protected void prepareConsoleAfterProcessEnd() {
myConsole.flushDeferredText(); // Console may have deferred text, lets flush it
}
/**
* Fetches console from process descriptor and sets it to {@link #myConsole}.
* If override, always set console!
*
* @param descriptor process descriptor
*/
protected void fetchConsoleAndSetToField(@NotNull final RunContentDescriptor descriptor) {
myConsole = (ConsoleViewImpl)descriptor.getExecutionConsole();
}
/**
* Called when configuration is created. Used by runners to configure configuration before run.
* Always call parent when override.
*
* @param configuration configuration to configure
*/
protected void configurationCreatedAndWillLaunch(@NotNull final CONF_T configuration) throws IOException {
}
@Override
protected final boolean shouldRunAgain() {
final ExecutionEnvironment rerunEnvironment = getEnvironmentToRerun(myLastProcessDescriptor);
if (rerunEnvironment == null) {
return false;
}
myRerunExecutionEnvironment = rerunEnvironment;
return true;
}
/**
* Fetches rerun environment from descriptor if any
*
* @param lastRunDescriptor descriptor of last process run
* @return environment to rerun or null if no rerun needed
*/
@Nullable
protected ExecutionEnvironment getEnvironmentToRerun(@NotNull final RunContentDescriptor lastRunDescriptor) {
return null;
}
/**
* @return list of runner ids which are allowed to run against current environment.
* Use it to check if desired runner (like debugger) exists
*/
@NotNull
public final List<ProgramRunner<?>> getAvailableRunnersForLastRun() {
return Collections.unmodifiableList(myAvailableRunnersForLastRun);
}
}