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    }