// 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 com.google.common.truth.Truth.assert_; import static org.junit.Assert.assertTrue; 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.Iterables; 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); } public static <T> void assertContentsAnyOrder( Iterable<? extends T> expected, Iterable<? extends T> actual, Comparator<? super T> comp) { assertThat(actual).hasSize(Iterables.size(expected)); int i = 0; for (T e : expected) { for (T a : actual) { if (comp.compare(e, a) == 0) { i++; } } } assertThat(actual).hasSize(i); } /** * 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 = new Predicate<Object>() { @Override public boolean apply(Object obj) { return clazz.isAssignableFrom(obj.getClass()); } }; if (isRetained(p, start)) { assert_().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; } f.setAccessible(true); 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) { assertTrue(message + " should contain '" + string + "' (with quotes)", message.contains("'" + string + "'")); } } 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 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 */ protected static void assertContainsEventsInOrder(Iterable<Event> eventCollector, String... expectedMessages) { String failure = containsSublistWithGapsAndEqualityChecker( ImmutableList.copyOf(eventCollector), new Function<Pair<Event, String>, Boolean> () { @Override public Boolean apply(Pair<Event, String> pair) { return 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; } }