Java程序  |  501行  |  16.53 KB

/*
 * 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);
            }
        }
    }
}