coverage: ability to avoid traversing library roots
[idea/community.git] / plugins / coverage-common / src / com / intellij / coverage / SimpleCoverageAnnotator.java
1 package com.intellij.coverage;
2
3 import com.intellij.openapi.project.Project;
4 import com.intellij.openapi.roots.ProjectFileIndex;
5 import com.intellij.openapi.roots.ProjectRootManager;
6 import com.intellij.openapi.util.Computable;
7 import com.intellij.openapi.util.SystemInfo;
8 import com.intellij.openapi.util.io.FileUtil;
9 import com.intellij.openapi.vfs.VfsUtilCore;
10 import com.intellij.openapi.vfs.VirtualFile;
11 import com.intellij.psi.PsiDirectory;
12 import com.intellij.psi.PsiFile;
13 import com.intellij.rt.coverage.data.ClassData;
14 import com.intellij.rt.coverage.data.LineCoverage;
15 import com.intellij.rt.coverage.data.LineData;
16 import com.intellij.rt.coverage.data.ProjectData;
17 import com.intellij.util.containers.ContainerUtil;
18 import com.intellij.util.containers.HashMap;
19 import org.jetbrains.annotations.NotNull;
20 import org.jetbrains.annotations.Nullable;
21
22 import java.io.File;
23 import java.util.Collections;
24 import java.util.Map;
25 import java.util.Set;
26
27 /**
28  * @author traff
29  */
30 public abstract class SimpleCoverageAnnotator extends BaseCoverageAnnotator {
31
32   private final Map<String, FileCoverageInfo> myFileCoverageInfos = new HashMap<String, FileCoverageInfo>();
33   private final Map<String, DirCoverageInfo> myTestDirCoverageInfos = new HashMap<String, DirCoverageInfo>();
34   private final Map<String, DirCoverageInfo> myDirCoverageInfos = new HashMap<String, DirCoverageInfo>();
35
36   public SimpleCoverageAnnotator(Project project) {
37     super(project);
38   }
39
40   @Override
41   public void onSuiteChosen(CoverageSuitesBundle newSuite) {
42     super.onSuiteChosen(newSuite);
43
44     myFileCoverageInfos.clear();
45     myTestDirCoverageInfos.clear();
46     myDirCoverageInfos.clear();
47   }
48
49   @Nullable
50   protected DirCoverageInfo getDirCoverageInfo(@NotNull final PsiDirectory directory,
51                                                @NotNull final CoverageSuitesBundle currentSuite) {
52     final VirtualFile dir = directory.getVirtualFile();
53
54     final ProjectFileIndex projectFileIndex = ProjectRootManager.getInstance(directory.getProject()).getFileIndex();
55     //final Module module = projectFileIndex.getModuleForFile(dir);
56
57     final boolean isInTestContent = projectFileIndex.isInTestSourceContent(dir);
58     if (!currentSuite.isTrackTestFolders() && isInTestContent) {
59       return null;
60     }
61
62     final String path = normalizeFilePath(dir.getPath());
63
64     return isInTestContent ? myTestDirCoverageInfos.get(path) : myDirCoverageInfos.get(path);
65   }
66
67   @Nullable
68   public String getDirCoverageInformationString(@NotNull final PsiDirectory directory,
69                                                 @NotNull final CoverageSuitesBundle currentSuite,
70                                                 @NotNull final CoverageDataManager manager) {
71     DirCoverageInfo coverageInfo = getDirCoverageInfo(directory, currentSuite);
72     if (coverageInfo == null) {
73       return null;
74     }
75
76     if (manager.isSubCoverageActive()) {
77       return coverageInfo.coveredLineCount > 0 ? "covered" : null;
78     }
79
80     final String filesCoverageInfo = getFilesCoverageInformationString(coverageInfo);
81     if (filesCoverageInfo != null) {
82       final StringBuilder builder = new StringBuilder();
83       builder.append(filesCoverageInfo);
84       final String linesCoverageInfo = getLinesCoverageInformationString(coverageInfo);
85       if (linesCoverageInfo != null) {
86         builder.append(", ").append(linesCoverageInfo);
87       }
88       return builder.toString();
89     }
90     return null;
91   }
92
93   // SimpleCoverageAnnotator doesn't require normalized file paths any more
94   // so now coverage report should work w/o usage of this method
95   @Deprecated
96   public static String getFilePath(final String filePath) {
97     return normalizeFilePath(filePath);
98   }
99
100   private static @NotNull String normalizeFilePath(@NotNull String filePath) {
101     if (SystemInfo.isWindows) {
102       filePath = filePath.toLowerCase();
103     }
104     return FileUtil.toSystemIndependentName(filePath);
105   }
106
107   @Nullable
108   public String getFileCoverageInformationString(@NotNull final PsiFile psiFile,
109                                                  @NotNull final CoverageSuitesBundle currentSuite,
110                                                  @NotNull final CoverageDataManager manager) {
111     final VirtualFile file = psiFile.getVirtualFile();
112     assert file != null;
113     final String path = normalizeFilePath(file.getPath());
114
115     final FileCoverageInfo coverageInfo = myFileCoverageInfos.get(path);
116     if (coverageInfo == null) {
117       return null;
118     }
119
120     if (manager.isSubCoverageActive()) {
121       return coverageInfo.coveredLineCount > 0 ? "covered" : null;
122     }
123
124     return getLinesCoverageInformationString(coverageInfo);
125   }
126
127   @Nullable
128   protected FileCoverageInfo collectBaseFileCoverage(@NotNull final VirtualFile file,
129                                                      @NotNull final Annotator annotator,
130                                                      @NotNull final ProjectData projectData,
131                                                      @NotNull final Map<String, String> normalizedFiles2Files)
132   {
133     final String filePath = normalizeFilePath(file.getPath());
134
135     // process file
136     final FileCoverageInfo info;
137
138     final ClassData classData = getClassData(filePath, projectData, normalizedFiles2Files);
139     if (classData != null) {
140       // fill info from coverage data
141       info = fileInfoForCoveredFile(classData);
142     }
143     else {
144       // file wasn't mentioned in coverage information
145       info = fillInfoForUncoveredFile(VfsUtilCore.virtualToIoFile(file));
146     }
147
148     if (info != null) {
149       annotator.annotateFile(filePath, info);
150     }
151     return info;
152   }
153
154   private static @Nullable ClassData getClassData(
155     final @NotNull String filePath,
156     final @NotNull ProjectData data,
157     final @NotNull Map<String, String> normalizedFiles2Files)
158   {
159     final String originalFileName = normalizedFiles2Files.get(filePath);
160     if (originalFileName == null) {
161       return null;
162     }
163     return data.getClassData(originalFileName);
164   }
165
166   @Nullable
167   protected DirCoverageInfo collectFolderCoverage(@NotNull final VirtualFile dir,
168                                                   final @NotNull CoverageDataManager dataManager,
169                                                   final Annotator annotator,
170                                                   final ProjectData projectInfo, boolean trackTestFolders,
171                                                   @NotNull final ProjectFileIndex index,
172                                                   @NotNull final CoverageEngine coverageEngine,
173                                                   Set<VirtualFile> visitedDirs,
174                                                   @NotNull final Map<String, String> normalizedFiles2Files)
175   {
176     if (!index.isInContent(dir)) {
177       return null;
178     }
179
180     if (visitedDirs.contains(dir)) {
181       return null;
182     }
183
184     if (!shouldCollectCoverageInsideLibraryDirs()) {
185       if (index.isInLibrarySource(dir) || index.isInLibraryClasses(dir)) {
186         return null;
187       }
188     }
189     visitedDirs.add(dir);
190
191     final boolean isInTestSrcContent = index.isInTestSourceContent(dir);
192
193     // Don't count coverage for tests folders if track test folders is switched off
194     if (!trackTestFolders && isInTestSrcContent) {
195       return null;
196     }
197
198     final VirtualFile[] children = dataManager.doInReadActionIfProjectOpen(new Computable<VirtualFile[]>() {
199       public VirtualFile[] compute() {
200         return dir.getChildren();
201       }
202     });
203
204     if (children == null) {
205       return null;
206     }
207
208     final DirCoverageInfo dirCoverageInfo = new DirCoverageInfo();
209
210     for (VirtualFile fileOrDir : children) {
211       if (fileOrDir.isDirectory()) {
212         final DirCoverageInfo childCoverageInfo =
213           collectFolderCoverage(fileOrDir, dataManager, annotator, projectInfo, trackTestFolders, index,
214                                 coverageEngine, visitedDirs, normalizedFiles2Files);
215
216         if (childCoverageInfo != null) {
217           dirCoverageInfo.totalFilesCount += childCoverageInfo.totalFilesCount;
218           dirCoverageInfo.coveredFilesCount += childCoverageInfo.coveredFilesCount;
219           dirCoverageInfo.totalLineCount += childCoverageInfo.totalLineCount;
220           dirCoverageInfo.coveredLineCount += childCoverageInfo.coveredLineCount;
221         }
222       }
223       else if (coverageEngine.coverageProjectViewStatisticsApplicableTo(fileOrDir)) {
224         // let's count statistics only for ruby-based files
225
226         final FileCoverageInfo fileInfo =
227           collectBaseFileCoverage(fileOrDir, annotator, projectInfo, normalizedFiles2Files);
228
229         if (fileInfo != null) {
230           dirCoverageInfo.totalLineCount += fileInfo.totalLineCount;
231           dirCoverageInfo.totalFilesCount++;
232
233           if (fileInfo.coveredLineCount > 0) {
234             dirCoverageInfo.coveredFilesCount++;
235             dirCoverageInfo.coveredLineCount += fileInfo.coveredLineCount;
236           }
237         }
238       }
239     }
240
241
242     //TODO - toplevelFilesCoverage - is unused variable!
243
244     // no sense to include directories without ruby files
245     if (dirCoverageInfo.totalFilesCount == 0) {
246       return null;
247     }
248
249     final String dirPath = normalizeFilePath(dir.getPath());
250     if (isInTestSrcContent) {
251       annotator.annotateTestDirectory(dirPath, dirCoverageInfo);
252     }
253     else {
254       annotator.annotateSourceDirectory(dirPath, dirCoverageInfo);
255     }
256
257     return dirCoverageInfo;
258   }
259
260   protected boolean shouldCollectCoverageInsideLibraryDirs() {
261     // By default returns "true" for backward compatibility
262     return true;
263   }
264
265   public void annotate(@NotNull final VirtualFile contentRoot,
266                        @NotNull final CoverageSuitesBundle suite,
267                        final @NotNull CoverageDataManager dataManager, @NotNull final ProjectData data,
268                        final Project project,
269                        final Annotator annotator)
270   {
271     if (!contentRoot.isValid()) {
272       return;
273     }
274
275     // TODO: check name filter!!!!!
276
277     final ProjectFileIndex index = ProjectRootManager.getInstance(project).getFileIndex();
278
279     @SuppressWarnings("unchecked") final Set<String> files = data.getClasses().keySet();
280     final Map<String, String> normalizedFiles2Files = ContainerUtil.newHashMap();
281     for (final String file : files) {
282       normalizedFiles2Files.put(normalizeFilePath(file), file);
283     }
284     collectFolderCoverage(contentRoot, dataManager, annotator, data,
285                           suite.isTrackTestFolders(),
286                           index,
287                           suite.getCoverageEngine(),
288                           ContainerUtil.<VirtualFile>newHashSet(),
289                           Collections.unmodifiableMap(normalizedFiles2Files));
290   }
291
292   @Override
293   @Nullable
294   protected Runnable createRenewRequest(@NotNull final CoverageSuitesBundle suite, final @NotNull CoverageDataManager dataManager) {
295     final ProjectData data = suite.getCoverageData();
296     if (data == null) {
297       return null;
298     }
299
300     return new Runnable() {
301       public void run() {
302         final Project project = getProject();
303
304         final ProjectRootManager rootManager = ProjectRootManager.getInstance(project);
305
306         // find all modules content roots
307         final VirtualFile[] modulesContentRoots = dataManager.doInReadActionIfProjectOpen(new Computable<VirtualFile[]>() {
308           public VirtualFile[] compute() {
309             return rootManager.getContentRoots();
310           }
311         });
312
313         if (modulesContentRoots == null) {
314           return;
315         }
316
317         // gather coverage from all content roots
318         for (VirtualFile root : modulesContentRoots) {
319           annotate(root, suite, dataManager, data, project, new Annotator() {
320             public void annotateSourceDirectory(final String dirPath, final DirCoverageInfo info) {
321               myDirCoverageInfos.put(dirPath, info);
322             }
323
324             public void annotateTestDirectory(final String dirPath, final DirCoverageInfo info) {
325               myTestDirCoverageInfos.put(dirPath, info);
326             }
327
328             public void annotateFile(@NotNull final String filePath, @NotNull final FileCoverageInfo info) {
329               myFileCoverageInfos.put(filePath, info);
330             }
331           });
332         }
333
334         //final VirtualFile[] roots = ProjectRootManagerEx.getInstanceEx(project).getContentRootsFromAllModules();
335         //index.iterateContentUnderDirectory(roots[0], new ContentIterator() {
336         //  public boolean processFile(final VirtualFile fileOrDir) {
337         //    // TODO support for libraries and sdk
338         //    if (index.isInContent(fileOrDir)) {
339         //      final String normalizedPath = RubyCoverageEngine.rcovalizePath(fileOrDir.getPath(), (RubyCoverageSuite)suite);
340         //
341         //      // TODO - check filters
342         //
343         //      if (fileOrDir.isDirectory()) {
344         //        //// process dir
345         //        //if (index.isInTestSourceContent(fileOrDir)) {
346         //        //  //myTestDirCoverageInfos.put(RubyCoverageEngine.rcovalizePath(fileOrDir.getPath(), (RubyCoverageSuite)suite), )
347         //        //} else {
348         //        //  myDirCoverageInfos.put(normalizedPath, new FileCoverageInfo());
349         //        //}
350         //      } else {
351         //        // process file
352         //        final ClassData classData = data.getOrCreateClassData(normalizedPath);
353         //        if (classData != null) {
354         //          final int count = classData.getLines().length;
355         //          if (count != 0) {
356         //            final FileCoverageInfo info = new FileCoverageInfo();
357         //            info.totalLineCount = count;
358         //            // let's count covered lines
359         //            for (int i = 1; i <= count; i++) {
360         //              final LineData lineData = classData.getLineData(i);
361         //              if (lineData.getStatus() != LineCoverage.NONE){
362         //                info.coveredLineCount++;
363         //              }
364         //            }
365         //            myFileCoverageInfos.put(normalizedPath, info);
366         //          }
367         //        }
368         //      }
369         //    }
370         //    return true;
371         //  }
372         //});
373
374         dataManager.triggerPresentationUpdate();
375       }
376     };
377   }
378
379   @Nullable
380   protected String getLinesCoverageInformationString(@NotNull final FileCoverageInfo info) {
381     return calcCoveragePercentage(info) + "% lines covered";
382   }
383
384   protected static int calcCoveragePercentage(FileCoverageInfo info) {
385     return calcPercent(info.coveredLineCount, info.totalLineCount);
386   }
387
388   private static int calcPercent(final int covered, final int total) {
389     return total != 0 ? (int)((double)covered / total * 100) : 100;
390   }
391
392   @Nullable
393   protected String getFilesCoverageInformationString(@NotNull final DirCoverageInfo info) {
394     return calcPercent(info.coveredFilesCount, info.totalFilesCount) + "% files";
395   }
396
397   @Nullable
398   private static FileCoverageInfo fileInfoForCoveredFile(@NotNull final ClassData classData) {
399     final Object[] lines = classData.getLines();
400
401     // class data lines = [0, 1, ... count] but first element with index = #0 is fake and isn't
402     // used thus count = length = 1
403     final int count = lines.length - 1;
404
405     if (count == 0) {
406       return null;
407     }
408
409     final FileCoverageInfo info = new FileCoverageInfo();
410
411     int srcLinesCount = 0;
412     int coveredLinesCount = 0;
413     // let's count covered lines
414     for (int i = 1; i <= count; i++) {
415       final LineData lineData = classData.getLineData(i);
416       if (lineData == null) {
417         // Ignore not src code
418         continue;
419       }
420       final int status = lineData.getStatus();
421       // covered - if src code & covered (or inferred covered)
422       if (status != LineCoverage.NONE) {
423         coveredLinesCount++;
424       }
425       srcLinesCount++;
426     }
427     info.totalLineCount = srcLinesCount;
428     info.coveredLineCount = coveredLinesCount;
429     return info;
430   }
431
432   @Nullable
433   protected FileCoverageInfo fillInfoForUncoveredFile(@NotNull File file) {
434     return null;
435   }
436
437   private interface Annotator {
438     void annotateSourceDirectory(final String dirPath, final DirCoverageInfo info);
439
440     void annotateTestDirectory(final String dirPath, final DirCoverageInfo info);
441
442     void annotateFile(@NotNull final String filePath, @NotNull final FileCoverageInfo info);
443   }
444 }