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