
org.robolectric.internal.bytecode.NativeCallHandler Maven / Gradle / Ivy
Show all versions of sandbox Show documentation
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.
*
* 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 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();
}
}