001package org.junit.experimental.max; 002 003import java.io.File; 004import java.util.ArrayList; 005import java.util.Collections; 006import java.util.List; 007 008import junit.framework.TestSuite; 009import org.junit.internal.requests.SortingRequest; 010import org.junit.internal.runners.ErrorReportingRunner; 011import org.junit.internal.runners.JUnit38ClassRunner; 012import org.junit.runner.Description; 013import org.junit.runner.JUnitCore; 014import org.junit.runner.Request; 015import org.junit.runner.Result; 016import org.junit.runner.Runner; 017import org.junit.runners.Suite; 018import org.junit.runners.model.InitializationError; 019 020/** 021 * A replacement for JUnitCore, which keeps track of runtime and failure history, and reorders tests 022 * to maximize the chances that a failing test occurs early in the test run. 023 * 024 * The rules for sorting are: 025 * <ol> 026 * <li> Never-run tests first, in arbitrary order 027 * <li> Group remaining tests by the date at which they most recently failed. 028 * <li> Sort groups such that the most recent failure date is first, and never-failing tests are at the end. 029 * <li> Within a group, run the fastest tests first. 030 * </ol> 031 */ 032public class MaxCore { 033 private static final String MALFORMED_JUNIT_3_TEST_CLASS_PREFIX = "malformed JUnit 3 test class: "; 034 035 /** 036 * Create a new MaxCore from a serialized file stored at storedResults 037 * 038 * @deprecated use storedLocally() 039 */ 040 @Deprecated 041 public static MaxCore forFolder(String folderName) { 042 return storedLocally(new File(folderName)); 043 } 044 045 /** 046 * Create a new MaxCore from a serialized file stored at storedResults 047 */ 048 public static MaxCore storedLocally(File storedResults) { 049 return new MaxCore(storedResults); 050 } 051 052 private final MaxHistory history; 053 054 private MaxCore(File storedResults) { 055 history = MaxHistory.forFolder(storedResults); 056 } 057 058 /** 059 * Run all the tests in <code>class</code>. 060 * 061 * @return a {@link Result} describing the details of the test run and the failed tests. 062 */ 063 public Result run(Class<?> testClass) { 064 return run(Request.aClass(testClass)); 065 } 066 067 /** 068 * Run all the tests contained in <code>request</code>. 069 * 070 * @param request the request describing tests 071 * @return a {@link Result} describing the details of the test run and the failed tests. 072 */ 073 public Result run(Request request) { 074 return run(request, new JUnitCore()); 075 } 076 077 /** 078 * Run all the tests contained in <code>request</code>. 079 * 080 * This variant should be used if {@code core} has attached listeners that this 081 * run should notify. 082 * 083 * @param request the request describing tests 084 * @param core a JUnitCore to delegate to. 085 * @return a {@link Result} describing the details of the test run and the failed tests. 086 */ 087 public Result run(Request request, JUnitCore core) { 088 core.addListener(history.listener()); 089 return core.run(sortRequest(request).getRunner()); 090 } 091 092 /** 093 * @return a new Request, which contains all of the same tests, but in a new order. 094 */ 095 public Request sortRequest(Request request) { 096 if (request instanceof SortingRequest) { 097 // We'll pay big karma points for this 098 return request; 099 } 100 List<Description> leaves = findLeaves(request); 101 Collections.sort(leaves, history.testComparator()); 102 return constructLeafRequest(leaves); 103 } 104 105 private Request constructLeafRequest(List<Description> leaves) { 106 final List<Runner> runners = new ArrayList<Runner>(); 107 for (Description each : leaves) { 108 runners.add(buildRunner(each)); 109 } 110 return new Request() { 111 @Override 112 public Runner getRunner() { 113 try { 114 return new Suite((Class<?>) null, runners) { 115 }; 116 } catch (InitializationError e) { 117 return new ErrorReportingRunner(null, e); 118 } 119 } 120 }; 121 } 122 123 private Runner buildRunner(Description each) { 124 if (each.toString().equals("TestSuite with 0 tests")) { 125 return Suite.emptySuite(); 126 } 127 if (each.toString().startsWith(MALFORMED_JUNIT_3_TEST_CLASS_PREFIX)) { 128 // This is cheating, because it runs the whole class 129 // to get the warning for this method, but we can't do better, 130 // because JUnit 3.8's 131 // thrown away which method the warning is for. 132 return new JUnit38ClassRunner(new TestSuite(getMalformedTestClass(each))); 133 } 134 Class<?> type = each.getTestClass(); 135 if (type == null) { 136 throw new RuntimeException("Can't build a runner from description [" + each + "]"); 137 } 138 String methodName = each.getMethodName(); 139 if (methodName == null) { 140 return Request.aClass(type).getRunner(); 141 } 142 return Request.method(type, methodName).getRunner(); 143 } 144 145 private Class<?> getMalformedTestClass(Description each) { 146 try { 147 return Class.forName(each.toString().replace(MALFORMED_JUNIT_3_TEST_CLASS_PREFIX, "")); 148 } catch (ClassNotFoundException e) { 149 return null; 150 } 151 } 152 153 /** 154 * @param request a request to run 155 * @return a list of method-level tests to run, sorted in the order 156 * specified in the class comment. 157 */ 158 public List<Description> sortedLeavesForTest(Request request) { 159 return findLeaves(sortRequest(request)); 160 } 161 162 private List<Description> findLeaves(Request request) { 163 List<Description> results = new ArrayList<Description>(); 164 findLeaves(null, request.getRunner().getDescription(), results); 165 return results; 166 } 167 168 private void findLeaves(Description parent, Description description, List<Description> results) { 169 if (description.getChildren().isEmpty()) { 170 if (description.toString().equals("warning(junit.framework.TestSuite$1)")) { 171 results.add(Description.createSuiteDescription(MALFORMED_JUNIT_3_TEST_CLASS_PREFIX + parent)); 172 } else { 173 results.add(description); 174 } 175 } else { 176 for (Description each : description.getChildren()) { 177 findLeaves(description, each, results); 178 } 179 } 180 } 181}