| /* |
| * Copyright 2000-2009 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* |
| * Created by IntelliJ IDEA. |
| * User: mike |
| * Date: Aug 19, 2002 |
| * Time: 8:21:52 PM |
| * To change template for new class use |
| * Code Style | Class Templates options (Tools | IDE Options). |
| */ |
| package com.intellij.openapi.application.ex; |
| |
| import com.intellij.openapi.application.PathManager; |
| import com.intellij.openapi.module.impl.ModuleManagerImpl; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.JDOMUtil; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.testFramework.TestRunnerUtil; |
| import com.intellij.util.PathUtil; |
| import com.intellij.util.containers.ConcurrentHashMap; |
| import gnu.trove.THashSet; |
| import junit.framework.TestCase; |
| import org.jdom.Element; |
| import org.jdom.JDOMException; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.jps.model.serialization.JDomSerializationUtil; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.reflect.Modifier; |
| import java.util.*; |
| import java.util.concurrent.ConcurrentMap; |
| |
| import static com.intellij.openapi.util.io.FileUtil.toSystemDependentName; |
| import static java.util.Arrays.asList; |
| |
| public class PathManagerEx { |
| |
| /** |
| * All IDEA project files may be logically divided by the following criteria: |
| * <ul> |
| * <li>files that are contained at <code>'community'</code> directory;</li> |
| * <li>all other files;</li> |
| * </ul> |
| * <p/> |
| * File location types implied by criteria mentioned above are enumerated here. |
| */ |
| private enum FileSystemLocation { |
| ULTIMATE, COMMUNITY |
| } |
| |
| /** Caches test data lookup strategy by class. */ |
| private static final ConcurrentMap<Class, TestDataLookupStrategy> CLASS_STRATEGY_CACHE = new ConcurrentHashMap<Class, TestDataLookupStrategy>(); |
| private static final ConcurrentMap<String, Class> CLASS_CACHE = new ConcurrentHashMap<String, Class>(); |
| private static Set<String> ourCommunityModules; |
| |
| private PathManagerEx() { |
| } |
| |
| /** |
| * Enumerates possible strategies of test data lookup. |
| * <p/> |
| * Check member-level javadoc for more details. |
| */ |
| public enum TestDataLookupStrategy { |
| /** |
| * Stands for algorithm that retrieves <code>'test data'</code> stored at the <code>'ultimate'</code> project level assuming |
| * that it's used from the test running in context of <code>'ultimate'</code> project as well. |
| * <p/> |
| * Is assumed to be default strategy for all <code>'ultimate'</code> tests. |
| */ |
| ULTIMATE, |
| |
| /** |
| * Stands for algorithm that retrieves <code>'test data'</code> stored at the <code>'community'</code> project level assuming |
| * that it's used from the test running in context of <code>'community'</code> project as well. |
| * <p/> |
| * Is assumed to be default strategy for all <code>'community'</code> tests. |
| */ |
| COMMUNITY, |
| |
| /** |
| * Stands for algorithm that retrieves <code>'test data'</code> stored at the <code>'community'</code> project level assuming |
| * that it's used from the test running in context of <code>'ultimate'</code> project. |
| */ |
| COMMUNITY_FROM_ULTIMATE |
| } |
| |
| /** |
| * It's assumed that test data location for both <code>community</code> and <code>ultimate</code> tests follows the same template: |
| * <code>'<IDEA_HOME>/<RELATIVE_PATH>'</code>. |
| * <p/> |
| * <code>'IDEA_HOME'</code> here stands for path to IDEA installation; <code>'RELATIVE_PATH'</code> defines a path to |
| * test data relative to IDEA installation path. That relative path may be different for <code>community</code> |
| * and <code>ultimate</code> tests. |
| * <p/> |
| * This collection contains mappings from test group type to relative paths to use, i.e. it's possible to define more than one |
| * relative path for the single test group. It's assumed that path definition algorithm iterates them and checks if |
| * resulting absolute path points to existing directory. The one is returned in case of success; last path is returned otherwise. |
| * <p/> |
| * Hence, the order of relative paths for the single test group matters. |
| */ |
| private static final Map<TestDataLookupStrategy, List<String>> TEST_DATA_RELATIVE_PATHS |
| = new EnumMap<TestDataLookupStrategy, List<String>>(TestDataLookupStrategy.class); |
| |
| static { |
| TEST_DATA_RELATIVE_PATHS.put(TestDataLookupStrategy.ULTIMATE, Collections.singletonList(toSystemDependentName("testData"))); |
| TEST_DATA_RELATIVE_PATHS.put( |
| TestDataLookupStrategy.COMMUNITY, |
| Collections.singletonList(toSystemDependentName("java/java-tests/testData")) |
| ); |
| TEST_DATA_RELATIVE_PATHS.put( |
| TestDataLookupStrategy.COMMUNITY_FROM_ULTIMATE, |
| Collections.singletonList(toSystemDependentName("community/java/java-tests/testData")) |
| ); |
| } |
| |
| /** |
| * Shorthand for calling {@link #getTestDataPath(TestDataLookupStrategy)} with |
| * {@link #guessTestDataLookupStrategy() guessed} lookup strategy. |
| * |
| * @return test data path with {@link #guessTestDataLookupStrategy() guessed} lookup strategy |
| * @throws IllegalStateException as defined by {@link #getTestDataPath(TestDataLookupStrategy)} |
| */ |
| @NonNls |
| public static String getTestDataPath() throws IllegalStateException { |
| TestDataLookupStrategy strategy = guessTestDataLookupStrategy(); |
| return getTestDataPath(strategy); |
| } |
| |
| public static String getTestDataPath(String path) throws IllegalStateException { |
| return getTestDataPath() + path.replace('/', File.separatorChar); |
| } |
| |
| /** |
| * Shorthand for calling {@link #getTestDataPath(TestDataLookupStrategy)} with strategy obtained via call to |
| * {@link #determineLookupStrategy(Class)} with the given class. |
| * <p/> |
| * <b>Note:</b> this method receives explicit class argument in order to solve the following limitation - we analyze calling |
| * stack trace in order to guess test data lookup strategy ({@link #guessTestDataLookupStrategyOnClassLocation()}). However, |
| * there is a possible case that super-class method is called on sub-class object. Stack trace shows super-class then. |
| * There is a possible situation that actual test is <code>'ultimate'</code> but its abstract super-class is |
| * <code>'community'</code>, hence, test data lookup is performed incorrectly. So, this method should be called from abstract |
| * base test class if its concrete sub-classes doesn't explicitly occur at stack trace. |
| * |
| * |
| * @param testClass target test class for which test data should be obtained |
| * @return base test data directory to use for the given test class |
| * @throws IllegalStateException as defined by {@link #getTestDataPath(TestDataLookupStrategy)} |
| */ |
| public static String getTestDataPath(Class<?> testClass) throws IllegalStateException { |
| TestDataLookupStrategy strategy = isLocatedInCommunity() ? TestDataLookupStrategy.COMMUNITY : determineLookupStrategy(testClass); |
| return getTestDataPath(strategy); |
| } |
| |
| /** |
| * @return path to 'community' project home irrespective of current project |
| */ |
| private static String getCommunityHomePath() { |
| String path = PathManager.getHomePath(); |
| return isLocatedInCommunity() ? path : path + File.separator + "community"; |
| } |
| |
| /** |
| * @return path to 'community' project home if {@code testClass} is located in the community project and path to 'ultimate' project otherwise |
| */ |
| public static String getHomePath(Class<? extends TestCase> testClass) { |
| TestDataLookupStrategy strategy = isLocatedInCommunity() ? TestDataLookupStrategy.COMMUNITY : determineLookupStrategy(testClass); |
| return strategy == TestDataLookupStrategy.COMMUNITY_FROM_ULTIMATE ? getCommunityHomePath() : PathManager.getHomePath(); |
| } |
| |
| /** |
| * Find file by its path relative to 'community' directory irrespective of current project |
| * @param relativePath path to file relative to 'community' directory |
| * @return file under the home directory of 'community' project |
| */ |
| public static File findFileUnderCommunityHome(String relativePath) { |
| File file = new File(getCommunityHomePath(), toSystemDependentName(relativePath)); |
| if (!file.exists()) { |
| throw new IllegalArgumentException("Cannot find file '" + relativePath + "' under '" + getCommunityHomePath() + "' directory"); |
| } |
| return file; |
| } |
| |
| /** |
| * Find file by its path relative to project home directory (the 'commmunity' project if {@code testClass} is located in the community project |
| * and the 'ultimate' project otherwise) |
| */ |
| public static File findFileUnderProjectHome(String relativePath, Class<? extends TestCase> testClass) { |
| String homePath = getHomePath(testClass); |
| File file = new File(homePath, toSystemDependentName(relativePath)); |
| if (!file.exists()) { |
| throw new IllegalArgumentException("Cannot find file '" + relativePath + "' under '" + homePath + "' directory"); |
| } |
| return file; |
| } |
| |
| private static boolean isLocatedInCommunity() { |
| FileSystemLocation projectLocation = parseProjectLocation(); |
| return projectLocation == FileSystemLocation.COMMUNITY; |
| // There is no other options then. |
| } |
| |
| /** |
| * Tries to return test data path for the given lookup strategy. |
| * |
| * @param strategy lookup strategy to use |
| * @return test data path for the given strategy |
| * @throws IllegalStateException if it's not possible to find valid test data path for the given strategy |
| */ |
| @NonNls |
| public static String getTestDataPath(TestDataLookupStrategy strategy) throws IllegalStateException { |
| String homePath = PathManager.getHomePath(); |
| |
| List<String> relativePaths = TEST_DATA_RELATIVE_PATHS.get(strategy); |
| if (relativePaths.isEmpty()) { |
| throw new IllegalStateException( |
| String.format("Can't determine test data path. Reason: no predefined relative paths are configured for test data " |
| + "lookup strategy %s. Configured mappings: %s", strategy, TEST_DATA_RELATIVE_PATHS) |
| ); |
| } |
| |
| File candidate = null; |
| for (String relativePath : relativePaths) { |
| candidate = new File(homePath, relativePath); |
| if (candidate.isDirectory()) { |
| return candidate.getPath(); |
| } |
| } |
| |
| if (candidate == null) { |
| throw new IllegalStateException("Can't determine test data path. Looks like programming error - reached 'if' block that was " |
| + "never expected to be executed"); |
| } |
| return candidate.getPath(); |
| } |
| |
| /** |
| * Tries to guess test data lookup strategy for the current execution. |
| * |
| * @return guessed lookup strategy for the current execution; defaults to {@link TestDataLookupStrategy#ULTIMATE} |
| */ |
| public static TestDataLookupStrategy guessTestDataLookupStrategy() { |
| TestDataLookupStrategy result = guessTestDataLookupStrategyOnClassLocation(); |
| if (result == null) { |
| result = guessTestDataLookupStrategyOnDirectoryAvailability(); |
| } |
| return result; |
| } |
| |
| @SuppressWarnings({"ThrowableInstanceNeverThrown"}) |
| @Nullable |
| private static TestDataLookupStrategy guessTestDataLookupStrategyOnClassLocation() { |
| if (isLocatedInCommunity()) return TestDataLookupStrategy.COMMUNITY; |
| |
| // The general idea here is to find test class at the bottom of hierarchy and try to resolve test data lookup strategy |
| // against it. Rationale is that there is a possible case that, say, 'ultimate' test class extends basic test class |
| // that remains at 'community'. We want to perform the processing against 'ultimate' test class then. |
| |
| // About special abstract classes processing - there is a possible case that target test class extends abstract base |
| // test class and call to this method is rooted from that parent. We need to resolve test data lookup against super |
| // class then, hence, we keep track of found abstract test class as well and fallback to it if no non-abstract class is found. |
| |
| Class<?> testClass = null; |
| Class<?> abstractTestClass = null; |
| StackTraceElement[] stackTrace = new Exception().getStackTrace(); |
| for (StackTraceElement stackTraceElement : stackTrace) { |
| String className = stackTraceElement.getClassName(); |
| Class<?> clazz = loadClass(className); |
| if (clazz == null || TestCase.class == clazz || !isJUnitClass(clazz)) { |
| continue; |
| } |
| |
| if (determineLookupStrategy(clazz) == TestDataLookupStrategy.ULTIMATE) return TestDataLookupStrategy.ULTIMATE; |
| if ((clazz.getModifiers() & Modifier.ABSTRACT) == 0) { |
| testClass = clazz; |
| } |
| else { |
| abstractTestClass = clazz; |
| } |
| } |
| |
| Class<?> classToUse = testClass == null ? abstractTestClass : testClass; |
| return classToUse == null ? null : determineLookupStrategy(classToUse); |
| } |
| |
| @Nullable |
| private static Class<?> loadClass(String className) { |
| ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); |
| |
| Class<?> clazz = CLASS_CACHE.get(className); |
| if (clazz != null) { |
| return clazz; |
| } |
| |
| ClassLoader definingClassLoader = PathManagerEx.class.getClassLoader(); |
| ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); |
| |
| for (ClassLoader classLoader : asList(contextClassLoader, definingClassLoader, systemClassLoader)) { |
| clazz = loadClass(className, classLoader); |
| if (clazz != null) { |
| CLASS_CACHE.put(className, clazz); |
| return clazz; |
| } |
| } |
| |
| CLASS_CACHE.put(className, TestCase.class); //dummy |
| return null; |
| } |
| |
| @Nullable |
| private static Class<?> loadClass(String className, ClassLoader classLoader) { |
| try { |
| return Class.forName(className, true, classLoader); |
| } |
| catch (NoClassDefFoundError e) { |
| return null; |
| } |
| catch (ClassNotFoundException e) { |
| return null; |
| } |
| } |
| |
| private static boolean isJUnitClass(Class<?> clazz) { |
| return TestCase.class.isAssignableFrom(clazz) || TestRunnerUtil.isJUnit4TestClass(clazz) || com.intellij.testFramework.Parameterized.class.isAssignableFrom(clazz); |
| } |
| |
| @Nullable |
| private static TestDataLookupStrategy determineLookupStrategy(Class<?> clazz) { |
| // Check if resulting strategy is already cached for the target class. |
| TestDataLookupStrategy result = CLASS_STRATEGY_CACHE.get(clazz); |
| if (result != null) { |
| return result; |
| } |
| |
| FileSystemLocation classFileLocation = computeClassLocation(clazz); |
| |
| // We know that project location is ULTIMATE if control flow reaches this place. |
| result = classFileLocation == FileSystemLocation.COMMUNITY ? TestDataLookupStrategy.COMMUNITY_FROM_ULTIMATE |
| : TestDataLookupStrategy.ULTIMATE; |
| CLASS_STRATEGY_CACHE.put(clazz, result); |
| return result; |
| } |
| |
| public static void replaceLookupStrategy(Class<?> substitutor, Class<?>... initial) { |
| CLASS_STRATEGY_CACHE.clear(); |
| for (Class<?> aClass : initial) { |
| CLASS_STRATEGY_CACHE.put(aClass, determineLookupStrategy(substitutor)); |
| } |
| } |
| |
| private static FileSystemLocation computeClassLocation(Class<?> clazz) { |
| String classRootPath = PathManager.getJarPathForClass(clazz); |
| if (classRootPath == null) { |
| throw new IllegalStateException("Cannot find root directory for " + clazz); |
| } |
| File root = new File(classRootPath); |
| if (!root.exists()) { |
| throw new IllegalStateException("Classes root " + root + " doesn't exist"); |
| } |
| if (!root.isDirectory()) { |
| //this means that clazz is located in a library, perhaps we should throw exception here |
| return FileSystemLocation.ULTIMATE; |
| } |
| |
| String moduleName = root.getName(); |
| String chunkPrefix = "ModuleChunk("; |
| if (moduleName.startsWith(chunkPrefix)) { |
| //todo[nik] this is temporary workaround to fix tests on TeamCity which compiles the whole modules cycle to a single output directory |
| moduleName = StringUtil.trimStart(moduleName, chunkPrefix); |
| moduleName = moduleName.substring(0, moduleName.indexOf(',')); |
| } |
| return getCommunityModules().contains(moduleName) ? FileSystemLocation.COMMUNITY : FileSystemLocation.ULTIMATE; |
| } |
| |
| private synchronized static Set<String> getCommunityModules() { |
| if (ourCommunityModules != null) { |
| return ourCommunityModules; |
| } |
| |
| ourCommunityModules = new THashSet<String>(); |
| File modulesXml = findFileUnderCommunityHome(Project.DIRECTORY_STORE_FOLDER + "/modules.xml"); |
| if (!modulesXml.exists()) { |
| throw new IllegalStateException("Cannot obtain test data path: " + modulesXml.getAbsolutePath() + " not found"); |
| } |
| |
| try { |
| Element componentRoot = JDomSerializationUtil |
| .findComponent(JDOMUtil.loadDocument(modulesXml).getRootElement(), ModuleManagerImpl.COMPONENT_NAME); |
| ModuleManagerImpl.ModulePath[] files = ModuleManagerImpl.getPathsToModuleFiles(componentRoot); |
| for (ModuleManagerImpl.ModulePath file : files) { |
| String name = FileUtil.getNameWithoutExtension(PathUtil.getFileName(file.getPath())); |
| ourCommunityModules.add(name); |
| } |
| return ourCommunityModules; |
| } |
| catch (JDOMException e) { |
| throw new RuntimeException("Cannot read modules from " + modulesXml.getAbsolutePath(), e); |
| } |
| catch (IOException e) { |
| throw new RuntimeException("Cannot read modules from " + modulesXml.getAbsolutePath(), e); |
| } |
| } |
| |
| /** |
| * Allows to determine project type by its file system location. |
| * |
| * @return project type implied by its file system location |
| */ |
| private static FileSystemLocation parseProjectLocation() { |
| return new File(PathManager.getHomePath(), "community/.idea").isDirectory() ? FileSystemLocation.ULTIMATE : FileSystemLocation.COMMUNITY; |
| } |
| |
| /** |
| * Tries to check test data lookup strategy by target test data directories availability. |
| * <p/> |
| * Such an approach has a drawback that it doesn't work correctly at number of scenarios, e.g. when |
| * <code>'community'</code> test is executed under <code>'ultimate'</code> project. |
| * |
| * @return test data lookup strategy based on target test data directories availability |
| */ |
| private static TestDataLookupStrategy guessTestDataLookupStrategyOnDirectoryAvailability() { |
| String homePath = PathManager.getHomePath(); |
| for (Map.Entry<TestDataLookupStrategy, List<String>> entry : TEST_DATA_RELATIVE_PATHS.entrySet()) { |
| for (String relativePath : entry.getValue()) { |
| if (new File(homePath, relativePath).isDirectory()) { |
| return entry.getKey(); |
| } |
| } |
| } |
| return TestDataLookupStrategy.ULTIMATE; |
| } |
| } |