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 }