    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    }