| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package vogar; |
| |
| import com.google.common.collect.Lists; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import vogar.util.MarkResetConsole; |
| |
| /** |
| * Controls, formats and emits output to the command line. This class emits |
| * output in two modes: |
| * <ul> |
| * <li><strong>Streaming</strong> output prints as it is received, but cannot |
| * support multiple concurrent output streams. |
| * <li><strong>Multiplexing</strong> buffers output until it is complete and |
| * then prints it completely. |
| * </ul> |
| */ |
| public abstract class Console implements Log { |
| static final long DAY_MILLIS = 1000 * 60 * 60 * 24; |
| static final long HOUR_MILLIS = 1000 * 60 * 60; |
| static final long WARNING_HOURS = 12; |
| static final long FAILURE_HOURS = 48; |
| |
| private boolean useColor; |
| private boolean ansi; |
| private boolean verbose; |
| protected String indent; |
| protected CurrentLine currentLine = CurrentLine.NEW; |
| protected final MarkResetConsole out = new MarkResetConsole(System.out); |
| protected MarkResetConsole.Mark currentVerboseMark; |
| protected MarkResetConsole.Mark currentStreamMark; |
| |
| private Console() {} |
| |
| public void setIndent(String indent) { |
| this.indent = indent; |
| } |
| |
| public void setUseColor( |
| boolean useColor, int passColor, int skipColor, int failColor, int warnColor) { |
| this.useColor = useColor; |
| Color.PASS.setCode(passColor); |
| Color.SKIP.setCode(skipColor); |
| Color.FAIL.setCode(failColor); |
| Color.WARN.setCode(warnColor); |
| Color.COMMENT.setCode(34); |
| } |
| |
| public void setAnsi(boolean ansi) { |
| this.ansi = ansi; |
| } |
| |
| public void setVerbose(boolean verbose) { |
| this.verbose = verbose; |
| } |
| |
| public boolean isVerbose() { |
| return verbose; |
| } |
| |
| public synchronized void verbose(String s) { |
| /* |
| * terminal does't support overwriting output, so don't print |
| * verbose message unless requested. |
| */ |
| if (!verbose && !ansi) { |
| return; |
| } |
| /* |
| * When writing verbose output in the middle of streamed output, keep |
| * the streamed mark location. That way we can remove the verbose output |
| * later without losing our position mid-line in the streamed output. |
| */ |
| MarkResetConsole.Mark savedStreamMark = currentLine == CurrentLine.STREAMED_OUTPUT |
| ? out.mark() |
| : currentStreamMark; |
| newLine(); |
| currentStreamMark = savedStreamMark; |
| |
| currentVerboseMark = out.mark(); |
| out.print(s); |
| currentLine = CurrentLine.VERBOSE; |
| } |
| |
| public synchronized void warn(String message) { |
| warn(message, Collections.<String>emptyList()); |
| } |
| |
| /** |
| * Warns, and also puts a list of strings afterwards. |
| */ |
| public synchronized void warn(String message, List<String> list) { |
| newLine(); |
| out.println(colorString("Warning: " + message, Color.WARN)); |
| for (String item : list) { |
| out.println(colorString(indent + item, Color.WARN)); |
| } |
| } |
| |
| public synchronized void info(String s) { |
| newLine(); |
| out.println(s); |
| } |
| |
| public synchronized void info(String message, Throwable throwable) { |
| newLine(); |
| out.println(message); |
| throwable.printStackTrace(System.out); |
| } |
| |
| /** |
| * Begins streaming output for the named action. |
| */ |
| public void action(String name) {} |
| |
| /** |
| * Begins streaming output for the named outcome. |
| */ |
| public void outcome(String name) {} |
| |
| /** |
| * Appends the action output immediately to the stream when streaming is on, |
| * or to a buffer when streaming is off. Buffered output will be held and |
| * printed only if the outcome is unsuccessful. |
| */ |
| public abstract void streamOutput(String outcomeName, String output); |
| |
| /** |
| * Hook to flush anything streamed via {@link #streamOutput}. |
| */ |
| protected void flushBufferedOutput(String outcomeName) {} |
| |
| /** |
| * Writes the action's outcome. |
| */ |
| public synchronized void printResult( |
| String outcomeName, Result result, ResultValue resultValue, Expectation expectation) { |
| // when the result is interesting, include the description and bug number |
| if (result != Result.SUCCESS || resultValue != ResultValue.OK) { |
| if (!expectation.getDescription().isEmpty()) { |
| streamOutput(outcomeName, "\n" + colorString(expectation.getDescription(), Color.COMMENT)); |
| } |
| if (expectation.getBug() != -1) { |
| streamOutput(outcomeName, "\n" + colorString("http://b/" + expectation.getBug(), Color.COMMENT)); |
| } |
| } |
| |
| flushBufferedOutput(outcomeName); |
| |
| if (currentLine == CurrentLine.NAME) { |
| out.print(" "); |
| } else { |
| newLine(); // TODO: backup the cursor up to the name if there's no streaming output |
| out.print(indent + outcomeName + " "); |
| } |
| |
| if (resultValue == ResultValue.OK) { |
| out.println(colorString("OK (" + result + ")", Color.PASS)); |
| } else if (resultValue == ResultValue.FAIL) { |
| out.println(colorString("FAIL (" + result + ")", Color.FAIL)); |
| } else if (resultValue == ResultValue.IGNORE) { |
| out.println(colorString("SKIP (" + result + ")", Color.WARN)); |
| } |
| |
| currentLine = CurrentLine.NEW; |
| } |
| |
| public synchronized void summarizeOutcomes(Collection<AnnotatedOutcome> annotatedOutcomes) { |
| List<AnnotatedOutcome> annotatedOutcomesSorted = |
| AnnotatedOutcome.ORDER_BY_NAME.sortedCopy(annotatedOutcomes); |
| |
| List<String> failures = Lists.newArrayList(); |
| List<String> skips = Lists.newArrayList(); |
| List<String> successes = Lists.newArrayList(); |
| List<String> warnings = Lists.newArrayList(); |
| |
| // figure out whether each outcome is noteworthy, and add a message to the appropriate list |
| for (AnnotatedOutcome annotatedOutcome : annotatedOutcomesSorted) { |
| if (!annotatedOutcome.isNoteworthy()) { |
| continue; |
| } |
| |
| Color color; |
| List<String> list; |
| ResultValue resultValue = annotatedOutcome.getResultValue(); |
| if (resultValue == ResultValue.OK) { |
| color = Color.PASS; |
| list = successes; |
| } else if (resultValue == ResultValue.FAIL) { |
| color = Color.FAIL; |
| list = failures; |
| } else if (resultValue == ResultValue.WARNING) { |
| color = Color.WARN; |
| list = warnings; |
| } else { |
| color = Color.SKIP; |
| list = skips; |
| } |
| |
| Long lastRun = annotatedOutcome.lastRun(null); |
| String timestamp; |
| if (lastRun == null) { |
| timestamp = colorString("unknown", Color.WARN); |
| } else { |
| timestamp = formatElapsedTime(new Date().getTime() - lastRun); |
| } |
| |
| String brokeThisMessage = ""; |
| ResultValue mostRecentResultValue = annotatedOutcome.getMostRecentResultValue(null); |
| if (mostRecentResultValue != null && resultValue != mostRecentResultValue) { |
| if (resultValue == ResultValue.OK) { |
| brokeThisMessage = colorString(" (you might have fixed this)", Color.WARN); |
| } else { |
| brokeThisMessage = colorString(" (you might have broken this)", Color.WARN); |
| } |
| } else if (mostRecentResultValue == null) { |
| brokeThisMessage = colorString(" (no test history available)", Color.WARN); |
| } |
| |
| List<ResultValue> previousResultValues = annotatedOutcome.getPreviousResultValues(); |
| int numPreviousResultValues = previousResultValues.size(); |
| int numResultValuesToShow = Math.min(10, numPreviousResultValues); |
| List<ResultValue> previousResultValuesToShow = previousResultValues.subList( |
| numPreviousResultValues - numResultValuesToShow, numPreviousResultValues); |
| |
| StringBuilder sb = new StringBuilder(); |
| sb.append(indent); |
| sb.append(colorString(annotatedOutcome.getOutcome().getName(), color)); |
| if (!previousResultValuesToShow.isEmpty()) { |
| sb.append(String.format(" [last %d: %s] [last run: %s]", |
| previousResultValuesToShow.size(), |
| generateSparkLine(previousResultValuesToShow), |
| timestamp)); |
| } |
| sb.append(brokeThisMessage); |
| list.add(sb.toString()); |
| } |
| |
| newLine(); |
| if (!successes.isEmpty()) { |
| out.println("Success summary:"); |
| for (String success : successes) { |
| out.println(success); |
| } |
| } |
| if (!failures.isEmpty()) { |
| out.println("Failure summary:"); |
| for (String failure : failures) { |
| out.println(failure); |
| } |
| } |
| if (!skips.isEmpty()) { |
| out.println("Skips summary:"); |
| for (String skip : skips) { |
| out.println(skip); |
| } |
| } |
| if (!warnings.isEmpty()) { |
| out.println("Warnings summary:"); |
| for (String warning : warnings) { |
| out.println(warning); |
| } |
| } |
| } |
| |
| private String formatElapsedTime(long elapsedTime) { |
| if (elapsedTime < 0) { |
| throw new IllegalArgumentException("non-negative elapsed times only"); |
| } |
| |
| String formatted; |
| if (elapsedTime >= DAY_MILLIS) { |
| long days = elapsedTime / DAY_MILLIS; |
| formatted = String.format("%d days ago", days); |
| } else if (elapsedTime >= HOUR_MILLIS) { |
| long hours = elapsedTime / HOUR_MILLIS; |
| formatted = String.format("%d hours ago", hours); |
| } else { |
| formatted = "less than an hour ago"; |
| } |
| |
| Color color = elapsedTimeWarningColor(elapsedTime); |
| return colorString(formatted, color); |
| } |
| |
| private Color elapsedTimeWarningColor(long elapsedTime) { |
| if (elapsedTime < WARNING_HOURS * HOUR_MILLIS) { |
| return Color.PASS; |
| } else if (elapsedTime < FAILURE_HOURS * HOUR_MILLIS) { |
| return Color.WARN; |
| } else { |
| return Color.FAIL; |
| } |
| } |
| |
| private String generateSparkLine(List<ResultValue> resultValues) { |
| StringBuilder sb = new StringBuilder(); |
| for (ResultValue resultValue : resultValues) { |
| if (resultValue == ResultValue.OK) { |
| sb.append(colorString("\u2713", Color.PASS)); |
| } else if (resultValue == ResultValue.FAIL) { |
| sb.append(colorString("X", Color.FAIL)); |
| } else { |
| sb.append(colorString("-", Color.WARN)); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| /** |
| * Prints the action output with appropriate indentation. |
| */ |
| public synchronized void streamOutput(CharSequence streamedOutput) { |
| if (streamedOutput.length() == 0) { |
| return; |
| } |
| |
| String[] lines = messageToLines(streamedOutput.toString()); |
| |
| if (currentLine == CurrentLine.VERBOSE && currentStreamMark != null && ansi) { |
| currentStreamMark.reset(); |
| currentStreamMark = null; |
| } else if (currentLine != CurrentLine.STREAMED_OUTPUT) { |
| newLine(); |
| out.print(indent); |
| out.print(indent); |
| } |
| out.print(lines[0]); |
| currentLine = CurrentLine.STREAMED_OUTPUT; |
| |
| for (int i = 1; i < lines.length; i++) { |
| newLine(); |
| |
| if (lines[i].length() > 0) { |
| out.print(indent); |
| out.print(indent); |
| out.print(lines[i]); |
| currentLine = CurrentLine.STREAMED_OUTPUT; |
| } |
| } |
| } |
| |
| /** |
| * Inserts a linebreak if necessary. |
| */ |
| protected void newLine() { |
| currentStreamMark = null; |
| |
| if (currentLine == CurrentLine.VERBOSE && !verbose && ansi) { |
| /* |
| * Verbose means we leave all verbose output on the screen. |
| * Otherwise we overwrite verbose output when new output arrives. |
| */ |
| currentVerboseMark.reset(); |
| } else if (currentLine != CurrentLine.NEW) { |
| out.print("\n"); |
| } |
| |
| currentLine = CurrentLine.NEW; |
| } |
| |
| /** |
| * Status of a currently-in-progress line of output. |
| */ |
| enum CurrentLine { |
| |
| /** |
| * The line is blank. |
| */ |
| NEW, |
| |
| /** |
| * The line contains streamed application output. Additional streamed |
| * output may be appended without additional line separators or |
| * indentation. |
| */ |
| STREAMED_OUTPUT, |
| |
| /** |
| * The line contains the name of an action or outcome. The outcome's |
| * result (such as "OK") can be appended without additional line |
| * separators or indentation. |
| */ |
| NAME, |
| |
| /** |
| * The line contains verbose output, and may be overwritten. |
| */ |
| VERBOSE, |
| } |
| |
| /** |
| * Returns an array containing the lines of the given text. |
| */ |
| private String[] messageToLines(String message) { |
| // pass Integer.MAX_VALUE so split doesn't trim trailing empty strings. |
| return message.split("\r\n|\r|\n", Integer.MAX_VALUE); |
| } |
| |
| private enum Color { |
| PASS, FAIL, SKIP, WARN, COMMENT; |
| |
| int code = 0; |
| |
| public int getCode() { |
| return code; |
| } |
| |
| public void setCode(int code) { |
| this.code = code; |
| } |
| } |
| |
| protected String colorString(String message, Color color) { |
| return useColor ? ("\u001b[" + color.getCode() + ";1m" + message + "\u001b[0m") : message; |
| } |
| |
| /** |
| * This console prints output as it's emitted. It supports at most one |
| * action at a time. |
| */ |
| static class StreamingConsole extends Console { |
| private String currentName; |
| |
| @Override public synchronized void action(String name) { |
| newLine(); |
| out.print("Action " + name); |
| currentName = name; |
| currentLine = CurrentLine.NAME; |
| } |
| |
| /** |
| * Prints the beginning of the named outcome. |
| */ |
| @Override public synchronized void outcome(String name) { |
| // if the outcome and action names are the same, omit the outcome name |
| if (name.equals(currentName)) { |
| return; |
| } |
| |
| currentName = name; |
| newLine(); |
| out.print(indent + name); |
| currentLine = CurrentLine.NAME; |
| } |
| |
| @Override public synchronized void streamOutput(String outcomeName, String output) { |
| streamOutput(output); |
| } |
| } |
| |
| /** |
| * This console buffers output, only printing when a result is found. It |
| * supports multiple concurrent actions. |
| */ |
| static class MultiplexingConsole extends Console { |
| private final Map<String, StringBuilder> bufferedOutputByOutcome = new HashMap<String, StringBuilder>(); |
| |
| @Override public synchronized void streamOutput(String outcomeName, String output) { |
| StringBuilder buffer = bufferedOutputByOutcome.get(outcomeName); |
| if (buffer == null) { |
| buffer = new StringBuilder(); |
| bufferedOutputByOutcome.put(outcomeName, buffer); |
| } |
| |
| buffer.append(output); |
| } |
| |
| @Override protected synchronized void flushBufferedOutput(String outcomeName) { |
| newLine(); |
| out.print(indent + outcomeName); |
| currentLine = CurrentLine.NAME; |
| |
| StringBuilder buffer = bufferedOutputByOutcome.remove(outcomeName); |
| if (buffer != null) { |
| streamOutput(buffer); |
| } |
| } |
| } |
| } |