1 package org.robolectric.junit.rules; 2 3 import android.util.Log; 4 import com.google.common.collect.ImmutableSet; 5 import java.util.HashSet; 6 import java.util.List; 7 import java.util.Set; 8 import org.junit.rules.TestRule; 9 import org.junit.runner.Description; 10 import org.junit.runners.model.Statement; 11 import org.robolectric.shadows.ShadowLog; 12 import org.robolectric.shadows.ShadowLog.LogItem; 13 14 /** 15 * Allows tests to assert about the presence of log messages, and turns logged errors that are not 16 * explicitly expected into test failures. 17 */ 18 public final class ExpectedLogMessagesRule implements TestRule { 19 /** Tags that apps can't prevent. We whitelist them globally. */ 20 private static final ImmutableSet<String> UNPREVENTABLE_TAGS = 21 ImmutableSet.of("Typeface", "RingtoneManager"); 22 23 private final Set<LogItem> expectedLogs = new HashSet<>(); 24 private final Set<LogItem> observedLogs = new HashSet<>(); 25 private final Set<String> expectedTags = new HashSet<>(); 26 private final Set<String> observedTags = new HashSet<>(); 27 28 private boolean shouldIgnoreMissingLoggedTags = false; 29 30 @Override 31 public Statement apply(final Statement base, Description description) { 32 return new Statement() { 33 @Override 34 public void evaluate() throws Throwable { 35 base.evaluate(); 36 List<LogItem> logs = ShadowLog.getLogs(); 37 for (LogItem log : logs) { 38 LogItem throwLessLogItem = new LogItem(log.type, log.tag, log.msg, null); 39 if (expectedLogs.contains(throwLessLogItem)) { 40 observedLogs.add(throwLessLogItem); 41 continue; 42 } 43 if (log.type >= Log.ERROR) { 44 if (UNPREVENTABLE_TAGS.contains(log.tag)) { 45 continue; 46 } 47 if (expectedTags.contains(log.tag)) { 48 observedTags.add(log.tag); 49 continue; 50 } 51 if (log.throwable != null) { 52 throw new AssertionError( 53 "Unexpected error log message: " + log.tag + ": " + log.msg, log.throwable); 54 } else { 55 throw new AssertionError("Unexpected error log message: " + log.tag + ": " + log.msg); 56 } 57 } 58 } 59 if (!expectedLogs.equals(observedLogs)) { 60 throw new AssertionError( 61 "Some expected logs were not printed." 62 + "\nExpected: " 63 + expectedLogs 64 + "\nObserved: " 65 + observedLogs); 66 } 67 if (!expectedTags.equals(observedTags) && !shouldIgnoreMissingLoggedTags) { 68 throw new AssertionError( 69 "Some expected tags were not printed. " 70 + "Expected tags should not be used to suppress errors, only expect them." 71 + "\nExpected: " 72 + expectedTags 73 + "\nObserved: " 74 + observedTags); 75 } 76 } 77 }; 78 } 79 80 /** 81 * Adds an expected log statement. If this log is not printed during test execution, the test case 82 * will fail. Do not use this to suppress failures. Use this to test that expected error cases in 83 * your code cause log messages to be printed. 84 */ 85 public void expectLogMessage(int level, String tag, String message) { 86 checkTag(tag); 87 expectedLogs.add(new LogItem(level, tag, message, null)); 88 } 89 90 /** 91 * Blanket suppress test failures due to errors from a tag. If this tag is not printed at 92 * Log.ERROR during test execution, the test case will fail (unless {@link 93 * #ignoreMissingLoggedTags()} is used). 94 * 95 * <p>Avoid using this method when possible. Prefer to assert on the presence of a specific 96 * message using {@link #expectLogMessage} in test cases that *intentionally* trigger an error. 97 */ 98 public void expectErrorsForTag(String tag) { 99 checkTag(tag); 100 if (UNPREVENTABLE_TAGS.contains(tag)) { 101 throw new AssertionError("Tag `" + tag + "` is already suppressed."); 102 } 103 expectedTags.add(tag); 104 } 105 106 /** 107 * If set true, tests that call {@link #expectErrorsForTag()} but do not log errors for the given 108 * tag will not fail. By default this is false. 109 * 110 * <p>Avoid using this method when possible. Prefer tests that print (or do not print) log 111 * messages deterministically. 112 */ 113 public void ignoreMissingLoggedTags(boolean shouldIgnore) { 114 shouldIgnoreMissingLoggedTags = shouldIgnore; 115 } 116 117 private void checkTag(String tag) { 118 if (tag.length() > 23) { 119 throw new IllegalArgumentException("Tag length cannot exceed 23 characters: " + tag); 120 } 121 } 122 } 123