/* * 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: * */ 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.emptyList()); } /** * Warns, and also puts a list of strings afterwards. */ public synchronized void warn(String message, List 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 annotatedOutcomes) { List annotatedOutcomesSorted = AnnotatedOutcome.ORDER_BY_NAME.sortedCopy(annotatedOutcomes); List failures = Lists.newArrayList(); List skips = Lists.newArrayList(); List successes = Lists.newArrayList(); List 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 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 previousResultValues = annotatedOutcome.getPreviousResultValues(); int numPreviousResultValues = previousResultValues.size(); int numResultValuesToShow = Math.min(10, numPreviousResultValues); List 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 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 bufferedOutputByOutcome = new HashMap(); @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); } } } }