Files
LihonT/tests/test_profile_flags.py
cheb0chik ce1d870d54 LihonT 2 Luna beta: source release
Mod for TikTok 47.1.3 (arm64) in Material 3 style: bottom bar, profile menu, TikTok settings and
comments; downloads without the watermark; switches for ads, live streams and the Shop tab; badges of
other mods next to names and the niche mod badge (members service); app icons; mod settings screen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 20:32:13 +03:00

204 lines
7.2 KiB
Python

"""Native flag reads are routed through LihonTFlags, and LihonTFlags obeys the settings.
The smali rewriter is exercised on small synthetic methods. The runtime class is
compiled and run on a real JVM against the production settings classes; Android
itself is only on the classpath, as in test_settings_model.
"""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from lihont import profile_features
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src" / "lihont" / "two"
HARNESS = ROOT / "tests" / "java" / "lihont" / "two" / "FlagsTest.java"
HOOK = "Llihont/two/LihonTFlags;"
ALL_FLAGS = {**profile_features.PROFILE_FLAGS, **profile_features.REPOST_FLAGS}
INT_READ = """\
.class public Lcom/example/Reader;
.super Ljava/lang/Object;
.method public static read()I
.registers 3
const-string v0, "profile_left_align"
const/4 v1, 0x0
invoke-static {v0, v1}, Lcom/bytedance/ies/abmock/SettingsManager;->LIZ(Ljava/lang/String;I)I
move-result v2
return v2
.end method
"""
BOOL_READ = INT_READ.replace("(Ljava/lang/String;I)I", "(Ljava/lang/String;Z)Z").replace(
"read()I", "read()Z")
PARAMETER_READ = """\
.class public Lcom/example/Reader;
.super Ljava/lang/Object;
.method public run(I)I
.registers 5
const-string v0, "repost_support_digg"
invoke-static {v0, p1}, LX/0abc;->LIZ(Ljava/lang/String;I)I
move-result p1
return p1
.end method
"""
LEGACY = """\
.class public Lcom/example/Reader;
.super Ljava/lang/Object;
.method public static read()I
.registers 3
const-string v0, "profile_left_align"
const/4 v1, 0x0
invoke-static {v0, v1}, Lcom/bytedance/ies/abmock/SettingsManager;->LIZ(Ljava/lang/String;I)I
move-result v2
# lihont feature flag: profile_left_align
const/16 v2, 0x1
return v2
.end method
"""
class RewriteTest(unittest.TestCase):
def test_int_read_goes_through_the_hook(self):
text, hits = profile_features.rewrite_smali(INT_READ, ALL_FLAGS)
self.assertEqual(hits, {"profile_left_align": 1})
self.assertIn(
" move-result v2\n"
" # lihont feature flag: profile_left_align\n"
" invoke-static/range {v2 .. v2}, %s->profile_left_align(I)I\n"
" move-result v2\n" % HOOK, text)
self.assertNotIn("const/16", text)
self.assertIn("SettingsManager;->LIZ(Ljava/lang/String;I)I", text)
self.assertIn(" return v2\n", text)
def test_boolean_read_uses_the_boolean_overload(self):
text, hits = profile_features.rewrite_smali(BOOL_READ, ALL_FLAGS)
self.assertEqual(hits, {"profile_left_align": 1})
self.assertIn("%s->profile_left_align(Z)Z" % HOOK, text)
def test_parameter_register_is_kept(self):
text, hits = profile_features.rewrite_smali(PARAMETER_READ, ALL_FLAGS)
self.assertEqual(hits, {"repost_support_digg": 1})
self.assertIn("invoke-static/range {p1 .. p1}, %s->repost_support_digg(I)I" % HOOK, text)
self.assertIn(" move-result p1\n\n return p1\n", text)
def test_unknown_keys_are_left_alone(self):
text, hits = profile_features.rewrite_smali(
INT_READ.replace("profile_left_align", "some_other_flag"), ALL_FLAGS)
self.assertEqual(hits, {})
self.assertNotIn("LihonTFlags", text)
def test_rewriting_twice_changes_nothing(self):
once, _ = profile_features.rewrite_smali(INT_READ, ALL_FLAGS)
twice, hits = profile_features.rewrite_smali(once, ALL_FLAGS)
self.assertEqual(hits, {})
self.assertEqual(once, twice)
def test_old_const_override_is_replaced(self):
text, hits = profile_features.rewrite_smali(LEGACY, ALL_FLAGS)
self.assertEqual(hits, {"profile_left_align": 1})
self.assertNotIn("const/16", text)
self.assertEqual(text.count("# lihont feature flag"), 1)
self.assertIn("(I)I\n move-result v2\n\n return v2\n", text)
def test_marker_without_a_known_block_does_not_eat_code(self):
stray = INT_READ.replace(
" move-result v2\n",
" move-result v2\n # lihont feature flag: profile_left_align\n")
text, hits = profile_features.rewrite_smali(stray, ALL_FLAGS)
self.assertEqual(hits, {"profile_left_align": 1})
self.assertIn(" return v2\n", text)
class RuntimeTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.javac = shutil.which("javac")
cls.java = shutil.which("java")
if not cls.javac or not cls.java:
raise RuntimeError("Flag runtime tests require a JDK with java and javac")
cls.android_jar = ROOT / "tools" / "android.jar"
if not cls.android_jar.is_file():
raise RuntimeError("tools/android.jar must be available")
cls.temporary = tempfile.TemporaryDirectory(prefix="lihont-flags-tests-")
cls.addClassCleanup(cls.temporary.cleanup)
cls.classes = Path(cls.temporary.name) / "classes"
cls.classes.mkdir()
cls.environment = dict(os.environ)
cls.environment.pop("JAVA_TOOL_OPTIONS", None)
settings = SRC / "settings"
cls.run_command([
cls.javac, "--release", "17", "-encoding", "UTF-8",
"-cp", str(cls.android_jar), "-d", str(cls.classes),
str(settings / "ModSettings.java"), str(settings / "SettingsCodec.java"),
str(settings / "SettingsRepository.java"), str(settings / "AndroidSettingsStorage.java"),
str(settings / "SettingsHost.java"), str(SRC / "LihonTFlags.java"), str(HARNESS),
])
@classmethod
def run_command(cls, command):
result = subprocess.run(
command, capture_output=True, text=True, encoding="utf-8",
env=cls.environment, timeout=60,
)
if result.returncode:
raise AssertionError(
f"Command failed with exit {result.returncode}: {command[0]}\n"
+ result.stdout + result.stderr
)
return result
@staticmethod
def entries():
return (["banners:%s:%d" % item for item in profile_features.PROFILE_FLAGS.items()]
+ ["reposts:%s:%d" % item for item in profile_features.REPOST_FLAGS.items()])
def run_scenario(self, name):
result = self.run_command([
self.java, "-cp", str(self.classes) + os.pathsep + str(self.android_jar),
"lihont.two.FlagsTest", name,
] + self.entries())
return result.stdout.strip()
def test_each_switch_controls_only_its_own_flags(self):
self.assertEqual(self.run_scenario("matrix"), "PASS matrix")
def test_saved_change_applies_to_the_next_process(self):
self.assertEqual(self.run_scenario("snapshot"), "PASS snapshot")
def test_defaults_apply_while_settings_cannot_be_read(self):
self.assertEqual(self.run_scenario("unavailable"), "PASS unavailable")
def test_runtime_offers_exactly_what_the_patcher_calls(self):
offered = set(self.run_scenario("surface").splitlines())
called = {target.split("->", 1)[1] for target in profile_features.runtime_targets()}
self.assertEqual(offered, called)
if __name__ == "__main__":
unittest.main()