github.phpd.cn/thought-machine/please@v12.2.0+incompatible/tools/junit_runner/src/build/please/test/TestCoverage.java (about) 1 package build.please.test; 2 3 import java.io.BufferedReader; 4 import java.io.InputStreamReader; 5 import java.io.InputStream; 6 import java.io.IOException; 7 import java.lang.Thread; 8 import java.util.HashMap; 9 import java.util.LinkedHashMap; 10 import java.util.HashSet; 11 import java.util.Map; 12 import java.util.Set; 13 14 import org.jacoco.core.analysis.Analyzer; 15 import org.jacoco.core.analysis.CoverageBuilder; 16 import org.jacoco.core.analysis.IClassCoverage; 17 import org.jacoco.core.analysis.ICounter; 18 import org.jacoco.core.data.ExecutionDataStore; 19 import org.jacoco.core.data.SessionInfoStore; 20 import org.jacoco.core.instr.Instrumenter; 21 import org.jacoco.core.runtime.IRuntime; 22 import org.jacoco.core.runtime.LoggerRuntime; 23 import org.jacoco.core.runtime.RuntimeData; 24 25 import javax.xml.parsers.DocumentBuilder; 26 import javax.xml.parsers.DocumentBuilderFactory; 27 28 import org.w3c.dom.Document; 29 import org.w3c.dom.Element; 30 31 import static java.nio.charset.StandardCharsets.UTF_8; 32 33 34 public class TestCoverage { 35 // Class handling coverage instrumentation using Jacoco. 36 // This is very heavily based on the example given with Jacoco. 37 private static final String OUTPUT_FILE = System.getenv("COVERAGE_FILE"); 38 39 public static void RunTestClasses(Set<Class> classes, Set<Class> allClasses) throws Exception { 40 IRuntime runtime = new LoggerRuntime(); 41 Instrumenter instrumenter = new Instrumenter(runtime); 42 RuntimeData data = new RuntimeData(); 43 runtime.startup(data); 44 45 // This is a little bit fiddly; we want to instrument all relevant classes and then 46 // once that's done run just the test classes. 47 MemoryClassLoader memoryClassLoader = new MemoryClassLoader(instrumenter, allClasses); 48 // Inject our class loader so anything that tries to dynamically load classes will use it 49 // instead of the normal one and get the instrumented classes back. 50 // This probably isn't completely reliable but certainly fixes some problems. 51 Thread.currentThread().setContextClassLoader(memoryClassLoader); 52 Set<String> testClassNames = new HashSet<>(); 53 for (Class cls : allClasses) { 54 // don't instrument the test runner classes here, nobody else wants to see them. 55 if (!cls.getPackage().getName().equals("build.please.test")) { 56 memoryClassLoader.loadClass(cls.getName()); 57 } 58 } 59 for (Class testClass : classes) { 60 TestMain.runClass(memoryClassLoader.loadClass(testClass.getName())); 61 testClassNames.add(testClass.getName()); 62 } 63 64 ExecutionDataStore executionData = new ExecutionDataStore(); 65 SessionInfoStore sessionInfo = new SessionInfoStore(); 66 data.collect(executionData, sessionInfo, false); 67 runtime.shutdown(); 68 69 CoverageBuilder coverageBuilder = new CoverageBuilder(); 70 Analyzer analyzer = new Analyzer(executionData, coverageBuilder); 71 for (Class testClass : allClasses) { 72 analyzer.analyzeClass(getTargetClass(testClass, testClass.getName()), testClass.getName()); 73 } 74 writeResults(coverageBuilder, testClassNames); 75 } 76 77 private static InputStream getTargetClass(Class cls, String name) { 78 final String resource = '/' + name.replace('.', '/') + ".class"; 79 return cls.getResourceAsStream(resource); 80 } 81 82 // Loads and instruments classes for coverage. 83 private static class MemoryClassLoader extends ClassLoader { 84 private final Instrumenter instrumenter; 85 private final Map<String, Class> instrumentedClasses = new HashMap<>(); 86 87 public MemoryClassLoader(Instrumenter instrumenter, Set<Class> classes) { 88 this.instrumenter = instrumenter; 89 for (Class cls : classes) { 90 instrumentedClasses.put(cls.getName(), null); 91 } 92 } 93 94 @Override 95 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 96 try { 97 Class cls = instrumentedClasses.get(name); 98 if (cls != null) { 99 return cls; 100 } else if (instrumentedClasses.containsKey(name)) { 101 byte[] instrumented = instrumenter.instrument(getTargetClass(MemoryClassLoader.class, name), name); 102 cls = defineClass(name, instrumented, 0, instrumented.length, this.getClass().getProtectionDomain()); 103 instrumentedClasses.put(name, cls); 104 return cls; 105 } 106 return super.loadClass(name, resolve); 107 } catch (IOException ex) { 108 throw new RuntimeException(ex); 109 } 110 } 111 } 112 113 private static void writeResults(CoverageBuilder coverageBuilder, Set<String> testClassNames) throws Exception { 114 Map<String, String> sourceMap = readSourceMap(); 115 DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 116 Document doc = docBuilder.newDocument(); 117 doc.setXmlVersion("1.0"); 118 119 Element root = doc.createElement("coverage"); 120 doc.appendChild(root); 121 Element packages = doc.createElement("packages"); 122 root.appendChild(packages); 123 // TODO(pebers): split up classes properly into separate packages here. 124 // It won't really make any difference to plz but it'd be nicer. 125 Element pkg = doc.createElement("package"); 126 packages.appendChild(pkg); 127 Element classes = doc.createElement("classes"); 128 pkg.appendChild(classes); 129 130 for (final IClassCoverage cc : coverageBuilder.getClasses()) { 131 if (cc.getName().startsWith("build/please/test") || testClassNames.contains(cc.getName().replace("/", "."))) { 132 continue; // keep these out of results 133 } 134 135 Element cls = doc.createElement("class"); 136 cls.setAttribute("branch-rate", String.valueOf(cc.getBranchCounter().getCoveredRatio())); 137 cls.setAttribute("complexity", String.valueOf(cc.getComplexityCounter().getCoveredRatio())); 138 cls.setAttribute("line-rate", String.valueOf(cc.getLineCounter().getCoveredRatio())); 139 cls.setAttribute("name", cc.getName()); 140 String name = sourceMap.get(cc.getPackageName().replace(".", "/") + "/" + cc.getSourceFileName()); 141 cls.setAttribute("filename", name != null ? name : cc.getName()); 142 143 Element lines = doc.createElement("lines"); 144 for (int i = cc.getFirstLine(); i <= cc.getLastLine(); ++i) { 145 if (cc.getLine(i).getStatus() != ICounter.EMPTY) { // assume this means not executable? 146 Element line = doc.createElement("line"); 147 line.setAttribute("number", String.valueOf(i)); 148 line.setAttribute("hits", String.valueOf(cc.getLine(i).getInstructionCounter().getCoveredCount())); 149 // TODO(pebers): more useful output here. 150 lines.appendChild(line); 151 } 152 } 153 cls.appendChild(lines); 154 classes.appendChild(cls); 155 } 156 157 TestMain.writeXMLDocumentToFile(OUTPUT_FILE, doc); 158 } 159 160 /** 161 * Read the sourcemap file that we use to map Java class names back to their path in the repo. 162 */ 163 public static Map<String, String> readSourceMap() { 164 Map<String, String> sourceMap = new LinkedHashMap<>(); 165 try { 166 InputStream is = TestCoverage.class.getClassLoader().getResourceAsStream("META-INF/please_sourcemap"); 167 BufferedReader br = new BufferedReader(new InputStreamReader(is, UTF_8)); 168 for(String line; (line = br.readLine()) != null; ) { 169 String[] parts = line.trim().split(" "); 170 if (parts.length == 2) { 171 sourceMap.put(parts[1], deriveOriginalFilename(parts[0], parts[1])); 172 } else if (parts.length == 1 && line.startsWith(" ")) { 173 // Special case for repo root, where there is no first part. 174 sourceMap.put(parts[0], parts[0]); 175 } 176 } 177 } catch (IOException ex) { 178 ex.printStackTrace(); 179 System.out.println("Failed to read sourcemap. Coverage results may be inaccurate."); 180 } 181 return sourceMap; 182 } 183 184 /** 185 * Derives the original file name from the package and class paths. 186 * For example, the package might be src/build/java/build/please/test and 187 * the class would be build/please/test/TestCoverage; we want to 188 * produce src/build/java/build/please/test/TestCoverage. 189 */ 190 public static String deriveOriginalFilename(String packageName, String className) { 191 String packagePath[] = packageName.split("/"); 192 String classPath[] = className.split("/"); 193 for (int size = classPath.length - 1; size > 0; --size) { 194 if (size < packagePath.length && matchArrays(packagePath, classPath, size)) { 195 StringBuilder sb = new StringBuilder(); 196 for (int i = 0; i < packagePath.length; ++i) { 197 sb.append(packagePath[i]); 198 sb.append('/'); 199 } 200 for (int i = size; i < classPath.length; ++i) { 201 if (i > size) { 202 sb.append('/'); 203 } 204 sb.append(classPath[i]); 205 } 206 return sb.toString(); 207 } 208 } 209 if (!packageName.isEmpty()) { 210 return packageName + '/' + className; 211 } 212 return className; 213 } 214 215 private static boolean matchArrays(String[] a, String[] b, int size) { 216 for (int i = 0, j = a.length - size; i < size; ++i, ++j) { 217 if (!a[j].equals(b[i])) { 218 return false; 219 } 220 } 221 return true; 222 } 223 }