1package org.junit.experimental.max;
2
3import java.io.File;
4import java.io.FileInputStream;
5import java.io.FileOutputStream;
6import java.io.IOException;
7import java.io.ObjectInputStream;
8import java.io.ObjectOutputStream;
9import java.io.Serializable;
10import java.util.Comparator;
11import java.util.HashMap;
12import java.util.Map;
13
14import org.junit.runner.Description;
15import org.junit.runner.Result;
16import org.junit.runner.notification.Failure;
17import org.junit.runner.notification.RunListener;
18
19/**
20 * Stores a subset of the history of each test:
21 * <ul>
22 * <li>Last failure timestamp
23 * <li>Duration of last execution
24 * </ul>
25 */
26public class MaxHistory implements Serializable {
27    private static final long serialVersionUID = 1L;
28
29    /**
30     * Loads a {@link MaxHistory} from {@code file}, or generates a new one that
31     * will be saved to {@code file}.
32     */
33    public static MaxHistory forFolder(File file) {
34        if (file.exists()) {
35            try {
36                return readHistory(file);
37            } catch (CouldNotReadCoreException e) {
38                e.printStackTrace();
39                file.delete();
40            }
41        }
42        return new MaxHistory(file);
43    }
44
45    private static MaxHistory readHistory(File storedResults)
46            throws CouldNotReadCoreException {
47        try {
48            FileInputStream file = new FileInputStream(storedResults);
49            try {
50                ObjectInputStream stream = new ObjectInputStream(file);
51                try {
52                    return (MaxHistory) stream.readObject();
53                } finally {
54                    stream.close();
55                }
56            } finally {
57                file.close();
58            }
59        } catch (Exception e) {
60            throw new CouldNotReadCoreException(e);
61        }
62    }
63
64    /*
65     * We have to use the f prefix until the next major release to ensure
66     * serialization compatibility.
67     * See https://github.com/junit-team/junit/issues/976
68     */
69    private final Map<String, Long> fDurations = new HashMap<String, Long>();
70    private final Map<String, Long> fFailureTimestamps = new HashMap<String, Long>();
71    private final File fHistoryStore;
72
73    private MaxHistory(File storedResults) {
74        fHistoryStore = storedResults;
75    }
76
77    private void save() throws IOException {
78        ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream(
79                fHistoryStore));
80        stream.writeObject(this);
81        stream.close();
82    }
83
84    Long getFailureTimestamp(Description key) {
85        return fFailureTimestamps.get(key.toString());
86    }
87
88    void putTestFailureTimestamp(Description key, long end) {
89        fFailureTimestamps.put(key.toString(), end);
90    }
91
92    boolean isNewTest(Description key) {
93        return !fDurations.containsKey(key.toString());
94    }
95
96    Long getTestDuration(Description key) {
97        return fDurations.get(key.toString());
98    }
99
100    void putTestDuration(Description description, long duration) {
101        fDurations.put(description.toString(), duration);
102    }
103
104    private final class RememberingListener extends RunListener {
105        private long overallStart = System.currentTimeMillis();
106
107        private Map<Description, Long> starts = new HashMap<Description, Long>();
108
109        @Override
110        public void testStarted(Description description) throws Exception {
111            starts.put(description, System.nanoTime()); // Get most accurate
112            // possible time
113        }
114
115        @Override
116        public void testFinished(Description description) throws Exception {
117            long end = System.nanoTime();
118            long start = starts.get(description);
119            putTestDuration(description, end - start);
120        }
121
122        @Override
123        public void testFailure(Failure failure) throws Exception {
124            putTestFailureTimestamp(failure.getDescription(), overallStart);
125        }
126
127        @Override
128        public void testRunFinished(Result result) throws Exception {
129            save();
130        }
131    }
132
133    private class TestComparator implements Comparator<Description> {
134        public int compare(Description o1, Description o2) {
135            // Always prefer new tests
136            if (isNewTest(o1)) {
137                return -1;
138            }
139            if (isNewTest(o2)) {
140                return 1;
141            }
142            // Then most recently failed first
143            int result = getFailure(o2).compareTo(getFailure(o1));
144            return result != 0 ? result
145                    // Then shorter tests first
146                    : getTestDuration(o1).compareTo(getTestDuration(o2));
147        }
148
149        private Long getFailure(Description key) {
150            Long result = getFailureTimestamp(key);
151            if (result == null) {
152                return 0L; // 0 = "never failed (that I know about)"
153            }
154            return result;
155        }
156    }
157
158    /**
159     * @return a listener that will update this history based on the test
160     *         results reported.
161     */
162    public RunListener listener() {
163        return new RememberingListener();
164    }
165
166    /**
167     * @return a comparator that ranks tests based on the JUnit Max sorting
168     *         rules, as described in the {@link MaxCore} class comment.
169     */
170    public Comparator<Description> testComparator() {
171        return new TestComparator();
172    }
173}
174