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}