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  }