diff options
author | Michael Hoisie <hoisie@google.com> | 2024-01-25 09:39:02 -0800 |
---|---|---|
committer | Copybara-Service <copybara-worker@google.com> | 2024-01-25 09:39:37 -0800 |
commit | f11f710b333969cf1413833bee6d98c87e02272f (patch) | |
tree | c20b676f2a3ef673fa7845e5601dd46b20c9e91c /sandbox | |
parent | ce2bc5153c204248415f5b70ae368a5cb9afc94e (diff) | |
download | robolectric-f11f710b333969cf1413833bee6d98c87e02272f.tar.gz |
Remove exemptionlist-based throw-on-native method functionality
Now that native-backed shadows for graphics and SQLite are using
callNativeMethodsByDefault, any native methods that are not properly shadowed
will automatically throw an UnsatisfiedLinkError,
This makes the exemptionlist-based system for throwing on native methods
redundant as an UnsatisfiedLinkError will be thrown instead of a
NativeMethodNotFoundException.
The only potential drawback is that throw-on-native functionality will not be
enabled for legacy graphics and legacy SQLite shadows. However:
1) Legacy graphics and SQLite shadows are in maintenance mode and no new
functionality is being added to them.
2) The exemptionlist-based system could not differentiate between legacy and
native-backed shadows, which meant that exemptions were often added for legacy
shadows, and prevented throw-on-native from occurring in native-backed shadows,
where it was most useful.
3) It would be straightforward to add a warning for legacy shadows of native
methods at runtime. This is because ShadowWrangler now has information about
whether a method was originally native or not.
PiperOrigin-RevId: 601474699
Diffstat (limited to 'sandbox')
6 files changed, 1 insertions, 477 deletions
diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java index b3a251514..a9b532a26 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ClassInstrumentor.java @@ -38,7 +38,6 @@ import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; -import org.robolectric.sandbox.NativeMethodNotFoundException; import org.robolectric.util.PerfStatsCollector; /** @@ -54,7 +53,6 @@ public class ClassInstrumentor { protected static final Type OBJECT_TYPE = Type.getType(Object.class); private static final ShadowImpl SHADOW_IMPL = new ShadowImpl(); final Decorator decorator; - private NativeCallHandler nativeCallHandler; static { String className = Type.getInternalName(InvokeDynamicSupport.class); @@ -566,17 +564,6 @@ public class ClassInstrumentor { RobolectricGeneratorAdapter generator = new RobolectricGeneratorAdapter(method); - if (nativeCallHandler != null) { - String descriptor = - String.format("%s#%s%s", mutableClass.getName(), method.name, method.desc); - nativeCallHandler.logNativeCall(descriptor); - if (nativeCallHandler.shouldThrow(descriptor)) { - String message = - nativeCallHandler.getExceptionMessage(descriptor, mutableClass.getName(), method.name); - generator.throwException(Type.getType(NativeMethodNotFoundException.class), message); - } - } - Type returnType = generator.getReturnType(); generator.pushDefaultReturnValueToStack(returnType); generator.returnValue(); @@ -780,10 +767,6 @@ public class ClassInstrumentor { return -1; } - public void setNativeCallHandler(NativeCallHandler nativeCallHandler) { - this.nativeCallHandler = nativeCallHandler; - } - public interface Decorator { void decorate(MutableClass mutableClass); } diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java deleted file mode 100644 index 89034d63f..000000000 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/NativeCallHandler.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.robolectric.internal.bytecode; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Set; -import java.util.TreeSet; -import javax.annotation.Nonnull; - -/** - * Handler for native calls instrumented by ClassInstrumentor. - * - * <p>Native Calls can either be instrumented as no-op calls (returning a default value or 0 or - * null) or throw an exception. This helper class helps maintain a list of exemptions to indicates - * which native calls should be no-op and never throw. - */ -public class NativeCallHandler { - - private final File exemptionsFile; - private final boolean writeExemptions; - private final boolean throwOnNatives; - private final Set<String> descriptors = new TreeSet<>(); - - /** - * Initializes the native calls handler. - * - * @param exemptionsFile The exemptions file to read from and/or to generate. - * @param writeExemptions When true, native calls are added to the exemption list. - * @param throwOnNatives Whether native calls should throw by default unless their signature is - * listed in the exemption list. When false, all native calls become no-op. - * @throws IOException if there's an issue reading an existing exemption list. - */ - public NativeCallHandler( - @Nonnull File exemptionsFile, boolean writeExemptions, boolean throwOnNatives) - throws IOException { - this.exemptionsFile = exemptionsFile; - this.writeExemptions = writeExemptions; - this.throwOnNatives = throwOnNatives; - - if (exemptionsFile.exists()) { - readExemptionsList(exemptionsFile); - } - } - - private String getExemptionFileName() { - return exemptionsFile.getName(); - } - - private void readExemptionsList(File exemptionsFile) throws IOException { - try (BufferedReader reader = - new BufferedReader(new FileReader(exemptionsFile.getPath(), UTF_8))) { - String line; - while ((line = reader.readLine()) != null) { - // Sanitize input. Ignore empty lines and commented lines starting with #. - line = sanitize(line.trim()); - if (line.isEmpty() || line.charAt(0) == '#') { - continue; - } - descriptors.add(line); - } - } - System.out.println( - "Loaded " + descriptors.size() + " exemptions from " + exemptionsFile.getPath()); - } - - public void writeExemptionsList() throws IOException { - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { - for (String descriptor : descriptors) { - writer.write(descriptor); - writer.write('\n'); - } - } - System.out.println( - "Wrote " + descriptors.size() + " exemptions to " + exemptionsFile.getPath()); - } - - /** - * Adds the method description to the native call exemption list if {@link #writeExemptions} is - * set. - */ - public void logNativeCall(@Nonnull String descriptor) { - if (!writeExemptions) { - return; - } - descriptors.add(sanitize(descriptor)); - } - - /** Returns whether the ClassInstrumentor should generate an exception or a no-op bytecode. */ - public boolean shouldThrow(@Nonnull String descriptor) { - return throwOnNatives && !descriptors.contains(sanitize(descriptor)); - } - - private String sanitize(String descriptor) { - // Post-processing of the exemptions files is made complicated by the presence of $ signs - // in the FQCN. Instead of escaping them, just replace them by another unused character - // that is not so sensitive to shell or make mangling. - return descriptor.replace('$', '^'); - } - - /** - * Returns the detailed message to be used by the ClassInstrumentor in the generated bytecode. - * - * @param descriptor The ASM descriptor as it should be written in the exemption file. - * @param className The fully qualified class name, used for the user description. - * @param methodName The method name, used for the user description. - */ - public String getExceptionMessage( - @Nonnull String descriptor, @Nonnull String className, @Nonnull String methodName) { - // The shadow message is merely a hint based on the last component of the FQCN, which is - // typically the pattern used for shadow classes. - String shadowHint = - "Shadow" + className.replaceAll("[^.]+\\.", "").replaceAll("\\$.*", "") + ".java"; - // The message below tries to educate the user that shadow overrides are not necessarily - // needed nor desired for trivial cases that are better covered by a no-op return operation. - return "Unexpected Robolectric native method call to '" - + className - + "#" - + methodName - + "()'.\n" - + "Option 1: If customizing this method is useful, add an implementation in " - + shadowHint - + ".\n" - + "Option 2: If this method just needs to trivially return 0 or null, please add an" - + " exemption entry for\n" - + " " - + sanitize(descriptor) - + "\n" - + "to exemption file " - + getExemptionFileName(); - } -} diff --git a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java index 5e03aa945..1c6d8c19c 100644 --- a/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java +++ b/sandbox/src/main/java/org/robolectric/internal/bytecode/ShadowWrangler.java @@ -23,7 +23,6 @@ import javax.annotation.Nonnull; import javax.annotation.Priority; import org.robolectric.annotation.RealObject; import org.robolectric.annotation.ReflectorObject; -import org.robolectric.sandbox.NativeMethodNotFoundException; import org.robolectric.sandbox.ShadowMatcher; import org.robolectric.util.Function; import org.robolectric.util.PerfStatsCollector; @@ -183,21 +182,7 @@ public class ShadowWrangler implements ClassHandler { } else { RobolectricInternals.performStaticInitialization(clazz); } - } catch (InvocationTargetException e) { - // Note: target exception originates from the sandbox classloader. - // "instanceof" does not check class equality across classloaders (since they differ). - // A simple workaround is to check the class FQCN instead. - String nativeMethodNotFoundException = NativeMethodNotFoundException.class.getName(); - - for (Throwable t = e.getTargetException(); t != null; ) { - if (nativeMethodNotFoundException.equals(t.getClass().getName())) { - throw (RuntimeException) t; - } - - t = t.getCause(); - } - throw new RuntimeException(e); - } catch (IllegalAccessException e) { + } catch (InvocationTargetException | IllegalAccessException e) { throw new RuntimeException(e); } } diff --git a/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java b/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java deleted file mode 100644 index ad04d959f..000000000 --- a/sandbox/src/main/java/org/robolectric/sandbox/NativeMethodNotFoundException.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.robolectric.sandbox; - -/** - * Thrown when a particular Robolectric native method cannot be found. - * - * <p>Instrumented native methods throw this exception when the NativeCallHandler is set to - * throw-on-native and that the dedicated method signature has not been exempted. - */ -public class NativeMethodNotFoundException extends RuntimeException { - - public NativeMethodNotFoundException() { - super(); - } - - public NativeMethodNotFoundException(String message) { - super(message); - } -} diff --git a/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java b/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java index bd4bbe08e..0d89cba47 100644 --- a/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java +++ b/sandbox/src/test/java/org/robolectric/internal/bytecode/ClassInstrumentorTest.java @@ -1,13 +1,8 @@ package org.robolectric.internal.bytecode; import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.Iterables; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -59,72 +54,6 @@ public class ClassInstrumentorTest { } @Test - public void instrumentNativeMethod_withoutExemption_generatesThrowException() throws IOException { - File exemptionsFile = tempFolder.newFile("natives.txt"); - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { - writer.write("org.example.MyClass#someOtherMethod()V\n"); - } - - NativeCallHandler nativeCallHandler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); - instrumentor.setNativeCallHandler(nativeCallHandler); - - ClassNode classNode = createClassWithNativeMethod(); - MutableClass clazz = - new MutableClass( - classNode, InstrumentationConfiguration.newBuilder().build(), classNodeProvider); - instrumentor.instrument(clazz); - - String someFunctionName = Shadow.directMethodName("org.example.MyClass", "someFunction"); - MethodNode methodNode = findMethodNode(classNode, someFunctionName); - // Side effect: original method has been made private. - assertThat(methodNode.access & Opcodes.ACC_PRIVATE).isNotEqualTo(0); - // Side effect: instructions have been rewritten to throw and return. - assertThat(methodNode.instructions.size()).isEqualTo(7); - assertThat(methodNode.instructions.get(0).getOpcode()).isEqualTo(Opcodes.NEW); - assertThat(methodNode.instructions.get(1).getOpcode()).isEqualTo(Opcodes.DUP); - assertThat(methodNode.instructions.get(2).getOpcode()).isEqualTo(Opcodes.LDC); - assertThat(methodNode.instructions.get(3).getOpcode()).isEqualTo(Opcodes.INVOKESPECIAL); - assertThat(methodNode.instructions.get(4).getOpcode()).isEqualTo(Opcodes.ATHROW); - assertThat(methodNode.instructions.get(5).getOpcode()).isEqualTo(Opcodes.ICONST_0); - assertThat(methodNode.instructions.get(6).getOpcode()).isEqualTo(Opcodes.IRETURN); - } - - @Test - public void instrumentNativeMethod_withExemption_generatesNoOpReturn() throws IOException { - File exemptionsFile = tempFolder.newFile("natives.txt"); - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { - writer.write("org.example.MyClass#someOtherMethod()V\n"); - writer.write("org.example.MyClass#someFunction()I\n"); - } - - NativeCallHandler nativeCallHandler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); - instrumentor.setNativeCallHandler(nativeCallHandler); - - ClassNode classNode = createClassWithNativeMethod(); - - MutableClass clazz = - new MutableClass( - classNode, InstrumentationConfiguration.newBuilder().build(), classNodeProvider); - instrumentor.instrument(clazz); - - String someFunctionName = Shadow.directMethodName("org.example.MyClass", "someFunction"); - MethodNode methodNode = findMethodNode(classNode, someFunctionName); - - // Side effect: original method has been made private. - assertThat(methodNode.access & Opcodes.ACC_PRIVATE).isNotEqualTo(0); - // Side effect: instructions have been rewritten to return 0. - assertThat(methodNode.instructions.size()).isEqualTo(2); - assertThat(methodNode.instructions.get(0).getOpcode()).isEqualTo(Opcodes.ICONST_0); - assertThat(methodNode.instructions.get(1).getOpcode()).isEqualTo(Opcodes.IRETURN); - } - - @Test public void instrumentNativeMethod_generatesNativeBindingMethod() { ClassNode classNode = createClassWithNativeMethod(); MutableClass clazz = diff --git a/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java b/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java deleted file mode 100644 index 04035346e..000000000 --- a/sandbox/src/test/java/org/robolectric/internal/bytecode/NativeCallHandlerTest.java +++ /dev/null @@ -1,218 +0,0 @@ -package org.robolectric.internal.bytecode; - -import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.io.Files; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -/** Test for {@link NativeCallHandler}. */ -@RunWith(JUnit4.class) -public class NativeCallHandlerTest { - @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); - - @Test - public void jarInstrumentorLegacyUsage() throws IOException { - // CUJ: Legacy jarInstrumentor usage; there is no exemption file, native methods do not throw. - - File exemptionsFile = tempFolder.newFile("natives.txt"); - assertThat(exemptionsFile.delete()).isTrue(); - - // Create handler, which loads exemptions from file. It's fine for the file to be missing. - NativeCallHandler handler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ false); - - // No method descriptor should throw. - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse(); - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isFalse(); - } - - @Test - public void jarInstrumentorUsage_throwOnNativesEnabled() throws IOException { - // CUJ: jarInstrumentor usage with an exemption list and non-exempted native methods should - // throw. - - File exemptionsFile = tempFolder.newFile("natives.txt"); - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { - writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n"); - writer.write("libcore.io.Linux#chmod(Ljava/lang/String;I)V\n"); - writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n"); - writer.write("android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n"); - writer.write("org.example.MyClass#someOtherMethod()V\n"); - // empty or white-space lines are ignored - writer.write("\n"); - writer.write(" \t \n"); - // A # prefix denotes a comment and is ignored too - writer.write("# org.example.Ignored#comment()V\n"); - writer.write(" # org.example.Ignored#thisIsACommentToo()V \n"); - } - - // Create handler, which loads exemptions from file. ThrowOnNatives is enabled. - NativeCallHandler handler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); - - // Test exempted methods - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse(); - - // Test non-exempted methods - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isTrue(); - - // Empty lines and comments are ignored and not present in the exemption list. - assertThat(handler.shouldThrow("")).isTrue(); - assertThat(handler.shouldThrow(" \t ")).isTrue(); - assertThat(handler.shouldThrow("# org.example.Ignored#comment()V")).isTrue(); - assertThat(handler.shouldThrow(" # org.example.Ignored#thisIsACommentToo()V ")).isTrue(); - } - - @Test - public void jarInstrumentorUsage_throwOnNativesDisabled() throws IOException { - // CUJ: jarInstrumentor usage with an exemption list and non-exempted native methods should - // throw. - - File exemptionsFile = tempFolder.newFile("natives.txt"); - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { - writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n"); - writer.write("libcore.io.Linux#chmod(Ljava/lang/String;I)V\n"); - writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n"); - writer.write("android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n"); - writer.write("org.example.MyClass#someOtherMethod()V\n"); - } - - // Create handler, which loads exemptions from file. ThrowOnNatives is disabled. - NativeCallHandler handler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ false); - - // Test exempted methods - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse(); - - // Test non-exempted methods - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod(II)V")).isFalse(); - } - - @Test - public void jarInstrumentorUsage_logNativeCall_ignored() throws IOException { - // When not writing the exemption list, logNativeCall calls are no-op. - - File exemptionsFile = tempFolder.newFile("natives.txt"); - - // Create handler, which loads exemptions from file. ThrowOnNatives is enabled. - NativeCallHandler handler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); - - // No methods are exempted -- initial list is empty. - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue(); - assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue(); - - handler.logNativeCall("org.example.MyClass#someOtherMethod()V"); - handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V"); - - // LogNativeCall did not capture. These methods are still not exempted. - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue(); - assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue(); - } - - @Test - public void exemptionListGeneratorUsage_logNativeCall_capturesCalls() throws IOException { - // CUJ: jarInstrumentor called to generate the exemption list. - - File exemptionsFile = tempFolder.newFile("natives.txt"); - - // Create handler, which loads exemptions from file. ThrowOnNatives is enabled. - NativeCallHandler handler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ true, /* throwOnNatives= */ true); - - // No methods are exempted -- initial list is empty. - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isTrue(); - assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isTrue(); - - handler.logNativeCall("org.example.MyClass#someOtherMethod()V"); - handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V"); - - // These methods are now exempted. - assertThat(handler.shouldThrow("org.example.MyClass#someOtherMethod()V")).isFalse(); - assertThat(handler.shouldThrow("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V")).isFalse(); - } - - @Test - public void exemptionListGeneratorUsage_writeExemptionFile() throws IOException { - // CUJ: jarInstrumentor called to generate the exemption list. - - File exemptionsFile = tempFolder.newFile("natives.txt"); - try (BufferedWriter writer = - new BufferedWriter(new FileWriter(exemptionsFile.getPath(), UTF_8))) { - writer.write("android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n"); - writer.write("libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n"); - } - - // Create handler, which loads exemptions from file. ThrowOnNatives is disabled. - NativeCallHandler handler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ true, /* throwOnNatives= */ false); - - handler.logNativeCall("org.example.MyClass#someOtherMethod()V"); - // Multiple calls with same value are idempotent. - handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V"); - handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V"); - handler.logNativeCall("org.example.MyClass#someOtherMethod(I)V"); - handler.logNativeCall("org.example.MyClass#someOtherMethod(II)V"); - handler.logNativeCall("libcore.io.Linux#chmod(Ljava/lang/String;I)V"); - // Case of a nested class with $ in the FQCN. - handler.logNativeCall("android.graphics.fonts.Font$Builder#nAddAxis(JIF)V"); - - handler.writeExemptionsList(); - - // Note: due to how the generated files are manipulated in the shell/makefile build system, - // '$' characters are a problem and would need to be escaped (and potentially differently for - // shell vs makefiles). The workaround is to have '$' rewritten as '^'. - - assertThat(Files.asCharSource(exemptionsFile, UTF_8).read()) - .isEqualTo( - "android.app.ActivityThread#dumpGraphicsInfo(Ljava/io/FileDescriptor;)V\n" - // Font$Builder gets written as Font^Builder. - + "android.graphics.fonts.Font^Builder#nAddAxis(JIF)V\n" - + "libcore.io.Linux#chmod(Ljava/lang/String;I)V\n" - + "libcore.io.Linux#fchmod(Ljava/io/FileDescriptor;I)V\n" - + "org.example.MyClass#someOtherMethod()V\n" - + "org.example.MyClass#someOtherMethod(I)V\n" - + "org.example.MyClass#someOtherMethod(II)V\n"); - } - - @Test - public void getExceptionMessage() throws IOException { - File exemptionsFile = tempFolder.newFile("natives.txt"); - NativeCallHandler handler = - new NativeCallHandler( - exemptionsFile, /* writeExemptions= */ false, /* throwOnNatives= */ true); - - // Test generated exception message for non-exempted methods. - assertThat( - handler.getExceptionMessage( - "org.example.MyClass$1#someOtherMethod(II)V", - "org.example.MyClass$1", - "someOtherMethod")) - .isEqualTo( - "Unexpected Robolectric native method call to" - + " 'org.example.MyClass$1#someOtherMethod()'.\n" - + "Option 1: If customizing this method is useful, add an implementation in" - + " ShadowMyClass.java.\n" - + "Option 2: If this method just needs to trivially return 0 or null, please add an" - + " exemption entry for\n" - + " org.example.MyClass^1#someOtherMethod(II)V\n" - + "to exemption file natives.txt"); - } -} |