1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package vogar;
18 
19 import com.google.common.collect.Lists;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.Date;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import vogar.util.MarkResetConsole;
27 
28 /**
29  * Controls, formats and emits output to the command line. This class emits
30  * output in two modes:
31  * <ul>
32  *   <li><strong>Streaming</strong> output prints as it is received, but cannot
33  *       support multiple concurrent output streams.
34  *   <li><strong>Multiplexing</strong> buffers output until it is complete and
35  *       then prints it completely.
36  * </ul>
37  */
38 public abstract class Console implements Log {
39     static final long DAY_MILLIS = 1000 * 60 * 60 * 24;
40     static final long HOUR_MILLIS = 1000 * 60 * 60;
41     static final long WARNING_HOURS = 12;
42     static final long FAILURE_HOURS = 48;
43 
44     private boolean useColor;
45     private boolean ansi;
46     private boolean verbose;
47     protected String indent;
48     protected CurrentLine currentLine = CurrentLine.NEW;
49     protected final MarkResetConsole out = new MarkResetConsole(System.out);
50     protected MarkResetConsole.Mark currentVerboseMark;
51     protected MarkResetConsole.Mark currentStreamMark;
52 
Console()53     private Console() {}
54 
setIndent(String indent)55     public void setIndent(String indent) {
56         this.indent = indent;
57     }
58 
setUseColor( boolean useColor, int passColor, int skipColor, int failColor, int warnColor)59     public void setUseColor(
60       boolean useColor, int passColor, int skipColor, int failColor, int warnColor) {
61         this.useColor = useColor;
62         Color.PASS.setCode(passColor);
63         Color.SKIP.setCode(skipColor);
64         Color.FAIL.setCode(failColor);
65         Color.WARN.setCode(warnColor);
66         Color.COMMENT.setCode(34);
67     }
68 
setAnsi(boolean ansi)69     public void setAnsi(boolean ansi) {
70         this.ansi = ansi;
71     }
72 
setVerbose(boolean verbose)73     public void setVerbose(boolean verbose) {
74         this.verbose = verbose;
75     }
76 
isVerbose()77     public boolean isVerbose() {
78         return verbose;
79     }
80 
verbose(String s)81     public synchronized void verbose(String s) {
82         /*
83          * terminal does't support overwriting output, so don't print
84          * verbose message unless requested.
85          */
86         if (!verbose && !ansi) {
87             return;
88         }
89         /*
90          * When writing verbose output in the middle of streamed output, keep
91          * the streamed mark location. That way we can remove the verbose output
92          * later without losing our position mid-line in the streamed output.
93          */
94         MarkResetConsole.Mark savedStreamMark = currentLine == CurrentLine.STREAMED_OUTPUT
95                 ? out.mark()
96                 : currentStreamMark;
97         newLine();
98         currentStreamMark = savedStreamMark;
99 
100         currentVerboseMark = out.mark();
101         out.print(s);
102         currentLine = CurrentLine.VERBOSE;
103     }
104 
warn(String message)105     public synchronized void warn(String message) {
106         warn(message, Collections.<String>emptyList());
107     }
108 
109     /**
110      * Warns, and also puts a list of strings afterwards.
111      */
warn(String message, List<String> list)112     public synchronized void warn(String message, List<String> list) {
113         newLine();
114         out.println(colorString("Warning: " + message, Color.WARN));
115         for (String item : list) {
116             out.println(colorString(indent + item, Color.WARN));
117         }
118     }
119 
info(String s)120     public synchronized void info(String s) {
121         newLine();
122         out.println(s);
123     }
124 
info(String message, Throwable throwable)125     public synchronized void info(String message, Throwable throwable) {
126         newLine();
127         out.println(message);
128         throwable.printStackTrace(System.out);
129     }
130 
131     /**
132      * Begins streaming output for the named action.
133      */
action(String name)134     public void action(String name) {}
135 
136     /**
137      * Begins streaming output for the named outcome.
138      */
outcome(String name)139     public void outcome(String name) {}
140 
141     /**
142      * Appends the action output immediately to the stream when streaming is on,
143      * or to a buffer when streaming is off. Buffered output will be held and
144      * printed only if the outcome is unsuccessful.
145      */
streamOutput(String outcomeName, String output)146     public abstract void streamOutput(String outcomeName, String output);
147 
148     /**
149      * Hook to flush anything streamed via {@link #streamOutput}.
150      */
flushBufferedOutput(String outcomeName)151     protected void flushBufferedOutput(String outcomeName) {}
152 
153     /**
154      * Writes the action's outcome.
155      */
printResult( String outcomeName, Result result, ResultValue resultValue, Expectation expectation)156     public synchronized void printResult(
157             String outcomeName, Result result, ResultValue resultValue, Expectation expectation) {
158         // when the result is interesting, include the description and bug number
159         if (result != Result.SUCCESS || resultValue != ResultValue.OK) {
160             if (!expectation.getDescription().isEmpty()) {
161                 streamOutput(outcomeName, "\n" + colorString(expectation.getDescription(), Color.COMMENT));
162             }
163             if (expectation.getBug() != -1) {
164                 streamOutput(outcomeName, "\n" + colorString("http://b/" + expectation.getBug(), Color.COMMENT));
165             }
166         }
167 
168         flushBufferedOutput(outcomeName);
169 
170         if (currentLine == CurrentLine.NAME) {
171             out.print(" ");
172         } else {
173             newLine(); // TODO: backup the cursor up to the name if there's no streaming output
174             out.print(indent + outcomeName + " ");
175         }
176 
177         if (resultValue == ResultValue.OK) {
178             out.println(colorString("OK (" + result + ")", Color.PASS));
179         } else if (resultValue == ResultValue.FAIL) {
180             out.println(colorString("FAIL (" + result + ")", Color.FAIL));
181         } else if (resultValue == ResultValue.IGNORE) {
182             out.println(colorString("SKIP (" + result + ")", Color.WARN));
183         }
184 
185         currentLine = CurrentLine.NEW;
186     }
187 
summarizeOutcomes(Collection<AnnotatedOutcome> annotatedOutcomes)188     public synchronized void summarizeOutcomes(Collection<AnnotatedOutcome> annotatedOutcomes) {
189         List<AnnotatedOutcome> annotatedOutcomesSorted =
190                 AnnotatedOutcome.ORDER_BY_NAME.sortedCopy(annotatedOutcomes);
191 
192         List<String> failures = Lists.newArrayList();
193         List<String> skips = Lists.newArrayList();
194         List<String> successes = Lists.newArrayList();
195         List<String> warnings = Lists.newArrayList();
196 
197         // figure out whether each outcome is noteworthy, and add a message to the appropriate list
198         for (AnnotatedOutcome annotatedOutcome : annotatedOutcomesSorted) {
199             if (!annotatedOutcome.isNoteworthy()) {
200                 continue;
201             }
202 
203             Color color;
204             List<String> list;
205             ResultValue resultValue = annotatedOutcome.getResultValue();
206             if (resultValue == ResultValue.OK) {
207                 color = Color.PASS;
208                 list = successes;
209             } else if (resultValue == ResultValue.FAIL) {
210                 color = Color.FAIL;
211                 list = failures;
212             } else if (resultValue == ResultValue.WARNING) {
213                 color = Color.WARN;
214                 list = warnings;
215             } else {
216                 color = Color.SKIP;
217                 list = skips;
218             }
219 
220             Long lastRun = annotatedOutcome.lastRun(null);
221             String timestamp;
222             if (lastRun == null) {
223                 timestamp = colorString("unknown", Color.WARN);
224             } else {
225                 timestamp = formatElapsedTime(new Date().getTime() - lastRun);
226             }
227 
228             String brokeThisMessage = "";
229             ResultValue mostRecentResultValue = annotatedOutcome.getMostRecentResultValue(null);
230             if (mostRecentResultValue != null && resultValue != mostRecentResultValue) {
231                 if (resultValue == ResultValue.OK) {
232                     brokeThisMessage = colorString(" (you might have fixed this)", Color.WARN);
233                 } else {
234                     brokeThisMessage = colorString(" (you might have broken this)", Color.WARN);
235                 }
236             } else if (mostRecentResultValue == null) {
237                 brokeThisMessage = colorString(" (no test history available)", Color.WARN);
238             }
239 
240             List<ResultValue> previousResultValues = annotatedOutcome.getPreviousResultValues();
241             int numPreviousResultValues = previousResultValues.size();
242             int numResultValuesToShow = Math.min(10, numPreviousResultValues);
243             List<ResultValue> previousResultValuesToShow = previousResultValues.subList(
244                     numPreviousResultValues - numResultValuesToShow, numPreviousResultValues);
245 
246             StringBuilder sb = new StringBuilder();
247             sb.append(indent);
248             sb.append(colorString(annotatedOutcome.getOutcome().getName(), color));
249             if (!previousResultValuesToShow.isEmpty()) {
250                 sb.append(String.format(" [last %d: %s] [last run: %s]",
251                         previousResultValuesToShow.size(),
252                         generateSparkLine(previousResultValuesToShow),
253                         timestamp));
254             }
255             sb.append(brokeThisMessage);
256             list.add(sb.toString());
257         }
258 
259         newLine();
260         if (!successes.isEmpty()) {
261             out.println("Success summary:");
262             for (String success : successes) {
263                 out.println(success);
264             }
265         }
266         if (!failures.isEmpty()) {
267             out.println("Failure summary:");
268             for (String failure : failures) {
269                 out.println(failure);
270             }
271         }
272         if (!skips.isEmpty()) {
273             out.println("Skips summary:");
274             for (String skip : skips) {
275                 out.println(skip);
276             }
277         }
278         if (!warnings.isEmpty()) {
279             out.println("Warnings summary:");
280             for (String warning : warnings) {
281                 out.println(warning);
282             }
283         }
284     }
285 
formatElapsedTime(long elapsedTime)286     private String formatElapsedTime(long elapsedTime) {
287         if (elapsedTime < 0) {
288             throw new IllegalArgumentException("non-negative elapsed times only");
289         }
290 
291         String formatted;
292         if (elapsedTime >= DAY_MILLIS) {
293             long days = elapsedTime / DAY_MILLIS;
294             formatted = String.format("%d days ago", days);
295         } else if (elapsedTime >= HOUR_MILLIS) {
296             long hours = elapsedTime / HOUR_MILLIS;
297             formatted = String.format("%d hours ago", hours);
298         } else {
299             formatted = "less than an hour ago";
300         }
301 
302         Color color = elapsedTimeWarningColor(elapsedTime);
303         return colorString(formatted, color);
304     }
305 
elapsedTimeWarningColor(long elapsedTime)306     private Color elapsedTimeWarningColor(long elapsedTime) {
307         if (elapsedTime < WARNING_HOURS * HOUR_MILLIS) {
308             return Color.PASS;
309         } else if (elapsedTime < FAILURE_HOURS * HOUR_MILLIS) {
310             return Color.WARN;
311         } else {
312             return Color.FAIL;
313         }
314     }
315 
generateSparkLine(List<ResultValue> resultValues)316     private String generateSparkLine(List<ResultValue> resultValues) {
317         StringBuilder sb = new StringBuilder();
318         for (ResultValue resultValue : resultValues) {
319             if (resultValue == ResultValue.OK) {
320                 sb.append(colorString("\u2713", Color.PASS));
321             } else if (resultValue == ResultValue.FAIL) {
322                 sb.append(colorString("X", Color.FAIL));
323             } else {
324                 sb.append(colorString("-", Color.WARN));
325             }
326         }
327         return sb.toString();
328     }
329 
330     /**
331      * Prints the action output with appropriate indentation.
332      */
streamOutput(CharSequence streamedOutput)333     public synchronized void streamOutput(CharSequence streamedOutput) {
334         if (streamedOutput.length() == 0) {
335             return;
336         }
337 
338         String[] lines = messageToLines(streamedOutput.toString());
339 
340         if (currentLine == CurrentLine.VERBOSE && currentStreamMark != null && ansi) {
341             currentStreamMark.reset();
342             currentStreamMark = null;
343         } else if (currentLine != CurrentLine.STREAMED_OUTPUT) {
344             newLine();
345             out.print(indent);
346             out.print(indent);
347         }
348         out.print(lines[0]);
349         currentLine = CurrentLine.STREAMED_OUTPUT;
350 
351         for (int i = 1; i < lines.length; i++) {
352             newLine();
353 
354             if (lines[i].length() > 0) {
355                 out.print(indent);
356                 out.print(indent);
357                 out.print(lines[i]);
358                 currentLine = CurrentLine.STREAMED_OUTPUT;
359             }
360         }
361     }
362 
363     /**
364      * Inserts a linebreak if necessary.
365      */
newLine()366     protected void newLine() {
367         currentStreamMark = null;
368 
369         if (currentLine == CurrentLine.VERBOSE && !verbose && ansi) {
370             /*
371              * Verbose means we leave all verbose output on the screen.
372              * Otherwise we overwrite verbose output when new output arrives.
373              */
374             currentVerboseMark.reset();
375         } else if (currentLine != CurrentLine.NEW) {
376             out.print("\n");
377         }
378 
379         currentLine = CurrentLine.NEW;
380     }
381 
382     /**
383      * Status of a currently-in-progress line of output.
384      */
385     enum CurrentLine {
386 
387         /**
388          * The line is blank.
389          */
390         NEW,
391 
392         /**
393          * The line contains streamed application output. Additional streamed
394          * output may be appended without additional line separators or
395          * indentation.
396          */
397         STREAMED_OUTPUT,
398 
399         /**
400          * The line contains the name of an action or outcome. The outcome's
401          * result (such as "OK") can be appended without additional line
402          * separators or indentation.
403          */
404         NAME,
405 
406         /**
407          * The line contains verbose output, and may be overwritten.
408          */
409         VERBOSE,
410     }
411 
412     /**
413      * Returns an array containing the lines of the given text.
414      */
messageToLines(String message)415     private String[] messageToLines(String message) {
416         // pass Integer.MAX_VALUE so split doesn't trim trailing empty strings.
417         return message.split("\r\n|\r|\n", Integer.MAX_VALUE);
418     }
419 
420     private enum Color {
421         PASS, FAIL, SKIP, WARN, COMMENT;
422 
423         int code = 0;
424 
getCode()425         public int getCode() {
426             return code;
427         }
428 
setCode(int code)429         public void setCode(int code) {
430             this.code = code;
431         }
432     }
433 
colorString(String message, Color color)434     protected String colorString(String message, Color color) {
435         return useColor ? ("\u001b[" + color.getCode() + ";1m" + message + "\u001b[0m") : message;
436     }
437 
438     /**
439      * This console prints output as it's emitted. It supports at most one
440      * action at a time.
441      */
442     static class StreamingConsole extends Console {
443         private String currentName;
444 
action(String name)445         @Override public synchronized void action(String name) {
446             newLine();
447             out.print("Action " + name);
448             currentName = name;
449             currentLine = CurrentLine.NAME;
450         }
451 
452         /**
453          * Prints the beginning of the named outcome.
454          */
outcome(String name)455         @Override public synchronized void outcome(String name) {
456             // if the outcome and action names are the same, omit the outcome name
457             if (name.equals(currentName)) {
458                 return;
459             }
460 
461             currentName = name;
462             newLine();
463             out.print(indent + name);
464             currentLine = CurrentLine.NAME;
465         }
466 
streamOutput(String outcomeName, String output)467         @Override public synchronized void streamOutput(String outcomeName, String output) {
468             streamOutput(output);
469         }
470     }
471 
472     /**
473      * This console buffers output, only printing when a result is found. It
474      * supports multiple concurrent actions.
475      */
476     static class MultiplexingConsole extends Console {
477         private final Map<String, StringBuilder> bufferedOutputByOutcome = new HashMap<String, StringBuilder>();
478 
streamOutput(String outcomeName, String output)479         @Override public synchronized void streamOutput(String outcomeName, String output) {
480             StringBuilder buffer = bufferedOutputByOutcome.get(outcomeName);
481             if (buffer == null) {
482                 buffer = new StringBuilder();
483                 bufferedOutputByOutcome.put(outcomeName, buffer);
484             }
485 
486             buffer.append(output);
487         }
488 
flushBufferedOutput(String outcomeName)489         @Override protected synchronized void flushBufferedOutput(String outcomeName) {
490             newLine();
491             out.print(indent + outcomeName);
492             currentLine = CurrentLine.NAME;
493 
494             StringBuilder buffer = bufferedOutputByOutcome.remove(outcomeName);
495             if (buffer != null) {
496                 streamOutput(buffer);
497             }
498         }
499     }
500 }
501