Reflective events passed from device to test suite

Change-Id: Ibca52998d7954167d4c7aa8f95aa7d4a5614401e
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java
new file mode 100644
index 0000000..e942bd6
--- /dev/null
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveEventType.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.assistant.runtime;
+
+public enum ReflectiveEventType {
+  CLASS_NEW_INSTANCE,
+  CLASS_GET_DECLARED_METHOD,
+  CLASS_GET_DECLARED_METHODS,
+  CLASS_GET_DECLARED_FIELD,
+  CLASS_GET_DECLARED_FIELDS,
+  CLASS_GET_DECLARED_CONSTRUCTOR,
+  CLASS_GET_DECLARED_CONSTRUCTORS,
+  CLASS_GET_METHOD,
+  CLASS_GET_METHODS,
+  CLASS_GET_FIELD,
+  CLASS_GET_FIELDS,
+  CLASS_GET_CONSTRUCTOR,
+  CLASS_GET_CONSTRUCTORS,
+  CLASS_GET_NAME,
+  CLASS_FOR_NAME,
+  CLASS_GET_COMPONENT_TYPE,
+  CLASS_GET_PACKAGE,
+  CLASS_IS_ASSIGNABLE_FROM,
+  CLASS_GET_SUPERCLASS,
+  CLASS_AS_SUBCLASS,
+  CLASS_IS_INSTANCE,
+  CLASS_CAST,
+  CLASS_FLAG,
+  ATOMIC_INTEGER_FIELD_UPDATER_NEW_UPDATER,
+  ATOMIC_LONG_FIELD_UPDATER_NEW_UPDATER,
+  ATOMIC_REFERENCE_FIELD_UPDATER_NEW_UPDATER,
+  SERVICE_LOADER_LOAD,
+  PROXY_NEW_PROXY_INSTANCE;
+}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java
new file mode 100644
index 0000000..57d4ca1
--- /dev/null
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOperationJsonLogger.java
@@ -0,0 +1,278 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.assistant.runtime;
+
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.ATOMIC_INTEGER_FIELD_UPDATER_NEW_UPDATER;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.ATOMIC_LONG_FIELD_UPDATER_NEW_UPDATER;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.ATOMIC_REFERENCE_FIELD_UPDATER_NEW_UPDATER;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_AS_SUBCLASS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_CAST;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_FLAG;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_FOR_NAME;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_COMPONENT_TYPE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_CONSTRUCTOR;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_CONSTRUCTORS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTOR;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_CONSTRUCTORS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_FIELD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_FIELDS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_METHOD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_DECLARED_METHODS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_FIELD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_FIELDS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_METHOD;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_METHODS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_NAME;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_PACKAGE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_GET_SUPERCLASS;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_IS_ASSIGNABLE_FROM;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_IS_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.CLASS_NEW_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.PROXY_NEW_PROXY_INSTANCE;
+import static com.android.tools.r8.assistant.runtime.ReflectiveEventType.SERVICE_LOADER_LOAD;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveOracle.Stack;
+import com.android.tools.r8.keepanno.annotations.KeepForApi;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.reflect.InvocationHandler;
+
+// This logs the information in JSON-like format,
+// manually encoded to avoid loading gson on the mobile.
+@KeepForApi
+public class ReflectiveOperationJsonLogger implements ReflectiveOperationReceiver {
+
+  private final FileWriter output;
+
+  public ReflectiveOperationJsonLogger() throws IOException {
+    String property = System.getProperty("com.android.tools.r8.reflectiveJsonLogger", "log.txt");
+    File file = new File(property);
+    file.createNewFile();
+    this.output = new FileWriter(file);
+    output.write("[");
+  }
+
+  public void finished() throws IOException {
+    output.write("{}]");
+    output.close();
+  }
+
+  private String[] methodToString(Class<?> holder, String method, Class<?>... parameters) {
+    String[] methodStrings = new String[parameters.length + 2];
+    methodStrings[0] = printClass(holder);
+    methodStrings[1] = method;
+    for (int i = 0; i < parameters.length; i++) {
+      methodStrings[i + 2] = printClass(parameters[i]);
+    }
+    return methodStrings;
+  }
+
+  private String[] constructorToString(Class<?> holder, Class<?>... parameters) {
+    return methodToString(holder, "<init>", parameters);
+  }
+
+  private String printClass(Class<?> clazz) {
+    return clazz.getName();
+  }
+
+  private String printClassLoader(ClassLoader classLoader) {
+    return classLoader == null ? "null" : printClass(classLoader.getClass());
+  }
+
+  private void output(ReflectiveEventType event, Stack stack, String... args) {
+    try {
+      output.write("{\"event\": \"");
+      output.write(event.name());
+      output.write("\"");
+      if (stack != null) {
+        output.write(", \"stack\": ");
+        printArray(stack.stackTraceElementsAsString());
+      }
+      assert args != null;
+      output.write(", \"args\": ");
+      printArray(args);
+      output.write("},\n");
+      output.flush();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void printArray(String... args) throws IOException {
+    output.write("[");
+    for (int i = 0; i < args.length; i++) {
+      output.write("\"");
+      output.write(args[i]);
+      output.write("\"");
+      if (i != args.length - 1) {
+        output.write(", ");
+      }
+    }
+    output.write("]");
+  }
+
+  @Override
+  public void onClassNewInstance(Stack stack, Class<?> clazz) {
+    output(CLASS_NEW_INSTANCE, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetDeclaredMethod(
+      Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+    output(CLASS_GET_DECLARED_METHOD, stack, methodToString(clazz, method, parameters));
+  }
+
+  @Override
+  public void onClassGetDeclaredMethods(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_DECLARED_METHODS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetDeclaredField(Stack stack, Class<?> clazz, String fieldName) {
+    output(CLASS_GET_DECLARED_FIELD, stack, printClass(clazz), fieldName);
+  }
+
+  @Override
+  public void onClassGetDeclaredFields(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_DECLARED_FIELDS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetDeclaredConstructor(Stack stack, Class<?> clazz, Class<?>... parameters) {
+    output(CLASS_GET_DECLARED_CONSTRUCTOR, stack, constructorToString(clazz, parameters));
+  }
+
+  @Override
+  public void onClassGetDeclaredConstructors(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_DECLARED_CONSTRUCTORS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetMethod(Stack stack, Class<?> clazz, String method, Class<?>... parameters) {
+    output(CLASS_GET_METHOD, stack, methodToString(clazz, method, parameters));
+  }
+
+  @Override
+  public void onClassGetMethods(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_METHODS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetField(Stack stack, Class<?> clazz, String fieldName) {
+    output(CLASS_GET_FIELD, stack, printClass(clazz), fieldName);
+  }
+
+  @Override
+  public void onClassGetFields(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_FIELDS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetConstructor(Stack stack, Class<?> clazz, Class<?>... parameters) {
+    output(CLASS_GET_CONSTRUCTOR, stack, constructorToString(clazz, parameters));
+  }
+
+  @Override
+  public void onClassGetConstructors(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_CONSTRUCTORS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetName(Stack stack, Class<?> clazz, NameLookupType lookupType) {
+    output(CLASS_GET_NAME, stack, printClass(clazz), lookupType.name());
+  }
+
+  @Override
+  public void onClassForName(
+      Stack stack, String className, boolean initialize, ClassLoader classLoader) {
+    output(
+        CLASS_FOR_NAME,
+        stack,
+        className,
+        Boolean.toString(initialize),
+        printClassLoader(classLoader));
+  }
+
+  @Override
+  public void onClassGetComponentType(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_COMPONENT_TYPE, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassGetPackage(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_PACKAGE, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassIsAssignableFrom(Stack stack, Class<?> clazz, Class<?> sup) {
+    output(CLASS_IS_ASSIGNABLE_FROM, stack, printClass(clazz), printClass(sup));
+  }
+
+  @Override
+  public void onClassGetSuperclass(Stack stack, Class<?> clazz) {
+    output(CLASS_GET_SUPERCLASS, stack, printClass(clazz));
+  }
+
+  @Override
+  public void onClassAsSubclass(Stack stack, Class<?> holder, Class<?> clazz) {
+    output(CLASS_AS_SUBCLASS, stack, printClass(holder), printClass(clazz));
+  }
+
+  @Override
+  public void onClassIsInstance(Stack stack, Class<?> holder, Object object) {
+    output(CLASS_IS_INSTANCE, stack, printClass(holder), printClass(object.getClass()));
+  }
+
+  @Override
+  public void onClassCast(Stack stack, Class<?> holder, Object object) {
+    output(CLASS_CAST, stack, printClass(holder), printClass(object.getClass()));
+  }
+
+  @Override
+  public void onClassFlag(Stack stack, Class<?> clazz, ClassFlag classFlag) {
+    output(CLASS_FLAG, stack, printClass(clazz), classFlag.name());
+  }
+
+  @Override
+  public void onAtomicIntegerFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
+    output(ATOMIC_INTEGER_FIELD_UPDATER_NEW_UPDATER, stack, printClass(clazz), name);
+  }
+
+  @Override
+  public void onAtomicLongFieldUpdaterNewUpdater(Stack stack, Class<?> clazz, String name) {
+    output(ATOMIC_LONG_FIELD_UPDATER_NEW_UPDATER, stack, printClass(clazz), name);
+  }
+
+  @Override
+  public void onAtomicReferenceFieldUpdaterNewUpdater(
+      Stack stack, Class<?> clazz, Class<?> fieldClass, String name) {
+    output(ATOMIC_REFERENCE_FIELD_UPDATER_NEW_UPDATER, stack, printClass(clazz), name);
+  }
+
+  @Override
+  public void onServiceLoaderLoad(Stack stack, Class<?> clazz, ClassLoader classLoader) {
+    output(SERVICE_LOADER_LOAD, stack, printClass(clazz), printClassLoader(classLoader));
+  }
+
+  @Override
+  public void onProxyNewProxyInstance(
+      Stack stack,
+      ClassLoader classLoader,
+      Class<?>[] interfaces,
+      InvocationHandler invocationHandler) {
+    String[] methodStrings = new String[interfaces.length + 2];
+    methodStrings[0] = printClassLoader(classLoader);
+    methodStrings[1] = invocationHandler.toString();
+    for (int i = 0; i < interfaces.length; i++) {
+      methodStrings[i + 2] = printClass(interfaces[i]);
+    }
+    output(PROXY_NEW_PROXY_INSTANCE, stack, methodStrings);
+  }
+
+  public boolean requiresStackInformation() {
+    return true;
+  }
+}
diff --git a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
index 32c841c..e28ce3c 100644
--- a/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
+++ b/src/assistant/java/com/android/tools/r8/assistant/runtime/ReflectiveOracle.java
@@ -65,6 +65,14 @@
       }
       return sb.toString();
     }
+
+    public String[] stackTraceElementsAsString() {
+      String[] result = new String[stackTraceElements.length];
+      for (int i = 0; i < stackTraceElements.length; i++) {
+        result[i] = stackTraceElements[i].toString();
+      }
+      return result;
+    }
   }
 
   public static void onClassNewInstance(Class<?> clazz) {
diff --git a/src/main/java/com/android/tools/r8/R8AssistantCommand.java b/src/main/java/com/android/tools/r8/R8AssistantCommand.java
index 55efbd7..c939563 100644
--- a/src/main/java/com/android/tools/r8/R8AssistantCommand.java
+++ b/src/main/java/com/android/tools/r8/R8AssistantCommand.java
@@ -5,6 +5,8 @@
 
 import com.android.tools.r8.assistant.ClassInjectionHelper;
 import com.android.tools.r8.assistant.runtime.EmptyReflectiveOperationReceiver;
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationJsonLogger;
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationLogger;
 import com.android.tools.r8.assistant.runtime.ReflectiveOperationReceiver;
 import com.android.tools.r8.assistant.runtime.ReflectiveOracle;
@@ -148,6 +150,8 @@
     R8AssistantCommand makeCommand() {
       injectClasses(
           EmptyReflectiveOperationReceiver.class,
+          ReflectiveEventType.class,
+          ReflectiveOperationJsonLogger.class,
           ReflectiveOperationLogger.class,
           ReflectiveOperationReceiver.NameLookupType.class,
           ReflectiveOperationReceiver.ClassFlag.class,
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
new file mode 100644
index 0000000..b60812e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/ReflectiveOperationJsonParser.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.assistant.postprocessing;
+
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReflectiveOperationJsonParser {
+  public List<ReflectiveEvent> parse(Path file) throws IOException {
+    List<ReflectiveEvent> result = new ArrayList<>();
+    String contents = Files.readString(file) + "{}]";
+    JsonArray events = new JsonParser().parse(contents).getAsJsonArray();
+    for (JsonElement eventElement : events) {
+      JsonObject event = eventElement.getAsJsonObject();
+      if (event.isEmpty()) {
+        break;
+      }
+      ReflectiveEventType eventType = ReflectiveEventType.valueOf(event.get("event").getAsString());
+      JsonElement stackElement = event.get("stack");
+      String[] stack = stackElement != null ? toStringArray(stackElement) : null;
+      JsonElement argsElement = event.get("args");
+      String[] args = argsElement != null ? toStringArray(argsElement) : null;
+      result.add(ReflectiveEvent.instantiate(eventType, stack, args));
+    }
+    return result;
+  }
+
+  private String[] toStringArray(JsonElement argsElement) {
+    JsonArray jsonArray = argsElement.getAsJsonArray();
+    String[] strings = new String[jsonArray.size()];
+    for (int i = 0; i < strings.length; i++) {
+      strings[i] = jsonArray.get(i).getAsString();
+    }
+    return strings;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java
new file mode 100644
index 0000000..538f131
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/assistant/postprocessing/model/ReflectiveEvent.java
@@ -0,0 +1,32 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.assistant.postprocessing.model;
+
+import com.android.tools.r8.assistant.runtime.ReflectiveEventType;
+import java.util.Arrays;
+
+public class ReflectiveEvent {
+
+  private final ReflectiveEventType eventType;
+  private final String[] stack;
+  private final String[] args;
+
+  protected ReflectiveEvent(ReflectiveEventType eventType, String[] stack, String[] args) {
+    this.eventType = eventType;
+    this.stack = stack;
+    this.args = args;
+  }
+
+  @Override
+  public String toString() {
+    return eventType + (stack != null ? "[s]" : "") + "(" + Arrays.toString(args) + ")";
+  }
+
+  public static ReflectiveEvent instantiate(
+      ReflectiveEventType eventType, String[] stack, String[] args) {
+    // TODO(b/428836085): Switch on the eventType and build a subclass, make this class abstract.
+    return new ReflectiveEvent(eventType, stack, args);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java b/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java
new file mode 100644
index 0000000..960d60f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/assistant/JavaLangClassJsonTest.java
@@ -0,0 +1,55 @@
+// Copyright (c) 2025, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.assistant;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.assistant.JavaLangClassTestClass.Bar;
+import com.android.tools.r8.assistant.JavaLangClassTestClass.Foo;
+import com.android.tools.r8.assistant.postprocessing.ReflectiveOperationJsonParser;
+import com.android.tools.r8.assistant.postprocessing.model.ReflectiveEvent;
+import com.android.tools.r8.assistant.runtime.ReflectiveOperationJsonLogger;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class JavaLangClassJsonTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNativeMultidexDexRuntimes().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testInstrumentationWithCustomOracle() throws Exception {
+    Path path = Paths.get(temp.newFile().getAbsolutePath());
+    testForAssistant()
+        .addProgramClasses(JavaLangClassTestClass.class, Foo.class, Bar.class)
+        .addInstrumentationClasses(Instrumentation.class)
+        .setCustomReflectiveOperationReceiver(Instrumentation.class)
+        .setMinApi(parameters)
+        .compile()
+        .addVmArguments("-Dcom.android.tools.r8.reflectiveJsonLogger=" + path)
+        .run(parameters.getRuntime(), JavaLangClassTestClass.class)
+        .assertSuccess();
+    List<ReflectiveEvent> reflectiveEvents = new ReflectiveOperationJsonParser().parse(path);
+    Assert.assertEquals(28, reflectiveEvents.size());
+  }
+
+  public static class Instrumentation extends ReflectiveOperationJsonLogger {
+    public Instrumentation() throws IOException {}
+  }
+}