Count missed branches in case of partial coverage in total ration denominator (PY...
[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     final String filePath = normalizeFilePath(file.getPath());
145
146     // process file
147     final FileCoverageInfo info;
148
149     final ClassData classData = getClassData(filePath, projectData, normalizedFiles2Files);
150     if (classData != null) {
151       // fill info from coverage data
152       info = fileInfoForCoveredFile(classData);
153     }
154     else {
155       // file wasn't mentioned in coverage information
156       info = fillInfoForUncoveredFile(VfsUtilCore.virtualToIoFile(file));
157     }
158
159     if (info != null) {
160       annotator.annotateFile(filePath, info);
161     }
162     return info;
163   }
164
165   private static @Nullable ClassData getClassData(
166     final @NotNull String filePath,
167     final @NotNull ProjectData data,
168     final @NotNull Map<String, String> normalizedFiles2Files) {
169     final String originalFileName = normalizedFiles2Files.get(filePath);
170     if (originalFileName == null) {
171       return null;
172     }
173     return data.getClassData(originalFileName);
174   }
175
176   @Nullable
177   protected DirCoverageInfo collectFolderCoverage(@NotNull final VirtualFile dir,
178                                                   final @NotNull CoverageDataManager dataManager,
179                                                   final Annotator annotator,
180                                                   final ProjectData projectInfo, boolean trackTestFolders,
181                                                   @NotNull final ProjectFileIndex index,
182                                                   @NotNull final CoverageEngine coverageEngine,
183                                                   Set<VirtualFile> visitedDirs,
184                                                   @NotNull final Map<String, String> normalizedFiles2Files) {
185     if (!index.isInContent(dir)) {
186       return null;
187     }
188
189     if (visitedDirs.contains(dir)) {
190       return null;
191     }
192
193     if (!shouldCollectCoverageInsideLibraryDirs()) {
194       if (index.isInLibrarySource(dir) || index.isInLibraryClasses(dir)) {
195         return null;
196       }
197     }
198     visitedDirs.add(dir);
199
200     final boolean isInTestSrcContent = TestSourcesFilter.isTestSources(dir, getProject());
201
202     // Don't count coverage for tests folders if track test folders is switched off
203     if (!trackTestFolders && isInTestSrcContent) {
204       return null;
205     }
206
207     final VirtualFile[] children = dataManager.doInReadActionIfProjectOpen(dir::getChildren);
208     if (children == null) {
209       return null;
210     }
211
212     final DirCoverageInfo dirCoverageInfo = new DirCoverageInfo();
213
214     for (VirtualFile fileOrDir : children) {
215       if (fileOrDir.isDirectory()) {
216         final DirCoverageInfo childCoverageInfo =
217           collectFolderCoverage(fileOrDir, dataManager, annotator, projectInfo, trackTestFolders, index,
218                                 coverageEngine, visitedDirs, normalizedFiles2Files);
219
220         if (childCoverageInfo != null) {
221           dirCoverageInfo.totalFilesCount += childCoverageInfo.totalFilesCount;
222           dirCoverageInfo.coveredFilesCount += childCoverageInfo.coveredFilesCount;
223           dirCoverageInfo.totalLineCount += childCoverageInfo.totalLineCount;
224           dirCoverageInfo.coveredLineCount += childCoverageInfo.coveredLineCount;
225         }
226       }
227       else if (coverageEngine.coverageProjectViewStatisticsApplicableTo(fileOrDir)) {
228         // let's count statistics only for ruby-based files
229
230         final FileCoverageInfo fileInfo =
231           collectBaseFileCoverage(fileOrDir, annotator, projectInfo, normalizedFiles2Files);
232
233         if (fileInfo != null) {
234           dirCoverageInfo.totalLineCount += fileInfo.totalLineCount;
235           dirCoverageInfo.totalFilesCount++;
236
237           if (fileInfo.coveredLineCount > 0) {
238             dirCoverageInfo.coveredFilesCount++;
239             dirCoverageInfo.coveredLineCount += fileInfo.coveredLineCount;
240           }
241         }
242       }
243     }
244
245
246     //TODO - toplevelFilesCoverage - is unused variable!
247
248     // no sense to include directories without ruby files
249     if (dirCoverageInfo.totalFilesCount == 0) {
250       return null;
251     }
252
253     final String dirPath = normalizeFilePath(dir.getPath());
254     if (isInTestSrcContent) {
255       annotator.annotateTestDirectory(dirPath, dirCoverageInfo);
256     }
257     else {
258       annotator.annotateSourceDirectory(dirPath, dirCoverageInfo);
259     }
260
261     return dirCoverageInfo;
262   }
263
264   protected boolean shouldCollectCoverageInsideLibraryDirs() {
265     // By default returns "true" for backward compatibility
266     return true;
267   }
268
269   public void annotate(@NotNull final VirtualFile contentRoot,
270                        @NotNull final CoverageSuitesBundle suite,
271                        final @NotNull CoverageDataManager dataManager, @NotNull final ProjectData data,
272                        final Project project,
273                        final Annotator annotator) {
274     if (!contentRoot.isValid()) {
275       return;
276     }
277
278     // TODO: check name filter!!!!!
279
280     final ProjectFileIndex index = ProjectRootManager.getInstance(project).getFileIndex();
281
282     @SuppressWarnings("unchecked") final Set<String> files = data.getClasses().keySet();
283     final Map<String, String> normalizedFiles2Files = ContainerUtil.newHashMap();
284     for (final String file : files) {
285       normalizedFiles2Files.put(normalizeFilePath(file), file);
286     }
287     collectFolderCoverage(contentRoot, dataManager, annotator, data,
288                           suite.isTrackTestFolders(),
289                           index,
290                           suite.getCoverageEngine(),
291                           ContainerUtil.newHashSet(),
292                           Collections.unmodifiableMap(normalizedFiles2Files));
293   }
294
295   @Override
296   @Nullable
297   protected Runnable createRenewRequest(@NotNull final CoverageSuitesBundle suite, final @NotNull CoverageDataManager dataManager) {
298     final ProjectData data = suite.getCoverageData();
299     if (data == null) {
300       return null;
301     }
302
303     return () -> {
304       final Project project = getProject();
305
306       final ProjectRootManager rootManager = ProjectRootManager.getInstance(project);
307
308       // find all modules content roots
309       final VirtualFile[] modulesContentRoots = dataManager.doInReadActionIfProjectOpen(() -> rootManager.getContentRoots());
310
311       if (modulesContentRoots == null) {
312         return;
313       }
314
315       // gather coverage from all content roots
316       for (VirtualFile root : modulesContentRoots) {
317         annotate(root, suite, dataManager, data, project, new Annotator() {
318           public void annotateSourceDirectory(final String dirPath, final DirCoverageInfo info) {
319             myDirCoverageInfos.put(dirPath, info);
320           }
321
322           public void annotateTestDirectory(final String dirPath, final DirCoverageInfo info) {
323             myTestDirCoverageInfos.put(dirPath, info);
324           }
325
326           public void annotateFile(@NotNull final String filePath, @NotNull final FileCoverageInfo info) {
327             myFileCoverageInfos.put(filePath, info);
328           }
329         });
330       }
331
332       //final VirtualFile[] roots = ProjectRootManagerEx.getInstanceEx(project).getContentRootsFromAllModules();
333       //index.iterateContentUnderDirectory(roots[0], new ContentIterator() {
334       //  public boolean processFile(final VirtualFile fileOrDir) {
335       //    // TODO support for libraries and sdk
336       //    if (index.isInContent(fileOrDir)) {
337       //      final String normalizedPath = RubyCoverageEngine.rcovalizePath(fileOrDir.getPath(), (RubyCoverageSuite)suite);
338       //
339       //      // TODO - check filters
340       //
341       //      if (fileOrDir.isDirectory()) {
342       //        //// process dir
343       //        //if (index.isInTestSourceContent(fileOrDir)) {
344       //        //  //myTestDirCoverageInfos.put(RubyCoverageEngine.rcovalizePath(fileOrDir.getPath(), (RubyCoverageSuite)suite), )
345       //        //} else {
346       //        //  myDirCoverageInfos.put(normalizedPath, new FileCoverageInfo());
347       //        //}
348       //      } else {
349       //        // process file
350       //        final ClassData classData = data.getOrCreateClassData(normalizedPath);
351       //        if (classData != null) {
352       //          final int count = classData.getLines().length;
353       //          if (count != 0) {
354       //            final FileCoverageInfo info = new FileCoverageInfo();
355       //            info.totalLineCount = count;
356       //            // let's count covered lines
357       //            for (int i = 1; i <= count; i++) {
358       //              final LineData lineData = classData.getLineData(i);
359       //              if (lineData.getStatus() != LineCoverage.NONE){
360       //                info.coveredLineCount++;
361       //              }
362       //            }
363       //            myFileCoverageInfos.put(normalizedPath, info);
364       //          }
365       //        }
366       //      }
367       //    }
368       //    return true;
369       //  }
370       //});
371
372       dataManager.triggerPresentationUpdate();
373     };
374   }
375
376   @Nullable
377   protected String getLinesCoverageInformationString(@NotNull final FileCoverageInfo info) {
378     return calcCoveragePercentage(info) + "% lines covered";
379   }
380
381   protected static int calcCoveragePercentage(FileCoverageInfo info) {
382     return calcPercent(info.coveredLineCount, info.totalLineCount);
383   }
384
385   private static int calcPercent(final int covered, final int total) {
386     return total != 0 ? (int)((double)covered / total * 100) : 100;
387   }
388
389   @Nullable
390   protected String getFilesCoverageInformationString(@NotNull final DirCoverageInfo info) {
391     return calcPercent(info.coveredFilesCount, info.totalFilesCount) + "% files";
392   }
393
394   @Nullable
395   private FileCoverageInfo fileInfoForCoveredFile(@NotNull final ClassData classData) {
396     final Object[] lines = classData.getLines();
397
398     // class data lines = [0, 1, ... count] but first element with index = #0 is fake and isn't
399     // used thus count = length = 1
400     final int count = lines.length - 1;
401
402     if (count == 0) {
403       return null;
404     }
405
406     final FileCoverageInfo info = new FileCoverageInfo();
407
408     info.coveredLineCount = 0;
409     info.totalLineCount = 0;
410     // let's count covered lines
411     for (int i = 1; i <= count; i++) {
412       final LineData lineData = classData.getLineData(i);
413
414       processLineData(info, lineData);
415     }
416     return info;
417   }
418
419   protected void processLineData(@NotNull  FileCoverageInfo info, @Nullable  LineData lineData) {
420     if (lineData == null) {
421       // Ignore not src code
422       return;
423     }
424     final int status = lineData.getStatus();
425     // covered - if src code & covered (or inferred covered)
426
427     if (status != LineCoverage.NONE) {
428       info.coveredLineCount++;
429     }
430     info.totalLineCount++;
431   }
432
433   @Nullable
434   protected FileCoverageInfo fillInfoForUncoveredFile(@NotNull File file) {
435     return null;
436   }
437
438   private interface Annotator {
439     void annotateSourceDirectory(final String dirPath, final DirCoverageInfo info);
440
441     void annotateTestDirectory(final String dirPath, final DirCoverageInfo info);
442
443     void annotateFile(@NotNull final String filePath, @NotNull final FileCoverageInfo info);
444   }
445 }