1 package org.junit.experimental.max; 2 3 import java.io.File; 4 import java.util.ArrayList; 5 import java.util.Collections; 6 import java.util.List; 7 8 import junit.framework.TestSuite; 9 import org.junit.internal.requests.SortingRequest; 10 import org.junit.internal.runners.ErrorReportingRunner; 11 import org.junit.internal.runners.JUnit38ClassRunner; 12 import org.junit.runner.Description; 13 import org.junit.runner.JUnitCore; 14 import org.junit.runner.Request; 15 import org.junit.runner.Result; 16 import org.junit.runner.Runner; 17 import org.junit.runners.Suite; 18 import org.junit.runners.model.InitializationError; 19 20 /** 21 * A replacement for JUnitCore, which keeps track of runtime and failure history, and reorders tests 22 * to maximize the chances that a failing test occurs early in the test run. 23 * 24 * The rules for sorting are: 25 * <ol> 26 * <li> Never-run tests first, in arbitrary order 27 * <li> Group remaining tests by the date at which they most recently failed. 28 * <li> Sort groups such that the most recent failure date is first, and never-failing tests are at the end. 29 * <li> Within a group, run the fastest tests first. 30 * </ol> 31 */ 32 public class MaxCore { 33 private static final String MALFORMED_JUNIT_3_TEST_CLASS_PREFIX = "malformed JUnit 3 test class: "; 34 35 /** 36 * Create a new MaxCore from a serialized file stored at storedResults 37 * 38 * @deprecated use storedLocally() 39 */ 40 @Deprecated forFolder(String folderName)41 public static MaxCore forFolder(String folderName) { 42 return storedLocally(new File(folderName)); 43 } 44 45 /** 46 * Create a new MaxCore from a serialized file stored at storedResults 47 */ storedLocally(File storedResults)48 public static MaxCore storedLocally(File storedResults) { 49 return new MaxCore(storedResults); 50 } 51 52 private final MaxHistory history; 53 MaxCore(File storedResults)54 private MaxCore(File storedResults) { 55 history = MaxHistory.forFolder(storedResults); 56 } 57 58 /** 59 * Run all the tests in <code>class</code>. 60 * 61 * @return a {@link Result} describing the details of the test run and the failed tests. 62 */ run(Class<?> testClass)63 public Result run(Class<?> testClass) { 64 return run(Request.aClass(testClass)); 65 } 66 67 /** 68 * Run all the tests contained in <code>request</code>. 69 * 70 * @param request the request describing tests 71 * @return a {@link Result} describing the details of the test run and the failed tests. 72 */ run(Request request)73 public Result run(Request request) { 74 return run(request, new JUnitCore()); 75 } 76 77 /** 78 * Run all the tests contained in <code>request</code>. 79 * 80 * This variant should be used if {@code core} has attached listeners that this 81 * run should notify. 82 * 83 * @param request the request describing tests 84 * @param core a JUnitCore to delegate to. 85 * @return a {@link Result} describing the details of the test run and the failed tests. 86 */ run(Request request, JUnitCore core)87 public Result run(Request request, JUnitCore core) { 88 core.addListener(history.listener()); 89 return core.run(sortRequest(request).getRunner()); 90 } 91 92 /** 93 * @return a new Request, which contains all of the same tests, but in a new order. 94 */ sortRequest(Request request)95 public Request sortRequest(Request request) { 96 if (request instanceof SortingRequest) { 97 // We'll pay big karma points for this 98 return request; 99 } 100 List<Description> leaves = findLeaves(request); 101 Collections.sort(leaves, history.testComparator()); 102 return constructLeafRequest(leaves); 103 } 104 constructLeafRequest(List<Description> leaves)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 }