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