scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
158 lines
6.2 KiB
Python
Executable File
158 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import importlib.util
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
SCRIPT = Path(__file__).with_name("check_shrinker_rules.py")
|
|
SPEC = importlib.util.spec_from_file_location("check_shrinker_rules", SCRIPT)
|
|
CHECKER = importlib.util.module_from_spec(SPEC)
|
|
assert SPEC.loader is not None
|
|
SPEC.loader.exec_module(CHECKER)
|
|
|
|
# The descriptor is split across two literals exactly as ffmpeg_jni.cc has it, so the
|
|
# fixture also covers C string concatenation.
|
|
JNI_SOURCE = """
|
|
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|
jclass clazz = env->FindClass("androidx/media3/decoder/ffmpeg/FfmpegAudioDecoder");
|
|
growOutputBufferMethod = env->GetMethodID(
|
|
clazz, "growOutputBuffer",
|
|
"(Landroidx/media3/decoder/"
|
|
"SimpleDecoderOutputBuffer;I)Ljava/nio/ByteBuffer;");
|
|
return JNI_VERSION_1_6;
|
|
}
|
|
"""
|
|
|
|
FULL_RULES = (
|
|
"-keep class androidx.media3.decoder.ffmpeg.** { *; }\n"
|
|
"-keep class androidx.media3.decoder.SimpleDecoderOutputBuffer { *; }\n"
|
|
)
|
|
|
|
|
|
class ShrinkerRulesCheckerTest(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.temporary = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temporary.name)
|
|
java_path = self.root / "android/app/src/main/java/androidx/media3/decoder/ffmpeg"
|
|
java_path.mkdir(parents=True)
|
|
(java_path / "FfmpegAudioRenderer.java").write_text("// fixture\n", encoding="utf-8")
|
|
(java_path / "FfmpegAudioDecoder.java").write_text("// fixture\n", encoding="utf-8")
|
|
self.cpp_path = self.root / "android/app/src/main/cpp/media3_ffmpeg_decoder"
|
|
self.cpp_path.mkdir(parents=True)
|
|
self.jni_path = self.cpp_path / "ffmpeg_jni.cc"
|
|
self.jni_path.write_text(JNI_SOURCE, encoding="utf-8")
|
|
self.rules_path = self.root / "android/app/proguard-rules.pro"
|
|
self.rules_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
def tearDown(self) -> None:
|
|
self.temporary.cleanup()
|
|
|
|
def _write_rules(self, rules: str) -> None:
|
|
self.rules_path.write_text(rules, encoding="utf-8")
|
|
|
|
@staticmethod
|
|
def _failure_kinds(errors: list[str]) -> set[str]:
|
|
kinds = set()
|
|
for error in errors:
|
|
for marker in ("reflected namespace", "with FindClass", "from native code", "in the descriptor"):
|
|
if marker in error:
|
|
kinds.add(marker)
|
|
return kinds
|
|
|
|
@staticmethod
|
|
def _every_failure_kind() -> set[str]:
|
|
return {"reflected namespace", "with FindClass", "from native code", "in the descriptor"}
|
|
|
|
def test_repository_rules_cover_every_name_reached_class_and_member(self) -> None:
|
|
self.assertEqual([], CHECKER.validate(Path(__file__).resolve().parents[2]))
|
|
|
|
def test_package_keep_plus_descriptor_keep_passes(self) -> None:
|
|
self._write_rules(FULL_RULES)
|
|
|
|
self.assertEqual([], CHECKER.validate(self.root))
|
|
|
|
def test_missing_rules_file_is_reported(self) -> None:
|
|
errors = CHECKER.validate(self.root)
|
|
|
|
self.assertEqual(1, len(errors))
|
|
self.assertIn("proguard-rules.pro is missing", errors[0])
|
|
|
|
def test_uncovered_reflected_class_is_reported(self) -> None:
|
|
self._write_rules(
|
|
"-keep class androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder { *; }\n"
|
|
"-keep class androidx.media3.decoder.SimpleDecoderOutputBuffer { *; }\n"
|
|
)
|
|
|
|
self.assertEqual(
|
|
["androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer lives in a reflected namespace "
|
|
"but no -keep in android/app/proguard-rules.pro covers it"],
|
|
CHECKER.validate(self.root),
|
|
)
|
|
|
|
def test_single_star_does_not_span_packages(self) -> None:
|
|
self._write_rules("-keep class androidx.media3.* { *; }\n")
|
|
|
|
self.assertEqual(self._every_failure_kind(), self._failure_kinds(CHECKER.validate(self.root)))
|
|
|
|
def test_keepclassmembers_alone_does_not_count_as_a_keep(self) -> None:
|
|
# media3's consumer rules only keep the constructor, which neither keeps the class
|
|
# nor pins its name. That is the state that shipped a release without the decoder.
|
|
self._write_rules(
|
|
"-keepclassmembers class androidx.media3.decoder.ffmpeg.FfmpegAudioRenderer {\n"
|
|
" <init>(android.os.Handler);\n"
|
|
"}\n"
|
|
)
|
|
|
|
self.assertEqual(self._every_failure_kind(), self._failure_kinds(CHECKER.validate(self.root)))
|
|
|
|
def test_class_keep_without_the_native_callback_member_is_reported(self) -> None:
|
|
self._write_rules(
|
|
"-keep class androidx.media3.decoder.ffmpeg.** {\n"
|
|
" <init>(android.os.Handler);\n"
|
|
"}\n"
|
|
"-keep class androidx.media3.decoder.SimpleDecoderOutputBuffer { *; }\n"
|
|
)
|
|
|
|
self.assertEqual(
|
|
["android/app/src/main/cpp/media3_ffmpeg_decoder/ffmpeg_jni.cc resolves "
|
|
"androidx.media3.decoder.ffmpeg.FfmpegAudioDecoder.growOutputBuffer from native code "
|
|
"but no -keep retains that member"],
|
|
CHECKER.validate(self.root),
|
|
)
|
|
|
|
def test_renamable_descriptor_class_is_reported(self) -> None:
|
|
self._write_rules("-keep class androidx.media3.decoder.ffmpeg.** { *; }\n")
|
|
|
|
errors = CHECKER.validate(self.root)
|
|
|
|
self.assertEqual(1, len(errors))
|
|
self.assertIn("names androidx.media3.decoder.SimpleDecoderOutputBuffer in the descriptor", errors[0])
|
|
|
|
def test_includedescriptorclasses_satisfies_the_descriptor_requirement(self) -> None:
|
|
self._write_rules("-keep,includedescriptorclasses class androidx.media3.decoder.ffmpeg.** { *; }\n")
|
|
|
|
self.assertEqual([], CHECKER.validate(self.root))
|
|
|
|
def test_platform_descriptor_types_need_no_keep(self) -> None:
|
|
# java.nio.ByteBuffer is in the return descriptor and must not be demanded.
|
|
self._write_rules(FULL_RULES)
|
|
|
|
self.assertEqual([], CHECKER.validate(self.root))
|
|
|
|
def test_untraceable_member_lookup_fails_loudly(self) -> None:
|
|
self.jni_path.write_text(
|
|
'growOutputBufferMethod = env->GetMethodID(someClass, "growOutputBuffer", "()V");\n',
|
|
encoding="utf-8",
|
|
)
|
|
self._write_rules(FULL_RULES)
|
|
|
|
errors = CHECKER.validate(self.root)
|
|
|
|
self.assertEqual(1, len(errors))
|
|
self.assertIn("cannot trace", errors[0])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|