reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,334 @@
"""Device-side command construction, exercised without a device.
The behaviour under test is the one the repository's own documentation says is
the reason these helpers exist: a device command is built as an **argv list** and
handed to `subprocess` directly, so the host shell never expands `$`, `|`, `>`,
`;` or quotes before adb sees them. Every assertion here inspects the argv list
that would have been executed; none of them launches adb.
"""
from __future__ import annotations
import re
import subprocess
import sys
import pytest
from conftest import load_script
devsh = load_script("devsh.py")
coldstart = load_script("coldstart.py")
install_test = load_script("install_test.py")
pytestmark = pytest.mark.integration
class Recorder:
"""Captures the argv lists handed to a patched subprocess runner."""
def __init__(self, stdout="", stderr="", returncode=0):
self.calls = []
self.stdout = stdout
self.stderr = stderr
self.returncode = returncode
def __call__(self, cmd, *args, **kwargs):
self.calls.append((list(cmd), kwargs))
return subprocess.CompletedProcess(cmd, self.returncode, self.stdout, self.stderr)
@property
def last(self):
return self.calls[-1][0]
# --------------------------------------------------------------------------- #
# devsh.py
# --------------------------------------------------------------------------- #
@pytest.fixture
def devsh_recorder(monkeypatch):
rec = Recorder(stdout="ok\n")
monkeypatch.setattr(devsh.subprocess, "run", rec)
monkeypatch.delenv("ADB_PATH", raising=False)
monkeypatch.delenv("ADB_SERIAL", raising=False)
return rec
def run_devsh(monkeypatch, rec, argv):
monkeypatch.setattr(sys, "argv", ["devsh.py"] + argv)
return devsh.main()
class TestDevshCommandConstruction:
def test_shell_command_is_one_argv_element_not_a_shell_string(self, monkeypatch, devsh_recorder):
"""`|` and `grep` must survive as literal text inside a single argument."""
assert run_devsh(monkeypatch, devsh_recorder, ["sh", "pm list packages | grep myapp"]) == 0
assert devsh_recorder.last == ["adb", "shell", "pm list packages | grep myapp"]
def test_serial_is_injected_as_dash_s(self, monkeypatch, devsh_recorder):
run_devsh(monkeypatch, devsh_recorder, ["--serial", "SERIAL123", "sh", "id"])
assert devsh_recorder.last == ["adb", "-s", "SERIAL123", "shell", "id"]
def test_serial_equals_form_is_accepted(self, monkeypatch, devsh_recorder):
run_devsh(monkeypatch, devsh_recorder, ["--adb=x", "--serial=SERIAL123", "sh", "id"])
assert devsh_recorder.last == ["x", "-s", "SERIAL123", "shell", "id"]
def test_adb_path_comes_from_the_environment_when_not_given(self, monkeypatch, devsh_recorder):
monkeypatch.setenv("ADB_PATH", "/opt/platform-tools/adb")
monkeypatch.setenv("ADB_SERIAL", "ENVSERIAL")
run_devsh(monkeypatch, devsh_recorder, ["sh", "id"])
assert devsh_recorder.last == ["/opt/platform-tools/adb", "-s", "ENVSERIAL", "shell", "id"]
def test_device_listing_explicitly_ignores_the_serial(self, monkeypatch, devsh_recorder):
run_devsh(monkeypatch, devsh_recorder, ["dev"])
assert devsh_recorder.last == ["adb", "devices", "-l"]
def test_pull_and_push_keep_their_arguments_positional(self, monkeypatch, devsh_recorder):
run_devsh(monkeypatch, devsh_recorder, ["pull", "/data/local/tmp/x.apk", "./x.apk"])
assert devsh_recorder.last == ["adb", "pull", "/data/local/tmp/x.apk", "./x.apk"]
run_devsh(monkeypatch, devsh_recorder, ["push", "./x.apk", "/data/local/tmp/x.apk"])
assert devsh_recorder.last == ["adb", "push", "./x.apk", "/data/local/tmp/x.apk"]
def test_a_nonzero_device_status_is_reported_as_one(self, monkeypatch, devsh_recorder):
devsh_recorder.returncode = 1
assert run_devsh(monkeypatch, devsh_recorder, ["sh", "false"]) == 1
def test_the_su_wrapper_is_posix_single_quoted(self, monkeypatch, devsh_recorder):
# Assert the *property*, not the representation. The previous version of this test pinned
# `su -c "..."` with `\"` escapes, which is exactly the form that let `;`, `$( )` and
# backticks through to the device shell -- the test was guarding the defect.
run_devsh(monkeypatch, devsh_recorder, ["su", 'pm grant com.example.app "android.permission.X"'])
payload = devsh_recorder.last[-1]
assert payload.startswith("su -c '") and payload.endswith("'")
inner = payload[len("su -c '"):-1]
# A POSIX single-quoted string: no bare quote may appear except as the '\'' escape.
assert re.fullmatch(r"[^']*('\\''[^']*)*", inner), payload
assert '"' in inner # the caller's double quotes survive as literal characters
def test_an_unknown_mode_prints_usage_and_exits_two(self, monkeypatch, devsh_recorder):
assert run_devsh(monkeypatch, devsh_recorder, ["frobnicate", "x"]) == 2
def test_no_arguments_prints_usage_and_exits_two(self, monkeypatch, devsh_recorder):
assert run_devsh(monkeypatch, devsh_recorder, []) == 2
class TestDevshSuQuotingGap:
"""The quoting contract, now unified through `scripts/device_shell.py`.
This class used to record a *gap*: `devsh.su` wrapped the command in double quotes and escaped
only `"`, so `$`, backticks, `;` and `|` from a caller-supplied value still reached the device
shell, while `coldstart.Device.shell` used the POSIX single-quote escape and did neutralise them.
Both now go through the shared module; the tests below pin the property from both sides, so a
future edit that reintroduces double-quote escaping fails here instead of on a device.
"""
@pytest.mark.parametrize("payload", [
"id; rm -rf /sdcard/x",
"id$(touch /data/local/tmp/pwned)",
"id`touch /data/local/tmp/pwned`",
"id | tee /data/local/tmp/pwned",
"id 'quoted'",
])
def test_su_wrapper_neutralises_shell_metacharacters(self, monkeypatch, devsh_recorder, payload):
run_devsh(monkeypatch, devsh_recorder, ["su", payload])
wrapped = devsh_recorder.last[-1]
assert wrapped.startswith("su -c '"), wrapped
inner = wrapped[len("su -c '"):-1]
# The payload must be present verbatim, with only the '\'' sequence able to break out --
# i.e. the whole payload is one argument to sh, not syntax.
assert re.fullmatch(r"[^']*('\\''[^']*)*", inner), wrapped
assert payload.replace("'", "'\\''") == inner
def test_a_malformed_path_is_refused_before_the_device_shell(self, monkeypatch, devsh_recorder):
"""`suf` interpolates a path, so it validates rather than quotes-and-hopes."""
rc = run_devsh(monkeypatch, devsh_recorder, ["suf", "/data/local/tmp/x;id", "200"])
assert rc == 2
assert not devsh_recorder.calls, "the refused command must never reach adb"
def test_coldstart_uses_the_posix_single_quote_escape(self, monkeypatch):
"""The contrasting implementation: this one does neutralise metacharacters."""
rec = Recorder()
monkeypatch.setattr(coldstart, "run", rec)
dev = coldstart.Device(None, use_su=False)
dev.su = True
dev.shell("echo 'hi'; rm -rf /sdcard/x")
payload = rec.last[-1]
assert payload.startswith("su -c '")
assert payload.endswith("'")
assert "'\\''" in payload, "the POSIX single-quote escape is missing"
# --------------------------------------------------------------------------- #
# coldstart.Device: command shape and screenshot handling
# --------------------------------------------------------------------------- #
class TestDeviceHelperCommands:
def make_device(self, monkeypatch, serial=None):
rec = Recorder()
monkeypatch.setattr(coldstart, "run", rec)
dev = coldstart.Device(serial, use_su=False)
return dev, rec
def test_shell_without_root_passes_the_command_as_one_argument(self, monkeypatch):
dev, rec = self.make_device(monkeypatch, serial="SERIAL123")
dev.shell("dumpsys window | grep mCurrentFocus")
assert rec.last == ["adb", "-s", "SERIAL123", "shell", "dumpsys window | grep mCurrentFocus"]
def test_shell_without_a_serial_omits_dash_s(self, monkeypatch):
dev, rec = self.make_device(monkeypatch)
dev.shell("id")
assert rec.last == ["adb", "shell", "id"]
def test_foreground_activity_parses_the_resumed_component(self, monkeypatch):
dev, rec = self.make_device(monkeypatch, serial="S")
rec.stdout = ("mResumedActivity: ActivityRecord{9f2c1 u0 "
"com.example.app/.MainActivity t42}\n")
assert coldstart.foreground_activity(dev) == "com.example.app/.MainActivity"
def test_foreground_activity_returns_none_when_absent(self, monkeypatch):
dev, rec = self.make_device(monkeypatch, serial="S")
rec.stdout = "no matches\n"
assert coldstart.foreground_activity(dev) is None
def test_installed_facts_extracts_version_and_uid(self, monkeypatch):
dev, rec = self.make_device(monkeypatch, serial="S")
def respond(cmd, *args, **kwargs):
rec.calls.append((list(cmd), kwargs))
if "versionCode" in cmd[-1]:
out = " versionCode=42 minSdk=24 targetSdk=34\n versionName=1.2.3\n"
elif "sha256sum" in cmd[-1]:
out = "a" * 64 + " /data/app/base.apk\n"
elif "base.apk" in cmd[-1]:
out = "/data/app/~~x==/com.example.app-1/base.apk\n"
elif "userId=" in cmd[-1]:
out = " userId=10123\n"
else:
out = ""
return subprocess.CompletedProcess(cmd, 0, out, "")
monkeypatch.setattr(coldstart, "run", respond)
facts = coldstart.installed_facts(dev, "com.example.app")
assert facts["versionName"] == "1.2.3"
assert facts["uid"] == "10123"
assert facts["apk_sha256"] == "a" * 64
def test_version_code_swallows_the_rest_of_the_line(self, monkeypatch):
"""Measured sub-string behaviour of `installed_facts`, recorded as a work item.
The extraction is `r'\\s*(versionCode|...)=(.*)'`, so on a real
`dumpsys package` line -- `versionCode=42 minSdk=24 targetSdk=34` --
`facts['versionCode']` is the whole remainder, not `42`. It is still a
usable before/after fingerprint, which is how the caller uses it, but the
field name overstates what it holds.
"""
dev = coldstart.Device(None, use_su=False)
def respond(cmd, *args, **kwargs):
if "versionCode" in cmd[-1]:
out = " versionCode=42 minSdk=24 targetSdk=34\n"
else:
out = ""
return subprocess.CompletedProcess(cmd, 0, out, "")
monkeypatch.setattr(coldstart, "run", respond)
facts = coldstart.installed_facts(dev, "com.example.app")
assert facts["versionCode"] == "42 minSdk=24 targetSdk=34"
class TestScreenshotValidation:
"""`screencap` is judged by the bytes on disk, not by the command's exit status."""
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
def make_device(self, monkeypatch):
dev = coldstart.Device(None, use_su=False)
return dev
def test_a_png_header_is_accepted_and_the_size_returned(self, monkeypatch, tmp_path):
dev = self.make_device(monkeypatch)
payload = self.PNG_MAGIC + b"\x00" * 100
def fake_run(cmd, *args, **kwargs):
kwargs["stdout"].write(payload)
return subprocess.CompletedProcess(cmd, 0, "", "")
monkeypatch.setattr(coldstart.subprocess, "run", fake_run)
out = tmp_path / "shot.png"
assert dev.screencap(str(out)) == len(payload)
assert out.read_bytes() == payload
def test_the_capture_uses_exec_out_not_shell_redirect(self, monkeypatch, tmp_path):
dev = self.make_device(monkeypatch)
seen = []
def fake_run(cmd, *args, **kwargs):
seen.append(list(cmd))
kwargs["stdout"].write(self.PNG_MAGIC + b"\x00" * 100)
return subprocess.CompletedProcess(cmd, 0, "", "")
monkeypatch.setattr(coldstart.subprocess, "run", fake_run)
dev.screencap(str(tmp_path / "shot.png"))
assert seen[0] == ["adb", "exec-out", "screencap", "-p"]
def test_a_non_png_body_is_rejected(self, monkeypatch, tmp_path):
"""A device that printed an error into the stream must not be read as a frame."""
dev = self.make_device(monkeypatch)
def fake_run(cmd, *args, **kwargs):
kwargs["stdout"].write(b"error: device offline\n")
return subprocess.CompletedProcess(cmd, 0, "", "")
monkeypatch.setattr(coldstart.subprocess, "run", fake_run)
assert dev.screencap(str(tmp_path / "shot.png")) == -1
def test_a_truncated_body_is_rejected(self, monkeypatch, tmp_path):
dev = self.make_device(monkeypatch)
def fake_run(cmd, *args, **kwargs):
kwargs["stdout"].write(b"\x89PNG")
return subprocess.CompletedProcess(cmd, 0, "", "")
monkeypatch.setattr(coldstart.subprocess, "run", fake_run)
assert dev.screencap(str(tmp_path / "shot.png")) == -1
def test_a_timeout_is_reported_as_a_failed_capture(self, monkeypatch, tmp_path):
dev = self.make_device(monkeypatch)
def fake_run(cmd, *args, **kwargs):
raise subprocess.TimeoutExpired(cmd, 30)
monkeypatch.setattr(coldstart.subprocess, "run", fake_run)
assert dev.screencap(str(tmp_path / "shot.png")) == -1
# --------------------------------------------------------------------------- #
# install_test.py: argument plumbing
# --------------------------------------------------------------------------- #
class TestInstallTestAdbWrappers:
def test_serial_is_injected_as_dash_s(self, monkeypatch):
rec = Recorder()
monkeypatch.setattr(install_test.subprocess, "run", rec)
install_test.adb(["shell", "id"], "SERIAL")
assert rec.last == ["adb", "-s", "SERIAL", "shell", "id"]
def test_without_a_serial_no_dash_s_is_added(self, monkeypatch):
rec = Recorder()
monkeypatch.setattr(install_test.subprocess, "run", rec)
install_test.adb(["devices", "-l"])
assert rec.last == ["adb", "devices", "-l"]
def test_stderr_is_appended_so_device_errors_are_not_silently_dropped(self, monkeypatch):
rec = Recorder(stdout="out", stderr="error: device offline")
monkeypatch.setattr(install_test.subprocess, "run", rec)
combined = install_test.adb(["devices"])
assert "out" in combined
assert "device offline" in combined
def test_su_wrapper_keeps_the_command_as_one_argument(self, monkeypatch):
rec = Recorder()
monkeypatch.setattr(install_test.subprocess, "run", rec)
install_test.su('pm grant com.example.app "android.permission.X"', "S")
assert rec.last == [
"adb", "-s", "S", "shell",
'su -c "pm grant com.example.app \\"android.permission.X\\""',
]

View File

@@ -0,0 +1,199 @@
"""Regression tests for the documented-command gate itself.
`check_commands.py` is the gate that proves other documentation is runnable, so a blind spot in it
is worth more than a blind spot elsewhere: it turns "146 commands checked, 0 drift" into a claim the
repository cannot back. One such blind spot is pinned here, exactly as it was found.
**What was wrong.** `resolve_script()` tried the *document's own directory* first when resolving a
relative path. A bare `python analyze_pair.py` written inside `docs/tool-verification/` therefore
resolved to `tools/_work/bench/unpack/b3/analyze_pair.py` -- a real file, inside the gitignored
workbench -- and scored `ok`. Seven commands in two evidence files used that form while the same
files wrote the correct path elsewhere, and a reader copying one from the repository root gets
"No such file". The gate said the documentation was clean.
The tests below drive the resolver and the finding classifier directly rather than through the CLI,
so they stay fast and do not depend on the current state of the documentation.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
from conftest import REPO_ROOT
pytestmark = pytest.mark.integration
CHECK_COMMANDS = REPO_ROOT / "check_commands.py"
@pytest.fixture(scope="module")
def gate():
"""The gate as a module, so its internals can be driven without a subprocess."""
sys.path.insert(0, str(REPO_ROOT))
import importlib
return importlib.import_module("check_commands")
def _workbench_script_name(gate):
"""A `.py` basename that exists only under tools/, or skip if the workbench is absent."""
names = sorted(gate._workbench_names())
if not names:
pytest.skip("no workbench present (tools/ is gitignored and may not exist in a checkout)")
shipped = {p.name for p in (REPO_ROOT / "skills" / "apk-reverse" / "scripts").glob("*.py")}
root = {p.name for p in REPO_ROOT.glob("*.py")}
for name in names:
if name not in shipped and name not in root:
return name
pytest.skip("every workbench script name is also shipped; nothing to test with")
def test_a_bare_name_that_only_exists_in_the_workbench_is_drift(gate):
"""The exact defect: the name exists *somewhere*, which used to be enough to pass it."""
name = _workbench_script_name(gate)
doc = str(REPO_ROOT / "docs" / "tool-verification" / "EXTENSION-extraction-shell-bench.md")
path, why = gate.resolve_script(name, doc, include_workbench=False)
assert path is None
assert why == "unqualified-workbench", why
text = "```\n$ python %s --flag\n```\n" % name
findings = gate.command_findings(doc, text, {}, False)
kinds = [f["kind"] for f in findings]
assert "unqualified-workbench" in kinds, findings
def test_a_path_qualified_workbench_reference_is_a_skip_not_drift(gate):
"""`tools/_work/...` is deliberate in an evidence record: a skip, reported with its reason."""
name = _workbench_script_name(gate)
doc = str(REPO_ROOT / "docs" / "tool-verification" / "EXTENSION-extraction-shell-bench.md")
_path, why = gate.resolve_script("tools/_work/bench/unpack/b3/%s" % name, doc,
include_workbench=False)
assert why == "workbench"
@pytest.fixture
def fake_repo(gate, tmp_path, monkeypatch):
"""A repository-shaped tree with its own workbench, so resolution can be tested anywhere.
This exists because the workbench-dependent tests above cannot run on a clean checkout: the
`tools/` tree is gitignored, so on CI they skip and the gate's most important property goes
unverified exactly where it matters. The tree built here has the layout the resolver assumes
(a root, a skill dir, a gitignored `tools/`) and nothing else.
"""
root = tmp_path / "repo"
(root / "skills" / "apk-reverse" / "scripts").mkdir(parents=True)
(root / "docs" / "tool-verification").mkdir(parents=True)
(root / "tools" / "_work" / "bench").mkdir(parents=True)
(root / "README.md").write_text("# root\n", encoding="utf-8")
(root / "skills" / "apk-reverse" / "scripts" / "shipped.py").write_text(
'"""capability: x\n"""\nimport argparse\np = argparse.ArgumentParser()\n'
'p.add_argument("--flag")\n', encoding="utf-8")
(root / "tools" / "_work" / "bench" / "solo_in_workbench.py").write_text(
"print('x')\n", encoding="utf-8")
(root / "docs" / "tool-verification" / "record.md").write_text("# record\n", encoding="utf-8")
monkeypatch.setattr(gate, "ROOT", str(root))
monkeypatch.setattr(gate, "SKILL_DIR", str(root / "skills" / "apk-reverse"))
monkeypatch.setattr(gate, "SCRIPT_DIR", str(root / "skills" / "apk-reverse" / "scripts"))
gate._WORKBENCH_INDEX.clear()
yield root
gate._WORKBENCH_INDEX.clear()
def test_resolution_order_is_root_then_skill_then_document(gate, fake_repo):
"""The corrected order, asserted against a tree that exists on any machine."""
doc = str(fake_repo / "docs" / "tool-verification" / "record.md")
path, why = gate.resolve_script("README.md", doc, include_workbench=False)
assert why == "ok" and path.endswith("README.md")
path, why = gate.resolve_script("shipped.py", doc, include_workbench=False)
assert why == "ok" and path.endswith("shipped.py")
def test_a_workbench_only_name_is_drift_and_a_qualified_one_is_not(gate, fake_repo):
doc = str(fake_repo / "docs" / "tool-verification" / "record.md")
path, why = gate.resolve_script("solo_in_workbench.py", doc, include_workbench=False)
assert (path, why) == (None, "unqualified-workbench")
path, why = gate.resolve_script("tools/_work/bench/solo_in_workbench.py", doc,
include_workbench=False)
assert (path, why) == (None, "workbench")
def test_a_bare_name_the_document_paths_elsewhere_is_a_skip(gate, fake_repo):
"""The fix that made the gate behave the same on CI as on a populated machine.
`solo_in_workbench.py` appears once bare and once with a path in the same document. The
document has already told the reader where it lives, so the bare line is a shorthand -- and
that verdict must not depend on the gitignored tree being visible, which is how the first
version of this fix passed locally and failed on CI.
"""
doc = str(fake_repo / "docs" / "tool-verification" / "record.md")
text = ("Path: `tools/_work/bench/solo_in_workbench.py` explains it.\n\n"
"```\n$ python solo_in_workbench.py\n```\n")
findings = gate.command_findings(doc, text, {}, False)
kinds = [f["kind"] for f in findings]
assert "unqualified-workbench" not in kinds, findings
assert "missing-script" not in kinds, findings
assert "skipped" in kinds, findings
def test_a_bare_name_after_cd_with_no_resolution_is_skipped_not_drift(gate, fake_repo):
"""`cd <somewhere cloned>` then a bare name: unverifiable, and reported as such."""
doc = str(fake_repo / "docs" / "tool-verification" / "record.md")
text = "```\n$ cd tools/_work/bench/repos/elsewhere\n$ python upstream_tool.py --x\n```\n"
findings = gate.command_findings(doc, text, {}, False)
kinds = [f["kind"] for f in findings]
assert "missing-script" not in kinds, findings
assert "after-cd-unresolved" in kinds, findings
def test_the_external_dcc_entry_point_is_never_drift(gate):
"""`python dcc.py` after `cd tools/_work/bench/repos/dcc` runs inside the external dcc
checkout, so it is a skip on every machine -- including one without the workbench.
The name is declared in EXTERNAL_SCRIPTS because recognition by filename needs `tools/` to
exist: on a clean checkout `dcc.py` degraded to `missing-script` drift and the gate failed away
from the machine that wrote the record. Asserted as a *property* rather than as one specific
reason, because two mechanisms can legitimately produce it -- the declared entry point and the
workbench-after-`cd` rule -- and pinning either one alone makes the test machine-dependent,
which is the very defect it is guarding against.
"""
doc = str(REPO_ROOT / "docs" / "tool-verification" / "EXTENSION-java2c.md")
_path, why = gate.resolve_script("dcc.py", doc, include_workbench=False, chdir=True)
assert why in ("external", "workbench-after-cd"), why
text = "```\n$ cd tools/_work/bench/repos/dcc\n$ python dcc.py --no-build\n```\n"
findings = gate.command_findings(doc, text, {}, False)
kinds = [f["kind"] for f in findings]
assert "missing-script" not in kinds, findings
assert "unqualified-workbench" not in kinds, findings
assert any(k in ("workbench-after-cd", "external") for k in kinds), findings
def test_a_shipped_script_resolves_from_the_repository_root(gate):
"""The order that matters: root, then the skill's scripts, then the document's own directory."""
doc = str(REPO_ROOT / "docs" / "tool-verification" / "README.md")
path, why = gate.resolve_script("check_repo.py", doc, include_workbench=False)
assert why == "ok" and path and path.endswith("check_repo.py")
path, why = gate.resolve_script("skills/apk-reverse/scripts/scan_leaks.py", doc,
include_workbench=False)
assert why == "ok" and path and path.endswith("scan_leaks.py")
def test_the_committed_tree_reports_no_drift(gate, tmp_path):
"""The gate's own verdict on this repository, run in-process so it cannot drift silently."""
docs = gate.collect_docs(None)
findings = []
cache = {}
for doc in docs:
try:
text = Path(doc).read_text(encoding="utf-8")
except OSError:
continue
findings.extend(gate.command_findings(doc, text, cache, False))
drift = [f for f in findings
if f["kind"] in ("unknown-flag", "missing-script", "unqualified-workbench")]
detail = "\n".join(" %s:%s %s (%s)" % (f["doc"], f["line"], f["command"], f["kind"])
for f in drift[:20])
assert not drift, "documented commands that cannot run:\n%s" % detail

View File

@@ -0,0 +1,252 @@
"""No-device flows: launch-health verdicts, log parsing, tombstone parsing.
Everything here runs on a bare checkout. The device commands are intercepted, so
what is being tested is the *decision logic* -- which is exactly the part that a
device test would otherwise hide behind "it looked fine on screen".
"""
from __future__ import annotations
import sys
import pytest
from conftest import load_script, run_script
install_test = load_script("install_test.py")
coldstart = load_script("coldstart.py")
native_crash = load_script("native_crash.py")
pytestmark = pytest.mark.integration
TOMBSTONE = "tombstone_sample.txt"
LOGCAT = "logcat_sample.txt"
# --------------------------------------------------------------------------- #
# install_test.py: the two-sample pid verdict
# --------------------------------------------------------------------------- #
def drive(monkeypatch, pids, log="", capsys=None):
"""Run install_test.main() with the device replaced by a scripted responder.
`pids` is consumed in order by successive `pidof` calls, which is the whole
point of the check: one sample cannot tell a running app from a crash loop.
"""
pids = list(pids)
calls = []
def fake_adb(args, serial=None, timeout=600):
calls.append(list(args))
if list(args)[:1] == ["logcat"] and "-d" in args:
return log
return ""
def fake_sh(cmd, serial=None):
calls.append(cmd)
if cmd.startswith("pidof"):
return pids.pop(0) if pids else ""
if "mCurrentFocus" in cmd:
return "mCurrentFocus=Window{a1b2 u0 com.example.app/.MainActivity}"
return ""
monkeypatch.setattr(install_test, "adb", fake_adb)
monkeypatch.setattr(install_test, "sh", fake_sh)
monkeypatch.setattr(install_test.time, "sleep", lambda _s: None)
monkeypatch.setattr(sys, "argv", [
"install_test.py", "--apk", "out/app.apk", "--pkg", "com.example.app",
"--activity", ".MainActivity",
])
return install_test.main(), calls
class TestLaunchHealthVerdict:
def test_a_stable_pid_with_no_fatal_signature_is_ok(self, monkeypatch, capsys):
code, calls = drive(monkeypatch, ["1234", "1234"])
assert code == 0, capsys.readouterr().out
def test_a_changed_pid_is_a_failure_even_though_the_app_is_running(self, monkeypatch, capsys):
"""The measured shape of a crash loop: alive at the first sample, restarted by the second."""
code, _calls = drive(monkeypatch, ["1234", "5678"])
out = capsys.readouterr().out
assert code == 1
assert "pid @+6s : 1234" in out
assert "5678" in out
def test_a_dead_app_is_a_failure(self, monkeypatch, capsys):
code, _calls = drive(monkeypatch, ["", ""])
assert code == 1
assert "(dead)" in capsys.readouterr().out
def test_a_fatal_signature_fails_even_with_a_stable_pid(self, monkeypatch, capsys):
"""Stability is not health: a caught-but-fatal error still fails the check."""
code, _calls = drive(monkeypatch, ["1234", "1234"],
log="01-01 10:00:01 E AndroidRuntime: FATAL EXCEPTION: main\n")
assert code == 1
assert "FATAL EXCEPTION" in capsys.readouterr().out
def test_both_pid_samples_are_actually_taken(self, monkeypatch, capsys):
_code, calls = drive(monkeypatch, ["1234", "1234"])
capsys.readouterr()
pidof_calls = [c for c in calls if isinstance(c, str) and c.startswith("pidof")]
assert len(pidof_calls) == 2, "the second sample is what catches a crash loop"
def test_the_launch_uses_the_package_slash_activity_component(self, monkeypatch, capsys):
_code, calls = drive(monkeypatch, ["1234", "1234"])
capsys.readouterr()
assert any("am start -n com.example.app/.MainActivity" == c for c in calls
if isinstance(c, str))
# --------------------------------------------------------------------------- #
# coldstart.py: logcat signal counting
# --------------------------------------------------------------------------- #
class TestLogcatSignalCounting:
@pytest.fixture
def counts(self, fixtures_dir):
path = fixtures_dir / LOGCAT
assert path.exists(), "the logcat fixture is missing from tests/fixtures"
return coldstart.signal_counts(str(path), coldstart.SIGNAL_PATTERNS)
@pytest.mark.parametrize("name,expected", [
("FATAL EXCEPTION", 1),
("VerifyError", 1),
("Bad checksum", 1),
("ANR", 1),
("Displayed", 1),
("IncompatibleClassChangeError", 0),
("ClassNotFoundException", 0),
("uncaughtException", 0),
("am_crash", 0),
])
def test_counts(self, counts, name, expected):
assert counts[name] == expected
def test_every_pattern_in_the_table_is_reported(self, counts):
assert set(counts) == set(coldstart.SIGNAL_PATTERNS)
def test_a_missing_log_yields_no_counts_rather_than_raising(self, tmp_path):
assert coldstart.signal_counts(str(tmp_path / "nope.txt"),
coldstart.SIGNAL_PATTERNS) == {}
def test_the_displayed_pattern_captures_the_milliseconds(self):
import re
match = re.search(coldstart.SIGNAL_PATTERNS["Displayed"],
"I ActivityManager: Displayed com.example.app/.MainActivity: +842ms")
assert match is not None
assert match.group(1) == "842"
# --------------------------------------------------------------------------- #
# native_crash.py: tombstone parsing
# --------------------------------------------------------------------------- #
@pytest.fixture
def crash(fixtures_dir):
text = (fixtures_dir / TOMBSTONE).read_text(encoding="utf-8")
crashes = native_crash.parse(text)
assert len(crashes) == 1, "the fixture holds exactly one crash block"
return crashes[0]
class TestTombstoneParsing:
def test_signal_and_code(self, crash):
assert crash["signal"] == 11
assert crash["signame"] == "SIGSEGV"
assert crash["si_code"] == "SEGV_MAPERR"
assert crash["fault"] == "0x0000000000000010"
def test_process_identity_is_picked_up(self, crash):
assert crash["pid"] == "12345"
assert crash["tid"] == "12345"
assert crash["proc"] == "com.example.app"
def test_uptime_and_cause(self, crash):
assert crash["uptime"] == "5"
assert "null pointer" in crash["cause"]
def test_backtrace_frames_are_complete(self, crash):
assert len(crash["frames"]) == 4
assert crash["frames"][0]["path"].endswith("libfoo.so")
assert crash["frames"][0]["symbol"] == ""
assert crash["frames"][1]["symbol"] == "Java_com_example_app_Native_check+120"
assert crash["frames"][2]["path"].startswith("/apex/")
assert crash["frames"][2]["symbol"] == "__pthread_start+256"
def test_registers_are_collected_from_the_dump_line(self, crash):
assert crash["regs"]["x0"] == 0x10
assert crash["regs"]["x1"] == 0x7A12345678
assert crash["regs"]["sp"] == 0x7FFABC1110
assert crash["regs"]["lr"] == 0x7A11112222
def test_the_pc_key_is_overwritten_by_the_last_backtrace_frame(self, crash):
"""Measured behaviour, recorded rather than smoothed over.
Register extraction scans every line of the block with one permissive
pattern, so a `#NN pc <hex>` backtrace line also matches and the last one
wins. The register dump's own `pc 0000007a111128cc` is therefore not what
`regs['pc']` holds. It is a reporting wart, not a wrong verdict -- but it
is a wart, so it is pinned here and listed in the harness report.
"""
assert crash["regs"]["pc"] == 0x55488, "the last frame's pc"
assert crash["regs"]["pc"] != 0x7A111128CC, "the register dump's pc"
def test_empty_input_produces_no_crashes(self):
assert native_crash.parse("") == []
def test_a_log_without_a_signal_line_produces_no_crashes(self):
assert native_crash.parse("I/whatever: nothing to see\n") == []
class TestLibraryClassification:
@pytest.mark.parametrize("path,expected", [
("/apex/com.android.runtime/lib64/bionic/libc.so", True),
("/system/lib64/libart.so", True),
("/vendor/lib64/libqcom.so", True),
("/data/dalvik-cache/arm64/system@framework@boot.oat", True),
("/data/app/~~AbCdEf==/com.example.app-1/lib/arm64/libfoo.so", False),
("/data/local/tmp/libpayload.so", False),
])
def test_classification(self, path, expected):
assert native_crash.is_system_lib(path) is expected
def test_a_bare_name_is_treated_as_system(self):
"""Measured behaviour: a path that is not absolute cannot be attributed to the app."""
assert native_crash.is_system_lib("libfoo.so") is True
class TestVerdictHint:
def test_a_small_fault_address_held_in_a_register_is_flagged_as_arranged(self, crash):
hints = " ".join(native_crash.verdict_hint(crash))
assert "small integer" in hints
assert "ARRANGED" in hints
assert "x0" in hints
def test_an_ordinary_null_dereference_gets_no_arranged_hint(self):
ordinary = {"fault": "0x0", "regs": {"x1": 0x7A111128CC}, "signame": "SIGSEGV"}
hints = native_crash.verdict_hint(ordinary)
assert any("small integer" in h for h in hints)
assert not any("ARRANGED" in h for h in hints)
def test_sigkill_is_called_out_as_a_deliberate_termination(self):
hints = native_crash.verdict_hint({"fault": "0x0", "regs": {}, "signame": "SIGKILL"})
assert any("deliberate termination" in h for h in hints)
class TestNativeCrashCli:
def test_the_fixture_tombstone_is_parsed_by_the_cli(self, fixtures_dir):
result = run_script("native_crash.py", [str(fixtures_dir / TOMBSTONE)], timeout=120)
assert not result.timed_out
assert not result.has_traceback, result.output
assert result.returncode == 0, result.output
assert "libfoo.so" in result.stdout
assert "SIGSEGV" in result.stdout
def test_a_missing_log_is_reported_without_a_traceback(self, tmp_path):
result = run_script("native_crash.py", [str(tmp_path / "absent.txt")])
assert not result.has_traceback, result.output
assert result.returncode != 0
assert "no such file" in result.output
def test_a_malformed_lib_spec_is_refused(self, fixtures_dir):
result = run_script("native_crash.py",
[str(fixtures_dir / TOMBSTONE), "--lib", "notapair"])
assert result.returncode != 0
assert "NAME=PATH" in result.output