View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.api.runorder;
20  
21  import java.io.BufferedWriter;
22  import java.io.File;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.OutputStream;
28  import java.io.OutputStreamWriter;
29  import java.util.ArrayList;
30  import java.util.Comparator;
31  import java.util.HashMap;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Map.Entry;
36  import java.util.Scanner;
37  import java.util.concurrent.ConcurrentHashMap;
38  
39  import org.apache.maven.surefire.api.report.ReportEntry;
40  import org.apache.maven.surefire.api.util.internal.ClassMethod;
41  
42  import static java.lang.Integer.parseInt;
43  import static java.nio.charset.StandardCharsets.UTF_8;
44  import static java.util.Collections.sort;
45  import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
46  
47  /**
48   * @author Kristian Rosenvold
49   */
50  public final class RunEntryStatisticsMap {
51      private final Map<ClassMethod, RunEntryStatistics> runEntryStatistics;
52  
53      private RunEntryStatisticsMap(Map<ClassMethod, RunEntryStatistics> runEntryStatistics) {
54          this.runEntryStatistics = new ConcurrentHashMap<>(runEntryStatistics);
55      }
56  
57      public RunEntryStatisticsMap() {
58          runEntryStatistics = new ConcurrentHashMap<>();
59      }
60  
61      public static RunEntryStatisticsMap fromFile(File file) {
62          if (file.exists()) {
63              try {
64                  return fromStream(new FileInputStream(file));
65              } catch (IOException e) {
66                  throw new RuntimeException(e);
67              }
68          } else {
69              return new RunEntryStatisticsMap();
70          }
71      }
72  
73      public static RunEntryStatisticsMap fromStream(InputStream fileReader) {
74          Map<ClassMethod, RunEntryStatistics> result = new HashMap<>();
75          try (Scanner scanner = new Scanner(fileReader, "UTF-8")) {
76              RunEntryStatistics previous = null;
77              while (scanner.hasNextLine()) {
78                  String line = scanner.nextLine();
79  
80                  if (line.charAt(0) == ' ') {
81                      previous = new RunEntryStatistics(
82                              previous.getRunTime(),
83                              previous.getSuccessfulBuilds(),
84                              previous.getClassMethod().getClazz(),
85                              previous.getClassMethod().getMethod() + NL + line.substring(1));
86                  } else {
87                      if (previous != null) {
88                          result.put(previous.getClassMethod(), previous);
89                      }
90  
91                      int to = line.indexOf(',');
92  
93                      String successfulBuildsString = line.substring(0, to);
94                      int successfulBuilds = parseInt(successfulBuildsString);
95  
96                      int from = 1 + to;
97                      to = line.indexOf(',', from + 1);
98  
99                      String runTimeString = line.substring(from, to);
100                     int runTime = parseInt(runTimeString);
101 
102                     from = 1 + to;
103                     to = line.indexOf(',', from + 1);
104 
105                     String className = to == -1 ? line.substring(from) : line.substring(from, to);
106 
107                     from = 1 + to;
108 
109                     String methodName = to == -1 ? null : line.substring(from);
110 
111                     ClassMethod classMethod = new ClassMethod(className, methodName);
112                     previous = new RunEntryStatistics(runTime, successfulBuilds, classMethod);
113                 }
114             }
115             if (previous != null) {
116                 result.put(previous.getClassMethod(), previous);
117             }
118         }
119         return new RunEntryStatisticsMap(result);
120     }
121 
122     public void serialize(File statsFile) throws IOException {
123         if (statsFile.isFile()) {
124             //noinspection ResultOfMethodCallIgnored
125             statsFile.delete();
126         }
127         OutputStream os = new FileOutputStream(statsFile);
128         try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, UTF_8), 64 * 1024)) {
129             List<RunEntryStatistics> items = new ArrayList<>(runEntryStatistics.values());
130             sort(items, new RunCountComparator());
131             for (Iterator<RunEntryStatistics> it = items.iterator(); it.hasNext(); ) {
132                 RunEntryStatistics item = it.next();
133                 ClassMethod test = item.getClassMethod();
134                 String line = item.getSuccessfulBuilds() + "," + item.getRunTime() + "," + test.getClazz() + ",";
135                 writer.write(line);
136                 boolean wasFirstLine = false;
137                 String method = test.getMethod();
138                 if (method == null) {
139                     continue;
140                 }
141 
142                 for (Scanner scanner = new Scanner(method); scanner.hasNextLine(); wasFirstLine = true) {
143                     String methodLine = scanner.nextLine();
144                     if (wasFirstLine) {
145                         writer.write(' ');
146                     }
147                     writer.write(methodLine);
148                     if (scanner.hasNextLine()) {
149                         writer.newLine();
150                     }
151                 }
152 
153                 if (it.hasNext()) {
154                     writer.newLine();
155                 }
156             }
157         }
158     }
159 
160     private RunEntryStatistics findOrCreate(ReportEntry reportEntry) {
161         ClassMethod classMethod = new ClassMethod(reportEntry.getSourceName(), reportEntry.getName());
162         RunEntryStatistics item = runEntryStatistics.get(classMethod);
163         return item != null ? item : new RunEntryStatistics(reportEntry.getElapsed(0), 0, classMethod);
164     }
165 
166     public RunEntryStatistics createNextGeneration(ReportEntry reportEntry) {
167         RunEntryStatistics newItem = findOrCreate(reportEntry);
168         return newItem.nextGeneration(reportEntry.getElapsed(0));
169     }
170 
171     public RunEntryStatistics createNextGenerationFailure(ReportEntry reportEntry) {
172         RunEntryStatistics newItem = findOrCreate(reportEntry);
173         return newItem.nextGenerationFailure(reportEntry.getElapsed(0));
174     }
175 
176     public void add(RunEntryStatistics item) {
177         runEntryStatistics.put(item.getClassMethod(), item);
178     }
179 
180     static final class RunCountComparator implements Comparator<RunEntryStatistics> {
181         @Override
182         public int compare(RunEntryStatistics o, RunEntryStatistics o1) {
183             int runtime = o.getSuccessfulBuilds() - o1.getSuccessfulBuilds();
184             return runtime == 0 ? o.getRunTime() - o1.getRunTime() : runtime;
185         }
186     }
187 
188     public List<Class<?>> getPrioritizedTestsClassRunTime(List<Class<?>> testsToRun, int threadCount) {
189         List<PrioritizedTest> prioritizedTests = getPrioritizedTests(testsToRun, new TestRuntimeComparator());
190         ThreadedExecutionScheduler threadedExecutionScheduler = new ThreadedExecutionScheduler(threadCount);
191         for (Object prioritizedTest1 : prioritizedTests) {
192             threadedExecutionScheduler.addTest((PrioritizedTest) prioritizedTest1);
193         }
194 
195         return threadedExecutionScheduler.getResult();
196     }
197 
198     public List<Class<?>> getPrioritizedTestsByFailureFirst(List<Class<?>> testsToRun) {
199         List<PrioritizedTest> prioritizedTests = getPrioritizedTests(testsToRun, new LeastFailureComparator());
200         return transformToClasses(prioritizedTests);
201     }
202 
203     private List<PrioritizedTest> getPrioritizedTests(
204             List<Class<?>> testsToRun, Comparator<Priority> priorityComparator) {
205         Map<String, Priority> classPriorities = getPriorities(priorityComparator);
206 
207         List<PrioritizedTest> tests = new ArrayList<>();
208         for (Class<?> clazz : testsToRun) {
209             Priority pri = classPriorities.get(clazz.getName());
210             if (pri == null) {
211                 pri = Priority.newTestClassPriority(clazz.getName());
212             }
213             PrioritizedTest prioritizedTest = new PrioritizedTest(clazz, pri);
214             tests.add(prioritizedTest);
215         }
216         sort(tests, new PrioritizedTestComparator());
217         return tests;
218     }
219 
220     private static List<Class<?>> transformToClasses(List<PrioritizedTest> tests) {
221         List<Class<?>> result = new ArrayList<>();
222         for (PrioritizedTest test : tests) {
223             result.add(test.getClazz());
224         }
225         return result;
226     }
227 
228     private Map<String, Priority> getPriorities(Comparator<Priority> priorityComparator) {
229         Map<String, Priority> priorities = new HashMap<>();
230         for (Entry<ClassMethod, RunEntryStatistics> testNames : runEntryStatistics.entrySet()) {
231             String clazzName = testNames.getKey().getClazz();
232             Priority priority = priorities.get(clazzName);
233             if (priority == null) {
234                 priority = new Priority(clazzName);
235                 priorities.put(clazzName, priority);
236             }
237             priority.addItem(testNames.getValue());
238         }
239 
240         List<Priority> items = new ArrayList<>(priorities.values());
241         sort(items, priorityComparator);
242         Map<String, Priority> result = new HashMap<>();
243         int i = 0;
244         for (Priority pri : items) {
245             pri.setPriority(i++);
246             result.put(pri.getClassName(), pri);
247         }
248         return result;
249     }
250 
251     static final class PrioritizedTestComparator implements Comparator<PrioritizedTest> {
252         @Override
253         public int compare(PrioritizedTest o, PrioritizedTest o1) {
254             return o.getPriority() - o1.getPriority();
255         }
256     }
257 
258     static final class TestRuntimeComparator implements Comparator<Priority> {
259         @Override
260         public int compare(Priority o, Priority o1) {
261             return o1.getTotalRuntime() - o.getTotalRuntime();
262         }
263     }
264 
265     static final class LeastFailureComparator implements Comparator<Priority> {
266         @Override
267         public int compare(Priority o, Priority o1) {
268             return o.getMinSuccessRate() - o1.getMinSuccessRate();
269         }
270     }
271 }