This is an automated email from the ASF dual-hosted git repository. ifesdjeen pushed a commit to branch dev in repository https://gitbox.apache.org/repos/asf/cassandra-simulator.git
commit ad1e2740555ae889594c28261981108203ffbcad Author: Alex Petrov <[email protected]> AuthorDate: Thu Jul 23 17:04:40 2026 +0200 Refactor intercept rules, add tests for exiting intercept rules, add new Method intercept. --- .../simulator/asm/GlobalMethodTransformer.java | 183 ++++++++++---------- .../cassandra/simulator/asm/InterceptRule.java | 187 +++++++++++++++++---- .../cassandra/simulator/InterceptHelper.java | 53 ++++++ .../org/apache/cassandra/simulator/Simulator.java | 105 +++++++++--- .../InterceptRuleIntegrationTest.java | 135 +++++++++++++++ .../simulator_test/MethodOverrideTest.java | 102 +++++++++++ 6 files changed, 616 insertions(+), 149 deletions(-) diff --git a/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java b/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java index cc76a8b..7f1de1e 100644 --- a/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java +++ b/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/GlobalMethodTransformer.java @@ -174,13 +174,9 @@ class GlobalMethodTransformer extends MethodVisitor else super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } - else if (globalMethods && applyFactoryMethodRule(opcode, owner, name, descriptor, isInterface)) + else if (globalMethods && applyMethodRule(opcode, owner, name, descriptor, isFirstMethodInsn)) { - // handled by applyFactoryMethodRule - } - else if (globalMethods && applyConstructorInitRule(opcode, owner, name, descriptor, isFirstMethodInsn)) - { - // handled by applyConstructorInitRule + // emitted by the matching rule } else { @@ -188,115 +184,118 @@ class GlobalMethodTransformer extends MethodVisitor } } - /** Returns true and emits the redirect if a FactoryMethod rule matches this INVOKESTATIC. */ - private boolean applyFactoryMethodRule(int opcode, String owner, String name, String descriptor, boolean isInterface) + /** + * Finds and applies at most one rule, traversing the rule set once for this invocation. + */ + private boolean applyMethodRule(int opcode, String owner, String name, String descriptor, boolean isFirstMethodInsn) { - if (opcode != Opcodes.INVOKESTATIC) - return false; for (InterceptRule rule : customRules) { - if (rule instanceof InterceptRule.FactoryMethod) + if (!rule.match(opcode, owner, name, descriptor)) + continue; + + switch (rule.kind()) { - InterceptRule.FactoryMethod fm = (InterceptRule.FactoryMethod) rule; - if (owner.equals(fm.ownerInternal) && name.equals(fm.methodName)) - { + case FACTORY_METHOD: + applyFactoryMethod((InterceptRule.FactoryMethod) rule, descriptor); + return true; + + case CONSTRUCTOR: + InterceptRule.Constructor constructor = (InterceptRule.Constructor) rule; + // Preserve the replacement class's own super/this constructor invocation. + if (transformer.className().equals(constructor.toClass) + && methodName.equals("<init>") + && isFirstMethodInsn) + return false; + transformer.witness(GLOBAL_METHOD); - int closeParen = descriptor.indexOf(')'); - String paramPart = descriptor.substring(1, closeParen); // e.g. "" or "I" - String returnDescriptor = descriptor.substring(closeParen + 1); - String key = fm.ownerInternal + '.' + name; + super.visitMethodInsn(opcode, constructor.toClass, name, descriptor, false); + return true; - if (paramPart.isEmpty()) - { - // Zero-arg factory: push key, call dispatchZeroArg(String) - super.visitLdcInsn(key); - super.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", - "dispatchZeroArg", "(Ljava/lang/String;)Ljava/lang/Object;", false); - } - else if (paramPart.equals("I")) - { - // Int-arg factory: int is already on stack. Push key, SWAP, call dispatchIntArg(String, int). - super.visitLdcInsn(key); - super.visitInsn(Opcodes.SWAP); - super.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", - "dispatchIntArg", "(Ljava/lang/String;I)Ljava/lang/Object;", false); - } - else if (paramPart.equals("Ljava/util/concurrent/ThreadFactory;")) - { - // Single-ThreadFactory variant: POP the factory (ignored - intercepting executor - // always creates InterceptibleThread internally), then dispatch as zero-arg. - super.visitInsn(Opcodes.POP); - super.visitLdcInsn(key); - super.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", - "dispatchZeroArg", "(Ljava/lang/String;)Ljava/lang/Object;", false); - } - else if (paramPart.equals("ILjava/util/concurrent/ThreadFactory;")) - { - // Int + ThreadFactory variant: POP the factory (top of stack), then dispatch as int-arg. - super.visitInsn(Opcodes.POP); - super.visitLdcInsn(key); - super.visitInsn(Opcodes.SWAP); - super.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", - "dispatchIntArg", "(Ljava/lang/String;I)Ljava/lang/Object;", false); - } - else - { - throw new UnsupportedOperationException("Unsupported factory descriptor: " + descriptor); - } - // CHECKCAST the result back to the original return type. - if (returnDescriptor.startsWith("L") && returnDescriptor.endsWith(";")) - super.visitTypeInsn(Opcodes.CHECKCAST, returnDescriptor.substring(1, returnDescriptor.length() - 1)); + case METHOD_CALL: + InterceptRule.MethodCall methodCall = (InterceptRule.MethodCall) rule; + transformer.witness(GLOBAL_METHOD); + // The original receiver is already beneath the arguments on the operand stack. + // The target descriptor consumes it as its first parameter. + super.visitMethodInsn(Opcodes.INVOKESTATIC, + methodCall.toClass, + methodCall.toMethod, + methodCall.toMethodDescriptor, + false); return true; - } + + default: + throw new AssertionError(rule.kind()); } } return false; } - /** Returns true and emits the redirect if a Constructor rule matches this INVOKESPECIAL <init>. */ - private boolean applyConstructorInitRule(int opcode, String owner, String name, String descriptor, boolean isFirstMethodInsn) + private void applyFactoryMethod(InterceptRule.FactoryMethod factoryMethod, String descriptor) { - if (opcode != Opcodes.INVOKESPECIAL || !name.equals("<init>")) - return false; - for (InterceptRule rule : customRules) + transformer.witness(GLOBAL_METHOD); + + int closeParen = descriptor.indexOf(')'); + String parameters = descriptor.substring(1, closeParen); + String returnDescriptor = descriptor.substring(closeParen + 1); + String key = factoryMethod.fromClass + '.' + factoryMethod.methodName; + + if (parameters.isEmpty()) { - if (rule instanceof InterceptRule.Constructor) - { - InterceptRule.Constructor cr = (InterceptRule.Constructor) rule; - if (owner.equals(cr.ownerInternal)) - { - // Don't redirect the super.<init> call inside the replacement class itself - if (transformer.className().equals(cr.implInternal) && methodName.equals("<init>") && isFirstMethodInsn) - return false; - transformer.witness(GLOBAL_METHOD); - super.visitMethodInsn(opcode, cr.implInternal, name, descriptor, false); - return true; - } - } + super.visitLdcInsn(key); + super.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", + "dispatchZeroArg", "(Ljava/lang/String;)Ljava/lang/Object;", false); } - return false; + else if (parameters.equals("I")) + { + super.visitLdcInsn(key); + super.visitInsn(Opcodes.SWAP); + super.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", + "dispatchIntArg", "(Ljava/lang/String;I)Ljava/lang/Object;", false); + } + else if (parameters.equals("Ljava/util/concurrent/ThreadFactory;")) + { + super.visitInsn(Opcodes.POP); + super.visitLdcInsn(key); + super.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", + "dispatchZeroArg", "(Ljava/lang/String;)Ljava/lang/Object;", false); + } + else if (parameters.equals("ILjava/util/concurrent/ThreadFactory;")) + { + super.visitInsn(Opcodes.POP); + super.visitLdcInsn(key); + super.visitInsn(Opcodes.SWAP); + super.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/apache/cassandra/simulator/systems/InterceptorOfGlobalMethods$Global", + "dispatchIntArg", "(Ljava/lang/String;I)Ljava/lang/Object;", false); + } + else + { + throw new UnsupportedOperationException("Unsupported factory descriptor: " + descriptor); + } + + if (returnDescriptor.startsWith("L") && returnDescriptor.endsWith(";")) + super.visitTypeInsn(Opcodes.CHECKCAST, returnDescriptor.substring(1, returnDescriptor.length() - 1)); } @Override public void visitTypeInsn(int opcode, String type) { - if (globalMethods && opcode == Opcodes.NEW) + if (globalMethods) { for (InterceptRule rule : customRules) { - if (rule instanceof InterceptRule.Constructor) - { - InterceptRule.Constructor cr = (InterceptRule.Constructor) rule; - if (type.equals(cr.ownerInternal)) - { - super.visitTypeInsn(opcode, cr.implInternal); - return; - } - } + if (!rule.match(opcode, type, null, null)) + continue; + + if (rule.kind() != InterceptRule.Kind.CONSTRUCTOR) + throw new AssertionError(rule.kind()); + + super.visitTypeInsn(opcode, ((InterceptRule.Constructor) rule).toClass); + return; } } super.visitTypeInsn(opcode, type); diff --git a/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/InterceptRule.java b/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/InterceptRule.java index 0ea11f0..d911b44 100644 --- a/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/InterceptRule.java +++ b/simulator-asm/src/main/java/org/apache/cassandra/simulator/asm/InterceptRule.java @@ -20,92 +20,219 @@ package org.apache.cassandra.simulator.asm; import java.util.Objects; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + /** - * A single ASM bytecode redirect rule, either for a static factory method or a constructor. - * - * Rules are registered via {@code Simulator.intercept()} and passed into {@link InterceptClasses} - * before simulation starts. {@link GlobalMethodTransformer} applies them at class-load time. + * A bytecode interception rule applied by {@link GlobalMethodTransformer}. */ public abstract class InterceptRule { - private InterceptRule() {} + public enum Kind + { + FACTORY_METHOD, + CONSTRUCTOR, + METHOD_CALL + } + + private final Kind kind; + + private InterceptRule(Kind kind) + { + this.kind = kind; + } + + public final Kind kind() + { + return kind; + } /** - * Redirects {@code INVOKESTATIC ownerInternal.methodName(descriptor)} to - * {@code INVOKESTATIC InterceptorOfGlobalMethods$Global.methodName(descriptor)}. - * - * The descriptor and stack layout are unchanged; only the call target class changes. - * A matching static method must exist in {@code InterceptorOfGlobalMethods.Global}. + * Tests an ASM instruction. For type instructions, {@code name} and {@code descriptor} + * are {@code null} and {@code owner} contains the instruction's type. + */ + public abstract boolean match(int opcode, String owner, String name, String descriptor); + + /** + * Redirects a static factory invocation through + * {@code InterceptorOfGlobalMethods.Global}'s factory dispatcher. */ public static final class FactoryMethod extends InterceptRule { - public final String ownerInternal; + public final String fromClass; public final String methodName; public final String descriptor; - public FactoryMethod(String ownerInternal, String methodName, String descriptor) + public FactoryMethod(String fromClass, String methodName, String descriptor) { - this.ownerInternal = ownerInternal; + super(Kind.FACTORY_METHOD); + this.fromClass = fromClass; this.methodName = methodName; this.descriptor = descriptor; } + @Override + public boolean match(int opcode, String owner, String name, String descriptor) + { + return opcode == Opcodes.INVOKESTATIC + && fromClass.equals(owner) + && methodName.equals(name) + && this.descriptor.equals(descriptor); + } + @Override public boolean equals(Object o) { - if (!(o instanceof FactoryMethod)) return false; + if (!(o instanceof FactoryMethod)) + return false; FactoryMethod that = (FactoryMethod) o; - return ownerInternal.equals(that.ownerInternal) && methodName.equals(that.methodName); + return fromClass.equals(that.fromClass) + && methodName.equals(that.methodName) + && descriptor.equals(that.descriptor); } @Override public int hashCode() { - return Objects.hash(ownerInternal, methodName); + return Objects.hash(fromClass, methodName, descriptor); } @Override public String toString() { - return "FactoryMethod(" + ownerInternal + '.' + methodName + ')'; + return "FactoryMethod(" + fromClass + '.' + methodName + descriptor + ')'; } } /** - * Redirects {@code NEW ownerInternal} and {@code INVOKESPECIAL ownerInternal.<init>} to - * {@code NEW implInternal} and {@code INVOKESPECIAL implInternal.<init>} respectively. - * - * The {@code implInternal} class must have constructors compatible with {@code ownerInternal}. + * Redirects both allocation and constructor invocation from one concrete class to another. */ public static final class Constructor extends InterceptRule { - public final String ownerInternal; - public final String implInternal; + public final String fromClass; + public final String toClass; + + public Constructor(String fromClass, String toClass) + { + super(Kind.CONSTRUCTOR); + this.fromClass = fromClass; + this.toClass = toClass; + } - public Constructor(String ownerInternal, String implInternal) + @Override + public boolean match(int opcode, String owner, String name, String descriptor) { - this.ownerInternal = ownerInternal; - this.implInternal = implInternal; + return fromClass.equals(owner) + && (opcode == Opcodes.NEW + || (opcode == Opcodes.INVOKESPECIAL && "<init>".equals(name))); } @Override public boolean equals(Object o) { - if (!(o instanceof Constructor)) return false; + if (!(o instanceof Constructor)) + return false; Constructor that = (Constructor) o; - return ownerInternal.equals(that.ownerInternal); + return fromClass.equals(that.fromClass); + } + + @Override + public int hashCode() + { + return fromClass.hashCode(); + } + + @Override + public String toString() + { + return "Constructor(" + fromClass + " -> " + toClass + ')'; + } + } + + /** + * Redirects an instance invocation to a static method. + * + * The source receiver remains on the operand stack. Consequently, the target method's + * first parameter must accept the source receiver, followed by the source method's original + * parameters. The return types must be identical. + */ + public static final class MethodCall extends InterceptRule + { + public final String fromClass; + public final String fromMethod; + public final String fromMethodDescriptor; + public final String toClass; + public final String toMethod; + public final String toMethodDescriptor; + + public MethodCall(String fromClass, + String fromMethod, + String fromMethodDescriptor, + String toClass, + String toMethod, + String toMethodDescriptor) + { + super(Kind.METHOD_CALL); + this.fromClass = fromClass; + this.fromMethod = fromMethod; + this.fromMethodDescriptor = fromMethodDescriptor; + validateDescriptors(fromMethodDescriptor, toMethodDescriptor); + this.toClass = toClass; + this.toMethod = toMethod; + this.toMethodDescriptor = toMethodDescriptor; + } + + private static void validateDescriptors(String fromDescriptor, String toDescriptor) + { + Type[] fromArguments = Type.getArgumentTypes(fromDescriptor); + Type[] toArguments = Type.getArgumentTypes(toDescriptor); + if (toArguments.length != fromArguments.length + 1 + || (toArguments[0].getSort() != Type.OBJECT && toArguments[0].getSort() != Type.ARRAY) + || !Type.getReturnType(fromDescriptor).equals(Type.getReturnType(toDescriptor))) + throw new IllegalArgumentException("Target descriptor must prepend the source receiver: " + + fromDescriptor + " -> " + toDescriptor); + + for (int i = 0; i < fromArguments.length; i++) + { + if (!fromArguments[i].equals(toArguments[i + 1])) + throw new IllegalArgumentException("Target descriptor must prepend the source receiver: " + + fromDescriptor + " -> " + toDescriptor); + } + } + + @Override + public boolean match(int opcode, String owner, String name, String descriptor) + { + return (opcode == Opcodes.INVOKEVIRTUAL + || opcode == Opcodes.INVOKEINTERFACE + || opcode == Opcodes.INVOKESPECIAL) + && fromClass.equals(owner) + && fromMethod.equals(name) + && fromMethodDescriptor.equals(descriptor); + } + + @Override + public boolean equals(Object o) + { + if (!(o instanceof MethodCall)) + return false; + MethodCall that = (MethodCall) o; + return fromClass.equals(that.fromClass) + && fromMethod.equals(that.fromMethod) + && fromMethodDescriptor.equals(that.fromMethodDescriptor); } @Override public int hashCode() { - return ownerInternal.hashCode(); + return Objects.hash(fromClass, fromMethod, fromMethodDescriptor); } @Override public String toString() { - return "Constructor(" + ownerInternal + " -> " + implInternal + ')'; + return "MethodCall(" + fromClass + '.' + fromMethod + fromMethodDescriptor + + " -> " + toClass + '.' + toMethod + toMethodDescriptor + ')'; } } } diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/InterceptHelper.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/InterceptHelper.java new file mode 100644 index 0000000..b6fabba --- /dev/null +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/InterceptHelper.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.simulator; + +import java.lang.reflect.Method; + +final class InterceptHelper +{ + private InterceptHelper() + { + } + + static String methodDescriptor(Method method) + { + StringBuilder descriptor = new StringBuilder("("); + for (Class<?> parameter : method.getParameterTypes()) + descriptor.append(typeDescriptor(parameter)); + return descriptor.append(')') + .append(typeDescriptor(method.getReturnType())) + .toString(); + } + + private static String typeDescriptor(Class<?> type) + { + if (type == void.class) return "V"; + if (type == boolean.class) return "Z"; + if (type == byte.class) return "B"; + if (type == char.class) return "C"; + if (type == short.class) return "S"; + if (type == int.class) return "I"; + if (type == long.class) return "J"; + if (type == float.class) return "F"; + if (type == double.class) return "D"; + if (type.isArray()) return type.getName().replace('.', '/'); + return 'L' + type.getName().replace('.', '/') + ';'; + } +} diff --git a/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java b/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java index 8ff5ee4..ef53f70 100644 --- a/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java +++ b/simulator-core/src/main/java/org/apache/cassandra/simulator/Simulator.java @@ -65,6 +65,7 @@ import org.apache.cassandra.simulator.utils.NoSimulation; import static org.apache.cassandra.simulator.Action.Modifiers.NONE; import static org.apache.cassandra.simulator.Action.Modifiers.START_THREAD; +import static org.apache.cassandra.simulator.InterceptHelper.methodDescriptor; /** * Thin orchestrator that wires together all simulator subsystems for testing. @@ -232,6 +233,78 @@ public class Simulator implements AutoCloseable "(I)Ljava/util/concurrent/ScheduledExecutorService;")); customRules.add(new InterceptRule.FactoryMethod("java/util/concurrent/Executors", "newSingleThreadScheduledExecutor", "()Ljava/util/concurrent/ScheduledExecutorService;")); + + // Preserve the standard ThreadFactory overloads. Their factory argument is intentionally + // discarded because simulated executors create InterceptibleThread instances themselves. + customRules.add(new InterceptRule.FactoryMethod("java/util/concurrent/Executors", "newFixedThreadPool", + "(ILjava/util/concurrent/ThreadFactory;)Ljava/util/concurrent/ExecutorService;")); + customRules.add(new InterceptRule.FactoryMethod("java/util/concurrent/Executors", "newSingleThreadExecutor", + "(Ljava/util/concurrent/ThreadFactory;)Ljava/util/concurrent/ExecutorService;")); + customRules.add(new InterceptRule.FactoryMethod("java/util/concurrent/Executors", "newCachedThreadPool", + "(Ljava/util/concurrent/ThreadFactory;)Ljava/util/concurrent/ExecutorService;")); + customRules.add(new InterceptRule.FactoryMethod("java/util/concurrent/Executors", "newScheduledThreadPool", + "(ILjava/util/concurrent/ThreadFactory;)Ljava/util/concurrent/ScheduledExecutorService;")); + customRules.add(new InterceptRule.FactoryMethod("java/util/concurrent/Executors", "newSingleThreadScheduledExecutor", + "(Ljava/util/concurrent/ThreadFactory;)Ljava/util/concurrent/ScheduledExecutorService;")); + } + + /** + * Redirects selected instance methods on {@code fromClass} to public static methods on + * {@code interceptorClass}. Each target method must be annotated with {@link Intercept}; its + * first parameter receives the original invocation's receiver and its remaining signature + * must match the source method exactly. + */ + public Simulator interceptMethods(Class<?> fromClass, Class<?> interceptorClass) + { + if (state != State.NEW) + throw new IllegalStateException("interceptMethods() must be called before ensureStarted()"); + + String fromClassInternal = fromClass.getName().replace('.', '/'); + boolean found = false; + for (Method interceptor : interceptorClass.getMethods()) + { + if (!interceptor.isAnnotationPresent(Intercept.class)) + continue; + found = true; + + if (!Modifier.isStatic(interceptor.getModifiers())) + throw new IllegalArgumentException("Intercept method must be static: " + interceptor); + + Class<?>[] interceptorParameters = interceptor.getParameterTypes(); + if (interceptorParameters.length == 0 || !interceptorParameters[0].isAssignableFrom(fromClass)) + throw new IllegalArgumentException("First parameter must accept " + fromClass.getName() + ": " + interceptor); + + Class<?>[] sourceParameters = new Class<?>[interceptorParameters.length - 1]; + System.arraycopy(interceptorParameters, 1, sourceParameters, 0, sourceParameters.length); + + final Method source; + try + { + source = fromClass.getMethod(interceptor.getName(), sourceParameters); + } + catch (NoSuchMethodException e) + { + throw new IllegalArgumentException("No source method matching intercept method " + interceptor, e); + } + + if (Modifier.isStatic(source.getModifiers())) + throw new IllegalArgumentException("Source method must be an instance method: " + source); + if (source.getReturnType() != interceptor.getReturnType()) + throw new IllegalArgumentException("Intercept method must return " + source.getReturnType().getName() + ": " + interceptor); + + InterceptRule.MethodCall rule = new InterceptRule.MethodCall(fromClassInternal, + source.getName(), + methodDescriptor(source), + interceptor.getDeclaringClass().getName().replace('.', '/'), + interceptor.getName(), + methodDescriptor(interceptor)); + customRules.remove(rule); + customRules.add(rule); + } + + if (!found) + throw new IllegalArgumentException(interceptorClass.getName() + " declares no public @Intercept methods"); + return this; } /** @@ -311,7 +384,7 @@ public class Simulator implements AutoCloseable String ownerInternal = target.getName().replace('.', '/'); String implInternal = implClass.getName().replace('.', '/'); customRules.removeIf(r -> r instanceof InterceptRule.Constructor - && ((InterceptRule.Constructor) r).ownerInternal.equals(ownerInternal)); + && ((InterceptRule.Constructor) r).fromClass.equals(ownerInternal)); customRules.add(new InterceptRule.Constructor(ownerInternal, implInternal)); validateConstructorCompatibility(target, implClass); @@ -363,31 +436,6 @@ public class Simulator implements AutoCloseable throw new IllegalArgumentException(implClass.getName() + " is missing constructors matching: " + missing); } - /** Returns the JVM method descriptor for a {@link Method}. */ - private static String methodDescriptor(Method m) - { - StringBuilder sb = new StringBuilder("("); - for (Class<?> p : m.getParameterTypes()) - sb.append(typeDescriptor(p)); - sb.append(')').append(typeDescriptor(m.getReturnType())); - return sb.toString(); - } - - private static String typeDescriptor(Class<?> type) - { - if (type == void.class) return "V"; - if (type == boolean.class) return "Z"; - if (type == byte.class) return "B"; - if (type == char.class) return "C"; - if (type == short.class) return "S"; - if (type == int.class) return "I"; - if (type == long.class) return "J"; - if (type == float.class) return "F"; - if (type == double.class) return "D"; - if (type.isArray()) return type.getName().replace('.', '/'); - return "L" + type.getName().replace('.', '/') + ";"; - } - /** * Register a custom factory for a specific static method, bypassing constructor-matching. * Use this when the implementation class constructor signature differs from the interface @@ -404,7 +452,10 @@ public class Simulator implements AutoCloseable String ownerInternal = owner.getName().replace('.', '/'); for (Method m : owner.getMethods()) { - if (!m.getName().equals(methodName) || !Modifier.isStatic(m.getModifiers())) + if (!m.getName().equals(methodName) + || !Modifier.isStatic(m.getModifiers()) + || m.getParameterCount() != 1 + || m.getParameterTypes()[0] != int.class) continue; customRules.add(new InterceptRule.FactoryMethod(ownerInternal, methodName, methodDescriptor(m))); InterceptorOfGlobalMethods.Global.registerFactory(ownerInternal, methodName, factory); diff --git a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/InterceptRuleIntegrationTest.java b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/InterceptRuleIntegrationTest.java new file mode 100644 index 0000000..8a1ac1d --- /dev/null +++ b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/InterceptRuleIntegrationTest.java @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.simulator_test; + +import org.apache.cassandra.simulator.Simulator; +import org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable; +import org.apache.cassandra.simulator.context.SharedTestState; +import org.apache.cassandra.simulator.utils.Intercept; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class InterceptRuleIntegrationTest +{ + @BeforeEach + void reset() + { + SharedTestState.reset(); + } + + @Test + void factoryMethodRuleRedirectsFactoryInvocation() + { + try (Simulator simulator = new Simulator(42L)) + { + simulator.intercept(ValueFactory.class, RedirectedValue.class); + simulator.simulate((SerializableRunnable) () -> + SharedTestState.value1.set(ValueFactory.create(41).value())); + } + + assertEquals(42, SharedTestState.value1.get()); + } + + @Test + void constructorRuleRedirectsAllocationAndInitialization() + { + try (Simulator simulator = new Simulator(42L)) + { + simulator.intercept(OriginalValue.class, RedirectedConstructorValue.class); + simulator.simulate((SerializableRunnable) () -> + SharedTestState.value1.set(new OriginalValue(41).value())); + } + + assertEquals(42, SharedTestState.value1.get()); + } + + public interface ValueFactory + { + @Intercept + static ValueFactory create(int value) + { + return new OriginalFactoryValue(value); + } + + int value(); + } + + public static class OriginalFactoryValue implements ValueFactory + { + private final int value; + + public OriginalFactoryValue(int value) + { + this.value = value; + } + + @Override + public int value() + { + return value; + } + } + + public static class RedirectedValue implements ValueFactory + { + private final int value; + + public RedirectedValue(int value) + { + this.value = value; + } + + @Override + public int value() + { + return value + 1; + } + } + + public static class OriginalValue + { + private final int value; + + public OriginalValue(int value) + { + this.value = value; + } + + public int value() + { + return value; + } + } + + public static class RedirectedConstructorValue extends OriginalValue + { + public RedirectedConstructorValue(int value) + { + super(value); + } + + @Override + public int value() + { + return super.value() + 1; + } + } +} diff --git a/simulator-core/src/test/java/org/apache/cassandra/simulator_test/MethodOverrideTest.java b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/MethodOverrideTest.java new file mode 100644 index 0000000..82c7deb --- /dev/null +++ b/simulator-core/src/test/java/org/apache/cassandra/simulator_test/MethodOverrideTest.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.cassandra.simulator_test; + +import org.apache.cassandra.simulator.Simulator; +import org.apache.cassandra.simulator.context.IIsolatedExecutor.SerializableRunnable; +import org.apache.cassandra.simulator.context.SharedTestState; +import org.apache.cassandra.simulator.utils.Intercept; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class MethodOverrideTest +{ + @BeforeEach + void reset() + { + SharedTestState.reset(); + } + + @Test + void redirectsInstanceMethodAndPassesOriginalReceiver() + { + try (Simulator simulator = new Simulator(42L, 1.0f)) + { + simulator.interceptMethods(Source.class, Redirects.class); + simulator.simulate((SerializableRunnable) () -> { + Source source = new Source(37); + SharedTestState.value1.set(source.add(5)); + }); + } + + assertEquals(37, SharedTestState.value2.get(), "redirect did not receive the source receiver"); + assertEquals(42, SharedTestState.value1.get(), "redirect result was not returned to the caller"); + assertEquals(1, SharedTestState.eventCount.get(), "source method ran instead of the redirect"); + } + + @Test + void rejectsNonStaticRedirect() + { + try (Simulator simulator = new Simulator(42L, 1.0f)) + { + assertThrows(IllegalArgumentException.class, + () -> simulator.interceptMethods(Source.class, InvalidRedirect.class)); + } + } + + public static class Source + { + private final int base; + + public Source(int base) + { + this.base = base; + } + + public int add(int value) + { + SharedTestState.eventCount.set(-100); + return -100; + } + } + + public static class Redirects + { + @Intercept + public static int add(Object receiver, int value) + { + Source source = (Source) receiver; + SharedTestState.value2.set(source.base); + SharedTestState.eventCount.incrementAndGet(); + return source.base + value; + } + } + + public static class InvalidRedirect + { + @Intercept + public int add(Source receiver, int value) + { + return receiver.base + value; + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
