1package org.junit.experimental.max;
2
3import java.io.File;
4import java.util.ArrayList;
5import java.util.Collections;
6import java.util.List;
7
8import junit.framework.TestSuite;
9
10import org.junit.internal.requests.SortingRequest;
11import org.junit.internal.runners.ErrorReportingRunner;
12import org.junit.internal.runners.JUnit38ClassRunner;
13import org.junit.runner.Description;
14import org.junit.runner.JUnitCore;
15import org.junit.runner.Request;
16import org.junit.runner.Result;
17import org.junit.runner.Runner;
18import org.junit.runners.Suite;
19import org.junit.runners.model.InitializationError;
20
21/**
22 * A replacement for JUnitCore, which keeps track of runtime and failure history, and reorders tests
23 * to maximize the chances that a failing test occurs early in the test run.
24 *
25 * The rules for sorting are:
26 * <ol>
27 * <li> Never-run tests first, in arbitrary order
28 * <li> Group remaining tests by the date at which they most recently failed.
29 * <li> Sort groups such that the most recent failure date is first, and never-failing tests are at the end.
30 * <li> Within a group, run the fastest tests first.
31 * </ol>
32 */
33public class MaxCore {
34	private static final String MALFORMED_JUNIT_3_TEST_CLASS_PREFIX= "malformed JUnit 3 test class: ";
35
36	/**
37	 * Create a new MaxCore from a serialized file stored at storedResults
38	 * @deprecated use storedLocally()
39	 */
40	@Deprecated
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	 */
48	public static MaxCore storedLocally(File storedResults) {
49		return new MaxCore(storedResults);
50	}
51
52	private final MaxHistory fHistory;
53
54	private MaxCore(File storedResults) {
55		fHistory = MaxHistory.forFolder(storedResults);
56	}
57
58	/**
59	 * Run all the tests in <code>class</code>.
60	 * @return a {@link Result} describing the details of the test run and the failed tests.
61	 */
62	public Result run(Class<?> testClass) {
63		return run(Request.aClass(testClass));
64	}
65
66	/**
67	 * Run all the tests contained in <code>request</code>.
68	 * @param request the request describing tests
69	 * @return a {@link Result} describing the details of the test run and the failed tests.
70	 */
71	public Result run(Request request) {
72		return run(request, new JUnitCore());
73	}
74
75	/**
76	 * Run all the tests contained in <code>request</code>.
77	 *
78	 * This variant should be used if {@code core} has attached listeners that this
79	 * run should notify.
80	 *
81	 * @param request the request describing tests
82	 * @param core a JUnitCore to delegate to.
83	 * @return a {@link Result} describing the details of the test run and the failed tests.
84	 */
85	public Result run(Request request, JUnitCore core) {
86		core.addListener(fHistory.listener());
87		return core.run(sortRequest(request).getRunner());
88	}
89
90	/**
91	 * @param request
92	 * @return a new Request, which contains all of the same tests, but in a new order.
93	 */
94	public Request sortRequest(Request request) {
95		if (request instanceof SortingRequest) // We'll pay big karma points for this
96			return request;
97		List<Description> leaves= findLeaves(request);
98		Collections.sort(leaves, fHistory.testComparator());
99		return constructLeafRequest(leaves);
100	}
101
102	private Request constructLeafRequest(List<Description> leaves) {
103		final List<Runner> runners = new ArrayList<Runner>();
104		for (Description each : leaves)
105			runners.add(buildRunner(each));
106		return new Request() {
107			@Override
108			public Runner getRunner() {
109				try {
110					return new Suite((Class<?>)null, runners) {};
111				} catch (InitializationError e) {
112					return new ErrorReportingRunner(null, e);
113				}
114			}
115		};
116	}
117
118	private Runner buildRunner(Description each) {
119		if (each.toString().equals("TestSuite with 0 tests"))
120			return Suite.emptySuite();
121		if (each.toString().startsWith(MALFORMED_JUNIT_3_TEST_CLASS_PREFIX))
122			// This is cheating, because it runs the whole class
123			// to get the warning for this method, but we can't do better,
124			// because JUnit 3.8's
125			// thrown away which method the warning is for.
126			return new JUnit38ClassRunner(new TestSuite(getMalformedTestClass(each)));
127		Class<?> type= each.getTestClass();
128		if (type == null)
129			throw new RuntimeException("Can't build a runner from description [" + each + "]");
130		String methodName= each.getMethodName();
131		if (methodName == null)
132			return Request.aClass(type).getRunner();
133		return Request.method(type, methodName).getRunner();
134	}
135
136	private Class<?> getMalformedTestClass(Description each) {
137		try {
138			return Class.forName(each.toString().replace(MALFORMED_JUNIT_3_TEST_CLASS_PREFIX, ""));
139		} catch (ClassNotFoundException e) {
140			return null;
141		}
142	}
143
144	/**
145	 * @param request a request to run
146	 * @return a list of method-level tests to run, sorted in the order
147	 * specified in the class comment.
148	 */
149	public List<Description> sortedLeavesForTest(Request request) {
150		return findLeaves(sortRequest(request));
151	}
152
153	private List<Description> findLeaves(Request request) {
154		List<Description> results= new ArrayList<Description>();
155		findLeaves(null, request.getRunner().getDescription(), results);
156		return results;
157	}
158
159	private void findLeaves(Description parent, Description description, List<Description> results) {
160		if (description.getChildren().isEmpty())
161			if (description.toString().equals("warning(junit.framework.TestSuite$1)"))
162				results.add(Description.createSuiteDescription(MALFORMED_JUNIT_3_TEST_CLASS_PREFIX + parent));
163			else
164				results.add(description);
165		else
166			for (Description each : description.getChildren())
167				findLeaves(description, each, results);
168	}
169}
170
171