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 {}
+ }
+}