| // Copyright 2014 The Bazel Authors. All rights reserved. |
| // |
| // 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.devtools.build.lib.testutil; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| import static org.junit.Assert.fail; |
| |
| import com.google.common.base.Function; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Predicate; |
| import com.google.common.base.Predicates; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.events.Event; |
| import com.google.devtools.build.lib.events.EventCollector; |
| import com.google.devtools.build.lib.events.EventKind; |
| import com.google.devtools.build.lib.util.Pair; |
| import java.lang.ref.Reference; |
| import java.lang.reflect.Field; |
| import java.util.ArrayDeque; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Queue; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| |
| /** |
| * A helper class for tests providing a simple interface for asserts. |
| */ |
| public class MoreAsserts { |
| |
| public static <T> void assertEquals(T expected, T actual, Comparator<T> comp) { |
| assertThat(comp.compare(expected, actual)).isEqualTo(0); |
| } |
| |
| /** |
| * Scans if an instance of given class is strongly reachable from a given |
| * object. |
| * <p>Runs breadth-first search in object reachability graph to check if |
| * an instance of <code>clz</code> can be reached. |
| * <strong>Note:</strong> This method can take a long time if analyzed |
| * data structure spans across large part of heap and may need a lot of |
| * memory. |
| * |
| * @param start object to start the search from |
| * @param clazz class to look for |
| */ |
| public static void assertInstanceOfNotReachable( |
| Object start, final Class<?> clazz) { |
| Predicate<Object> p = obj -> clazz.isAssignableFrom(obj.getClass()); |
| if (isRetained(p, start)) { |
| fail("Found an instance of " + clazz.getCanonicalName() + " reachable from " + start); |
| } |
| } |
| |
| private static final Field NON_STRONG_REF; |
| |
| static { |
| try { |
| NON_STRONG_REF = Reference.class.getDeclaredField("referent"); |
| } catch (SecurityException | NoSuchFieldException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| static final Predicate<Field> ALL_STRONG_REFS = Predicates.equalTo(NON_STRONG_REF); |
| |
| private static boolean isRetained(Predicate<Object> predicate, Object start) { |
| Map<Object, Object> visited = Maps.newIdentityHashMap(); |
| visited.put(start, start); |
| Queue<Object> toScan = new ArrayDeque<>(); |
| toScan.add(start); |
| |
| while (!toScan.isEmpty()) { |
| Object current = toScan.poll(); |
| if (current.getClass().isArray()) { |
| if (current.getClass().getComponentType().isPrimitive()) { |
| continue; |
| } |
| |
| for (Object ref : (Object[]) current) { |
| if (ref != null) { |
| if (predicate.apply(ref)) { |
| return true; |
| } |
| if (visited.put(ref, ref) == null) { |
| toScan.add(ref); |
| } |
| } |
| } |
| } else { |
| // iterate *all* fields (getFields() returns only accessible ones) |
| for (Class<?> clazz = current.getClass(); clazz != null; |
| clazz = clazz.getSuperclass()) { |
| for (Field f : clazz.getDeclaredFields()) { |
| if (f.getType().isPrimitive() || ALL_STRONG_REFS.apply(f)) { |
| continue; |
| } |
| |
| try { |
| f.setAccessible(true); |
| } catch (RuntimeException e) { |
| // JDK9 can throw InaccessibleObjectException when internal modules are accessed. |
| // This isn't available in JDK8, so catch RuntimeException |
| // We can use a JVM arg --add_opens to suppress that, but that involves every |
| // test adding every JVM module to the target. |
| continue; |
| } |
| try { |
| Object ref = f.get(current); |
| if (ref != null) { |
| if (predicate.apply(ref)) { |
| return true; |
| } |
| if (visited.put(ref, ref) == null) { |
| toScan.add(ref); |
| } |
| } |
| } catch (IllegalArgumentException | IllegalAccessException e) { |
| throw new IllegalStateException("Error when scanning the heap", e); |
| } |
| } |
| } |
| } |
| } |
| return false; |
| } |
| |
| private static String getClassDescription(Object object) { |
| return object == null |
| ? "null" |
| : ("instance of " + object.getClass().getName()); |
| } |
| |
| public static String chattyFormat(String message, Object expected, Object actual) { |
| String expectedClass = getClassDescription(expected); |
| String actualClass = getClassDescription(actual); |
| |
| return Joiner.on('\n').join((message != null) ? ("\n" + message) : "", |
| " expected " + expectedClass + ": <" + expected + ">", |
| " but was " + actualClass + ": <" + actual + ">"); |
| } |
| |
| public static void assertEqualsUnifyingLineEnds(String expected, String actual) { |
| if (actual != null) { |
| actual = actual.replaceAll(System.getProperty("line.separator"), "\n"); |
| } |
| assertThat(actual).isEqualTo(expected); |
| } |
| |
| public static void assertContainsWordsWithQuotes(String message, |
| String... strings) { |
| for (String string : strings) { |
| assertWithMessage(message + " should contain '" + string + "' (with quotes)") |
| .that(message.contains("'" + string + "'")) |
| .isTrue(); |
| } |
| } |
| |
| public static void assertNonZeroExitCode(int exitCode, String stdout, String stderr) { |
| if (exitCode == 0) { |
| fail("expected non-zero exit code but exit code was 0 and stdout was <" |
| + stdout + "> and stderr was <" + stderr + ">"); |
| } |
| } |
| |
| public static void assertZeroExitCode(int exitCode, String stdout, String stderr) { |
| assertExitCode(0, exitCode, stdout, stderr); |
| } |
| |
| public static void assertExitCode(int expectedExitCode, |
| int exitCode, String stdout, String stderr) { |
| if (exitCode != expectedExitCode) { |
| fail(String.format("expected exit code <%d> but exit code was <%d> and stdout was <%s> " |
| + "and stderr was <%s>", expectedExitCode, exitCode, stdout, stderr)); |
| } |
| } |
| |
| public static void assertStdoutContainsString(String expected, String stdout, String stderr) { |
| if (!stdout.contains(expected)) { |
| fail("expected stdout to contain string <" + expected + "> but stdout was <" |
| + stdout + "> and stderr was <" + stderr + ">"); |
| } |
| } |
| |
| public static void assertStderrContainsString(String expected, String stdout, String stderr) { |
| if (!stderr.contains(expected)) { |
| fail("expected stderr to contain string <" + expected + "> but stdout was <" |
| + stdout + "> and stderr was <" + stderr + ">"); |
| } |
| } |
| |
| public static void assertStdoutContainsRegex(String expectedRegex, |
| String stdout, String stderr) { |
| if (!Pattern.compile(expectedRegex).matcher(stdout).find()) { |
| fail("expected stdout to contain regex <" + expectedRegex + "> but stdout was <" |
| + stdout + "> and stderr was <" + stderr + ">"); |
| } |
| } |
| |
| public static void assertStderrContainsRegex(String expectedRegex, |
| String stdout, String stderr) { |
| if (!Pattern.compile(expectedRegex).matcher(stderr).find()) { |
| fail("expected stderr to contain regex <" + expectedRegex + "> but stdout was <" |
| + stdout + "> and stderr was <" + stderr + ">"); |
| } |
| } |
| |
| public static Set<String> asStringSet(Iterable<?> collection) { |
| Set<String> set = Sets.newTreeSet(); |
| for (Object o : collection) { |
| set.add("\"" + o + "\""); |
| } |
| return set; |
| } |
| |
| /** |
| * If the specified EventCollector contains any events, an informative |
| * assertion fails in the context of the specified TestCase. |
| */ |
| public static void assertNoEvents(Iterable<Event> eventCollector) { |
| String eventsString = eventsToString(eventCollector); |
| assertThat(eventsString).isEmpty(); |
| } |
| |
| /** |
| * If the specified EventCollector contains an unexpected number of events, an informative |
| * assertion fails in the context of the specified TestCase. |
| */ |
| public static void assertEventCount(int expectedCount, EventCollector eventCollector) { |
| assertWithMessage(eventsToString(eventCollector)) |
| .that(eventCollector.count()).isEqualTo(expectedCount); |
| } |
| |
| /** |
| * If the specified EventCollector contains an unexpected number of events, an informative |
| * assertion fails in the context of the specified TestCase. |
| */ |
| public static void assertEventCountAtLeast(int minCount, EventCollector eventCollector) { |
| assertWithMessage(eventsToString(eventCollector)) |
| .that(eventCollector.count()) |
| .isAtLeast(minCount); |
| } |
| |
| /** |
| * If the specified EventCollector does not contain an event which has 'expectedEvent' as a |
| * substring, an informative assertion fails. Otherwise the matching event is returned. |
| */ |
| public static Event assertContainsEvent(Iterable<Event> eventCollector, String expectedEvent) { |
| return assertContainsEvent(eventCollector, expectedEvent, EventKind.ALL_EVENTS); |
| } |
| |
| /** |
| * If the specified EventCollector does not contain an event which has |
| * 'expectedEvent' as a substring, an informative assertion fails. Otherwise |
| * the matching event is returned. |
| */ |
| public static Event assertContainsEvent(Iterable<Event> eventCollector, |
| String expectedEvent, EventKind kind) { |
| return assertContainsEvent(eventCollector, expectedEvent, ImmutableSet.of(kind)); |
| } |
| |
| /** |
| * If the specified EventCollector does not contain an event of a kind of 'kinds' which has |
| * 'expectedEvent' as a substring, an informative assertion fails. Otherwise |
| * the matching event is returned. |
| */ |
| public static Event assertContainsEvent(Iterable<Event> eventCollector, |
| String expectedEvent, |
| Set<EventKind> kinds) { |
| for (Event event : eventCollector) { |
| // We want to be able to check for the location and the message type (error / warning). |
| // Consequently, we use toString() instead of getMessage(). |
| if (event.toString().contains(expectedEvent) && kinds.contains(event.getKind())) { |
| return event; |
| } |
| } |
| String eventsString = eventsToString(eventCollector); |
| assertWithMessage("Event '" + expectedEvent + "' not found" |
| + (eventsString.length() == 0 ? "" : ("; found these though:" + eventsString))) |
| .that(false).isTrue(); |
| return null; // unreachable |
| } |
| |
| /** |
| * If {@code eventCollector} does not contain an event which matches {@code expectedEventRegex}, |
| * fails with an informative assertion. |
| */ |
| public static void assertContainsEventRegex( |
| Iterable<Event> eventCollector, String expectedEventRegex) { |
| for (Event event : eventCollector) { |
| if (event.toString().matches(expectedEventRegex)) { |
| return; |
| } |
| } |
| String eventsString = eventsToString(eventCollector); |
| String failureMessage = "Event matching '" + expectedEventRegex + "' not found"; |
| if (!eventsString.isEmpty()) { |
| failureMessage += "; found these though: " + eventsString; |
| } |
| fail(failureMessage); |
| } |
| |
| public static void assertNotContainsEventRegex( |
| Iterable<Event> eventCollector, String unexpectedEventRegex) { |
| for (Event event : eventCollector) { |
| assertThat(event.toString()).doesNotMatch(unexpectedEventRegex); |
| } |
| } |
| |
| /** |
| * If the specified EventCollector contains an event which has |
| * 'expectedEvent' as a substring, an informative assertion fails. |
| */ |
| public static void assertDoesNotContainEvent(Iterable<Event> eventCollector, |
| String expectedEvent) { |
| for (Event event : eventCollector) { |
| assertWithMessage("Unexpected string '" + expectedEvent + "' matched following event:\n" |
| + event.getMessage()).that(event.getMessage()).doesNotContain(expectedEvent); |
| } |
| } |
| |
| /** |
| * If the specified EventCollector does not contain an event which has |
| * each of {@code words} surrounded by single quotes as a substring, an |
| * informative assertion fails. Otherwise the matching event is returned. |
| */ |
| public static Event assertContainsEventWithWordsInQuotes( |
| Iterable<Event> eventCollector, |
| String... words) { |
| for (Event event : eventCollector) { |
| boolean found = true; |
| for (String word : words) { |
| if (!event.getMessage().contains("'" + word + "'")) { |
| found = false; |
| break; |
| } |
| } |
| if (found) { |
| return event; |
| } |
| } |
| String eventsString = eventsToString(eventCollector); |
| assertWithMessage("Event containing words " + Arrays.toString(words) + " in " |
| + "single quotes not found" |
| + (eventsString.length() == 0 ? "" : ("; found these though:" + eventsString))) |
| .that(false).isTrue(); |
| return null; // unreachable |
| } |
| |
| /** |
| * Returns a string consisting of each event in the specified collector, |
| * preceded by a newline. |
| */ |
| private static String eventsToString(Iterable<Event> eventCollector) { |
| StringBuilder buf = new StringBuilder(); |
| eventLoop: for (Event event : eventCollector) { |
| for (String ignoredPrefix : TestConstants.IGNORED_MESSAGE_PREFIXES) { |
| if (event.getMessage().startsWith(ignoredPrefix)) { |
| continue eventLoop; |
| } |
| } |
| buf.append('\n').append(event); |
| } |
| return buf.toString(); |
| } |
| |
| /** |
| * If "expectedSublist" is not a sublist of "arguments", an informative |
| * assertion is failed in the context of the specified TestCase. |
| * |
| * <p>Argument order mnemonic: assert(X)ContainsSublist(Y). |
| */ |
| @SuppressWarnings({"unchecked", "varargs"}) |
| public static <T> void assertContainsSublist(List<T> arguments, T... expectedSublist) { |
| List<T> sublist = Arrays.asList(expectedSublist); |
| try { |
| assertThat(Collections.indexOfSubList(arguments, sublist)).isNotEqualTo(-1); |
| } catch (AssertionError e) { |
| throw new AssertionError("Did not find " + sublist + " as a sublist of " + arguments, e); |
| } |
| } |
| |
| /** |
| * If "expectedSublist" is a sublist of "arguments", an informative |
| * assertion is failed in the context of the specified TestCase. |
| * |
| * <p>Argument order mnemonic: assert(X)DoesNotContainSublist(Y). |
| */ |
| @SuppressWarnings({"unchecked", "varargs"}) |
| public static <T> void assertDoesNotContainSublist(List<T> arguments, T... expectedSublist) { |
| List<T> sublist = Arrays.asList(expectedSublist); |
| try { |
| assertThat(Collections.indexOfSubList(arguments, sublist)).isEqualTo(-1); |
| } catch (AssertionError e) { |
| throw new AssertionError("Found " + sublist + " as a sublist of " + arguments, e); |
| } |
| } |
| |
| /** |
| * Check to see if each element of expectedMessages is the beginning of a message in |
| * eventCollector, in order, as in {@link #containsSublistWithGapsAndEqualityChecker}. If not, an |
| * informative assertion is failed |
| */ |
| public static void assertContainsEventsInOrder( |
| Iterable<Event> eventCollector, String... expectedMessages) { |
| String failure = |
| containsSublistWithGapsAndEqualityChecker( |
| ImmutableList.copyOf(eventCollector), |
| pair -> pair.first.getMessage().contains(pair.second), |
| expectedMessages); |
| |
| String eventsString = eventsToString(eventCollector); |
| assertWithMessage("Event '" + failure + "' not found in proper order" |
| + (eventsString.length() == 0 ? "" : ("; found these though:" + eventsString))) |
| .that(failure).isNull(); |
| } |
| |
| /** |
| * Check to see if each element of expectedSublist is in arguments, according to |
| * the equalityChecker, in the same order as in expectedSublist (although with |
| * other interspersed elements in arguments allowed). |
| * @param equalityChecker function that takes a Pair<S, T> element and returns true |
| * if the elements of the pair are equal by its lights. |
| * @return first element not in arguments in order, or null if success. |
| */ |
| @SuppressWarnings({"unchecked"}) |
| protected static <S, T> T containsSublistWithGapsAndEqualityChecker(List<S> arguments, |
| Function<Pair<S, T>, Boolean> equalityChecker, T... expectedSublist) { |
| Iterator<S> iter = arguments.iterator(); |
| outerLoop: |
| for (T expected : expectedSublist) { |
| while (iter.hasNext()) { |
| S actual = iter.next(); |
| if (equalityChecker.apply(Pair.of(actual, expected))) { |
| continue outerLoop; |
| } |
| } |
| return expected; |
| } |
| return null; |
| } |
| |
| public static List<Event> assertContainsEventWithFrequency(Iterable<Event> events, |
| String expectedMessage, int expectedFrequency) { |
| ImmutableList.Builder<Event> builder = ImmutableList.builder(); |
| for (Event event : events) { |
| if (event.getMessage().contains(expectedMessage)) { |
| builder.add(event); |
| } |
| } |
| List<Event> foundEvents = builder.build(); |
| assertWithMessage(events.toString()).that(foundEvents).hasSize(expectedFrequency); |
| return foundEvents; |
| } |
| |
| /* |
| * This method will be in JUnit 4.13. Instead of patching Bazel's JUnit jar to contain the |
| * <a href="https://github.com/junit-team/junit4/commit/bdb1799">patch</a>, we define it here. |
| * Once JUnit 4.13 is released, we will switcher callers to use org.junit.Assert#assertThrows |
| * instead. See https://github.com/bazelbuild/bazel/issues/3729. |
| */ |
| public static <T extends Throwable> T assertThrows( |
| Class<T> expectedThrowable, ThrowingRunnable runnable) { |
| return assertThrows("", expectedThrowable, runnable); |
| } |
| |
| /* |
| * This method will be in JUnit 4.13. Instead of patching Bazel's JUnit jar to contain the |
| * <a href="https://github.com/junit-team/junit4/commit/bdb1799">patch</a>, we define it here. |
| * Once JUnit 4.13 is released, we will switcher callers to use org.junit.Assert#assertThrows |
| * instead. See https://github.com/bazelbuild/bazel/issues/3729. |
| */ |
| public static <T extends Throwable> T assertThrows( |
| String message, Class<T> expectedThrowable, ThrowingRunnable runnable) { |
| try { |
| runnable.run(); |
| } catch (Throwable actualThrown) { |
| if (expectedThrowable.isInstance(actualThrown)) { |
| @SuppressWarnings("unchecked") |
| T retVal = (T) actualThrown; |
| return retVal; |
| } else { |
| throw new AssertionError( |
| buildPrefix(message) |
| + String.format( |
| "expected %s to be thrown, but %s was thrown", |
| expectedThrowable.getSimpleName(), actualThrown.getClass().getSimpleName()), |
| actualThrown); |
| } |
| } |
| String mismatchMessage = |
| buildPrefix(message) |
| + String.format( |
| "expected %s to be thrown, but nothing was thrown", |
| expectedThrowable.getSimpleName()); |
| throw new AssertionError(mismatchMessage); |
| } |
| |
| private static String buildPrefix(String message) { |
| return message != null && message.length() != 0 ? message + ": " : ""; |
| } |
| |
| /** A helper interface for {@link #assertThrows}. */ |
| public interface ThrowingRunnable { |
| void run() throws Throwable; |
| } |
| } |