mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
test: check the Java methods Rust calls on Android
This commit is contained in:
1 parent
9c96f2a2cf
commit
3b37623040
1 file changed
+90
-3
@@ -20,6 +20,77 @@ spec = importlib.util.spec_from_file_location(
|
||||
native = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(native)
|
||||
|
||||
ANDROID = Path(__file__).resolve().parents[1] / "platform/android"
|
||||
APP_JAVA = ANDROID / "gradle/app/src/main/java/com/flectar/mail"
|
||||
JAVA_PRIMITIVES = {
|
||||
"boolean": "Z", "byte": "B", "char": "C", "short": "S",
|
||||
"int": "I", "long": "J", "float": "F", "double": "D", "void": "V",
|
||||
}
|
||||
JAVA_STATEMENTS = {"if", "for", "while", "switch", "catch", "synchronized", "return", "new",
|
||||
"throw", "else"}
|
||||
JAVA_MODIFIERS = {"public", "protected", "private", "static", "final", "synchronized", "native"}
|
||||
|
||||
|
||||
def java_descriptor(java_type, imports, app_classes):
|
||||
"""The JNI descriptor of a type as a Java source file names it."""
|
||||
dimensions = java_type.count("[]") + java_type.count("...")
|
||||
name = java_type.replace("[]", "").replace("...", "").strip()
|
||||
if name in JAVA_PRIMITIVES:
|
||||
return "[" * dimensions + JAVA_PRIMITIVES[name]
|
||||
outer, _, inner = name.partition(".")
|
||||
if inner and outer[0].islower():
|
||||
# Named with its package.
|
||||
qualified = name
|
||||
elif outer in imports:
|
||||
qualified = imports[outer] + ("$" + inner.replace(".", "$") if inner else "")
|
||||
elif outer in app_classes:
|
||||
qualified = "com.flectar.mail." + name.replace(".", "$")
|
||||
else:
|
||||
# Java imports java.lang itself.
|
||||
qualified = "java.lang." + name
|
||||
return "[" * dimensions + "L" + qualified.replace(".", "/") + ";"
|
||||
|
||||
|
||||
def java_methods():
|
||||
"""The descriptor of each method the app's Java declares, with whether it is static."""
|
||||
sources = {source.stem: source.read_text() for source in APP_JAVA.glob("*.java")}
|
||||
methods = {}
|
||||
for text in sources.values():
|
||||
imports = {name: path for path, name in re.findall(r"^import\s+([\w.]+\.(\w+));", text, re.M)}
|
||||
# Descriptors leave out type arguments.
|
||||
while re.search(r"<[\w.?, \[\]]*>", text):
|
||||
text = re.sub(r"<[\w.?, \[\]]*>", "", text)
|
||||
for modifiers, returns, name, parameters in re.findall(
|
||||
rf"^[ \t]*((?:(?:{'|'.join(JAVA_MODIFIERS)})\s+)*)"
|
||||
r"([\w.$\[\]]+)\s+(\w+)\s*\(([^)]*)\)", text, re.M):
|
||||
if returns in JAVA_STATEMENTS | JAVA_MODIFIERS:
|
||||
continue
|
||||
types = [re.sub(r"\s+\w+$", "", re.sub(r"@\w+\s+|\bfinal\s+", "", parameter.strip()))
|
||||
for parameter in parameters.split(",") if parameter.strip()]
|
||||
descriptor = ("(" + "".join(java_descriptor(t, imports, sources) for t in types) + ")"
|
||||
+ java_descriptor(returns, imports, sources))
|
||||
methods.setdefault(name, set()).add((descriptor, "static" in modifiers.split()))
|
||||
return methods
|
||||
|
||||
|
||||
def rust_calls_into_java():
|
||||
"""Each method the Android crate calls by name, with its descriptor and whether it is static."""
|
||||
calls = []
|
||||
for source in sorted((ANDROID / "src").glob("*.rs")):
|
||||
# A backslash at the end of a line continues a Rust string without the line break.
|
||||
text = re.sub(r"\\\n\s*", "", source.read_text())
|
||||
for match in re.finditer(r'"(\w+)",\s*"(\([^"]*\)[^"]*)"', text):
|
||||
before = text[:match.start()]
|
||||
static = before.rfind("call_static_method(") > before.rfind("call_method(")
|
||||
calls.append((source.name, match[1], match[2], static))
|
||||
# Helpers that take the method's name, as the settings ones do.
|
||||
for helper, descriptor in re.findall(
|
||||
r'fn (\w+)\(&self, class: &str, method: &str\)[^}]*?'
|
||||
r'call_static_method\(\s*class,\s*method,\s*"([^"]+)"', text, re.S):
|
||||
for name in re.findall(rf'\.{helper}\(\w+, "(\w+)"\)', text):
|
||||
calls.append((source.name, name, descriptor, True))
|
||||
return calls
|
||||
|
||||
|
||||
class JniExportsTests(unittest.TestCase):
|
||||
def build_library(self, directory, replacement=None, stripped=True):
|
||||
@@ -38,16 +109,32 @@ class JniExportsTests(unittest.TestCase):
|
||||
return library.read_bytes()
|
||||
|
||||
def test_every_native_declaration_is_required(self):
|
||||
package = (Path(__file__).resolve().parents[1] / "platform/android/gradle/app/src/main"
|
||||
/ "java/com/flectar/mail")
|
||||
declared = {
|
||||
f"Java_com_flectar_mail_{source.stem}_{method}"
|
||||
for source in package.glob("*.java")
|
||||
for source in APP_JAVA.glob("*.java")
|
||||
for method in re.findall(r"\bnative\s+\w+\s+(\w+)\s*\(", source.read_text())
|
||||
}
|
||||
required = {name for name in native.REQUIRED_EXPORTS if name.startswith("Java_")}
|
||||
self.assertEqual(declared, required)
|
||||
|
||||
def test_rust_calls_find_their_java_methods(self):
|
||||
# PR checks don't build the app, and ART reports a method it can't
|
||||
# find only when a phone makes the call.
|
||||
java = java_methods()
|
||||
sources = "\n".join(source.read_text() for source in APP_JAVA.glob("*.java"))
|
||||
declared = set(re.findall(r"\b(\w+)\s*\([^;{)]*\)\s*(?:throws\s+[\w.,\s]+)?\{",
|
||||
sources)) - JAVA_STATEMENTS
|
||||
checked = set()
|
||||
for source, name, descriptor, static in rust_calls_into_java():
|
||||
if name not in declared:
|
||||
continue # an Android or Java method
|
||||
with self.subTest(call=f"{source}: {name}{descriptor}"):
|
||||
self.assertIn((descriptor, static), java.get(name, set()))
|
||||
checked.add(name)
|
||||
# Calls in strings across lines, through the settings helper and in
|
||||
# every file are all read.
|
||||
self.assertLessEqual({"show", "openSettings", "authorize", "chooseDocument"}, checked)
|
||||
|
||||
def test_release_stripping_preserves_required_dynamic_exports(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
data = self.build_library(directory)
|
||||
|
||||
Reference in new issue
Block a user