View Javadoc
1   /*
2    * MIT License
3    * Copyright (c) 2006-2025 JMockit developers
4    * See LICENSE file for full license text.
5    */
6   package mockit.coverage.reporting.packages;
7   
8   import edu.umd.cs.findbugs.annotations.NonNull;
9   import edu.umd.cs.findbugs.annotations.Nullable;
10  
11  import java.io.File;
12  import java.io.IOException;
13  import java.lang.reflect.Method;
14  import java.util.ArrayList;
15  import java.util.Collection;
16  import java.util.Collections;
17  import java.util.Date;
18  import java.util.HashMap;
19  import java.util.List;
20  import java.util.Map;
21  
22  import mockit.coverage.CoveragePercentage;
23  import mockit.coverage.data.FileCoverageData;
24  import mockit.coverage.reporting.OutputFile;
25  import mockit.coverage.testRedundancy.TestCoverage;
26  
27  import org.checkerframework.checker.index.qual.NonNegative;
28  
29  public final class IndexPage extends ListWithFilesAndPercentages {
30      @Nullable
31      private final List<File> sourceDirs;
32      @NonNull
33      private final Map<String, List<String>> packageToFiles;
34      @NonNull
35      private final Map<String, Integer> packageToPackagePercentages;
36      @NonNull
37      private final PackageCoverageReport packageReport;
38      @NonNegative
39      private final int totalFileCount;
40  
41      public IndexPage(@NonNull File outputFile, @Nullable List<File> sourceDirs,
42              @Nullable Collection<String> sourceFilesNotFound, @NonNull Map<String, List<String>> packageToFiles,
43              @NonNull Map<String, FileCoverageData> fileToFileData) throws IOException {
44          super(new OutputFile(outputFile), "    ");
45          this.sourceDirs = sourceDirs;
46          this.packageToFiles = packageToFiles;
47          packageToPackagePercentages = new HashMap<>();
48          packageReport = new PackageCoverageReport(output, sourceFilesNotFound, fileToFileData, packageToFiles.values());
49          totalFileCount = totalNumberOfSourceFilesWithCoverageData(fileToFileData.values());
50      }
51  
52      @NonNegative
53      private static int totalNumberOfSourceFilesWithCoverageData(@NonNull Collection<FileCoverageData> fileData) {
54          return fileData.size() - Collections.frequency(fileData, null);
55      }
56  
57      public void generate() {
58          try {
59              writeHeader();
60  
61              List<String> packages = new ArrayList<>(packageToFiles.keySet());
62              writeMetricsForEachFile(null, packages);
63              writeLineWithCoverageTotal();
64              output.println("  </table>");
65  
66              writeListOfRedundantTestsIfAny();
67              writeFooter();
68          } finally {
69              output.close();
70          }
71      }
72  
73      private void writeHeader() {
74          ((OutputFile) output).writeCommonHeader("Code Coverage Report");
75  
76          output.println("  <table id='packages'>");
77  
78          writeTableCaption();
79          writeTableFirstRowWithColumnTitles();
80      }
81  
82      private void writeTableCaption() {
83          if (sourceDirs == null) {
84              output.println("    <caption>All Packages and Files</caption>");
85          } else {
86              output.write("    <caption>All Packages and Files<div style='font-size: smaller'>");
87              output.write(getCommaSeparatedListOfSourceDirs());
88              output.println("</div></caption>");
89          }
90      }
91  
92      @NonNull
93      private String getCommaSeparatedListOfSourceDirs() {
94          List<File> dirs = sourceDirs;
95          assert dirs != null;
96          removeRedundantSourceDirectories(dirs);
97  
98          String concatenatedSourceDirs = dirs.toString();
99          String prefixToRemove = ".." + File.separatorChar;
100         String commaSepDirs = concatenatedSourceDirs.replace(prefixToRemove, "");
101         return commaSepDirs.substring(1, commaSepDirs.length() - 1);
102     }
103 
104     private static void removeRedundantSourceDirectories(@NonNull List<File> dirs) {
105         int i = 0;
106         while (i < dirs.size()) {
107             int newIndex = removeRedundantSourceDirectory(dirs, i);
108             if (newIndex == i) {
109                 i++;
110             } else {
111                 i = newIndex;
112             }
113         }
114     }
115 
116     private static int removeRedundantSourceDirectory(@NonNull List<File> dirs, @NonNegative int dirIndex) {
117         String dir1 = dirs.get(dirIndex).getPath();
118         int j = dirIndex + 1;
119 
120         while (j < dirs.size()) {
121             String dir2 = dirs.get(j).getPath();
122 
123             if (dir1.startsWith(dir2)) {
124                 dirs.remove(j);
125             } else if (dir2.startsWith(dir1)) {
126                 dirs.remove(dirIndex);
127                 dirIndex--;
128                 break;
129             } else {
130                 j++;
131             }
132         }
133 
134         return dirIndex;
135     }
136 
137     private void writeTableFirstRowWithColumnTitles() {
138         output.println("    <tr>");
139         output.write("      <th style='cursor: col-resize' onclick='showHideAllFiles()'>Packages: ");
140         output.print(packageToFiles.size());
141         output.println("</th>");
142         output.write("      <th onclick='location.reload()' style='cursor: n-resize' title='"
143                 + "Click on the column title to the right to sort by size (total number of items).'>Files: ");
144         output.print(totalFileCount);
145         output.println("</th>");
146         writeHeaderCellWithMetricNameAndDescription();
147         output.println("    </tr>");
148     }
149 
150     private void writeHeaderCellWithMetricNameAndDescription() {
151         output.println("      <th onclick='sortTables()' style='cursor: n-resize' title='"
152                 + "Measures how much of the executable production code (executable lines and fields) was exercised by tests.\n\n"
153                 + "An executable line of code contains one or more executable segments, separated by branching points\n"
154                 + "(if..else instructions, logical operators, etc.).\n\n"
155                 + "A non-final field must have the last value assigned to it read by at least one test, to be considered as covered.\n\n"
156                 + "Percentages are calculated as          100 × (CS + CF)\n"
157                 + "                                                         ────────\n"
158                 + "                                                                   S + F\n\n"
159                 + "where S+F is the total number of segments and fields, and CS+CF the covered segments and fields."
160                 + "'>Cvrg</th>");
161     }
162 
163     private void writeLineWithCoverageTotal() {
164         output.println("    <tr class='total'>");
165         output.println("      <td>Total</td><td>&nbsp;</td>");
166 
167         int covered = coveredItems;
168         int total = totalItems;
169         int percentage = CoveragePercentage.calculate(covered, total);
170         printCoveragePercentage(covered, total, percentage);
171 
172         output.println("    </tr>");
173     }
174 
175     @Override
176     @SuppressWarnings("ParameterNameDiffersFromOverriddenParameter")
177     protected void writeMetricsForFile(String unused, @NonNull String packageName) {
178         writeRowStart();
179         writeTableCellWithPackageName(packageName);
180         writeInternalTableForSourceFiles(packageName);
181         writeCoveragePercentageForPackage(packageName);
182         writeRowClose();
183     }
184 
185     private void writeTableCellWithPackageName(@NonNull String packageName) {
186         printIndent();
187         output.write("  <td");
188 
189         List<String> filesInPackage = packageToFiles.get(packageName);
190 
191         if (filesInPackage.size() > 1) {
192             output.write(" class='click' onclick='shFls(this)'");
193         }
194 
195         output.write('>');
196         output.write(packageName.replace('/', '.'));
197         output.println("</td>");
198     }
199 
200     private void writeInternalTableForSourceFiles(@NonNull String packageName) {
201         printIndent();
202         output.println("  <td>");
203 
204         printIndent();
205         output.println("    <table>");
206 
207         List<String> fileNames = packageToFiles.get(packageName);
208         packageReport.writeMetricsForEachFile(packageName, fileNames);
209 
210         recordCoverageInformationForPackage(packageName);
211 
212         printIndent();
213         output.println("    </table>");
214         printIndent();
215 
216         writeInitiallyHiddenSourceFileCount(fileNames.size());
217         printIndent();
218         output.println("  </td>");
219     }
220 
221     private void recordCoverageInformationForPackage(@NonNull String packageName) {
222         int coveredInPackage = packageReport.coveredItems;
223         int totalInPackage = packageReport.totalItems;
224         int packagePercentage = CoveragePercentage.calculate(coveredInPackage, totalInPackage);
225 
226         totalItems += totalInPackage;
227         coveredItems += coveredInPackage;
228         packageToPackagePercentages.put(packageName, packagePercentage);
229     }
230 
231     private void writeInitiallyHiddenSourceFileCount(@NonNegative int fileCount) {
232         output.write("    <span>(");
233         output.print(fileCount);
234         output.println(" source files)</span>");
235     }
236 
237     private void writeCoveragePercentageForPackage(@NonNull String packageName) {
238         int filePercentage = packageToPackagePercentages.get(packageName);
239         printCoveragePercentage(packageReport.coveredItems, packageReport.totalItems, filePercentage);
240     }
241 
242     private void writeListOfRedundantTestsIfAny() {
243         TestCoverage testCoverage = TestCoverage.INSTANCE;
244 
245         if (testCoverage == null) {
246             return;
247         }
248 
249         List<Method> redundantTests = testCoverage.getRedundantTests();
250 
251         if (!redundantTests.isEmpty()) {
252             output.println("  <br>Redundant tests:");
253             output.println("  <ol title=\"Tests are regarded as redundant when they don't cover any additional line "
254                     + "segments or fields that haven't already been covered by a previous test.\n"
255                     + "Note this means the list of redundant tests depends on the order of test execution.\n"
256                     + "Such a test can be removed without weakening the test suite, as long as another test "
257                     + "for the same scenario performs its assertions.\">");
258 
259             for (Method testMethod : redundantTests) {
260                 String testDescription = testMethod.getDeclaringClass().getSimpleName() + '.' + testMethod.getName();
261 
262                 output.append("");
263                 output.write("    <li>");
264                 output.write(testDescription);
265                 output.println("</li>");
266             }
267 
268             output.println("  </ol>");
269         }
270     }
271 
272     private void writeFooter() {
273         output.println("  <p>");
274         output.println("    <a href='http://jmockit.github.io'><img src='logo.png'></a>");
275         output.write("    Generated on ");
276         output.println(new Date());
277         output.println("  </p>");
278         ((OutputFile) output).writeCommonFooter();
279     }
280 }