diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc
index d52a9c7324..5722942ff4 100644
--- a/docs/release_notes.adoc
+++ b/docs/release_notes.adoc
@@ -26,6 +26,7 @@ include::include.adoc[]
* Size of data providers is no longer calculated multiple times but only once
* Fix exception when using `@RepeatUntilFailure` with a data provider with unknown iteration amount. spockPull:2031[]
* Clarified documentation about data providers and `size()` calls spockIssue:2022[]
+* Add best-effort error reporting for interactions on final methods when using the `byte-buddy` mock maker spockIssue:2039[]
== 2.4-M4 (2024-03-21)
diff --git a/spock-core/src/main/java/org/spockframework/compiler/AstUtil.java b/spock-core/src/main/java/org/spockframework/compiler/AstUtil.java
index 61c1287c40..9ada98adba 100644
--- a/spock-core/src/main/java/org/spockframework/compiler/AstUtil.java
+++ b/spock-core/src/main/java/org/spockframework/compiler/AstUtil.java
@@ -40,7 +40,6 @@
*/
public abstract class AstUtil {
private static final Pattern DATA_TABLE_SEPARATOR = Pattern.compile("_{2,}+");
- private static final String GET_METHOD_NAME = "get";
private static final String GET_AT_METHOD_NAME = new IntegerArrayGetAtMetaMethod().getName();
/**
diff --git a/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java b/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java
index 3b02908707..9fb14f8b81 100644
--- a/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java
+++ b/spock-core/src/main/java/org/spockframework/compiler/SpecRewriter.java
@@ -17,6 +17,7 @@
package org.spockframework.compiler;
import org.spockframework.compiler.model.*;
+import org.spockframework.runtime.GroovyRuntimeUtil;
import org.spockframework.runtime.SpockException;
import org.spockframework.util.*;
@@ -26,7 +27,6 @@
import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.*;
-import org.codehaus.groovy.runtime.MetaClassHelper;
import org.codehaus.groovy.syntax.*;
import org.objectweb.asm.Opcodes;
@@ -109,7 +109,7 @@ private void changeFinalFieldInternalName(Field field) {
}
private void createSharedFieldGetter(Field field) {
- String getterName = "get" + MetaClassHelper.capitalize(field.getName());
+ String getterName = GroovyRuntimeUtil.propertyToGetterMethodName(field.getName());
MethodNode getter = spec.getAst().getMethod(getterName, Parameter.EMPTY_ARRAY);
if (getter != null) {
errorReporter.error(field.getAst(),
@@ -135,7 +135,7 @@ private void createSharedFieldGetter(Field field) {
}
private void createFinalFieldGetter(Field field) {
- String getterName = "get" + MetaClassHelper.capitalize(field.getName());
+ String getterName = GroovyRuntimeUtil.propertyToGetterMethodName(field.getName());
MethodNode getter = spec.getAst().getMethod(getterName, Parameter.EMPTY_ARRAY);
if (getter != null) {
errorReporter.error(field.getAst(),
@@ -158,7 +158,7 @@ private void createFinalFieldGetter(Field field) {
}
private void createSharedFieldSetter(Field field) {
- String setterName = "set" + MetaClassHelper.capitalize(field.getName());
+ String setterName = GroovyRuntimeUtil.propertyToSetterMethodName(field.getName());
Parameter[] params = new Parameter[] { new Parameter(field.getAst().getType(), "$spock_value") };
MethodNode setter = spec.getAst().getMethod(setterName, params);
if (setter != null) {
diff --git a/spock-core/src/main/java/org/spockframework/compiler/SpockNames.java b/spock-core/src/main/java/org/spockframework/compiler/SpockNames.java
index aa4edf9024..181aeb4b8b 100644
--- a/spock-core/src/main/java/org/spockframework/compiler/SpockNames.java
+++ b/spock-core/src/main/java/org/spockframework/compiler/SpockNames.java
@@ -1,8 +1,18 @@
package org.spockframework.compiler;
+import org.spockframework.mock.ISpockMockObject;
+
public class SpockNames {
public static final String VALUE_RECORDER = "$spock_valueRecorder";
public static final String ERROR_COLLECTOR = "$spock_errorCollector";
public static final String OLD_VALUE = "$spock_oldValue";
public static final String SPOCK_EX = "$spock_ex";
+ /**
+ * Name of the method {@link ISpockMockObject#$spock_get()}.
+ */
+ public static final String SPOCK_GET = "$spock_get";
+ /**
+ * Name of the method {@link ISpockMockObject#$spock_mockInteractionValidator()}.
+ */
+ public static final String SPOCK_MOCK_INTERATION_VALIDATOR = "$spock_mockInteractionValidator";
}
diff --git a/spock-core/src/main/java/org/spockframework/mock/IMockObject.java b/spock-core/src/main/java/org/spockframework/mock/IMockObject.java
index 44407da2e8..4157ec5c29 100644
--- a/spock-core/src/main/java/org/spockframework/mock/IMockObject.java
+++ b/spock-core/src/main/java/org/spockframework/mock/IMockObject.java
@@ -15,6 +15,7 @@
package org.spockframework.mock;
import org.spockframework.mock.runtime.SpecificationAttachable;
+import org.spockframework.util.Beta;
import org.spockframework.util.Nullable;
import spock.lang.Specification;
@@ -29,6 +30,13 @@ public interface IMockObject extends SpecificationAttachable {
@Nullable
String getName();
+ /**
+ * Returns the {@link #getName()} of this mock object, or {@code "unnamed"} if it has no name.
+ *
+ * @return the name of this mock object, or {@code "unnamed"} if it has no name
+ */
+ String getMockName();
+
/**
* Returns the declared type of this mock object.
*
@@ -81,4 +89,12 @@ public interface IMockObject extends SpecificationAttachable {
* @return whether this mock object matches the target of the specified interaction
*/
boolean matches(Object target, IMockInteraction interaction);
+
+ /**
+ * Returns the used mock configuration which created this mock.
+ *
+ * @return the mock configuration
+ */
+ @Beta
+ IMockConfiguration getConfiguration();
}
diff --git a/spock-core/src/main/java/org/spockframework/mock/ISpockMockObject.java b/spock-core/src/main/java/org/spockframework/mock/ISpockMockObject.java
index c9e1595960..4ddd72002a 100644
--- a/spock-core/src/main/java/org/spockframework/mock/ISpockMockObject.java
+++ b/spock-core/src/main/java/org/spockframework/mock/ISpockMockObject.java
@@ -14,10 +14,28 @@
package org.spockframework.mock;
+import org.spockframework.mock.runtime.IMockInteractionValidator;
+import org.spockframework.util.Beta;
+import org.spockframework.util.Nullable;
+
/**
- * Marker-like interface implemented by all mock objects that allows
- * {@link MockUtil} to detect mock objects. Not intended for direct use.
+ * MockObject interface implemented by some {@link spock.mock.MockMakers} that allows the {@link org.spockframework.mock.runtime.MockMakerRegistry}
+ * to detect mock objects.
+ *
+ *
Not intended for direct use.
*/
public interface ISpockMockObject {
+
IMockObject $spock_get();
+
+ /**
+ * @return the {@link IMockInteractionValidator} used to verify {@link IMockInteraction}
+ * @see IMockInteractionValidator
+ * @since 2.4
+ */
+ @Nullable
+ @Beta
+ default IMockInteractionValidator $spock_mockInteractionValidator() {
+ return null;
+ }
}
diff --git a/spock-core/src/main/java/org/spockframework/mock/MockUtil.java b/spock-core/src/main/java/org/spockframework/mock/MockUtil.java
index 745525cd80..aa2e943fd1 100644
--- a/spock-core/src/main/java/org/spockframework/mock/MockUtil.java
+++ b/spock-core/src/main/java/org/spockframework/mock/MockUtil.java
@@ -35,7 +35,7 @@ public class MockUtil {
* @return whether the given object is a (Spock) mock object
*/
public boolean isMock(Object object) {
- return getMockMakerRegistry().asMockOrNull(object) != null;
+ return asMockOrNull(object) != null;
}
/**
@@ -69,6 +69,17 @@ public IMockObject asMock(Object object) {
return mockOrNull;
}
+ /**
+ * Returns information about a mock object or {@code null}, if the object is not a mock.
+ *
+ * @param object a mock object
+ * @return information about the mock object or {@code null}, if the object is not a mock.
+ */
+ @Nullable
+ public IMockObject asMockOrNull(Object object) {
+ return getMockMakerRegistry().asMockOrNull(object);
+ }
+
/**
* Attaches mock to a Specification.
*
diff --git a/spock-core/src/main/java/org/spockframework/mock/constraint/EqualMethodNameConstraint.java b/spock-core/src/main/java/org/spockframework/mock/constraint/EqualMethodNameConstraint.java
index 020120e600..82196a538f 100644
--- a/spock-core/src/main/java/org/spockframework/mock/constraint/EqualMethodNameConstraint.java
+++ b/spock-core/src/main/java/org/spockframework/mock/constraint/EqualMethodNameConstraint.java
@@ -31,6 +31,10 @@ public EqualMethodNameConstraint(String methodName) {
this.methodName = methodName;
}
+ public String getMethodName() {
+ return methodName;
+ }
+
@Override
public boolean isSatisfiedBy(IMockInvocation invocation) {
return invocation.getMethod().getName().equals(methodName);
diff --git a/spock-core/src/main/java/org/spockframework/mock/constraint/EqualPropertyNameConstraint.java b/spock-core/src/main/java/org/spockframework/mock/constraint/EqualPropertyNameConstraint.java
index 737916baef..eb21660dff 100644
--- a/spock-core/src/main/java/org/spockframework/mock/constraint/EqualPropertyNameConstraint.java
+++ b/spock-core/src/main/java/org/spockframework/mock/constraint/EqualPropertyNameConstraint.java
@@ -29,6 +29,10 @@ public EqualPropertyNameConstraint(String propertyName) {
this.propertyName = propertyName;
}
+ public String getPropertyName() {
+ return propertyName;
+ }
+
@Override
public boolean isSatisfiedBy(IMockInvocation invocation) {
return propertyName.equals(getPropertyName(invocation));
diff --git a/spock-core/src/main/java/org/spockframework/mock/constraint/TargetConstraint.java b/spock-core/src/main/java/org/spockframework/mock/constraint/TargetConstraint.java
index 5df131f027..b6e3753ac4 100644
--- a/spock-core/src/main/java/org/spockframework/mock/constraint/TargetConstraint.java
+++ b/spock-core/src/main/java/org/spockframework/mock/constraint/TargetConstraint.java
@@ -17,6 +17,7 @@
package org.spockframework.mock.constraint;
import org.spockframework.mock.*;
+import org.spockframework.mock.runtime.IMockInteractionValidator;
import org.spockframework.runtime.Condition;
import org.spockframework.runtime.InvalidSpecException;
import org.spockframework.util.CollectionUtil;
@@ -50,12 +51,27 @@ public String describeMismatch(IMockInvocation invocation) {
@Override
public void setInteraction(IMockInteraction interaction) {
this.interaction = interaction;
- if (interaction.isRequired() && MOCK_UTIL.isMock(target)) {
- IMockObject mockObject = MOCK_UTIL.asMock(target);
+ IMockObject mockObject = MOCK_UTIL.asMockOrNull(target);
+ if (mockObject != null) {
+ checkRequiredInteraction(mockObject, interaction);
+ validateMockInteraction(mockObject, interaction);
+ }
+ }
+
+ private void checkRequiredInteraction(IMockObject mockObject, IMockInteraction interaction) {
+ if (interaction.isRequired()) {
if (!mockObject.isVerified()) {
- String mockName = mockObject.getName() != null ? mockObject.getName() : "unnamed";
throw new InvalidSpecException("Stub '%s' matches the following required interaction:" +
- "\n\n%s\n\nRemove the cardinality (e.g. '1 *'), or turn the stub into a mock.\n").withArgs(mockName, interaction);
+ "\n\n%s\n\nRemove the cardinality (e.g. '1 *'), or turn the stub into a mock.\n").withArgs(mockObject.getMockName(), interaction);
+ }
+ }
+ }
+
+ private void validateMockInteraction(IMockObject mockObject, IMockInteraction interaction) {
+ if (target instanceof ISpockMockObject) {
+ IMockInteractionValidator validation = ((ISpockMockObject) target).$spock_mockInteractionValidator();
+ if (validation != null) {
+ validation.validateMockInteraction(mockObject, interaction);
}
}
}
diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/BaseMockInterceptor.java b/spock-core/src/main/java/org/spockframework/mock/runtime/BaseMockInterceptor.java
index 3af8948528..a4e0bf5510 100644
--- a/spock-core/src/main/java/org/spockframework/mock/runtime/BaseMockInterceptor.java
+++ b/spock-core/src/main/java/org/spockframework/mock/runtime/BaseMockInterceptor.java
@@ -1,5 +1,8 @@
package org.spockframework.mock.runtime;
+import org.spockframework.compiler.SpockNames;
+import org.spockframework.mock.IMockObject;
+import org.spockframework.mock.ISpockMockObject;
import org.spockframework.runtime.GroovyRuntimeUtil;
import java.lang.reflect.Method;
@@ -8,8 +11,13 @@
import groovy.lang.*;
import org.jetbrains.annotations.Nullable;
+import org.spockframework.util.ReflectionUtil;
+
+import static java.util.Objects.requireNonNull;
public abstract class BaseMockInterceptor implements IProxyBasedMockInterceptor {
+
+
private MetaClass mockMetaClass;
BaseMockInterceptor(MetaClass mockMetaClass) {
@@ -39,11 +47,11 @@ protected String handleGetProperty(GroovyObject target, Object[] args) {
MetaClass metaClass = target.getMetaClass();
//First try the isXXX before getXXX, because this is the expected behavior, if it is boolean property.
MetaMethod booleanVariant = metaClass
- .getMetaMethod(GroovyRuntimeUtil.propertyToMethodName("is", propertyName), GroovyRuntimeUtil.EMPTY_ARGUMENTS);
+ .getMetaMethod(GroovyRuntimeUtil.propertyToBooleanGetterMethodName(propertyName), GroovyRuntimeUtil.EMPTY_ARGUMENTS);
if (booleanVariant != null && booleanVariant.getReturnType() == boolean.class) {
methodName = booleanVariant.getName();
} else {
- methodName = GroovyRuntimeUtil.propertyToMethodName("get", propertyName);
+ methodName = GroovyRuntimeUtil.propertyToGetterMethodName(propertyName);
}
}
return methodName;
@@ -52,4 +60,11 @@ protected String handleGetProperty(GroovyObject target, Object[] args) {
protected boolean isMethod(Method method, String name, Class>... parameterTypes) {
return method.getName().equals(name) && Arrays.equals(method.getParameterTypes(), parameterTypes);
}
+
+ public static Object handleSpockMockInterface(Method method, IMockObject mockObject) {
+ if (method.getName().equals(SpockNames.SPOCK_MOCK_INTERATION_VALIDATOR)) {
+ return null; //This should be handled in the corresponding MockMakers.
+ }
+ return mockObject;
+ }
}
diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java
index 0e4086aa5f..1d174489ad 100644
--- a/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java
+++ b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java
@@ -22,6 +22,7 @@
import net.bytebuddy.TypeCache;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.modifier.MethodManifestation;
import net.bytebuddy.description.modifier.SynchronizationState;
import net.bytebuddy.description.modifier.SyntheticState;
import net.bytebuddy.description.modifier.Visibility;
@@ -31,19 +32,23 @@
import net.bytebuddy.dynamic.loading.MultipleParentClassLoader;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import net.bytebuddy.implementation.FieldAccessor;
+import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.Morph;
import org.codehaus.groovy.runtime.callsite.AbstractCallSite;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.VisibleForTesting;
+import org.spockframework.compiler.SpockNames;
import org.spockframework.mock.ISpockMockObject;
import org.spockframework.mock.codegen.Target;
+import org.spockframework.util.ReflectionUtil;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadLocalRandom;
+import static java.util.Objects.requireNonNull;
import static net.bytebuddy.matcher.ElementMatchers.none;
class ByteBuddyMockFactory {
@@ -60,6 +65,7 @@ class ByteBuddyMockFactory {
private static final Class> CODEGEN_TARGET_CLASS = Target.class;
private static final String CODEGEN_PACKAGE = CODEGEN_TARGET_CLASS.getPackage().getName();
private static final AnnotationDescription INTERNAL_ANNOTATION = AnnotationDescription.Builder.ofType(Internal.class).build();
+ private static final Method MOCK_INTERACTION_VALIDATOR_METHOD = requireNonNull(ReflectionUtil.getMethodByName(ISpockMockObject.class, SpockNames.SPOCK_MOCK_INTERATION_VALIDATOR));
/**
* This array contains {@link TypeCachingLock} instances, which are used as java monitor locks for
@@ -136,6 +142,10 @@ Object createMock(IMockMaker.IMockCreationSettings settings) {
.method(m -> !isGroovyMOPMethod(type, m))
.intercept(mockInterceptor())
.transform(mockTransformer())
+ .method(m -> m.represents(MOCK_INTERACTION_VALIDATOR_METHOD))
+ // Implement the $spock_mockInteractionValidation() method which returns the static field below, so we have a validation instance for every mock class
+ .intercept(FixedValue.reference(new ByteBuddyMockInteractionValidator(), SpockNames.SPOCK_MOCK_INTERATION_VALIDATOR))
+ .transform(validateMockInteractionTransformer())
.implement(ByteBuddyInterceptorAdapter.InterceptorAccess.class)
.intercept(FieldAccessor.ofField("$spock_interceptor"))
.defineField("$spock_interceptor", IProxyBasedMockInterceptor.class, Visibility.PRIVATE, SyntheticState.SYNTHETIC)
@@ -149,6 +159,10 @@ Object createMock(IMockMaker.IMockCreationSettings settings) {
return proxy;
}
+ private static Transformer validateMockInteractionTransformer() {
+ return Transformer.ForMethod.withModifiers(SynchronizationState.PLAIN, Visibility.PUBLIC, MethodManifestation.FINAL);
+ }
+
private static Transformer mockTransformer() {
return Transformer.ForMethod.withModifiers(SynchronizationState.PLAIN, Visibility.PUBLIC); //Overridden methods should be public and non-synchronized.
}
diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockInteractionValidator.java b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockInteractionValidator.java
new file mode 100644
index 0000000000..8af3a96868
--- /dev/null
+++ b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockInteractionValidator.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.spockframework.mock.runtime;
+
+import org.junit.platform.commons.support.ModifierSupport;
+import org.spockframework.mock.IInvocationConstraint;
+import org.spockframework.mock.IMockInteraction;
+import org.spockframework.mock.IMockObject;
+import org.spockframework.mock.MockImplementation;
+import org.spockframework.mock.constraint.EqualMethodNameConstraint;
+import org.spockframework.mock.constraint.EqualPropertyNameConstraint;
+import org.spockframework.runtime.InvalidSpecException;
+import org.spockframework.util.ThreadSafe;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static java.util.Objects.requireNonNull;
+import static org.spockframework.runtime.GroovyRuntimeUtil.propertyToBooleanGetterMethodName;
+import static org.spockframework.runtime.GroovyRuntimeUtil.propertyToGetterMethodName;
+
+/**
+ * {@link ByteBuddyMockInteractionValidator} validates the {@link IMockInteraction} for {@link ByteBuddyMockMaker} mocks.
+ *
+ *
+ *
Checks for method interactions on final methods
+ *
Checks for property interactions on final methods
+ *
+ *
+ *
Implementation note: The {@link ByteBuddyMockFactory} create a single instance for each mocked class
+ * and the same validation is used for multiple mocks of the same class.
+ *
+ * @author Andreas Turban
+ */
+@ThreadSafe
+final class ByteBuddyMockInteractionValidator implements IMockInteractionValidator {
+
+ private volatile Class> mockClass;
+ private volatile Set finalMethods;
+
+ ByteBuddyMockInteractionValidator() {
+ }
+
+ @Override
+ public void validateMockInteraction(IMockObject mockObject, IMockInteraction mockInteractionParam) {
+ requireNonNull(mockObject);
+ requireNonNull(mockInteractionParam);
+ Object instance = requireNonNull(mockObject.getInstance());
+
+ if (mockObject.getConfiguration().getImplementation() == MockImplementation.GROOVY) {
+ //We do not validate final methods for Groovy mocks, because final mocking can be done with the Groovy MOP.
+ return;
+ }
+
+ initializeClassData(instance);
+
+ validate(mockObject, (MockInteraction) mockInteractionParam);
+ }
+
+ private void validate(IMockObject mockObject, MockInteraction mockInteraction) {
+ for (IInvocationConstraint constraint : mockInteraction.getConstraints()) {
+ if (constraint instanceof EqualMethodNameConstraint) {
+ EqualMethodNameConstraint methodConstraint = (EqualMethodNameConstraint) constraint;
+ String methodName = methodConstraint.getMethodName();
+ validateNonFinalMethod(mockObject, methodName);
+ }
+ if (constraint instanceof EqualPropertyNameConstraint) {
+ EqualPropertyNameConstraint propNameConstraint = (EqualPropertyNameConstraint) constraint;
+ validateProperty(mockObject, propNameConstraint);
+ }
+ }
+ }
+
+ private void validateProperty(IMockObject mockObject, EqualPropertyNameConstraint propNameConstraint) {
+ String propName = propNameConstraint.getPropertyName();
+ //We do not need to check for setters, because a property access like x.prop = value has not mock interaction syntax
+ validateNonFinalMethod(mockObject, propertyToGetterMethodName(propName));
+ validateNonFinalMethod(mockObject, propertyToBooleanGetterMethodName(propName));
+ }
+
+ private void validateNonFinalMethod(IMockObject mockObject, String methodName) {
+ if (finalMethods.contains(methodName)) {
+ throw new InvalidSpecException("The final method '"
+ + methodName + "' of '"
+ + mockObject.getMockName()
+ + "' can't be mocked by the '"
+ + ByteBuddyMockMaker.ID +
+ "' mock maker. Please use another mock maker supporting final methods.");
+ }
+ }
+
+ private void initializeClassData(Object mock) {
+ Class> mockClassOfMockObj = mock.getClass();
+ if (mockClass == null) {
+ synchronized (this) {
+ if (mockClass == null) {
+ finalMethods = Arrays.stream(mockClassOfMockObj.getMethods())
+ .filter(m -> !ModifierSupport.isStatic(m))
+ .collect(Collectors.groupingBy(Method::getName))
+ .entrySet()
+ .stream()
+ .filter(e -> e.getValue()
+ .stream()
+ .allMatch(ModifierSupport::isFinal))
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toSet());
+
+ mockClass = mockClassOfMockObj;
+ }
+ }
+ }
+
+ if (mockClass != mockClassOfMockObj) {
+ throw new IllegalStateException();
+ }
+ }
+}
diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockInterceptor.java b/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockInterceptor.java
index 48d46fd64b..9e1fcfdd45 100644
--- a/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockInterceptor.java
+++ b/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockInterceptor.java
@@ -36,11 +36,10 @@ public GroovyMockInterceptor(IMockConfiguration mockConfiguration, Specification
@Override
public Object intercept(Object target, Method method, Object[] arguments, IResponseGenerator realMethodInvoker) {
- IMockObject mockObject = new MockObject(mockConfiguration.getName(), mockConfiguration.getExactType(), target,
- mockConfiguration.isVerified(), mockConfiguration.isGlobal(), mockConfiguration.getDefaultResponse(), specification, this);
+ IMockObject mockObject = new MockObject(mockConfiguration, target, specification, this);
if (method.getDeclaringClass() == ISpockMockObject.class) {
- return mockObject;
+ return handleSpockMockInterface(method, mockObject);
}
// we do not need the cast information from the wrappers here, the method selection
@@ -61,7 +60,7 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo
}
}
if (isMethod(method, "setProperty", String.class, Object.class)) {
- String methodName = GroovyRuntimeUtil.propertyToMethodName("set", (String) args[0]);
+ String methodName = GroovyRuntimeUtil.propertyToSetterMethodName((String) args[0]);
return GroovyRuntimeUtil.invokeMethod(target, methodName, args[1]);
}
if (isMethod(method, "methodMissing", String.class, Object.class)) {
diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockMetaClass.java b/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockMetaClass.java
index b8ef479d79..1af4f4e1a7 100644
--- a/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockMetaClass.java
+++ b/spock-core/src/main/java/org/spockframework/mock/runtime/GroovyMockMetaClass.java
@@ -53,17 +53,17 @@ public Object invokeConstructor(Object[] arguments) {
@Override
public Object getProperty(Object target, String property) {
- String methodName = GroovyRuntimeUtil.propertyToMethodName("is", property);
+ String methodName = GroovyRuntimeUtil.propertyToBooleanGetterMethodName(property);
MetaMethod metaMethod = delegate.getMetaMethod(methodName, GroovyRuntimeUtil.EMPTY_ARGUMENTS);
if (metaMethod == null || metaMethod.getReturnType() != boolean.class) {
- methodName = GroovyRuntimeUtil.propertyToMethodName("get", property);
+ methodName = GroovyRuntimeUtil.propertyToGetterMethodName(property);
}
return invokeMethod(target, methodName, GroovyRuntimeUtil.EMPTY_ARGUMENTS);
}
@Override
public void setProperty(Object target, String property, Object newValue) {
- String methodName = GroovyRuntimeUtil.propertyToMethodName("set", property);
+ String methodName = GroovyRuntimeUtil.propertyToSetterMethodName(property);
invokeMethod(target, methodName, new Object[] {newValue});
}
@@ -121,8 +121,7 @@ private boolean isGetMetaClassCallOnGroovyObject(Object target, String method, O
private IMockInvocation createMockInvocation(MetaMethod metaMethod, Object target,
String methodName, Object[] arguments, boolean isStatic) {
- IMockObject mockObject = new MockObject(configuration.getName(), configuration.getExactType(), target,
- configuration.isVerified(), configuration.isGlobal(), configuration.getDefaultResponse(), specification, this);
+ IMockObject mockObject = new MockObject(configuration, target, specification, this);
IMockMethod mockMethod;
if (metaMethod != null) {
List parameterTypes = asList(metaMethod.getNativeParameterTypes());
diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/IMockInteractionValidator.java b/spock-core/src/main/java/org/spockframework/mock/runtime/IMockInteractionValidator.java
new file mode 100644
index 0000000000..474d08a907
--- /dev/null
+++ b/spock-core/src/main/java/org/spockframework/mock/runtime/IMockInteractionValidator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * 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
+ *
+ * https://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 org.spockframework.mock.runtime;
+
+import org.spockframework.mock.IMockInteraction;
+import org.spockframework.mock.IMockObject;
+import org.spockframework.mock.ISpockMockObject;
+import org.spockframework.util.Beta;
+import org.spockframework.util.ThreadSafe;
+
+/**
+ * The {@link IMockInteractionValidator} interface allows {@link IMockMaker} implementations
+ * to implement different kinds of validations on mock interactions.
+ *
+ *
The instance is registered and the {@link ISpockMockObject#$spock_mockInteractionValidator()} method. The {@code IMockMaker} shall return the validation instance to use.
+ *
+ * @author Andreas Turban
+ * @since 2.4
+ */
+@ThreadSafe
+@Beta
+public interface IMockInteractionValidator {
+ /**
+ * Is called by the {@link IMockInteraction} on creation to allow the underlying {@link IMockMaker} to validate the interaction.
+ *
+ * @param mockObject the spock mock object
+ * @param interaction the interaction to validate
+ * @throws org.spockframework.runtime.InvalidSpecException if the interface is invalid
+ * @since 2.4
+ */
+ @Beta
+ void validateMockInteraction(IMockObject mockObject, IMockInteraction interaction);
+}
diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java b/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java
index a90eacb7dc..125b66e5e9 100644
--- a/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java
+++ b/spock-core/src/main/java/org/spockframework/mock/runtime/JavaMockInterceptor.java
@@ -37,11 +37,10 @@ public JavaMockInterceptor(IMockConfiguration mockConfiguration, Specification s
@Override
public Object intercept(Object target, Method method, Object[] arguments, IResponseGenerator realMethodInvoker) {
- IMockObject mockObject = new MockObject(mockConfiguration.getName(), mockConfiguration.getExactType(),
- target, mockConfiguration.isVerified(), false, mockConfiguration.getDefaultResponse(), specification, this);
+ IMockObject mockObject = new MockObject(mockConfiguration, target, specification, this);
if (method.getDeclaringClass() == ISpockMockObject.class) {
- return mockObject;
+ return handleSpockMockInterface(method, mockObject);
}
// here no instances of org.codehaus.groovy.runtime.wrappers.Wrapper subclasses
@@ -64,7 +63,7 @@ public Object intercept(Object target, Method method, Object[] arguments, IRespo
// HACK: for some reason, runtime dispatches direct property access on mock classes via ScriptBytecodeAdapter
// delegate to the corresponding setter method
// for abstract groovy classes and interfaces it uses InvokerHelper
- String methodName = GroovyRuntimeUtil.propertyToMethodName("set", (String)args[0]);
+ String methodName = GroovyRuntimeUtil.propertyToSetterMethodName((String) args[0]);
return GroovyRuntimeUtil.invokeMethod(target, methodName, GroovyRuntimeUtil.asArgumentArray(args[1]));
}
}
diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/MockInteraction.java b/spock-core/src/main/java/org/spockframework/mock/runtime/MockInteraction.java
index 4971e59033..c197e86b43 100644
--- a/spock-core/src/main/java/org/spockframework/mock/runtime/MockInteraction.java
+++ b/spock-core/src/main/java/org/spockframework/mock/runtime/MockInteraction.java
@@ -71,6 +71,10 @@ public boolean matches(IMockInvocation invocation) {
return true;
}
+ List getConstraints() {
+ return constraints;
+ }
+
@Override
public Supplier