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]

Reply via email to