replaced <code></code> with more concise {@code}
[idea/community.git] / platform / testFramework / src / com / intellij / openapi / application / ex / PathManagerEx.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.openapi.application.ex;
18
19 import com.intellij.openapi.application.PathManager;
20 import com.intellij.openapi.module.impl.ModuleManagerImpl;
21 import com.intellij.openapi.module.impl.ModulePath;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.util.JDOMUtil;
24 import com.intellij.openapi.util.text.StringUtil;
25 import com.intellij.testFramework.Parameterized;
26 import com.intellij.testFramework.TestRunnerUtil;
27 import com.intellij.util.containers.ContainerUtil;
28 import gnu.trove.THashSet;
29 import junit.framework.TestCase;
30 import org.jdom.Element;
31 import org.jdom.JDOMException;
32 import org.jetbrains.annotations.NonNls;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35 import org.jetbrains.jps.model.serialization.JDomSerializationUtil;
36
37 import java.io.File;
38 import java.io.IOException;
39 import java.lang.reflect.Modifier;
40 import java.util.*;
41 import java.util.concurrent.ConcurrentMap;
42
43 import static com.intellij.openapi.util.io.FileUtil.toSystemDependentName;
44 import static java.util.Arrays.asList;
45
46 public class PathManagerEx {
47
48   /**
49    * All IDEA project files may be logically divided by the following criteria:
50    * <ul>
51    *   <li>files that are contained at {@code 'community'} directory;</li>
52    *   <li>all other files;</li>
53    * </ul>
54    * <p/>
55    * File location types implied by criteria mentioned above are enumerated here.
56    */
57   private enum FileSystemLocation {
58     ULTIMATE, COMMUNITY
59   }
60
61   /**
62    * Caches test data lookup strategy by class.
63    */
64   private static final ConcurrentMap<Class, TestDataLookupStrategy> CLASS_STRATEGY_CACHE = ContainerUtil.newConcurrentMap();
65   private static final ConcurrentMap<String, Class> CLASS_CACHE = ContainerUtil.newConcurrentMap();
66   private static Set<String> ourCommunityModules;
67
68   private PathManagerEx() { }
69
70   /**
71    * Enumerates possible strategies of test data lookup.
72    * <p/>
73    * Check member-level javadoc for more details.
74    */
75   public enum TestDataLookupStrategy {
76     /**
77      * Stands for algorithm that retrieves {@code 'test data'} stored at the {@code 'ultimate'} project level assuming
78      * that it's used from the test running in context of {@code 'ultimate'} project as well.
79      * <p/>
80      * Is assumed to be default strategy for all {@code 'ultimate'} tests.
81      */
82     ULTIMATE,
83
84     /**
85      * Stands for algorithm that retrieves {@code 'test data'} stored at the {@code 'community'} project level assuming
86      * that it's used from the test running in context of {@code 'community'} project as well.
87      * <p/>
88      * Is assumed to be default strategy for all {@code 'community'} tests.
89      */
90     COMMUNITY,
91
92     /**
93      * Stands for algorithm that retrieves {@code 'test data'} stored at the {@code 'community'} project level assuming
94      * that it's used from the test running in context of {@code 'ultimate'} project.
95      */
96     COMMUNITY_FROM_ULTIMATE
97   }
98
99   /**
100    * It's assumed that test data location for both {@code community} and {@code ultimate} tests follows the same template:
101    * <code>'<IDEA_HOME>/<RELATIVE_PATH>'</code>.
102    * <p/>
103    * {@code 'IDEA_HOME'} here stands for path to IDEA installation; {@code 'RELATIVE_PATH'} defines a path to
104    * test data relative to IDEA installation path. That relative path may be different for {@code community}
105    * and {@code ultimate} tests.
106    * <p/>
107    * This collection contains mappings from test group type to relative paths to use, i.e. it's possible to define more than one
108    * relative path for the single test group. It's assumed that path definition algorithm iterates them and checks if
109    * resulting absolute path points to existing directory. The one is returned in case of success; last path is returned otherwise.
110    * <p/>
111    * Hence, the order of relative paths for the single test group matters.
112    */
113   private static final Map<TestDataLookupStrategy, List<String>> TEST_DATA_RELATIVE_PATHS
114     = new EnumMap<>(TestDataLookupStrategy.class);
115
116   static {
117     TEST_DATA_RELATIVE_PATHS.put(TestDataLookupStrategy.ULTIMATE, Collections.singletonList(toSystemDependentName("testData")));
118     TEST_DATA_RELATIVE_PATHS.put(
119       TestDataLookupStrategy.COMMUNITY,
120       Collections.singletonList(toSystemDependentName("java/java-tests/testData"))
121     );
122     TEST_DATA_RELATIVE_PATHS.put(
123       TestDataLookupStrategy.COMMUNITY_FROM_ULTIMATE,
124       Collections.singletonList(toSystemDependentName("community/java/java-tests/testData"))
125     );
126   }
127
128   /**
129    * Shorthand for calling {@link #getTestDataPath(TestDataLookupStrategy)} with
130    * {@link #guessTestDataLookupStrategy() guessed} lookup strategy.
131    *
132    * @return    test data path with {@link #guessTestDataLookupStrategy() guessed} lookup strategy
133    * @throws IllegalStateException    as defined by {@link #getTestDataPath(TestDataLookupStrategy)}
134    */
135   @NonNls
136   public static String getTestDataPath() throws IllegalStateException {
137     TestDataLookupStrategy strategy = guessTestDataLookupStrategy();
138     return getTestDataPath(strategy);
139   }
140
141   public static String getTestDataPath(String path) throws IllegalStateException {
142     return getTestDataPath() + path.replace('/', File.separatorChar);
143   }
144
145   /**
146    * Shorthand for calling {@link #getTestDataPath(TestDataLookupStrategy)} with strategy obtained via call to
147    * {@link #determineLookupStrategy(Class)} with the given class.
148    * <p/>
149    * <b>Note:</b> this method receives explicit class argument in order to solve the following limitation - we analyze calling
150    * stack trace in order to guess test data lookup strategy ({@link #guessTestDataLookupStrategyOnClassLocation()}). However,
151    * there is a possible case that super-class method is called on sub-class object. Stack trace shows super-class then.
152    * There is a possible situation that actual test is {@code 'ultimate'} but its abstract super-class is
153    * {@code 'community'}, hence, test data lookup is performed incorrectly. So, this method should be called from abstract
154    * base test class if its concrete sub-classes doesn't explicitly occur at stack trace.
155    *
156    *
157    * @param testClass     target test class for which test data should be obtained
158    * @return              base test data directory to use for the given test class
159    * @throws IllegalStateException    as defined by {@link #getTestDataPath(TestDataLookupStrategy)}
160    */
161   public static String getTestDataPath(Class<?> testClass) throws IllegalStateException {
162     TestDataLookupStrategy strategy = isLocatedInCommunity() ? TestDataLookupStrategy.COMMUNITY : determineLookupStrategy(testClass);
163     return getTestDataPath(strategy);
164   }
165
166   /**
167    * @return path to 'community' project home irrespective of current project
168    */
169   @NotNull
170   public static String getCommunityHomePath() {
171     String path = PathManager.getHomePath();
172     return isLocatedInCommunity() ? path : path + File.separator + "community";
173   }
174
175   /**
176    * @return path to 'community' project home if {@code testClass} is located in the community project and path to 'ultimate' project otherwise
177    */
178   public static String getHomePath(Class<?> testClass) {
179     TestDataLookupStrategy strategy = isLocatedInCommunity() ? TestDataLookupStrategy.COMMUNITY : determineLookupStrategy(testClass);
180     return strategy == TestDataLookupStrategy.COMMUNITY_FROM_ULTIMATE ? getCommunityHomePath() : PathManager.getHomePath();
181   }
182
183   /**
184    * Find file by its path relative to 'community' directory irrespective of current project
185    * @param relativePath path to file relative to 'community' directory
186    * @return file under the home directory of 'community' project
187    */
188   public static File findFileUnderCommunityHome(String relativePath) {
189     File file = new File(getCommunityHomePath(), toSystemDependentName(relativePath));
190     if (!file.exists()) {
191       throw new IllegalArgumentException("Cannot find file '" + relativePath + "' under '" + getCommunityHomePath() + "' directory");
192     }
193     return file;
194   }
195
196   /**
197    * Find file by its path relative to project home directory (the 'community' project if {@code testClass} is located
198    * in the community project, and the 'ultimate' project otherwise)
199    */
200   public static File findFileUnderProjectHome(String relativePath, Class<? extends TestCase> testClass) {
201     String homePath = getHomePath(testClass);
202     File file = new File(homePath, toSystemDependentName(relativePath));
203     if (!file.exists()) {
204       throw new IllegalArgumentException("Cannot find file '" + relativePath + "' under '" + homePath + "' directory");
205     }
206     return file;
207   }
208
209   private static boolean isLocatedInCommunity() {
210     FileSystemLocation projectLocation = parseProjectLocation();
211     return projectLocation == FileSystemLocation.COMMUNITY;
212     // There is no other options then.
213   }
214
215   /**
216    * Tries to return test data path for the given lookup strategy.
217    *
218    * @param strategy    lookup strategy to use
219    * @return            test data path for the given strategy
220    * @throws IllegalStateException    if it's not possible to find valid test data path for the given strategy
221    */
222   @NonNls
223   public static String getTestDataPath(TestDataLookupStrategy strategy) throws IllegalStateException {
224     String homePath = PathManager.getHomePath();
225
226     List<String> relativePaths = TEST_DATA_RELATIVE_PATHS.get(strategy);
227     if (relativePaths.isEmpty()) {
228       throw new IllegalStateException(
229         String.format("Can't determine test data path. Reason: no predefined relative paths are configured for test data "
230                       + "lookup strategy %s. Configured mappings: %s", strategy, TEST_DATA_RELATIVE_PATHS)
231       );
232     }
233
234     File candidate = null;
235     for (String relativePath : relativePaths) {
236       candidate = new File(homePath, relativePath);
237       if (candidate.isDirectory()) {
238         return candidate.getPath();
239       }
240     }
241
242     if (candidate == null) {
243       throw new IllegalStateException("Can't determine test data path. Looks like programming error - reached 'if' block that was "
244                                       + "never expected to be executed");
245     }
246     return candidate.getPath();
247   }
248
249   /**
250    * Tries to guess test data lookup strategy for the current execution.
251    *
252    * @return    guessed lookup strategy for the current execution; defaults to {@link TestDataLookupStrategy#ULTIMATE}
253    */
254   public static TestDataLookupStrategy guessTestDataLookupStrategy() {
255     TestDataLookupStrategy result = guessTestDataLookupStrategyOnClassLocation();
256     if (result == null) {
257       result = guessTestDataLookupStrategyOnDirectoryAvailability();
258     }
259     return result;
260   }
261
262   @SuppressWarnings({"ThrowableInstanceNeverThrown"})
263   @Nullable
264   private static TestDataLookupStrategy guessTestDataLookupStrategyOnClassLocation() {
265     if (isLocatedInCommunity()) return TestDataLookupStrategy.COMMUNITY;
266
267     // The general idea here is to find test class at the bottom of hierarchy and try to resolve test data lookup strategy
268     // against it. Rationale is that there is a possible case that, say, 'ultimate' test class extends basic test class
269     // that remains at 'community'. We want to perform the processing against 'ultimate' test class then.
270
271     // About special abstract classes processing - there is a possible case that target test class extends abstract base
272     // test class and call to this method is rooted from that parent. We need to resolve test data lookup against super
273     // class then, hence, we keep track of found abstract test class as well and fallback to it if no non-abstract class is found.
274
275     Class<?> testClass = null;
276     Class<?> abstractTestClass = null;
277     StackTraceElement[] stackTrace = new Exception().getStackTrace();
278     for (StackTraceElement stackTraceElement : stackTrace) {
279       String className = stackTraceElement.getClassName();
280       Class<?> clazz = loadClass(className);
281       if (clazz == null || TestCase.class == clazz || !isJUnitClass(clazz)) {
282         continue;
283       }
284
285       if (determineLookupStrategy(clazz) == TestDataLookupStrategy.ULTIMATE) return TestDataLookupStrategy.ULTIMATE;
286       if ((clazz.getModifiers() & Modifier.ABSTRACT) == 0) {
287         testClass = clazz;
288       }
289       else {
290         abstractTestClass = clazz;
291       }
292     }
293
294     Class<?> classToUse = testClass == null ? abstractTestClass : testClass;
295     return classToUse == null ? null : determineLookupStrategy(classToUse);
296   }
297
298   @Nullable
299   private static Class<?> loadClass(String className) {
300     ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
301
302     Class<?> clazz = CLASS_CACHE.get(className);
303     if (clazz != null) {
304       return clazz;
305     }
306
307     ClassLoader definingClassLoader = PathManagerEx.class.getClassLoader();
308     ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
309
310     for (ClassLoader classLoader : asList(contextClassLoader, definingClassLoader, systemClassLoader)) {
311       clazz = loadClass(className, classLoader);
312       if (clazz != null) {
313         CLASS_CACHE.put(className, clazz);
314         return clazz;
315       }
316     }
317
318     CLASS_CACHE.put(className, TestCase.class); //dummy
319     return null;
320   }
321
322   @Nullable
323   private static Class<?> loadClass(String className, ClassLoader classLoader) {
324     try {
325       return Class.forName(className, true, classLoader);
326     }
327     catch (NoClassDefFoundError | ClassNotFoundException e) {
328       return null;
329     }
330   }
331
332   @SuppressWarnings("TestOnlyProblems")
333   private static boolean isJUnitClass(Class<?> clazz) {
334     return TestCase.class.isAssignableFrom(clazz) || TestRunnerUtil.isJUnit4TestClass(clazz) || Parameterized.class.isAssignableFrom(clazz);
335   }
336
337   @Nullable
338   private static TestDataLookupStrategy determineLookupStrategy(Class<?> clazz) {
339     // Check if resulting strategy is already cached for the target class.
340     TestDataLookupStrategy result = CLASS_STRATEGY_CACHE.get(clazz);
341     if (result != null) {
342       return result;
343     }
344
345     FileSystemLocation classFileLocation = computeClassLocation(clazz);
346
347     // We know that project location is ULTIMATE if control flow reaches this place.
348     result = classFileLocation == FileSystemLocation.COMMUNITY ? TestDataLookupStrategy.COMMUNITY_FROM_ULTIMATE
349                                                                : TestDataLookupStrategy.ULTIMATE;
350     CLASS_STRATEGY_CACHE.put(clazz, result);
351     return result;
352   }
353
354   public static void replaceLookupStrategy(Class<?> substitutor, Class<?>... initial) {
355     CLASS_STRATEGY_CACHE.clear();
356     for (Class<?> aClass : initial) {
357       CLASS_STRATEGY_CACHE.put(aClass, determineLookupStrategy(substitutor));
358     }
359   }
360
361   private static FileSystemLocation computeClassLocation(Class<?> clazz) {
362     String classRootPath = PathManager.getJarPathForClass(clazz);
363     if (classRootPath == null) {
364       throw new IllegalStateException("Cannot find root directory for " + clazz);
365     }
366     File root = new File(classRootPath);
367     if (!root.exists()) {
368       throw new IllegalStateException("Classes root " + root + " doesn't exist");
369     }
370     if (!root.isDirectory()) {
371       //this means that clazz is located in a library, perhaps we should throw exception here
372       return FileSystemLocation.ULTIMATE;
373     }
374
375     String moduleName = root.getName();
376     String chunkPrefix = "ModuleChunk(";
377     if (moduleName.startsWith(chunkPrefix)) {
378       //todo[nik] this is temporary workaround to fix tests on TeamCity which compiles the whole modules cycle to a single output directory
379       moduleName = StringUtil.trimStart(moduleName, chunkPrefix);
380       moduleName = moduleName.substring(0, moduleName.indexOf(','));
381     }
382     return getCommunityModules().contains(moduleName) ? FileSystemLocation.COMMUNITY : FileSystemLocation.ULTIMATE;
383   }
384
385   private synchronized static Set<String> getCommunityModules() {
386     if (ourCommunityModules != null) {
387       return ourCommunityModules;
388     }
389
390     ourCommunityModules = new THashSet<>();
391     File modulesXml = findFileUnderCommunityHome(Project.DIRECTORY_STORE_FOLDER + "/modules.xml");
392     if (!modulesXml.exists()) {
393       throw new IllegalStateException("Cannot obtain test data path: " + modulesXml.getAbsolutePath() + " not found");
394     }
395
396     try {
397       Element element = JDomSerializationUtil.findComponent(JDOMUtil.load(modulesXml), ModuleManagerImpl.COMPONENT_NAME);
398       assert element != null;
399       for (ModulePath file : ModuleManagerImpl.getPathsToModuleFiles(element)) {
400         ourCommunityModules.add(file.getModuleName());
401       }
402       return ourCommunityModules;
403     }
404     catch (JDOMException | IOException e) {
405       throw new RuntimeException("Cannot read modules from " + modulesXml.getAbsolutePath(), e);
406     }
407   }
408
409   /**
410    * Allows to determine project type by its file system location.
411    *
412    * @return    project type implied by its file system location
413    */
414   private static FileSystemLocation parseProjectLocation() {
415     return new File(PathManager.getHomePath(), "community/.idea").isDirectory() ? FileSystemLocation.ULTIMATE : FileSystemLocation.COMMUNITY;
416   }
417
418   /**
419    * Tries to check test data lookup strategy by target test data directories availability.
420    * <p/>
421    * Such an approach has a drawback that it doesn't work correctly at number of scenarios, e.g. when
422    * {@code 'community'} test is executed under {@code 'ultimate'} project.
423    *
424    * @return    test data lookup strategy based on target test data directories availability
425    */
426   private static TestDataLookupStrategy guessTestDataLookupStrategyOnDirectoryAvailability() {
427     String homePath = PathManager.getHomePath();
428     for (Map.Entry<TestDataLookupStrategy, List<String>> entry : TEST_DATA_RELATIVE_PATHS.entrySet()) {
429       for (String relativePath : entry.getValue()) {
430         if (new File(homePath, relativePath).isDirectory()) {
431           return entry.getKey();
432         }
433       }
434     }
435     return TestDataLookupStrategy.ULTIMATE;
436   }
437 }