Merge branch 'master' into PY-9727
[idea/community.git] / python / src / com / jetbrains / python / run / AbstractPythonRunConfiguration.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.jetbrains.python.run;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.diagnostic.logging.LogConfigurationPanel;
20 import com.intellij.execution.ExecutionBundle;
21 import com.intellij.execution.Location;
22 import com.intellij.execution.configuration.AbstractRunConfiguration;
23 import com.intellij.execution.configuration.EnvironmentVariablesComponent;
24 import com.intellij.execution.configurations.*;
25 import com.intellij.execution.testframework.AbstractTestProxy;
26 import com.intellij.openapi.module.Module;
27 import com.intellij.openapi.module.ModuleManager;
28 import com.intellij.openapi.module.ModuleType;
29 import com.intellij.openapi.options.SettingsEditor;
30 import com.intellij.openapi.options.SettingsEditorGroup;
31 import com.intellij.openapi.project.Project;
32 import com.intellij.openapi.projectRoots.Sdk;
33 import com.intellij.openapi.roots.ProjectRootManager;
34 import com.intellij.openapi.util.InvalidDataException;
35 import com.intellij.openapi.util.JDOMExternalizerUtil;
36 import com.intellij.openapi.util.WriteExternalException;
37 import com.intellij.openapi.util.text.StringUtil;
38 import com.intellij.openapi.vfs.VirtualFile;
39 import com.intellij.psi.PsiElement;
40 import com.intellij.psi.util.PsiTreeUtil;
41 import com.intellij.util.PathMappingSettings;
42 import com.intellij.util.PlatformUtils;
43 import com.jetbrains.python.PyBundle;
44 import com.jetbrains.python.PythonModuleTypeBase;
45 import com.jetbrains.python.psi.PyClass;
46 import com.jetbrains.python.psi.PyFunction;
47 import com.jetbrains.python.sdk.PythonEnvUtil;
48 import com.jetbrains.python.sdk.PythonSdkType;
49 import org.jdom.Element;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52
53 import java.io.File;
54 import java.util.HashMap;
55 import java.util.List;
56 import java.util.Map;
57
58 /**
59  * @author Leonid Shalupov
60  */
61 public abstract class AbstractPythonRunConfiguration<T extends AbstractRunConfiguration> extends AbstractRunConfiguration
62   implements LocatableConfiguration, AbstractPythonRunConfigurationParams, CommandLinePatcher {
63   /**
64    * When passing path to test to runners, you should join parts with this char.
65    * I.e.: file.py::PyClassTest::test_method
66    */
67   public static final String TEST_NAME_PARTS_SPLITTER = "::";
68   private String myInterpreterOptions = "";
69   private String myWorkingDirectory = "";
70   private String mySdkHome = "";
71   private boolean myUseModuleSdk;
72   private boolean myAddContentRoots = true;
73   private boolean myAddSourceRoots = true;
74
75   protected PathMappingSettings myMappingSettings;
76   /**
77    * To prevent "double module saving" child may enable this flag
78    * and no module info would be saved
79    */
80   protected boolean mySkipModuleSerialization;
81
82   public AbstractPythonRunConfiguration(Project project, final ConfigurationFactory factory) {
83     super(project, factory);
84     getConfigurationModule().init();
85   }
86
87   public List<Module> getValidModules() {
88     return getValidModules(getProject());
89   }
90
91   public PathMappingSettings getMappingSettings() {
92     return myMappingSettings;
93   }
94
95   public void setMappingSettings(@Nullable PathMappingSettings mappingSettings) {
96     myMappingSettings = mappingSettings;
97   }
98
99   public static List<Module> getValidModules(Project project) {
100     final Module[] modules = ModuleManager.getInstance(project).getModules();
101     List<Module> result = Lists.newArrayList();
102     for (Module module : modules) {
103       if (PythonSdkType.findPythonSdk(module) != null) {
104         result.add(module);
105       }
106     }
107     return result;
108   }
109
110   public PyCommonOptionsFormData getCommonOptionsFormData() {
111     return new PyCommonOptionsFormData() {
112       @Override
113       public Project getProject() {
114         return AbstractPythonRunConfiguration.this.getProject();
115       }
116
117       @Override
118       public List<Module> getValidModules() {
119         return AbstractPythonRunConfiguration.this.getValidModules();
120       }
121
122       @Override
123       public boolean showConfigureInterpretersLink() {
124         return false;
125       }
126     };
127   }
128
129   @NotNull
130   @Override
131   public final SettingsEditor<T> getConfigurationEditor() {
132     final SettingsEditor<T> runConfigurationEditor = createConfigurationEditor();
133
134     final SettingsEditorGroup<T> group = new SettingsEditorGroup<T>();
135
136     // run configuration settings tab:
137     group.addEditor(ExecutionBundle.message("run.configuration.configuration.tab.title"), runConfigurationEditor);
138
139     // tabs provided by extensions:
140     //noinspection unchecked
141     PythonRunConfigurationExtensionsManager.getInstance().appendEditors(this, (SettingsEditorGroup)group);
142     group.addEditor(ExecutionBundle.message("logs.tab.title"), new LogConfigurationPanel<T>());
143
144     return group;
145   }
146
147   protected abstract SettingsEditor<T> createConfigurationEditor();
148
149   @Override
150   public void checkConfiguration() throws RuntimeConfigurationException {
151     super.checkConfiguration();
152
153     checkSdk();
154
155     checkExtensions();
156   }
157
158   private void checkExtensions() throws RuntimeConfigurationException {
159     try {
160       PythonRunConfigurationExtensionsManager.getInstance().validateConfiguration(this, false);
161     }
162     catch (RuntimeConfigurationException e) {
163       throw e;
164     }
165     catch (Exception ee) {
166       throw new RuntimeConfigurationException(ee.getMessage());
167     }
168   }
169
170   private void checkSdk() throws RuntimeConfigurationError {
171     if (PlatformUtils.isPyCharm()) {
172       final String path = getInterpreterPath();
173       if (path == null) {
174         throw new RuntimeConfigurationError("Please select a valid Python interpreter");
175       }
176     }
177     else {
178       if (!myUseModuleSdk) {
179         if (StringUtil.isEmptyOrSpaces(getSdkHome())) {
180           final Sdk projectSdk = ProjectRootManager.getInstance(getProject()).getProjectSdk();
181           if (projectSdk == null || !(projectSdk.getSdkType() instanceof PythonSdkType)) {
182             throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_sdk"));
183           }
184         }
185         else if (!PythonSdkType.getInstance().isValidSdkHome(getSdkHome())) {
186           throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_valid_sdk"));
187         }
188       }
189       else {
190         Sdk sdk = PythonSdkType.findPythonSdk(getModule());
191         if (sdk == null) {
192           throw new RuntimeConfigurationError(PyBundle.message("runcfg.unittest.no_module_sdk"));
193         }
194       }
195     }
196   }
197
198   public String getSdkHome() {
199     String sdkHome = mySdkHome;
200     if (StringUtil.isEmptyOrSpaces(mySdkHome)) {
201       final Sdk projectJdk = PythonSdkType.findPythonSdk(getModule());
202       if (projectJdk != null) {
203         sdkHome = projectJdk.getHomePath();
204       }
205     }
206     return sdkHome;
207   }
208
209   @Nullable
210   public String getInterpreterPath() {
211     String sdkHome;
212     if (myUseModuleSdk) {
213       Sdk sdk = PythonSdkType.findPythonSdk(getModule());
214       if (sdk == null) return null;
215       sdkHome = sdk.getHomePath();
216     }
217     else {
218       sdkHome = getSdkHome();
219     }
220     return sdkHome;
221   }
222
223   public Sdk getSdk() {
224     if (myUseModuleSdk) {
225       return PythonSdkType.findPythonSdk(getModule());
226     }
227     else {
228       return PythonSdkType.findSdkByPath(getSdkHome());
229     }
230   }
231
232   public void readExternal(Element element) throws InvalidDataException {
233     super.readExternal(element);
234     myInterpreterOptions = JDOMExternalizerUtil.readField(element, "INTERPRETER_OPTIONS");
235     readEnvs(element);
236     mySdkHome = JDOMExternalizerUtil.readField(element, "SDK_HOME");
237     myWorkingDirectory = JDOMExternalizerUtil.readField(element, "WORKING_DIRECTORY");
238     myUseModuleSdk = Boolean.parseBoolean(JDOMExternalizerUtil.readField(element, "IS_MODULE_SDK"));
239     final String addContentRoots = JDOMExternalizerUtil.readField(element, "ADD_CONTENT_ROOTS");
240     myAddContentRoots = addContentRoots == null || Boolean.parseBoolean(addContentRoots);
241     final String addSourceRoots = JDOMExternalizerUtil.readField(element, "ADD_SOURCE_ROOTS");
242     myAddSourceRoots = addSourceRoots == null || Boolean.parseBoolean(addSourceRoots);
243     if ( !mySkipModuleSerialization) {
244       getConfigurationModule().readExternal(element);
245     }
246
247     setMappingSettings(PathMappingSettings.readExternal(element));
248     // extension settings:
249     PythonRunConfigurationExtensionsManager.getInstance().readExternal(this, element);
250   }
251
252   protected void readEnvs(Element element) {
253     final String parentEnvs = JDOMExternalizerUtil.readField(element, "PARENT_ENVS");
254     if (parentEnvs != null) {
255       setPassParentEnvs(Boolean.parseBoolean(parentEnvs));
256     }
257     EnvironmentVariablesComponent.readExternal(element, getEnvs());
258   }
259
260   public void writeExternal(Element element) throws WriteExternalException {
261     super.writeExternal(element);
262     JDOMExternalizerUtil.writeField(element, "INTERPRETER_OPTIONS", myInterpreterOptions);
263     writeEnvs(element);
264     JDOMExternalizerUtil.writeField(element, "SDK_HOME", mySdkHome);
265     JDOMExternalizerUtil.writeField(element, "WORKING_DIRECTORY", myWorkingDirectory);
266     JDOMExternalizerUtil.writeField(element, "IS_MODULE_SDK", Boolean.toString(myUseModuleSdk));
267     JDOMExternalizerUtil.writeField(element, "ADD_CONTENT_ROOTS", Boolean.toString(myAddContentRoots));
268     JDOMExternalizerUtil.writeField(element, "ADD_SOURCE_ROOTS", Boolean.toString(myAddSourceRoots));
269     if ( !mySkipModuleSerialization) {
270       getConfigurationModule().writeExternal(element);
271     }
272
273     // extension settings:
274     PythonRunConfigurationExtensionsManager.getInstance().writeExternal(this, element);
275
276     PathMappingSettings.writeExternal(element, getMappingSettings());
277   }
278
279   protected void writeEnvs(Element element) {
280     JDOMExternalizerUtil.writeField(element, "PARENT_ENVS", Boolean.toString(isPassParentEnvs()));
281     EnvironmentVariablesComponent.writeExternal(element, getEnvs());
282   }
283
284   public String getInterpreterOptions() {
285     return myInterpreterOptions;
286   }
287
288   public void setInterpreterOptions(String interpreterOptions) {
289     myInterpreterOptions = interpreterOptions;
290   }
291
292   public String getWorkingDirectory() {
293     return myWorkingDirectory;
294   }
295
296   public void setWorkingDirectory(String workingDirectory) {
297     myWorkingDirectory = workingDirectory;
298   }
299
300   public void setSdkHome(String sdkHome) {
301     mySdkHome = sdkHome;
302   }
303
304   public Module getModule() {
305     return getConfigurationModule().getModule();
306   }
307
308   public boolean isUseModuleSdk() {
309     return myUseModuleSdk;
310   }
311
312   public void setUseModuleSdk(boolean useModuleSdk) {
313     myUseModuleSdk = useModuleSdk;
314   }
315
316   @Override
317   public boolean shouldAddContentRoots() {
318     return myAddContentRoots;
319   }
320
321   @Override
322   public boolean shouldAddSourceRoots() {
323     return myAddSourceRoots;
324   }
325
326   @Override
327   public void setAddSourceRoots(boolean flag) {
328     myAddSourceRoots = flag;
329   }
330
331   @Override
332   public void setAddContentRoots(boolean flag) {
333     myAddContentRoots = flag;
334   }
335
336   public static void copyParams(AbstractPythonRunConfigurationParams source, AbstractPythonRunConfigurationParams target) {
337     target.setEnvs(new HashMap<String, String>(source.getEnvs()));
338     target.setInterpreterOptions(source.getInterpreterOptions());
339     target.setPassParentEnvs(source.isPassParentEnvs());
340     target.setSdkHome(source.getSdkHome());
341     target.setWorkingDirectory(source.getWorkingDirectory());
342     target.setModule(source.getModule());
343     target.setUseModuleSdk(source.isUseModuleSdk());
344     target.setMappingSettings(source.getMappingSettings());
345     target.setAddContentRoots(source.shouldAddContentRoots());
346     target.setAddSourceRoots(source.shouldAddSourceRoots());
347   }
348
349   /**
350    * Some setups (e.g. virtualenv) provide a script that alters environment variables before running a python interpreter or other tools.
351    * Such settings are not directly stored but applied right before running using this method.
352    *
353    * @param commandLine what to patch
354    */
355   public void patchCommandLine(GeneralCommandLine commandLine) {
356     final String interpreterPath = getInterpreterPath();
357     Sdk sdk = getSdk();
358     if (sdk != null && interpreterPath != null) {
359       patchCommandLineFirst(commandLine, interpreterPath);
360       patchCommandLineForVirtualenv(commandLine, interpreterPath);
361       patchCommandLineForBuildout(commandLine, interpreterPath);
362       patchCommandLineLast(commandLine, interpreterPath);
363     }
364   }
365
366   /**
367    * Patches command line before virtualenv and buildout patchers.
368    * Default implementation does nothing.
369    *
370    * @param commandLine
371    * @param sdkHome
372    */
373   protected void patchCommandLineFirst(GeneralCommandLine commandLine, String sdkHome) {
374     // override
375   }
376
377   /**
378    * Patches command line after virtualenv and buildout patchers.
379    * Default implementation does nothing.
380    *
381    * @param commandLine
382    * @param sdkHome
383    */
384   protected void patchCommandLineLast(GeneralCommandLine commandLine, String sdkHome) {
385     // override
386   }
387
388   /**
389    * Gets called after {@link #patchCommandLineForVirtualenv(com.intellij.openapi.projectRoots.SdkType, com.intellij.openapi.projectRoots.SdkType)}
390    * Does nothing here, real implementations should use alter running script name or use engulfer.
391    *
392    * @param commandLine
393    * @param sdkHome
394    */
395   protected void patchCommandLineForBuildout(GeneralCommandLine commandLine, String sdkHome) {
396   }
397
398   /**
399    * Alters PATH so that a virtualenv is activated, if present.
400    *
401    * @param commandLine
402    * @param sdkHome
403    */
404   protected void patchCommandLineForVirtualenv(GeneralCommandLine commandLine, String sdkHome) {
405     PythonSdkType.patchCommandLineForVirtualenv(commandLine, sdkHome, isPassParentEnvs());
406   }
407
408   protected void setUnbufferedEnv() {
409     Map<String, String> envs = getEnvs();
410     // unbuffered I/O is easier for IDE to handle
411     PythonEnvUtil.setPythonUnbuffered(envs);
412   }
413
414   @Override
415   public boolean excludeCompileBeforeLaunchOption() {
416     final Module module = getModule();
417     return module != null ? ModuleType.get(module) instanceof PythonModuleTypeBase : true;
418   }
419
420   public boolean canRunWithCoverage() {
421     return true;
422   }
423
424   /**
425    * Create test spec (string to be passed to runner, probably glued with {@link #TEST_NAME_PARTS_SPLITTER})
426    * @param location test location as reported by runner
427    * @param failedTest failed test
428    * @return string spec or null if spec calculation is impossible
429    */
430   @Nullable
431   public String getTestSpec(@NotNull final Location<?> location, @NotNull final AbstractTestProxy failedTest) {
432     PsiElement element = location.getPsiElement();
433     PyClass pyClass = PsiTreeUtil.getParentOfType(element, PyClass.class, false);
434     PyFunction pyFunction = PsiTreeUtil.getParentOfType(element, PyFunction.class, false);
435     final VirtualFile virtualFile = location.getVirtualFile();
436     if (virtualFile != null) {
437       String path = virtualFile.getCanonicalPath();
438       if (pyClass != null) {
439         path += TEST_NAME_PARTS_SPLITTER + pyClass.getName();
440       }
441       if (pyFunction != null) {
442         path += TEST_NAME_PARTS_SPLITTER + pyFunction.getName();
443       }
444       return path;
445     }
446     return null;
447   }
448
449   /**
450    * @return working directory to run, never null, does its best to return project dir if empty.
451    * Unlike {@link #getWorkingDirectory()} it does not simply take directory from config.
452    */
453   @NotNull
454   public String getWorkingDirectorySafe() {
455     final String result = StringUtil.isEmpty(myWorkingDirectory) ? getProject().getBasePath() : myWorkingDirectory;
456     if (result == null) {
457       return new File(".").getAbsolutePath();
458     }
459     return result;
460   }
461
462   @Override
463   public String getModuleName() {
464     Module module = getModule();
465     return module != null ? module.getName() : null;
466   }
467
468   @Override
469   public boolean isCompileBeforeLaunchAddedByDefault() {
470     return false;
471   }
472 }