001package org.junit.experimental.max;
002
003import java.io.File;
004import java.io.FileInputStream;
005import java.io.FileOutputStream;
006import java.io.IOException;
007import java.io.ObjectInputStream;
008import java.io.ObjectOutputStream;
009import java.io.Serializable;
010import java.util.Comparator;
011import java.util.HashMap;
012import java.util.Map;
013
014import org.junit.runner.Description;
015import org.junit.runner.Result;
016import org.junit.runner.notification.Failure;
017import org.junit.runner.notification.RunListener;
018
019/**
020 * Stores a subset of the history of each test:
021 * <ul>
022 * <li>Last failure timestamp
023 * <li>Duration of last execution
024 * </ul>
025 */
026public class MaxHistory implements Serializable {
027    private static final long serialVersionUID = 1L;
028
029    /**
030     * Loads a {@link MaxHistory} from {@code file}, or generates a new one that
031     * will be saved to {@code file}.
032     */
033    public static MaxHistory forFolder(File file) {
034        if (file.exists()) {
035            try {
036                return readHistory(file);
037            } catch (CouldNotReadCoreException e) {
038                e.printStackTrace();
039                file.delete();
040            }
041        }
042        return new MaxHistory(file);
043    }
044
045    private static MaxHistory readHistory(File storedResults)
046            throws CouldNotReadCoreException {
047        try {
048            FileInputStream file = new FileInputStream(storedResults);
049            try {
050                ObjectInputStream stream = new ObjectInputStream(file);
051                try {
052                    return (MaxHistory) stream.readObject();
053                } finally {
054                    stream.close();
055                }
056            } finally {
057                file.close();
058            }
059        } catch (Exception e) {
060            throw new CouldNotReadCoreException(e);
061        }
062    }
063
064    /*
065     * We have to use the f prefix until the next major release to ensure
066     * serialization compatibility. 
067     * See https://github.com/junit-team/junit4/issues/976
068     */
069    private final Map<String, Long> fDurations = new HashMap<String, Long>();
070    private final Map<String, Long> fFailureTimestamps = new HashMap<String, Long>();
071    private final File fHistoryStore;
072
073    private MaxHistory(File storedResults) {
074        fHistoryStore = storedResults;
075    }
076
077    private void save() throws IOException {
078        ObjectOutputStream stream = null;
079        try {
080            stream = new ObjectOutputStream(new FileOutputStream(fHistoryStore));
081            stream.writeObject(this);
082        } finally {
083            if (stream != null) {
084                stream.close();
085            }
086        }
087    }
088
089    Long getFailureTimestamp(Description key) {
090        return fFailureTimestamps.get(key.toString());
091    }
092
093    void putTestFailureTimestamp(Description key, long end) {
094        fFailureTimestamps.put(key.toString(), end);
095    }
096
097    boolean isNewTest(Description key) {
098        return !fDurations.containsKey(key.toString());
099    }
100
101    Long getTestDuration(Description key) {
102        return fDurations.get(key.toString());
103    }
104
105    void putTestDuration(Description description, long duration) {
106        fDurations.put(description.toString(), duration);
107    }
108
109    private final class RememberingListener extends RunListener {
110        private long overallStart = System.currentTimeMillis();
111
112        private Map<Description, Long> starts = new HashMap<Description, Long>();
113
114        @Override
115        public void testStarted(Description description) throws Exception {
116            starts.put(description, System.nanoTime()); // Get most accurate
117            // possible time
118        }
119
120        @Override
121        public void testFinished(Description description) throws Exception {
122            long end = System.nanoTime();
123            long start = starts.get(description);
124            putTestDuration(description, end - start);
125        }
126
127        @Override
128        public void testFailure(Failure failure) throws Exception {
129            putTestFailureTimestamp(failure.getDescription(), overallStart);
130        }
131
132        @Override
133        public void testRunFinished(Result result) throws Exception {
134            save();
135        }
136    }
137
138    private class TestComparator implements Comparator<Description> {
139        public int compare(Description o1, Description o2) {
140            // Always prefer new tests
141            if (isNewTest(o1)) {
142                return -1;
143            }
144            if (isNewTest(o2)) {
145                return 1;
146            }
147            // Then most recently failed first
148            int result = getFailure(o2).compareTo(getFailure(o1));
149            return result != 0 ? result
150                    // Then shorter tests first
151                    : getTestDuration(o1).compareTo(getTestDuration(o2));
152        }
153
154        private Long getFailure(Description key) {
155            Long result = getFailureTimestamp(key);
156            if (result == null) {
157                return 0L; // 0 = "never failed (that I know about)"
158            }
159            return result;
160        }
161    }
162
163    /**
164     * @return a listener that will update this history based on the test
165     *         results reported.
166     */
167    public RunListener listener() {
168        return new RememberingListener();
169    }
170
171    /**
172     * @return a comparator that ranks tests based on the JUnit Max sorting
173     *         rules, as described in the {@link MaxCore} class comment.
174     */
175    public Comparator<Description> testComparator() {
176        return new TestComparator();
177    }
178}