test: prepare STEP-113 PBF Fast Path APK retest
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
Some checks failed
validate-scripts / validate-scripts (push) Has been cancelled
This commit is contained in:
@@ -111,3 +111,21 @@ MAPLIBRE_VALIDATION_OUT_DIR=/tmp/my-validation \
|
||||
- STEP 场景验收。
|
||||
|
||||
这样构建产物与上机验证保持分离,避免脚本意外操作设备。
|
||||
|
||||
## STEP-113 原 PBF 专用 Test App Fast Path 复测
|
||||
|
||||
本测试不使用 STEP-114 的简化 FAST_SMOKE。保持原 Step113PbfGateScene、PBF URL、63 个正式基础层与原 Native 诊断不变。需要用户手工完成新源码的标准 APK 编译和安装。工具先检查 build-info 中的 Git commit 与当前 HEAD、APK SHA、设备已安装 APK SHA 是否一致;任何不一致即拒绝并说明原因。
|
||||
|
||||
在 Mac 的 MapLibreParticleLab 根目录依序执行:
|
||||
|
||||
./tools/particle_android_validation/build_validation_apk.sh
|
||||
|
||||
/Users/tei/Library/Android/sdk/platform-tools/adb -s 192.168.200.139:5555 install -r artifacts/maplibre-particle/android-validation/MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk
|
||||
|
||||
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --preflight-only
|
||||
|
||||
python3 -B tools/particle_android_validation/verify_step113_fast_path.py --scenario STATIC_RC --seconds 75
|
||||
|
||||
前两项为用户手工 Android 构建和更新安装,不得在未授权情况下代劳。后两项由 RDC 或用户执行。脚本使用明确 ADB serial 启动原有 Activity 的 STEP-113 原场景,按当前 PID 还原 STEP113Gate 的 Base64 Logcat summary,计算实际 Fast/Full 计数差及 R Build/Reuse,保存到忽略 Git 的 artifacts/maplibre-particle/android-validation/step113-retest-时间/场景/ 目录。
|
||||
|
||||
FAST_OBSERVED 仅表示实际 Fast 数字增长,不代表像素等价、性能或所有安全回退已验收;NO_FAST_OBSERVED 表示仍未进入,记录完整 R fallback 和 24 Gate 聚合。STALE_APK / INSUFFICIENT_SAMPLES 不是 PASS。依次需要时用 --scenario PBF63、PARTICLE_OFF、PARTIAL_RCRR、SOURCE_CHANGE。SOURCE_CHANGE 的手动 Source mutation 仍须通过 App 原菜单做独立验证,脚本不会自行修改 Source。阶段事实详见 docs/50_Step记录/STEP-115_STEP113原PBF专用App复测准备_v1.md。
|
||||
|
||||
191
tools/particle_android_validation/verify_step113_fast_path.py
Executable file
191
tools/particle_android_validation/verify_step113_fast_path.py
Executable file
@@ -0,0 +1,191 @@
|
||||
#!/usr/bin/env python3
|
||||
"""STEP-113 on-device PBF Fast Path verifier. Does not build, install or alter APKs."""
|
||||
import argparse
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
ARTIFACTS = ROOT / "artifacts/maplibre-particle/android-validation"
|
||||
APK = ARTIFACTS / "MapLibreAndroidTestApp-validation-opengl-debug-x86_64.apk"
|
||||
BUILD_INFO = ARTIFACTS / "build-info.txt"
|
||||
PACKAGE = "org.maplibre.android.testapp"
|
||||
ACTIVITY = f"{PACKAGE}/{PACKAGE}.activity.particle.ParticleCoreLayerValidationActivity"
|
||||
SCENARIOS = ("STATIC_RC", "PBF63", "PARTICLE_OFF", "PARTIAL_RCRR", "SOURCE_CHANGE")
|
||||
LOG_ENTRY = re.compile(r"^[A-Z]/STEP113Gate\(\s*(\d+)\):\s*(\{.+\})\s*$")
|
||||
|
||||
|
||||
def call(args, *, check=True):
|
||||
result = subprocess.run(args, text=True, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, check=False)
|
||||
if check and result.returncode:
|
||||
raise RuntimeError(f"command returned {result.returncode}: {' '.join(args)}\n"
|
||||
f"{result.stderr[-1200:]}\n{result.stdout[-1200:]}")
|
||||
return result.stdout.strip()
|
||||
|
||||
|
||||
def sha256(path):
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for block in iter(lambda: f.read(1048576), b""):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def inspect(apk, adb, serial):
|
||||
if not apk.is_file() or not BUILD_INFO.is_file():
|
||||
raise RuntimeError("缺少 Validation APK/build-info.txt,必须先由用户手工运行标准构建脚本")
|
||||
info = dict(line.split("=", 1) for line in BUILD_INFO.read_text(encoding="utf-8").splitlines()
|
||||
if "=" in line)
|
||||
source_head = call(["git", "-C", str(ROOT), "rev-parse", "HEAD"])
|
||||
if info.get("commit") != source_head:
|
||||
raise RuntimeError(f"STALE_APK: build-info commit={info.get('commit')}, "
|
||||
f"current HEAD={source_head}; 禁止用旧APK验收本轮修复")
|
||||
local_sha = sha256(apk)
|
||||
if local_sha != info.get("sha256"):
|
||||
raise RuntimeError(f"APK_SHA_MISMATCH: actual={local_sha}, build-info={info.get('sha256')}")
|
||||
if info.get("abi") != "x86_64":
|
||||
raise RuntimeError(f"unexpected ABI: {info.get('abi')}")
|
||||
device = lambda *a: call([adb, "-s", serial, *a])
|
||||
abi = device("shell", "getprop", "ro.product.cpu.abi")
|
||||
if abi != "x86_64":
|
||||
raise RuntimeError(f"DEVICE_ABI_MISMATCH: {abi}")
|
||||
paths = device("shell", "pm", "path", PACKAGE).splitlines()
|
||||
if not paths or not paths[0].startswith("package:"):
|
||||
raise RuntimeError(f"TEST_APP_NOT_INSTALLED: {PACKAGE}")
|
||||
remote = paths[0].split("package:", 1)[1].strip()
|
||||
installed_sha = device("shell", "sha256sum", remote).split()[0]
|
||||
if installed_sha != local_sha:
|
||||
raise RuntimeError(f"STALE_INSTALLED_APK: installed={installed_sha} local={local_sha}; "
|
||||
"用户须核实后手工 adb install -r,不能用旧版本验收")
|
||||
return info, local_sha, source_head, device
|
||||
|
||||
|
||||
def extract(logcat, pid, expected):
|
||||
chunks = {}
|
||||
for line in logcat.splitlines():
|
||||
found = LOG_ENTRY.match(line.strip())
|
||||
if not found or int(found.group(1)) != pid:
|
||||
continue
|
||||
try:
|
||||
item = json.loads(found.group(2))
|
||||
if item.get("encoding") != "base64-utf8" or item.get("kind") != "summary":
|
||||
continue
|
||||
ident = (item["exportId"], item["kind"])
|
||||
record = chunks.setdefault(ident, {"count": item["chunkCount"], "items": {}})
|
||||
if record["count"] == item["chunkCount"]:
|
||||
record["items"][int(item["chunkIndex"])] = item["payload"]
|
||||
except (KeyError, ValueError, TypeError):
|
||||
continue
|
||||
result = []
|
||||
for (ident, _), record in chunks.items():
|
||||
if len(record["items"]) != record["count"]:
|
||||
continue
|
||||
try:
|
||||
packed = "".join(record["items"][i] for i in range(record["count"]))
|
||||
sample = json.loads(base64.b64decode(packed, validate=True).decode("utf-8"))
|
||||
if sample.get("scenario") == expected and sample.get("sceneReady") and not sample.get("blockedReason"):
|
||||
result.append((int(ident.split("-", 1)[0]), sample))
|
||||
except (KeyError, ValueError, TypeError, UnicodeError):
|
||||
continue
|
||||
return [x[1] for x in sorted(result, key=lambda item: item[0])]
|
||||
|
||||
|
||||
def run():
|
||||
parser = argparse.ArgumentParser(description="Check actual STEP-113 PBF on-device Fast frames")
|
||||
parser.add_argument("--serial", default="192.168.200.139:5555")
|
||||
parser.add_argument("--adb", default=str(Path.home() / "Library/Android/sdk/platform-tools/adb"))
|
||||
parser.add_argument("--apk", default=str(APK))
|
||||
parser.add_argument("--scenario", choices=SCENARIOS, default="STATIC_RC")
|
||||
parser.add_argument("--seconds", type=int, default=75)
|
||||
parser.add_argument("--preflight-only", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.seconds < 65 and not args.preflight_only:
|
||||
parser.error("至少65秒以取得首次及下一次30秒周期摘要;不是FPS测量")
|
||||
if not Path(args.adb).is_file():
|
||||
raise RuntimeError(f"adb not found: {args.adb}")
|
||||
info, local_sha, head, device = inspect(Path(args.apk), args.adb, args.serial)
|
||||
print(f"APK_SOURCE_HEAD={head}")
|
||||
print(f"APK_SHA256={local_sha}")
|
||||
print(f"DEVICE={args.serial} ABI=x86_64 INSTALLED_SHA_MATCH=True")
|
||||
if args.preflight_only:
|
||||
print("PREFLIGHT_PASS(仅证明同版本APK已安装,未测试Fast)")
|
||||
return 0
|
||||
|
||||
stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
output = ARTIFACTS / f"step113-retest-{stamp}" / args.scenario.lower()
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
command = ["shell", "am", "start", "-S", "-n", ACTIVITY,
|
||||
"--es", "step113_scenario", args.scenario]
|
||||
print(f"START_SCENARIO={args.scenario}")
|
||||
output.joinpath("launch.txt").write_text(device(*command) + "\n", encoding="utf-8")
|
||||
process_id = None
|
||||
for _ in range(12):
|
||||
time.sleep(1)
|
||||
pid_text = call([args.adb, "-s", args.serial, "shell", "pidof", PACKAGE], check=False)
|
||||
if pid_text.strip().split():
|
||||
process_id = int(pid_text.strip().split()[0])
|
||||
break
|
||||
if process_id is None:
|
||||
raise RuntimeError("APP_PROCESS_NOT_RUNNING after launch")
|
||||
print(f"APP_PID={process_id}")
|
||||
time.sleep(max(0, args.seconds - 12))
|
||||
raw_log = device("logcat", "-d", "-v", "brief", "-s", "STEP113Gate:I", "*:S")
|
||||
output.joinpath("logcat.txt").write_text(raw_log + "\n", encoding="utf-8")
|
||||
samples = extract(raw_log, process_id, args.scenario)
|
||||
if not samples:
|
||||
status = "NO_READY_SUMMARY"
|
||||
fast_delta = full_delta = 0
|
||||
else:
|
||||
start, end = samples[0], samples[-1]
|
||||
fast_delta = end["fastRendered"] - start["fastRendered"]
|
||||
full_delta = end["fullRendered"] - start["fullRendered"]
|
||||
if len(samples) < 2:
|
||||
status = "INSUFFICIENT_SAMPLES"
|
||||
elif any(s["decisionMismatch"] != 0 for s in samples):
|
||||
status = "DECISION_MISMATCH"
|
||||
elif fast_delta > 0:
|
||||
status = "FAST_OBSERVED"
|
||||
else:
|
||||
status = "NO_FAST_OBSERVED"
|
||||
report = {
|
||||
"schema": "step113-fast-path-retest",
|
||||
"sourceCommit": head,
|
||||
"apkSha256": local_sha,
|
||||
"installedShaMatched": True,
|
||||
"device": args.serial,
|
||||
"appPid": process_id,
|
||||
"scenario": args.scenario,
|
||||
"sourceReport": "STEP-113 original PBF test scene; not STEP-114 smoke",
|
||||
"durationSecondsRequested": args.seconds,
|
||||
"completeSummarySamples": len(samples),
|
||||
"status": status,
|
||||
"fastDelta": fast_delta,
|
||||
"fullDelta": full_delta,
|
||||
"initialSummary": samples[0] if samples else None,
|
||||
"finalSummary": samples[-1] if samples else None,
|
||||
}
|
||||
output.joinpath("result.json").write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n",
|
||||
encoding="utf-8")
|
||||
print(f"STATUS={status} FastDelta={fast_delta} FullDelta={full_delta} Samples={len(samples)}")
|
||||
print(f"EVIDENCE={output / 'result.json'}")
|
||||
# Fail closed: no Fast observed is a meaningful negative result, not PASS.
|
||||
if status == "FAST_OBSERVED":
|
||||
print("FAST_OBSERVED仅表示真实Fast计数增长;缓存/像素/安全回退仍须逐帧验收。")
|
||||
return 0
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(run())
|
||||
except (OSError, RuntimeError, ValueError, KeyError) as exc:
|
||||
print(f"STEP113_RETEST_BLOCKED: {exc}", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
Reference in New Issue
Block a user