summaryrefslogtreecommitdiff
path: root/platform/testFramework/src/com/intellij/openapi/application/ex/PathManagerEx.java
blob: 4844851b81e23764c383d9cae9efec0c6a974736 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
/*
 * 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").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;
  }
}