All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.common.truth.ActualValueInference Maven / Gradle / Ivy

The newest version!
// Copyright 2017 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.common.truth;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.getOnlyElement;
import static java.lang.Thread.currentThread;

import com.google.auto.value.AutoValue;
import com.google.auto.value.AutoValue.CopyAnnotations;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import org.objectweb.asm.Opcodes;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Map.Entry;
import org.jspecify.annotations.Nullable;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * Given the stack frame of a failing assertion, tries to describe what the user passed to {@code
 * assertThat}.
 *
 * 

For example, suppose that the test contains: * *

{@code
 * assertThat(logService.fetchLogMessages(startDate, endDate))
 *     .containsExactly(message1, message2)
 *     .inOrder();
 * }
* * If either {@code containsExactly} or {@code inOrder} fails, {@code ActualValueInference} reports * (if the rest of the test method is simple enough to analyze easily) that the user passed {@code * fetchLogMessages(...)}. This allows us to produce a failure message like: * *
 * value of   : fetchLogMessages(...)
 * missing (1): message1
 * ...
 * 
* * {@code ActualValueInference} accomplishes this by examining the bytecode of the test. Naturally, * this is all best-effort. */ @GwtIncompatible @J2ktIncompatible final class ActualValueInference { /** Call {@link Platform#inferDescription} rather than calling this directly. */ static @Nullable String describeActualValue(String className, String methodName, int lineNumber) { InferenceClassVisitor visitor; try { // TODO(cpovirk): Verify that methodName is correct for constructors and static initializers. visitor = new InferenceClassVisitor(methodName); } catch (IllegalArgumentException theVersionOfAsmIsOlderThanWeRequire) { // TODO(cpovirk): Consider what minimum version the class and method visitors really need. // TODO(cpovirk): Log a warning? return null; } ClassLoader loader = firstNonNull( currentThread().getContextClassLoader(), ActualValueInference.class.getClassLoader()); /* * We're assuming that classes were loaded in a simple way. In principle, we could do better * with java.lang.instrument. */ InputStream stream = null; try { stream = loader.getResourceAsStream(className.replace('.', '/') + ".class"); // TODO(cpovirk): Disable inference if the bytecode version is newer than we've tested on? new ClassReader(stream).accept(visitor, /* parsingOptions= */ 0); ImmutableSet actualsAtLine = visitor.actualValueAtLine.build().get(lineNumber); /* * It's very unlikely that more than one assertion would happen on the same line _but with * different root actual values_. * * That is, it's common to have: * assertThat(list).containsExactly(...).inOrder(); * * But it's not common to have, all on one line: * assertThat(list).isEmpty(); assertThat(list2).containsExactly(...); * * In principle, we could try to distinguish further by looking at what assertion method * failed (which our caller could pass us by looking higher on the stack). But it's hard to * imagine that it would be worthwhile. */ return actualsAtLine.size() == 1 ? getOnlyElement(actualsAtLine).description() : null; } catch (IOException e) { /* * Likely "Class not found," perhaps from generated bytecode (or from StackTraceCleaner's * pseudo-frames, which ideally ActualValueInference would tell it not to create). */ // TODO(cpovirk): Log a warning? return null; } catch (SecurityException e) { // Inside Google, some tests run under a security manager that forbids filesystem access. // TODO(cpovirk): Log a warning? return null; } finally { closeQuietly(stream); } } /** * An entry on the stack (or the local-variable table) with a {@linkplain InferredType type} and * sometimes a description of {@linkplain DescribedEntry how the value was produced} or, as a * special case, whether {@linkplain SubjectEntry the value is a Truth subject}. */ abstract static class StackEntry { abstract InferredType type(); // Each of these is overridden by a subclass: boolean isSubject() { return false; } StackEntry actualValue() { throw new ClassCastException(getClass().getName()); } @Nullable String description() { return null; } } /** An entry that we know nothing about except for its type. */ @AutoValue @CopyAnnotations @GwtIncompatible @J2ktIncompatible abstract static class OpaqueEntry extends StackEntry { @Override public final String toString() { return "unknown"; } } private static StackEntry opaque(InferredType type) { return new AutoValue_ActualValueInference_OpaqueEntry(type); } /** * An entry that contains a description of how it was created. Currently, the only case in which * we provide a description is when the value comes from a method call whose name looks * "interesting." */ @AutoValue @CopyAnnotations @GwtIncompatible @J2ktIncompatible abstract static class DescribedEntry extends StackEntry { @Override abstract String description(); @Override public final String toString() { return description(); } } private static StackEntry described(InferredType type, String description) { return new AutoValue_ActualValueInference_DescribedEntry(type, description); } /** * An entry for a {@link Subject} (or a similar object derived with a {@code Subject}, like {@link * Ordered}). * *

The entry contains the "root actual value" of the assertion. In an assertion like {@code * assertThat(e).hasMessageThat().contains("foo")}, the root actual value is the {@code Throwable} * {@code e}, even though the {@code contains} assertion operates on a string message. */ @AutoValue @CopyAnnotations @GwtIncompatible @J2ktIncompatible abstract static class SubjectEntry extends StackEntry { @Override abstract StackEntry actualValue(); @Override final boolean isSubject() { return true; } @Override public final String toString() { return String.format("subjectFor(%s)", actualValue()); } } private static StackEntry subjectFor(InferredType type, StackEntry actual) { return new AutoValue_ActualValueInference_SubjectEntry(type, actual); } private static final class InferenceMethodVisitor extends MethodVisitor { private boolean used = false; private final ArrayList localVariableSlots; private final ArrayList operandStack = new ArrayList<>(); private FrameInfo previousFrame; /** For debugging purpose. */ private final String methodSignature; /** * The ASM labels that we've seen so far, which we use to look up the closest line number for * each assertion. */ private final ImmutableList.Builder

I had hoped that we didn't need this: In the {@code .class} files I looked at, {@code * visitLineNumber} calls were interleaved with the actual instructions. (I even have evidence * that the current implementation visits labels and line numbers together: See Label.accept.) * If that were guaranteed, then we could identify the line number for each assertion just by * looking at which {@code visitLineNumber} call we'd seen most recently. However, that * doesn't appear to be guaranteed, so we store this mapping and then join it with the * labels at the end. * *

I would expect to be able to use a map here. But I'm seeing multiple line numbers for the * same label in some Kotlin code. */ private final ImmutableSetMultimap.Builder lineNumbersAtLabel = ImmutableSetMultimap.builder(); /** * The mapping that indexes every root actual value by the full list of labels we'd visited * before we visited it. */ private final ImmutableSetMultimap.Builder, StackEntry> actualValueAtLocation = ImmutableSetMultimap.builder(); /** Set to {@code true} whenever a method permits multiple execution paths. */ private boolean seenJump; /** * The output of this process: a mapping from line number to the root actual values with * assertions on that line. This builder is potentially shared across multiple method visitors * for the same class visitor. */ private final ImmutableSetMultimap.Builder actualValueAtLine; InferenceMethodVisitor( int access, String owner, String name, String methodDescriptor, ImmutableSetMultimap.Builder actualValueAtLine) { super(Opcodes.ASM9); localVariableSlots = createInitialLocalVariableSlots(access, owner, name, methodDescriptor); previousFrame = FrameInfo.create( ImmutableList.copyOf(localVariableSlots), ImmutableList.of()); this.methodSignature = owner + "." + name + methodDescriptor; this.actualValueAtLine = actualValueAtLine; } @Override public void visitCode() { checkState(!used, "Cannot reuse this method visitor."); used = true; super.visitCode(); } @Override public void visitEnd() { if (seenJump) { /* * If there are multiple paths through a method, we'd have to examine them all and make sure * that the values still match up. We could try someday, but it's hard. */ super.visitEnd(); return; } ImmutableSetMultimap lineNumbersAtLabel = this.lineNumbersAtLabel.build(); for (Entry, StackEntry> e : actualValueAtLocation.build().entries()) { for (int lineNumber : lineNumbers(e.getKey(), lineNumbersAtLabel)) { actualValueAtLine.put(lineNumber, e.getValue()); } } super.visitEnd(); } private static ImmutableSet lineNumbers( ImmutableList

If the descriptor is for a call, this method not only records the assertion made by it (if * any) but also examines its parameters to generate more detailed stack entries. * * @param desc the descriptor of the type to be added to the stack (or the descriptor of the * method whose return value is to be added to the stack) * @param invocation the method invocation being visited, or {@code null} if a non-method * descriptor is being visited */ private void pushDescriptorAndMaybeProcessMethodCall( String desc, @Nullable Invocation invocation) { if (invocation != null && invocation.isOnSubjectInstance()) { actualValueAtLocation.put( labelsSeen.build(), checkNotNull(invocation.receiver()).actualValue()); } boolean hasParams = invocation != null && (Type.getArgumentsAndReturnSizes(desc) >> 2) > 1; int index = desc.charAt(0) == '(' ? desc.indexOf(')') + 1 : 0; switch (desc.charAt(index)) { case 'V': return; case 'Z': case 'C': case 'B': case 'S': case 'I': pushMaybeDescribed(InferredType.INT, invocation, hasParams); break; case 'F': pushMaybeDescribed(InferredType.FLOAT, invocation, hasParams); break; case 'D': pushMaybeDescribed(InferredType.DOUBLE, invocation, hasParams); push(InferredType.TOP); break; case 'J': pushMaybeDescribed(InferredType.LONG, invocation, hasParams); push(InferredType.TOP); break; case 'L': case '[': pushMaybeDescribed(InferredType.create(desc.substring(index)), invocation, hasParams); break; default: throw new RuntimeException("Unhandled type: " + desc); } } private void pushMaybeDescribed( InferredType type, @Nullable Invocation invocation, boolean hasParams) { push(invocation == null ? opaque(type) : invocation.deriveEntry(type, hasParams)); } @CanIgnoreReturnValue private StackEntry pop() { return pop(1); } /** Pop elements from the end of the operand stack, and return the last popped element. */ @CanIgnoreReturnValue private StackEntry pop(int count) { checkArgument( count >= 1, "The count should be at least one: %s (In %s)", count, methodSignature); checkState( operandStack.size() >= count, "There are no enough elements in the stack. count=%s, stack=%s (In %s)", count, operandStack, methodSignature); int expectedLastIndex = operandStack.size() - count - 1; StackEntry lastPopped; do { lastPopped = operandStack.remove(operandStack.size() - 1); } while (operandStack.size() - 1 > expectedLastIndex); return lastPopped; } private void popDescriptor(String desc) { char c = desc.charAt(0); switch (c) { case '(': int argumentSize = (Type.getArgumentsAndReturnSizes(desc) >> 2) - 1; if (argumentSize > 0) { pop(argumentSize); } break; case 'J': case 'D': pop(2); break; default: pop(1); break; } } private StackEntry getLocalVariable(int index) { checkState( index < localVariableSlots.size(), "Cannot find type for var %s in method %s", index, methodSignature); return localVariableSlots.get(index); } private void setLocalVariable(int index, StackEntry entry) { while (localVariableSlots.size() <= index) { localVariableSlots.add(opaque(InferredType.TOP)); } localVariableSlots.set(index, entry); } private StackEntry top() { return Iterables.getLast(operandStack); } /** * Create the slots for local variables at the very beginning of the method with the information * of the declaring class and the method descriptor. */ private static ArrayList createInitialLocalVariableSlots( int access, String ownerClass, String methodName, String methodDescriptor) { ArrayList entries = new ArrayList<>(); if (!isStatic(access)) { // Instance method, and this is the receiver entries.add(opaque(InferredType.create(convertToDescriptor(ownerClass)))); } Type[] argumentTypes = Type.getArgumentTypes(methodDescriptor); for (Type argumentType : argumentTypes) { switch (argumentType.getSort()) { case Type.BOOLEAN: case Type.BYTE: case Type.CHAR: case Type.SHORT: case Type.INT: entries.add(opaque(InferredType.INT)); break; case Type.FLOAT: entries.add(opaque(InferredType.FLOAT)); break; case Type.LONG: entries.add(opaque(InferredType.LONG)); entries.add(opaque(InferredType.TOP)); break; case Type.DOUBLE: entries.add(opaque(InferredType.DOUBLE)); entries.add(opaque(InferredType.TOP)); break; case Type.ARRAY: case Type.OBJECT: entries.add(opaque(InferredType.create(argumentType.getDescriptor()))); break; default: throw new RuntimeException( "Unhandled argument type: " + argumentType + " in " + ownerClass + "." + methodName + methodDescriptor); } } return entries; } private static ImmutableList removeBackFromList( ImmutableList list, int countToRemove) { int origSize = list.size(); int index = origSize - 1; while (index >= 0 && countToRemove > 0) { InferredType type = list.get(index).type(); if (type.equals(InferredType.TOP) && index > 0 && list.get(index - 1).type().isCategory2()) { --index; // A category 2 takes two slots. } --index; // Eat this local variable. --countToRemove; } checkState( countToRemove == 0, "countToRemove is %s but not 0. index=%s, list=%s", countToRemove, index, list); return list.subList(0, index + 1); } private ImmutableList appendArrayToList( ImmutableList list, int size, Object[] array) { ImmutableList.Builder builder = ImmutableList.builder(); builder.addAll(list); for (int i = 0; i < size; ++i) { InferredType type = convertTypeInStackMapFrame(array[i]); builder.add(opaque(type)); if (type.isCategory2()) { builder.add(opaque(InferredType.TOP)); } } return builder.build(); } /** Convert the type in stack map frame to inference type. */ private InferredType convertTypeInStackMapFrame(Object typeInStackMapFrame) { if (typeInStackMapFrame == Opcodes.TOP) { return InferredType.TOP; } else if (typeInStackMapFrame == Opcodes.INTEGER) { return InferredType.INT; } else if (typeInStackMapFrame == Opcodes.FLOAT) { return InferredType.FLOAT; } else if (typeInStackMapFrame == Opcodes.DOUBLE) { return InferredType.DOUBLE; } else if (typeInStackMapFrame == Opcodes.LONG) { return InferredType.LONG; } else if (typeInStackMapFrame == Opcodes.NULL) { return InferredType.NULL; } else if (typeInStackMapFrame == Opcodes.UNINITIALIZED_THIS) { return InferredType.UNINITIALIZED_THIS; } else if (typeInStackMapFrame instanceof String) { String referenceTypeName = (String) typeInStackMapFrame; if (referenceTypeName.charAt(0) == '[') { return InferredType.create(referenceTypeName); } else { return InferredType.create('L' + referenceTypeName + ';'); } } else if (typeInStackMapFrame instanceof Label) { return InferredType.UNINITIALIZED; } else { throw new RuntimeException( "Cannot reach here. Unhandled element: value=" + typeInStackMapFrame + ", class=" + typeInStackMapFrame.getClass() + ". The current method being desugared is " + methodSignature); } } private ImmutableList convertTypesInStackMapFrame(int size, Object[] array) { ImmutableList.Builder builder = ImmutableList.builder(); for (int i = 0; i < size; ++i) { InferredType type = convertTypeInStackMapFrame(array[i]); builder.add(opaque(type)); if (type.isCategory2()) { builder.add(opaque(InferredType.TOP)); } } return builder.build(); } } /** A value class to represent a frame. */ @AutoValue @CopyAnnotations @GwtIncompatible @J2ktIncompatible abstract static class FrameInfo { static FrameInfo create(ImmutableList locals, ImmutableList stack) { return new AutoValue_ActualValueInference_FrameInfo(locals, stack); } abstract ImmutableList locals(); abstract ImmutableList stack(); } /** A method invocation. */ @AutoValue @CopyAnnotations @GwtIncompatible @J2ktIncompatible abstract static class Invocation { static Builder builder(String name) { return new AutoValue_ActualValueInference_Invocation.Builder().setName(name); } /** The receiver of this call, if it is an instance call. */ abstract @Nullable StackEntry receiver(); /** The value being passed to this call if it is an {@code assertThat} or {@code that} call. */ abstract @Nullable StackEntry actualValue(); /** * The value being passed to this call if it is a boxing call (e.g., {@code Integer.valueOf}). */ abstract @Nullable StackEntry boxingInput(); abstract String name(); final StackEntry deriveEntry(InferredType type, boolean hasParams) { if (boxingInput() != null && boxingInput().description() != null) { return described(type, boxingInput().description()); } else if (actualValue() != null) { return subjectFor(type, actualValue()); } else if (isOnSubjectInstance()) { return subjectFor(type, checkNotNull(receiver()).actualValue()); } else if (BORING_NAMES.contains(name())) { /* * TODO(cpovirk): For no-arg instance methods like get(), return "foo.get()," where "foo" is * the description we had for the receiver (if any). */ return opaque(type); } else { return described(type, name() + (hasParams ? "(...)" : "()")); } } final boolean isOnSubjectInstance() { return receiver() != null && receiver().isSubject(); } @AutoValue.Builder abstract static class Builder { abstract Builder setReceiver(StackEntry receiver); abstract Builder setActualValue(StackEntry actualValue); abstract Builder setBoxingInput(StackEntry boxingInput); abstract Builder setName(String name); abstract Invocation build(); } } /** This is the type used for type inference. */ @AutoValue @CopyAnnotations @GwtIncompatible @J2ktIncompatible abstract static class InferredType { static final String UNINITIALIZED_PREFIX = "UNINIT@"; static final InferredType BOOLEAN = new AutoValue_ActualValueInference_InferredType("Z"); static final InferredType BYTE = new AutoValue_ActualValueInference_InferredType("B"); static final InferredType INT = new AutoValue_ActualValueInference_InferredType("I"); static final InferredType FLOAT = new AutoValue_ActualValueInference_InferredType("F"); static final InferredType LONG = new AutoValue_ActualValueInference_InferredType("J"); static final InferredType DOUBLE = new AutoValue_ActualValueInference_InferredType("D"); /** Not a real value. */ static final InferredType TOP = new AutoValue_ActualValueInference_InferredType("TOP"); /** The value NULL */ static final InferredType NULL = new AutoValue_ActualValueInference_InferredType("NULL"); static final InferredType UNINITIALIZED_THIS = new AutoValue_ActualValueInference_InferredType("UNINITIALIZED_THIS"); static final InferredType UNINITIALIZED = new AutoValue_ActualValueInference_InferredType(UNINITIALIZED_PREFIX); /** Create a type for a value. */ static InferredType create(String descriptor) { if (descriptor.equals(UNINITIALIZED_PREFIX)) { return UNINITIALIZED; } char firstChar = descriptor.charAt(0); if (firstChar == 'L' || firstChar == '[') { // Reference, array. return new AutoValue_ActualValueInference_InferredType(descriptor); } switch (descriptor) { case "Z": return BOOLEAN; case "B": return BYTE; case "I": return INT; case "F": return FLOAT; case "J": return LONG; case "D": return DOUBLE; case "TOP": return TOP; case "NULL": return NULL; case "UNINITIALIZED_THIS": return UNINITIALIZED_THIS; default: throw new RuntimeException("Invalid descriptor: " + descriptor); } } abstract String descriptor(); @Override public final String toString() { return descriptor(); } /** Is a category 2 value? */ boolean isCategory2() { String descriptor = descriptor(); return descriptor.equals("J") || descriptor.equals("D"); } /** If the type is an array, return the element type. Otherwise, throw an exception. */ InferredType getElementTypeIfArrayOrThrow() { String descriptor = descriptor(); checkState(descriptor.charAt(0) == '[', "This type %s is not an array.", this); return create(descriptor.substring(1)); } /** Is an uninitialized value? */ boolean isUninitialized() { return descriptor().startsWith(UNINITIALIZED_PREFIX); } } private static final class InferenceClassVisitor extends ClassVisitor { /** * The method to visit. * *

We don't really need the method name: We could just visit the whole class, since we * look at data for only the relevant line. But it's nice not to process the whole class, * especially during debugging. (And it might also help avoid triggering any bugs in the * inference code.) */ private final String methodNameToVisit; private final ImmutableSetMultimap.Builder actualValueAtLine = ImmutableSetMultimap.builder(); // TODO(cpovirk): Can the class visitor pass the name in? private String className; InferenceClassVisitor(String methodNameToVisit) { super(Opcodes.ASM9); this.methodNameToVisit = methodNameToVisit; } @Override public void visit( int version, int access, String name, String signature, String superName, String[] interfaces) { className = name; } @Override public @Nullable MethodVisitor visitMethod( int access, String name, String desc, String signature, String[] exceptions) { /* * Each InferenceMethodVisitor instance may be used only once. Still, it might seem like we * can get away with creating a single instance at construction time. However, we know only * the name of the method that we're visiting, not its full signature, so we may need to visit * multiple methods with that name, each with a fresh visitor. */ return methodNameToVisit.equals(name) ? new InferenceMethodVisitor(access, className, name, desc, actualValueAtLine) : null; } } /* * TODO(cpovirk): Expand this, maybe based on data about the most common method calls passed to * assertThat(). */ private static final ImmutableSet BORING_NAMES = ImmutableSet.of( "asList", "build", "collect", "copyOf", "create", "from", "get", "iterator", "of", "toArray", "toString", "valueOf"); private static boolean isThatOrAssertThat(String owner, String name) { /* * TODO(cpovirk): Handle CustomSubjectBuilder. That requires looking at the type hierarchy, as * users always have an instance of a specific subtype. Also keep in mind that the that(...) * method might accept more than 1 parameter, like `that(className, methodName)` and/or that it * might have category-2 parameters. * * TODO(cpovirk): Handle custom assertThat methods. The challenges are similar. */ return (owner.equals("com/google/common/truth/Truth") && name.equals("assertThat")) || (owner.equals("com/google/common/truth/StandardSubjectBuilder") && name.equals("that")) || (owner.equals("com/google/common/truth/SimpleSubjectBuilder") && name.equals("that")) || (owner.equals("com/google/common/truth/Expect") && name.equals("that")); } private static boolean isBoxing(String owner, String name, String desc) { return name.equals("valueOf") && PRIMITIVE_WRAPPERS.contains(owner) /* * Don't handle valueOf(String s[, int radix]). The valueOf support is really here for * autoboxing, as in "assertThat(primitive)," not for * "assertThat(Integer.valueOf(...))." Not that there's anything really *wrong* with * handling manual boxing of primitives -- good thing, since we can't distinguish the two -- * but we're not interested in handling the valueOf methods that *parse*. That's mainly * because there's a type conversion, so some assertions might succeed on a string and fail * on the parsed number (or vice versa). */ && !Type.getArgumentTypes(desc)[0].equals(Type.getType(String.class)); } private static final ImmutableSet PRIMITIVE_WRAPPERS = ImmutableSet.of( "java/lang/Boolean", "java/lang/Byte", "java/lang/Character", "java/lang/Double", "java/lang/Float", "java/lang/Integer", "java/lang/Long", "java/lang/Short"); private static boolean isStatic(int access) { return isSet(access, Opcodes.ACC_STATIC); } /** * Returns {@code true} iff all bits in {@code bitmask} are set in {@code flags}. Trivially * returns {@code true} if {@code bitmask} is 0. */ private static boolean isSet(int flags, int bitmask) { return (flags & bitmask) == bitmask; } private static void closeQuietly(@Nullable InputStream stream) { if (stream == null) { return; } try { stream.close(); } catch (IOException e) { // TODO(cpovirk): Log a warning? } } private ActualValueInference() {} }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy