001 package org.junit.experimental.max;
002
003 import java.io.File;
004 import java.util.ArrayList;
005 import java.util.Collections;
006 import java.util.List;
007
008 import junit.framework.TestSuite;
009 import org.junit.internal.requests.SortingRequest;
010 import org.junit.internal.runners.ErrorReportingRunner;
011 import org.junit.internal.runners.JUnit38ClassRunner;
012 import org.junit.runner.Description;
013 import org.junit.runner.JUnitCore;
014 import org.junit.runner.Request;
015 import org.junit.runner.Result;
016 import org.junit.runner.Runner;
017 import org.junit.runners.Suite;
018 import 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 */
032 public 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 }