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