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