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.data;
7   
8   import edu.umd.cs.findbugs.annotations.NonNull;
9   import edu.umd.cs.findbugs.annotations.Nullable;
10  
11  import java.io.BufferedInputStream;
12  import java.io.BufferedOutputStream;
13  import java.io.File;
14  import java.io.IOException;
15  import java.io.ObjectInputStream;
16  import java.io.ObjectOutputStream;
17  import java.io.OutputStream;
18  import java.io.Serializable;
19  import java.nio.file.Files;
20  import java.nio.file.Path;
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Map.Entry;
27  import java.util.jar.JarEntry;
28  import java.util.jar.JarFile;
29  
30  import mockit.coverage.CoveragePercentage;
31  import mockit.internal.util.Utilities;
32  
33  import org.checkerframework.checker.index.qual.NonNegative;
34  
35  /**
36   * Coverage data captured for all source files exercised during a test run.
37   */
38  public final class CoverageData implements Serializable {
39      private static final long serialVersionUID = -4860004226098360259L;
40      @NonNull
41      private static final CoverageData instance = new CoverageData();
42  
43      @NonNull
44      public static CoverageData instance() {
45          return instance;
46      }
47  
48      private boolean withCallPoints;
49  
50      @NonNull
51      private final Map<String, FileCoverageData> fileToFileData = new LinkedHashMap<>();
52      @NonNull
53      private final List<FileCoverageData> indexedFileData = new ArrayList<>(100);
54  
55      public boolean isWithCallPoints() {
56          return withCallPoints;
57      }
58  
59      public void setWithCallPoints(boolean withCallPoints) {
60          this.withCallPoints = withCallPoints;
61      }
62  
63      @NonNull
64      public Map<String, FileCoverageData> getFileToFileData() {
65          return fileToFileData;
66      }
67  
68      @NonNull
69      public FileCoverageData getOrAddFile(@NonNull String file, @Nullable String kindOfTopLevelType) {
70          FileCoverageData fileData = fileToFileData.get(file);
71  
72          // For a class with nested/inner classes, a previous class in the same source file may already have been added.
73          if (fileData == null) {
74              int fileIndex = indexedFileData.size();
75              fileData = new FileCoverageData(fileIndex, kindOfTopLevelType);
76              indexedFileData.add(fileData);
77              fileToFileData.put(file, fileData);
78          } else if (kindOfTopLevelType != null) {
79              fileData.kindOfTopLevelType = kindOfTopLevelType;
80          }
81  
82          return fileData;
83      }
84  
85      @NonNull
86      public FileCoverageData getFileData(@NonNull String file) {
87          return fileToFileData.get(file);
88      }
89  
90      @NonNull
91      public FileCoverageData getFileData(@NonNegative int fileIndex) {
92          return indexedFileData.get(fileIndex);
93      }
94  
95      public boolean isEmpty() {
96          return fileToFileData.isEmpty();
97      }
98  
99      public void clear() {
100         fileToFileData.clear();
101     }
102 
103     /**
104      * Computes the coverage percentage over a subset of the available source files.
105      *
106      * @param fileNamePrefix
107      *            a regular expression for matching the names of the source files to be considered, or <code>null</code>
108      *            to consider <em>all</em> files
109      *
110      * @return the computed percentage from <code>0</code> to <code>100</code> (inclusive), or <code>-1</code> if no
111      *         meaningful value could be computed
112      */
113     public int getPercentage(@Nullable String fileNamePrefix) {
114         int coveredItems = 0;
115         int totalItems = 0;
116 
117         for (Entry<String, FileCoverageData> fileAndFileData : fileToFileData.entrySet()) {
118             String sourceFile = fileAndFileData.getKey();
119 
120             if (fileNamePrefix == null || sourceFile.startsWith(fileNamePrefix)) {
121                 FileCoverageData fileData = fileAndFileData.getValue();
122                 coveredItems += fileData.getCoveredItems();
123                 totalItems += fileData.getTotalItems();
124             }
125         }
126 
127         return CoveragePercentage.calculate(coveredItems, totalItems);
128     }
129 
130     /**
131      * Finds the source file with the smallest coverage percentage.
132      *
133      * @return the percentage value for the file found, or <code>Integer.MAX_VALUE</code> if no file is found with a
134      *         meaningful percentage
135      */
136     @NonNegative
137     public int getSmallestPerFilePercentage() {
138         int minPercentage = Integer.MAX_VALUE;
139 
140         for (FileCoverageData fileData : fileToFileData.values()) {
141             if (!fileData.wasLoadedAfterTestCompletion()) {
142                 int percentage = fileData.getCoveragePercentage();
143 
144                 if (percentage >= 0 && percentage < minPercentage) {
145                     minPercentage = percentage;
146                 }
147             }
148         }
149 
150         return minPercentage;
151     }
152 
153     public void fillLastModifiedTimesForAllClassFiles() {
154         for (Iterator<Entry<String, FileCoverageData>> itr = fileToFileData.entrySet().iterator(); itr.hasNext();) {
155             Entry<String, FileCoverageData> fileAndFileData = itr.next();
156             long lastModified = getLastModifiedTimeForClassFile(fileAndFileData.getKey());
157 
158             if (lastModified > 0L) {
159                 FileCoverageData fileCoverageData = fileAndFileData.getValue();
160                 fileCoverageData.lastModified = lastModified;
161                 continue;
162             }
163 
164             itr.remove();
165         }
166     }
167 
168     private long getLastModifiedTimeForClassFile(@NonNull String sourceFilePath) {
169         String sourceFilePathNoExt = sourceFilePath.substring(0, sourceFilePath.lastIndexOf('.'));
170         String className = sourceFilePathNoExt.replace('/', '.');
171 
172         Class<?> coveredClass = findCoveredClass(className);
173 
174         if (coveredClass == null) {
175             return 0L;
176         }
177 
178         String locationPath = Utilities.getClassFileLocationPath(coveredClass);
179 
180         if (locationPath.endsWith(".jar")) {
181             try {
182                 return getLastModifiedTimeFromJarEntry(sourceFilePathNoExt, locationPath);
183             } catch (IOException ignore) {
184                 return 0L;
185             }
186         }
187 
188         String pathToClassFile = locationPath + sourceFilePathNoExt + ".class";
189 
190         return Path.of(pathToClassFile).toFile().lastModified();
191     }
192 
193     private static long getLastModifiedTimeFromJarEntry(@NonNull String sourceFilePathNoExt,
194             @NonNull String locationPath) throws IOException {
195 
196         try (JarFile jarFile = new JarFile(locationPath)) {
197             JarEntry classEntry = jarFile.getJarEntry(sourceFilePathNoExt + ".class");
198             return classEntry.getTime();
199         }
200     }
201 
202     @Nullable
203     private Class<?> findCoveredClass(@NonNull String className) {
204         ClassLoader currentCL = getClass().getClassLoader();
205         Class<?> coveredClass = loadClass(className, currentCL);
206 
207         if (coveredClass == null) {
208             ClassLoader systemCL = ClassLoader.getSystemClassLoader();
209 
210             if (systemCL != currentCL) {
211                 coveredClass = loadClass(className, systemCL);
212             }
213 
214             if (coveredClass == null) {
215                 ClassLoader contextCL = Thread.currentThread().getContextClassLoader();
216 
217                 if (contextCL != null && contextCL != systemCL) {
218                     coveredClass = loadClass(className, contextCL);
219                 }
220             }
221         }
222 
223         return coveredClass;
224     }
225 
226     @Nullable
227     private static Class<?> loadClass(@NonNull String className, @Nullable ClassLoader loader) {
228         try {
229             return Class.forName(className, false, loader);
230         } catch (ClassNotFoundException | NoClassDefFoundError ignore) {
231             return null;
232         }
233     }
234 
235     /**
236      * Reads a serialized <code>CoverageData</code> object from the given file (normally, a "<code>coverage.ser</code>"
237      * file generated at the end of a previous test run).
238      *
239      * @param dataFile
240      *            the ".ser" file containing a serialized <code>CoverageData</code> instance
241      *
242      * @return a new object containing all coverage data resulting from a previous test run
243      */
244     @NonNull
245     public static CoverageData readDataFromFile(@NonNull File dataFile) throws IOException {
246         try (ObjectInputStream input = new ObjectInputStream(
247                 new BufferedInputStream(Files.newInputStream(dataFile.toPath())))) {
248             return (CoverageData) input.readObject();
249         } catch (ClassNotFoundException e) {
250             throw new RuntimeException(
251                     "Serialized class in coverage data file \"" + dataFile + "\" not found in classpath", e);
252         }
253     }
254 
255     public void writeDataToFile(@NonNull File dataFile) throws IOException {
256         try (OutputStream outputStream = Files.newOutputStream(dataFile.toPath());
257                 BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
258                 ObjectOutputStream output = new ObjectOutputStream(bufferedOutputStream)) {
259             output.writeObject(this);
260         }
261     }
262 
263     public void merge(@NonNull CoverageData previousData) {
264         withCallPoints |= previousData.withCallPoints;
265 
266         for (Entry<String, FileCoverageData> previousFileAndFileData : previousData.fileToFileData.entrySet()) {
267             String previousFile = previousFileAndFileData.getKey();
268             FileCoverageData previousFileData = previousFileAndFileData.getValue();
269             FileCoverageData fileData = fileToFileData.get(previousFile);
270 
271             if (fileData == null) {
272                 fileToFileData.put(previousFile, previousFileData);
273             } else if (fileData.lastModified > 0 && previousFileData.lastModified == fileData.lastModified) {
274                 fileData.mergeWithDataFromPreviousTestRun(previousFileData);
275             }
276         }
277     }
278 }