Skip to content

Commit

Permalink
Do not strip type information from arguments (spockframework#871)
Browse files Browse the repository at this point in the history
  • Loading branch information
Vampire committed Apr 5, 2020
1 parent b0507d9 commit ef0338f
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 29 deletions.
Expand Up @@ -43,35 +43,35 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo
return mockObject;
}

// sometimes we see an argument wrapped in PojoWrapper
// example is when GroovyObject.invokeMethod is invoked directly
Object[] normalizedArgs = GroovyRuntimeUtil.asUnwrappedArgumentArray(arguments);
// we do not need the cast information from the wrappers here, the method selection
// is already done, so unwrap the argument array if there are still wrappers present
arguments = GroovyRuntimeUtil.asUnwrappedArgumentArray(arguments);

if (isMethod(method, "getMetaClass")) {
return mockMetaClass;
}
if (isMethod(method, "invokeMethod", String.class, Object.class)) {
return GroovyRuntimeUtil.invokeMethod(target,
(String) normalizedArgs[0], GroovyRuntimeUtil.asArgumentArray(normalizedArgs[1]));
(String) arguments[0], GroovyRuntimeUtil.asArgumentArray(arguments[1]));
}
if (isMethod(method, "getProperty", String.class)) {
String methodName = GroovyRuntimeUtil.propertyToMethodName("get", (String) normalizedArgs[0]);
String methodName = GroovyRuntimeUtil.propertyToMethodName("get", (String) arguments[0]);
return GroovyRuntimeUtil.invokeMethod(target, methodName);
}
if (isMethod(method, "setProperty", String.class, Object.class)) {
String methodName = GroovyRuntimeUtil.propertyToMethodName("set", (String) normalizedArgs[0]);
return GroovyRuntimeUtil.invokeMethod(target, methodName, normalizedArgs[1]);
String methodName = GroovyRuntimeUtil.propertyToMethodName("set", (String) arguments[0]);
return GroovyRuntimeUtil.invokeMethod(target, methodName, arguments[1]);
}
if (isMethod(method, "methodMissing", String.class, Object.class)) {
throw new MissingMethodException((String) normalizedArgs[0],
mockConfiguration.getType(), new Object[] {normalizedArgs[1]}, false);
throw new MissingMethodException((String) arguments[0],
mockConfiguration.getType(), new Object[] {arguments[1]}, false);
}
if (isMethod(method, "propertyMissing", String.class)) {
throw new MissingPropertyException((String) normalizedArgs[0], mockConfiguration.getType());
throw new MissingPropertyException((String) arguments[0], mockConfiguration.getType());
}

IMockMethod mockMethod = new StaticMockMethod(method, mockConfiguration.getExactType());
IMockInvocation invocation = new MockInvocation(mockObject, mockMethod, Arrays.asList(normalizedArgs), realMethodInvoker);
IMockInvocation invocation = new MockInvocation(mockObject, mockMethod, Arrays.asList(arguments), realMethodInvoker);
IMockController controller = specification.getSpecificationContext().getMockController();

return controller.handle(invocation);
Expand Down
Expand Up @@ -62,8 +62,7 @@ public void setProperty(Object target, String property, Object newValue) {
}

private Object doInvokeMethod(Object target, String methodName, Object[] arguments, boolean isStatic) {
// get rid of PojoWrapper's; not sure where they come from, but they sometimes appear
arguments = GroovyRuntimeUtil.asUnwrappedArgumentArray(arguments);
arguments = GroovyRuntimeUtil.asArgumentArray(arguments);

if (isGetMetaClassCallOnGroovyObject(target, methodName, arguments, isStatic)) {
// We handle this case explicitly because strangely enough, delegate.pickMethod()
Expand All @@ -76,11 +75,13 @@ private Object doInvokeMethod(Object target, String methodName, Object[] argumen

MetaMethod metaMethod = delegate.pickMethod(methodName, ReflectionUtil.getTypes(arguments));
Method method = GroovyRuntimeUtil.toMethod(metaMethod);
// we evaluated the cast information from wrappers in getTypes above, now we need the pure arguments
Object[] unwrappedArgs = GroovyRuntimeUtil.asUnwrappedArgumentArray(arguments);

if (method != null && method.getDeclaringClass().isAssignableFrom(configuration.getType())) {
if (!isStatic && !ReflectionUtil.isFinalMethod(method) && !configuration.isGlobal()) {
// use standard proxy dispatch
return metaMethod.invoke(target, arguments);
return metaMethod.invoke(target, unwrappedArgs);
}
}

Expand All @@ -89,13 +90,13 @@ private Object doInvokeMethod(Object target, String methodName, Object[] argumen
// to check if a GroovyObject method was called
if (metaMethod != null && metaMethod.getDeclaringClass().getTheClass() == GroovyObject.class) {
if ("invokeMethod".equals(methodName)) {
return invokeMethod(target, (String) arguments[0], GroovyRuntimeUtil.asArgumentArray(arguments[1]));
return invokeMethod(target, (String) unwrappedArgs[0], GroovyRuntimeUtil.asArgumentArray(unwrappedArgs[1]));
}
if ("getProperty".equals(methodName)) {
return getProperty(target, (String) arguments[0]);
return getProperty(target, (String) unwrappedArgs[0]);
}
if ("setProperty".equals(methodName)) {
setProperty(target, (String) arguments[0], arguments[1]);
setProperty(target, (String) unwrappedArgs[0], unwrappedArgs[1]);
return null;
}
// getMetaClass was already handled earlier; setMetaClass isn't handled specially
Expand Down
Expand Up @@ -44,9 +44,13 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo
return mockObject;
}

// sometimes we see an argument wrapped in PojoWrapper
// example is when GroovyObject.invokeMethod is invoked directly
Object[] normalizedArgs = GroovyRuntimeUtil.asUnwrappedArgumentArray(arguments);
// here no instances of org.codehaus.groovy.runtime.wrappers.Wrapper subclasses
// should arrive in the arguments array. If there are some found, it should first
// be investigated whether they should have made it until here. If it is correct
// that they arrived here, maybe GroovyRuntimeUtil.asUnwrappedArgumentArray needs
// to be used to unwrap the arguments. Wrapper subclasses are used to transport
// type cast information to select proper overloaded methods.
arguments = GroovyRuntimeUtil.asArgumentArray(arguments);

if (target instanceof GroovyObject) {
if (isMethod(method, "getMetaClass")) {
Expand All @@ -58,8 +62,8 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo
if ("org.codehaus.groovy.runtime.ScriptBytecodeAdapter".equals(mockCaller.getClassName())) {
// HACK: for some reason, runtime dispatches direct property access on mock classes via ScriptBytecodeAdapter
// delegate to the corresponding setter method
String methodName = GroovyRuntimeUtil.propertyToMethodName("set", (String) normalizedArgs[0]);
return GroovyRuntimeUtil.invokeMethod(target, methodName, GroovyRuntimeUtil.asArgumentArray(normalizedArgs[1]));
String methodName = GroovyRuntimeUtil.propertyToMethodName("set", (String) arguments[0]);
return GroovyRuntimeUtil.invokeMethod(target, methodName, GroovyRuntimeUtil.asArgumentArray(arguments[1]));
}
}
if (GroovyRuntimeUtil.isGroovy3orNewer()) {
Expand All @@ -72,15 +76,15 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo
if (!("groovy.lang.GroovyObject$getProperty".equals(mockCaller.getClassName()) && "call".equals(mockCaller.getMethodName()))) {
//HACK: Only explicit getter executions (go.foo and go.getFoo()) should be deeper processed.
//go.getProperty("foo") is treated as is (to allow for its stubbing)
String methodName = GroovyRuntimeUtil.propertyToMethodName("get", (String) normalizedArgs[0]);
String methodName = GroovyRuntimeUtil.propertyToMethodName("get", (String) arguments[0]);
return GroovyRuntimeUtil.invokeMethod(target, methodName);
}
}
}
}

IMockMethod mockMethod = new StaticMockMethod(method, mockConfiguration.getExactType());
IMockInvocation invocation = new MockInvocation(mockObject, mockMethod, Arrays.asList(normalizedArgs), realMethodInvoker);
IMockInvocation invocation = new MockInvocation(mockObject, mockMethod, Arrays.asList(arguments), realMethodInvoker);
IMockController mockController = specification == null ? getFallbackMockController() :
specification.getSpecificationContext().getMockController();

Expand Down
Expand Up @@ -225,7 +225,7 @@ public static Iterator<Object> asIterator(Object object) {
}

public static Object[] asUnwrappedArgumentArray(Object args) {
return InvokerHelper.asUnwrappedArray(args);
return InvokerHelper.asUnwrappedArray(InvokerHelper.asArray(args).clone());
}

/**
Expand Down
Expand Up @@ -14,6 +14,7 @@

package org.spockframework.util;

import org.codehaus.groovy.runtime.MetaClassHelper;
import org.spockframework.gentyref.GenericTypeReflector;

import java.io.File;
Expand Down Expand Up @@ -187,10 +188,7 @@ public static boolean hasAnyOfTypes(Object value, Class<?>... types) {
}

public static Class[] getTypes(Object... objects) {
Class[] classes = new Class[objects.length];
for (int i = 0; i < objects.length; i++)
classes[i] = ObjectUtil.getClass(objects[i]);
return classes;
return MetaClassHelper.convertToTypeArray(objects);
}

@Nullable
Expand Down
Expand Up @@ -6,6 +6,7 @@

import groovy.lang.Range;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.wrappers.Wrapper;
import org.w3c.dom.Element;

public abstract class RenderUtil {
Expand Down Expand Up @@ -47,13 +48,21 @@ public static String toStringOrDump(@Nullable Object value) {
if (isHandledByInspectOrToString(value)) {
return GroovyRuntimeUtil.toString(value);
}
if (value instanceof Wrapper) {
Wrapper wrapper = (Wrapper) value;
return String.format("%s as %s", toStringOrDump(wrapper.unwrap()), wrapper.getType().getSimpleName());
}
return dump(value);
}

public static String inspectOrDump(@Nullable Object value) {
if (isHandledByInspectOrToString(value)) {
return DefaultGroovyMethods.inspect(value);
}
if (value instanceof Wrapper) {
Wrapper wrapper = (Wrapper) value;
return String.format("%s as %s", inspectOrDump(wrapper.unwrap()), wrapper.getType().getSimpleName());
}
return dump(value);
}

Expand Down
Expand Up @@ -15,7 +15,9 @@
package org.spockframework.smoke.mock

import java.lang.reflect.Modifier
import java.util.regex.Pattern

import spock.lang.Issue
import spock.lang.Specification

class GroovySpiesThatAreGlobal extends Specification {
Expand Down Expand Up @@ -209,6 +211,63 @@ class GroovySpiesThatAreGlobal extends Specification {
0 * _
}

@Issue("https://github.com/spockframework/spock/issues/871")
def "type casts are not swallowed"() {
given:
GroovySpy(PersonWithOverloadedMethods, global: true)

when:
def result = PersonWithOverloadedMethods.perform("null string" as String)

then:
result == "String"

when:
result = PersonWithOverloadedMethods.perform(null as Pattern)

then:
result == "Pattern"

when:
result = PersonWithOverloadedMethods.perform((String) null)

then:
result == "String"

when:
result = PersonWithOverloadedMethods.perform((Pattern) null)

then:
result == "Pattern"

when:
PersonWithOverloadedMethods.perform(_) >> "Foo"

and:
result = PersonWithOverloadedMethods.perform(null as String)

then:
result == "Foo"

when:
result = PersonWithOverloadedMethods.perform(null as Pattern)

then:
result == "Foo"

when:
result = PersonWithOverloadedMethods.perform((String) null)

then:
result == "Foo"

when:
result = PersonWithOverloadedMethods.perform((Pattern) null)

then:
result == "Foo"
}

static class Person {
String name
int age
Expand All @@ -233,4 +292,14 @@ class GroovySpiesThatAreGlobal extends Specification {
this.age = age
}
}

static class PersonWithOverloadedMethods {
static String perform(String work) {
"String"
}

static String perform(Pattern work) {
"Pattern"
}
}
}
Expand Up @@ -19,6 +19,8 @@ package org.spockframework.smoke.mock
import org.spockframework.EmbeddedSpecification
import org.spockframework.mock.TooManyInvocationsError

import java.util.regex.Pattern

class TooManyInvocations extends EmbeddedSpecification {
def "shows matched invocations, ordered by last occurrence"() {
when:
Expand Down Expand Up @@ -49,4 +51,24 @@ Matching invocations (ordered by last occurrence):
1 * list.add(1)
""".trim()
}

def "casts are printed for mock invocations"() {
given:
runner.addClassImport(Pattern)

when:
runner.runFeatureBody """
GroovyMock(Pattern, global: true)
when:
Pattern.compile(null as String)
then:
0 * Pattern.compile(_)
"""

then:
TooManyInvocationsError e = thrown()
e.message.contains('<Pattern>.compile(null as String)')
}
}

0 comments on commit ef0338f

Please sign in to comment.