c6bcdbd930272c5a683e83b9e286c78fcfdbb9aa
[idea/community.git] / plugins / junit / src / com / intellij / execution / junit / TestObject.java
1 /*
2  * Copyright 2000-2016 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
17 package com.intellij.execution.junit;
18
19 import com.intellij.execution.*;
20 import com.intellij.execution.configurations.JavaParameters;
21 import com.intellij.execution.configurations.ParametersList;
22 import com.intellij.execution.configurations.RuntimeConfigurationException;
23 import com.intellij.execution.junit.testDiscovery.TestBySource;
24 import com.intellij.execution.junit.testDiscovery.TestsByChanges;
25 import com.intellij.execution.process.KillableColoredProcessHandler;
26 import com.intellij.execution.process.OSProcessHandler;
27 import com.intellij.execution.process.ProcessTerminatedListener;
28 import com.intellij.execution.runners.ExecutionEnvironment;
29 import com.intellij.execution.testframework.SearchForTestsTask;
30 import com.intellij.execution.testframework.SourceScope;
31 import com.intellij.execution.testframework.TestSearchScope;
32 import com.intellij.execution.util.JavaParametersUtil;
33 import com.intellij.execution.util.ProgramParametersUtil;
34 import com.intellij.jarRepository.JarRepositoryManager;
35 import com.intellij.junit4.JUnit4IdeaTestRunner;
36 import com.intellij.openapi.application.ReadAction;
37 import com.intellij.openapi.diagnostic.Logger;
38 import com.intellij.openapi.module.Module;
39 import com.intellij.openapi.module.ModuleUtilCore;
40 import com.intellij.openapi.project.DumbService;
41 import com.intellij.openapi.project.Project;
42 import com.intellij.openapi.roots.OrderRootType;
43 import com.intellij.openapi.roots.ProjectFileIndex;
44 import com.intellij.openapi.roots.libraries.ui.OrderRoot;
45 import com.intellij.openapi.util.io.FileUtil;
46 import com.intellij.openapi.util.text.StringUtil;
47 import com.intellij.openapi.vfs.CharsetToolkit;
48 import com.intellij.openapi.vfs.JarFileSystem;
49 import com.intellij.openapi.vfs.VirtualFile;
50 import com.intellij.psi.*;
51 import com.intellij.psi.search.GlobalSearchScope;
52 import com.intellij.psi.search.GlobalSearchScopesCore;
53 import com.intellij.psi.util.PsiUtilCore;
54 import com.intellij.refactoring.listeners.RefactoringElementListener;
55 import com.intellij.rt.execution.junit.IDEAJUnitListener;
56 import com.intellij.rt.execution.junit.JUnitStarter;
57 import com.intellij.rt.execution.junit.RepeatCount;
58 import com.intellij.rt.execution.testFrameworks.ForkedDebuggerHelper;
59 import com.intellij.util.Function;
60 import com.intellij.util.ObjectUtils;
61 import com.intellij.util.PathUtil;
62 import com.intellij.util.PathsList;
63 import com.siyeh.ig.junit.JUnitCommonClassNames;
64 import org.jetbrains.annotations.NonNls;
65 import org.jetbrains.annotations.NotNull;
66 import org.jetbrains.annotations.Nullable;
67 import org.jetbrains.idea.maven.utils.library.RepositoryLibraryProperties;
68
69 import java.io.File;
70 import java.io.IOException;
71 import java.io.InputStream;
72 import java.util.*;
73 import java.util.jar.Attributes;
74 import java.util.jar.JarFile;
75 import java.util.jar.Manifest;
76
77 public abstract class TestObject extends JavaTestFrameworkRunnableState<JUnitConfiguration> {
78   private static final String DEBUG_RT_PATH = "idea.junit_rt.path";
79
80   protected static final Logger LOG = Logger.getInstance(TestObject.class);
81
82   private static final String MESSAGE = ExecutionBundle.message("configuration.not.speficied.message");
83   @NonNls private static final String JUNIT_TEST_FRAMEWORK_NAME = "JUnit";
84
85   private final JUnitConfiguration myConfiguration;
86   protected File myListenersFile;
87   protected <T> void addClassesListToJavaParameters(Collection<? extends T> elements,
88                                                     Function<T, String> nameFunction,
89                                                     String packageName,
90                                                     boolean createTempFile, JavaParameters javaParameters) throws CantRunException {
91     try {
92       if (createTempFile) {
93         createTempFiles(javaParameters);
94       }
95
96       final Map<Module, List<String>> perModule = forkPerModule() ? new TreeMap<>(
97         (o1, o2) -> StringUtil.compare(o1.getName(), o2.getName(), true)) : null;
98
99       final List<String> testNames = new ArrayList<>();
100
101       if (elements.isEmpty() && perModule != null) {
102         final SourceScope sourceScope = getSourceScope();
103         Project project = getConfiguration().getProject();
104         if (sourceScope != null && packageName != null
105             && JUnitStarter.JUNIT5_PARAMETER.equals(getRunner())) {
106           final PsiPackage aPackage = JavaPsiFacade.getInstance(getConfiguration().getProject()).findPackage(packageName);
107           if (aPackage != null) {
108             final TestSearchScope scope = getScope();
109             if (scope != null) {
110               final GlobalSearchScope configurationSearchScope = GlobalSearchScopesCore.projectTestScope(project)
111                 .intersectWith(sourceScope.getGlobalSearchScope());
112               final PsiDirectory[] directories = aPackage.getDirectories(configurationSearchScope);
113               for (PsiDirectory directory : directories) {
114                 Module module = ModuleUtilCore.findModuleForFile(directory.getVirtualFile(), project);
115                 if (module != null) {
116                   perModule.put(module, Collections.emptyList());
117                 }
118               }
119             }
120           }
121         }
122       }
123
124       for (final T element : elements) {
125         final String name = nameFunction.fun(element);
126         if (name == null) {
127           continue;
128         }
129
130         final PsiElement psiElement = retrievePsiElement(element);
131         if (perModule != null && psiElement != null) {
132           final Module module = ModuleUtilCore.findModuleForPsiElement(psiElement);
133           if (module != null) {
134             List<String> list = perModule.get(module);
135             if (list == null) {
136               list = new ArrayList<>();
137               perModule.put(module, list);
138             }
139             list.add(name);
140           }
141         }
142         else {
143           testNames.add(name);
144         }
145       }
146       final JUnitConfiguration.Data data = getConfiguration().getPersistentData();
147       if (perModule != null) {
148         for (List<String> perModuleClasses : perModule.values()) {
149           Collections.sort(perModuleClasses);
150           testNames.addAll(perModuleClasses);
151         }
152       }
153       else if (JUnitConfiguration.TEST_PACKAGE.equals(data.TEST_OBJECT)) {
154         Collections.sort(testNames); //sort tests in FQN order
155       }
156
157       final String category = JUnitConfiguration.TEST_CATEGORY.equals(data.TEST_OBJECT) ? data.getCategory()
158                                                                                         : JUnitConfiguration.TEST_TAGS.equals(data.TEST_OBJECT) ? StringUtil.join(data.getTags(), " ") : "";
159       final String filters = JUnitConfiguration.TEST_PATTERN.equals(data.TEST_OBJECT) ? data.getPatternPresentation() : "";
160       JUnitStarter.printClassesList(testNames, packageName, category, filters, myTempFile);
161
162       writeClassesPerModule(packageName, javaParameters, perModule);
163     }
164     catch (IOException e) {
165       LOG.error(e);
166     }
167   }
168
169   public Module[] getModulesToCompile() {
170     final SourceScope sourceScope = getSourceScope();
171     return sourceScope != null ? sourceScope.getModulesToCompile() : Module.EMPTY_ARRAY;
172   }
173
174   protected TestObject(JUnitConfiguration configuration, ExecutionEnvironment environment) {
175     super(environment);
176     myConfiguration = configuration;
177   }
178
179   public abstract String suggestActionName();
180
181   public abstract RefactoringElementListener getListener(PsiElement element, JUnitConfiguration configuration);
182
183   public abstract boolean isConfiguredByElement(JUnitConfiguration configuration,
184                                                 PsiClass testClass,
185                                                 PsiMethod testMethod,
186                                                 PsiPackage testPackage,
187                                                 PsiDirectory testDir);
188
189   public void checkConfiguration() throws RuntimeConfigurationException{
190     JavaParametersUtil.checkAlternativeJRE(getConfiguration());
191     ProgramParametersUtil.checkWorkingDirectoryExist(getConfiguration(), getConfiguration().getProject(),
192                                                      getConfiguration().getConfigurationModule().getModule());
193   }
194
195   @Nullable
196   public SourceScope getSourceScope() {
197     return SourceScope.modules(getConfiguration().getModules());
198   }
199
200   @Override
201   protected void configureRTClasspath(JavaParameters javaParameters) throws CantRunException{
202     final String path = System.getProperty(DEBUG_RT_PATH);
203     javaParameters.getClassPath().add(path != null ? path : PathUtil.getJarPathForClass(JUnitStarter.class));
204
205     //include junit5 listeners for the case custom junit 5 engines would be detected on runtime
206     javaParameters.getClassPath().add(getJUnit5RtFile());
207
208     String preferredRunner = getRunner();
209     if (JUnitStarter.JUNIT5_PARAMETER.equals(preferredRunner)) {
210       final Project project = getConfiguration().getProject();
211       GlobalSearchScope globalSearchScope = getScopeForJUnit(getConfiguration().getConfigurationModule().getModule(), project);
212       appendJUnit5LauncherClasses(javaParameters, project, globalSearchScope);
213     }
214   }
215
216   public static File getJUnit5RtFile() {
217     File junit4Rt = new File(PathUtil.getJarPathForClass(JUnit4IdeaTestRunner.class));
218     String junit5Name = junit4Rt.getName().replace("junit", "junit5");
219     return new File(junit4Rt.getParent(), junit5Name);
220   }
221
222   @Override
223   protected JavaParameters createJavaParameters() throws ExecutionException {
224     JavaParameters javaParameters = super.createJavaParameters();
225     javaParameters.setMainClass(JUnitConfiguration.JUNIT_START_CLASS);
226     javaParameters.getProgramParametersList().add(JUnitStarter.IDE_VERSION + JUnitStarter.VERSION);
227
228     final StringBuilder buf = new StringBuilder();
229     collectListeners(javaParameters, buf, IDEAJUnitListener.EP_NAME, "\n");
230     if (buf.length() > 0) {
231       try {
232         myListenersFile = FileUtil.createTempFile("junit_listeners_", "", true);
233         javaParameters.getProgramParametersList().add("@@" + myListenersFile.getPath());
234         FileUtil.writeToFile(myListenersFile, buf.toString().getBytes(CharsetToolkit.UTF8_CHARSET));
235       }
236       catch (IOException e) {
237         LOG.error(e);
238       }
239     }
240
241     String preferredRunner = getRunner();
242     if (preferredRunner != null) {
243       javaParameters.getProgramParametersList().add(preferredRunner);
244     }
245     
246     return javaParameters;
247   }
248
249   public void appendJUnit5LauncherClasses(JavaParameters javaParameters, Project project, GlobalSearchScope globalSearchScope) throws CantRunException{
250     final PathsList classPath = javaParameters.getClassPath();
251     JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
252     PsiClass classFromCommon = DumbService.getInstance(project).computeWithAlternativeResolveEnabled(() -> psiFacade.findClass("org.junit.platform.commons.JUnitException", globalSearchScope));
253     String launcherVersion = ObjectUtils.notNull(getVersion(classFromCommon), "1.0.0");
254     if (!hasPackageWithDirectories(psiFacade, "org.junit.platform.launcher", globalSearchScope)) {
255       downloadDependenciesWhenRequired(project, classPath,
256                                        new RepositoryLibraryProperties("org.junit.platform", "junit-platform-launcher", launcherVersion));
257     }
258
259     //add standard engines only if no engine api is present
260     if (!hasPackageWithDirectories(psiFacade, "org.junit.platform.engine", globalSearchScope) ||
261         !isCustomJUnit5(globalSearchScope)) {
262
263       PsiClass testAnnotation = DumbService.getInstance(project).computeWithAlternativeResolveEnabled(() -> psiFacade.findClass(JUnitUtil.TEST5_ANNOTATION, globalSearchScope));
264       String jupiterVersion = ObjectUtils.notNull(getVersion(testAnnotation), "5.0.0");
265       if (!hasPackageWithDirectories(psiFacade, "org.junit.jupiter.engine", globalSearchScope) &&
266           hasPackageWithDirectories(psiFacade, JUnitUtil.TEST5_PACKAGE_FQN, globalSearchScope)) {
267         downloadDependenciesWhenRequired(project, classPath,
268                                          new RepositoryLibraryProperties("org.junit.jupiter", "junit-jupiter-engine", jupiterVersion));
269       }
270
271       if (!hasPackageWithDirectories(psiFacade, "org.junit.vintage", globalSearchScope) &&
272           hasPackageWithDirectories(psiFacade, "junit.framework", globalSearchScope)) {
273         String version = StringUtil.compareVersionNumbers(launcherVersion, "1.1.0") < 0 
274                          ? "4.12." + StringUtil.getShortName(launcherVersion) 
275                          : jupiterVersion;
276         downloadDependenciesWhenRequired(project, classPath,
277                                          new RepositoryLibraryProperties("org.junit.vintage", "junit-vintage-engine", version));
278       }
279     }
280   }
281
282   private static String getVersion(PsiClass classFromCommon) {
283     VirtualFile virtualFile = PsiUtilCore.getVirtualFile(classFromCommon);
284     if (virtualFile == null) return null;
285     ProjectFileIndex index = ProjectFileIndex.SERVICE.getInstance(classFromCommon.getProject());
286     VirtualFile root = index.getClassRootForFile(virtualFile);
287     if (root != null && root.getFileSystem() instanceof JarFileSystem) {
288       VirtualFile manifestFile = root.findFileByRelativePath(JarFile.MANIFEST_NAME);
289       if (manifestFile == null) {
290         return null;
291       }
292
293       try (final InputStream inputStream = manifestFile.getInputStream()) {
294         return new Manifest(inputStream).getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
295       }
296       catch (IOException e) {
297         return null;
298       }
299     }
300     return null;
301   }
302
303   private static void downloadDependenciesWhenRequired(Project project,
304                                                        PathsList classPath, 
305                                                        RepositoryLibraryProperties properties) throws CantRunException {
306     Collection<OrderRoot> roots = 
307       JarRepositoryManager.loadDependenciesModal(project, properties, false, false, null, null);
308     if (roots.isEmpty()) {
309       throw new CantRunException("Failed to resolve " + properties.getMavenId());
310     }
311     for (OrderRoot root : roots) {
312       if (root.getType() == OrderRootType.CLASSES) {
313         classPath.add(root.getFile());
314       }
315     }
316   }
317
318   private static boolean hasPackageWithDirectories(JavaPsiFacade psiFacade,
319                                                    String packageQName,
320                                                    GlobalSearchScope globalSearchScope) {
321     PsiPackage aPackage = psiFacade.findPackage(packageQName);
322     return aPackage != null && aPackage.getDirectories(globalSearchScope).length > 0;
323   }
324
325   private static GlobalSearchScope getScopeForJUnit(@Nullable Module module, Project project) {
326     return module != null ? GlobalSearchScope.moduleRuntimeScope(module, true) : GlobalSearchScope.allScope(project);
327   }
328
329   public static GlobalSearchScope getScopeForJUnit(JUnitConfiguration configuration) {
330     return getScopeForJUnit(configuration.getConfigurationModule().getModule(),
331                             configuration.getProject() );
332   }
333
334   @NotNull
335   protected OSProcessHandler createHandler(Executor executor) throws ExecutionException {
336     appendForkInfo(executor);
337     appendRepeatMode();
338
339     final OSProcessHandler processHandler = new KillableColoredProcessHandler(createCommandLine());
340     ProcessTerminatedListener.attach(processHandler);
341     final SearchForTestsTask searchForTestsTask = createSearchingForTestsTask();
342     if (searchForTestsTask != null) {
343       searchForTestsTask.attachTaskToProcess(processHandler);
344     }
345     return processHandler;
346   }
347
348   public void appendRepeatMode() throws ExecutionException {
349     final String repeatMode = getConfiguration().getRepeatMode();
350     if (!RepeatCount.ONCE.equals(repeatMode)) {
351       final int repeatCount = getConfiguration().getRepeatCount();
352       final String countString = RepeatCount.N.equals(repeatMode) && repeatCount > 0
353                                  ? RepeatCount.getCountString(repeatCount)
354                                  : repeatMode;
355       getJavaParameters().getProgramParametersList().add(countString);
356     }
357   }
358
359   @Override
360   protected boolean isIdBasedTestTree() {
361     return JUnitStarter.JUNIT5_PARAMETER.equals(getRunner());
362   }
363
364   @NotNull
365   @Override
366   protected String getForkMode() {
367     return getConfiguration().getForkMode();
368   }
369
370   public static TestObject fromString(final String id,
371                                       final JUnitConfiguration configuration,
372                                       @NotNull ExecutionEnvironment environment) {
373     if (JUnitConfiguration.TEST_METHOD.equals(id)) {
374       return new TestMethod(configuration, environment);
375     }
376     if (JUnitConfiguration.TEST_CLASS.equals(id)) {
377       return new TestClass(configuration, environment);
378     }
379     if (JUnitConfiguration.TEST_PACKAGE.equals(id)){
380       return new TestPackage(configuration, environment);
381     }
382     if (JUnitConfiguration.TEST_DIRECTORY.equals(id)) {
383       return new TestDirectory(configuration, environment);
384     }
385     if (JUnitConfiguration.TEST_CATEGORY.equals(id)) {
386       return new TestCategory(configuration, environment);
387     }
388     if (JUnitConfiguration.TEST_PATTERN.equals(id)) {
389       return new TestsPattern(configuration, environment);
390     }
391     if (JUnitConfiguration.TEST_UNIQUE_ID.equals(id)) {
392       return new TestUniqueId(configuration, environment);
393     }
394     if (JUnitConfiguration.TEST_TAGS.equals(id)) {
395       return new TestTags(configuration, environment);
396     }
397     if (JUnitConfiguration.BY_SOURCE_POSITION.equals(id)) {
398       return new TestBySource(configuration, environment);
399     }
400     if (JUnitConfiguration.BY_SOURCE_CHANGES.equals(id)) {
401       return new TestsByChanges(configuration, environment);
402     }
403     LOG.info(MESSAGE + id);
404     return null;
405   }
406
407   protected PsiElement retrievePsiElement(Object element) {
408     return element instanceof PsiElement ? (PsiElement)element : null;
409   }
410
411   @Override
412   protected void deleteTempFiles() {
413     super.deleteTempFiles();
414     if (myListenersFile != null) {
415       FileUtil.delete(myListenersFile);
416     }
417   }
418
419   @NotNull
420   protected String getFrameworkName() {
421     return JUNIT_TEST_FRAMEWORK_NAME;
422   }
423
424   @NotNull
425   protected String getFrameworkId() {
426     return "junit";
427   }
428
429   protected void passTempFile(ParametersList parametersList, String tempFilePath) {
430     parametersList.add("@" + tempFilePath);
431   }
432
433   @NotNull
434   public JUnitConfiguration getConfiguration() {
435     return myConfiguration;
436   }
437
438   @Override
439   protected TestSearchScope getScope() {
440     return getConfiguration().getPersistentData().getScope();
441   }
442
443   protected void passForkMode(String forkMode, File tempFile, JavaParameters parameters) throws ExecutionException {
444     parameters.getProgramParametersList().add("@@@" + forkMode + ',' + tempFile.getAbsolutePath());
445     if (getForkSocket() != null) {
446       parameters.getProgramParametersList().add(ForkedDebuggerHelper.DEBUG_SOCKET + getForkSocket().getLocalPort());
447     }
448   }
449
450   private String myRunner;
451
452   protected String getRunner() {
453     if (myRunner == null) {
454       myRunner = getRunnerInner();
455     }
456     return myRunner;
457   }
458
459   private String getRunnerInner() {
460     final GlobalSearchScope globalSearchScope = getScopeForJUnit(myConfiguration);
461     JUnitConfiguration.Data data = myConfiguration.getPersistentData();
462     Project project = myConfiguration.getProject();
463     boolean isMethodConfiguration = JUnitConfiguration.TEST_METHOD.equals(data.TEST_OBJECT);
464     boolean isClassConfiguration = JUnitConfiguration.TEST_CLASS.equals(data.TEST_OBJECT);
465     final PsiClass psiClass = isMethodConfiguration || isClassConfiguration
466                               ? JavaExecutionUtil.findMainClass(project, data.getMainClassName(), globalSearchScope) : null;
467     if (psiClass != null) {
468       if (JUnitUtil.isJUnit5TestClass(psiClass, false)) {
469         return JUnitStarter.JUNIT5_PARAMETER;
470       }
471
472       if (isClassConfiguration || JUnitUtil.isJUnit4TestClass(psiClass)) {
473         return JUnitStarter.JUNIT4_PARAMETER;
474       }
475
476       final String methodName = data.getMethodName();
477       final PsiMethod[] methods = psiClass.findMethodsByName(methodName, true);
478       for (PsiMethod method : methods) {
479         if (JUnitUtil.isTestAnnotated(method)) {
480           return JUnitStarter.JUNIT4_PARAMETER;
481         }
482       }
483       return JUnitStarter.JUNIT3_PARAMETER;
484     }
485     return JUnitUtil.isJUnit5(globalSearchScope, project) || isCustomJUnit5(globalSearchScope) ? JUnitStarter.JUNIT5_PARAMETER : null;
486   }
487
488   private boolean isCustomJUnit5(GlobalSearchScope globalSearchScope) {
489     Project project = myConfiguration.getProject();
490     JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);
491     if (DumbService.getInstance(project)
492           .computeWithAlternativeResolveEnabled(() -> {
493             @Nullable PsiClass testEngine = ReadAction.compute(() -> psiFacade.findClass(JUnitCommonClassNames.ORG_JUNIT_PLATFORM_ENGINE_TEST_ENGINE, globalSearchScope));
494             return testEngine;
495           }) == null) {
496       return false;
497     }
498
499     ClassLoader loader = TestClassCollector.createUsersClassLoader(myConfiguration);
500     try {
501       ServiceLoader<?> serviceLoader = ServiceLoader.load(Class.forName(JUnitCommonClassNames.ORG_JUNIT_PLATFORM_ENGINE_TEST_ENGINE, false, loader), loader);
502       for (Object engine : serviceLoader) {
503         String engineClassName = engine.getClass().getName();
504         if (!"org.junit.jupiter.engine.JupiterTestEngine".equals(engineClassName) &&
505             !"org.junit.vintage.engine.VintageTestEngine".equals(engineClassName)) {
506           return true;
507         }
508       }
509       return false;
510     }
511     catch (Throwable e) {
512       return false;
513     }
514   }
515 }