blob: 01b1c0afeb72011897e0de91c1623ffa2970813f [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* 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 com.google.gwt.dev.util;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.util.log.AbstractTreeLogger;
import com.google.gwt.thirdparty.guava.common.collect.ComparisonChain;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import junit.framework.Assert;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Pattern;
/**
* A {@link TreeLogger} implementation that can be used during JUnit tests to
* check for a specified sequence of log events.
*/
public class UnitTestTreeLogger extends TreeLogger {
/**
* Simplifies the creation of a {@link UnitTestTreeLogger} by providing
* convenience methods for specifying the expected log events.
*/
public static class Builder {
private final List<LogEntry> expected = new ArrayList<LogEntry>();
private EnumSet<Type> loggableTypes = EnumSet.allOf(TreeLogger.Type.class);
public Builder() {
}
public UnitTestTreeLogger createLogger() {
return new UnitTestTreeLogger(expected, loggableTypes);
}
public void expect(TreeLogger.Type type, String msg, Class<? extends Throwable> caught) {
expected.add(new LogEntry(type, msg, caught));
}
public void expect(TreeLogger.Type type, Pattern msgPattern,
Class<? extends Throwable> caught) {
expected.add(new LogEntry(type, msgPattern, caught));
}
public void expectDebug(String msg, Class<? extends Throwable> caught) {
expect(TreeLogger.DEBUG, msg, caught);
}
public void expectError(String msg, Class<? extends Throwable> caught) {
expect(TreeLogger.ERROR, msg, caught);
}
public void expectError(Pattern msgPattern, Class<? extends Throwable> caught) {
expect(TreeLogger.ERROR, msgPattern, caught);
}
public void expectInfo(String msg, Class<? extends Throwable> caught) {
expect(TreeLogger.INFO, msg, caught);
}
public void expectSpam(String msg, Class<? extends Throwable> caught) {
expect(TreeLogger.SPAM, msg, caught);
}
public void expectTrace(String msg, Class<? extends Throwable> caught) {
expect(TreeLogger.TRACE, msg, caught);
}
public void expectWarn(String msg, Class<? extends Throwable> caught) {
expect(TreeLogger.WARN, msg, caught);
}
/**
* Sets the loggable types based on an explicit set.
*/
public void setLoggableTypes(EnumSet<TreeLogger.Type> loggableTypes) {
this.loggableTypes = loggableTypes;
}
/**
* Sets the loggable types based on a lowest log level.
*/
public void setLowestLogLevel(TreeLogger.Type lowestLogLevel) {
loggableTypes.clear();
for (Type type : TreeLogger.Type.values()) {
if (!type.isLowerPriorityThan(lowestLogLevel)) {
loggableTypes.add(type);
}
}
}
}
/**
* Represents a log event to check for.
*/
private static class LogEntry implements Comparable<LogEntry> {
private final Class<? extends Throwable> caught;
private String msg;
private Pattern msgPattern;
private final Type type;
public LogEntry(TreeLogger.Type type, String msg, Class<? extends Throwable> caught) {
assert (type != null);
this.type = type;
this.msg = msg;
this.caught = caught;
}
public LogEntry(TreeLogger.Type type, Pattern msgPattern, Class<? extends Throwable> caught) {
assert (type != null);
this.type = type;
this.msgPattern = msgPattern;
this.caught = caught;
}
public LogEntry(Type type, String msg, Throwable caught) {
this(type, msg, (caught == null) ? null : caught.getClass());
}
public Class<? extends Throwable> getCaught() {
return caught;
}
public String getMessage() {
return msg;
}
public Pattern getMessagePattern() {
return msgPattern;
}
public Type getType() {
return type;
}
public boolean equals(Object other) {
if (!(other instanceof LogEntry)) {
return false;
}
return this.toString().equals(other.toString());
}
public String getMessageOrPattern() {
return this.msg == null ? this.msgPattern.toString() : this.msg;
}
public int hashCode() {
return toString().hashCode();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(type.getLabel());
sb.append(": ");
if (getMessage() != null) {
sb.append(getMessage());
} else {
sb.append("like " + getMessagePattern().pattern());
}
Class<? extends Throwable> t = getCaught();
if (t != null) {
sb.append("; ");
sb.append(t.getName());
}
return sb.toString();
}
// Test whether this log entry matches {@code other} (not symmetrical, i.e. a matches b does not
// imply b matches a)
//
// NOTE: DO NOT IMPLEMENT EQUAL. The test harness relies on equal() being reference equality.
private boolean matches(LogEntry other) {
if (!type.equals(other.type)) {
return false;
}
if (msg != null) {
if (!msg.equals(other.msg)) {
return false;
}
} else {
if (!getMessagePattern().matcher(other.msg).matches()) {
return false;
}
}
if ((caught == null) != (other.caught == null)) {
return false;
}
if (caught != null && !caught.isAssignableFrom(other.caught)) {
return false;
}
return true;
}
@Override
public int compareTo(LogEntry that) {
return ComparisonChain.start()
.compare(this.getMessageOrPattern(), that.getMessageOrPattern(),
AbstractTreeLogger.LOG_LINE_COMPARATOR)
.compare(this.type, that.type)
.result();
}
}
private static void assertCorrectLogEntry(LogEntry expected, LogEntry actual) {
Assert.assertEquals("Log types do not match", expected.getType(), actual.getType());
if (expected.getMessage() != null) {
Assert.assertEquals("Log messages do not match", expected.getMessage(), actual.getMessage());
} else {
Assert.assertTrue("Log message '" + actual.getMessage() + "' does not match pattern "
+ expected.getMessagePattern().pattern(),
expected.getMessagePattern().matcher(actual.getMessage()).matches());
}
if (expected.getCaught() == null) {
Assert.assertNull("Actual log exception type should have been null", actual.getCaught());
} else {
Assert.assertNotNull("Actual log exception type should not have been null", actual
.getCaught());
Assert.assertTrue("Actual log exception type (" + actual.getCaught().getName()
+ ") cannot be assigned to expected log exception type ("
+ expected.getCaught().getName() + ")", expected.getCaught().isAssignableFrom(
actual.getCaught()));
}
}
private final List<LogEntry> actualEntries = new ArrayList<LogEntry>();
private final List<LogEntry> expectedEntries = new ArrayList<LogEntry>();
private final EnumSet<TreeLogger.Type> loggableTypes;
public UnitTestTreeLogger(List<LogEntry> expectedEntries, EnumSet<TreeLogger.Type> loggableTypes) {
this.expectedEntries.addAll(expectedEntries);
this.loggableTypes = loggableTypes;
// Sanity check that all expected entries are actually loggable.
for (LogEntry entry : expectedEntries) {
Type type = entry.getType();
Assert.assertTrue("Cannot expect an entry of a non-loggable type!", isLoggable(type));
loggableTypes.add(type);
}
}
/**
* Asserts that all expected log entries were logged in the correct order and
* no other entries were logged.
*/
public void assertCorrectLogEntries() {
Collections.sort(expectedEntries);
Collections.sort(actualEntries);
if (expectedEntries.size() != actualEntries.size()) {
List<LogEntry> missingEntries = Lists.newArrayList(expectedEntries);
missingEntries.removeAll(actualEntries);
List<LogEntry> unexpectedEntries = Lists.newArrayList(actualEntries);
unexpectedEntries.removeAll(expectedEntries);
Assert.fail("Wrong log count: missing=" + missingEntries + ", unexpected=" + unexpectedEntries);
}
for (int i = 0; i < expectedEntries.size(); ++i) {
assertCorrectLogEntry(expectedEntries.get(i), actualEntries.get(i));
}
}
/**
* A more loose check than {@link #assertCorrectLogEntries} that just checks
* to see that the expected log messages are somewhere in the actual logged
* messages.
*/
public void assertLogEntriesContainExpected() {
for (LogEntry expectedEntry : expectedEntries) {
boolean found = false;
for (LogEntry actualEntry : actualEntries) {
if (expectedEntry.matches(actualEntry)) {
found = true;
break;
}
}
Assert.assertTrue("No match for expected=" + expectedEntry + " in actual=" + actualEntries,
found);
}
}
@Override
public TreeLogger branch(Type type, String msg, Throwable caught, HelpInfo helpInfo) {
log(type, msg, caught);
return this;
}
@Override
public boolean isLoggable(Type type) {
return loggableTypes.contains(type);
}
@Override
public void log(Type type, String msg, Throwable caught, HelpInfo helpInfo) {
if (!isLoggable(type)) {
return;
}
LogEntry actualEntry = new LogEntry(type, msg, caught);
actualEntries.add(actualEntry);
}
}