202 lines
8.3 KiB
Python
202 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
||
"""Manual-test worker client reference for ../autool.
|
||
|
||
This script is intentionally self-contained on the control side. To run it from
|
||
the worker repository, place it under ../autool and execute it with that repo's
|
||
Python environment so imports from batch_run.py are available.
|
||
"""
|
||
|
||
import json
|
||
import socket
|
||
import sys
|
||
import time
|
||
import uuid
|
||
from pathlib import Path
|
||
from typing import Any, Dict, Optional
|
||
|
||
|
||
AUTOOL_ROOT = Path(__file__).resolve().parent
|
||
if (AUTOOL_ROOT / "batch_run.py").exists():
|
||
sys.path.insert(0, str(AUTOOL_ROOT))
|
||
|
||
|
||
def _load_worker_bits():
|
||
from batch_run import TestExecutor
|
||
from result_codes import ErrorInfo, InfraError
|
||
from utils_android.TaskWorker import TaskWorker
|
||
from utils_android.TaskWorker.task_monitor import TaskMonitor
|
||
from utils_android.TaskWorker.worker_integrated import _channel_name
|
||
|
||
return TestExecutor, ErrorInfo, InfraError, TaskWorker, TaskMonitor, _channel_name
|
||
|
||
|
||
def _status_prompt() -> Dict[str, str]:
|
||
print("\n采集状态:")
|
||
print(" 1. 采集正常结束")
|
||
print(" 2. 采集失败:应用闪退/无法打开")
|
||
print(" 3. 其他原因")
|
||
choice = input("请输入数字: ").strip()
|
||
if choice == "1":
|
||
return {"status_code": "normal", "status_label": "采集正常结束", "failure_reason": ""}
|
||
if choice == "2":
|
||
return {"status_code": "app_crash_or_open_failed", "status_label": "采集失败:应用闪退/无法打开", "failure_reason": "应用闪退/无法打开"}
|
||
detail = input("请描述其他原因: ").strip()
|
||
return {"status_code": "other", "status_label": "其他原因", "failure_reason": detail}
|
||
|
||
|
||
def _request_assignment(worker, tester_name: str, run_kind: str, tier_label: str = "") -> Optional[Dict[str, Any]]:
|
||
*_, _channel_name = _load_worker_bits()
|
||
request = {
|
||
"worker_id": worker.worker_id,
|
||
"tester_name": tester_name,
|
||
"run_kind": run_kind,
|
||
"tier_label": tier_label,
|
||
}
|
||
response = worker._send_request(
|
||
publish_channel=_channel_name("manual_test:request"),
|
||
response_channel=_channel_name(f"manual_test:response:{worker.worker_id}"),
|
||
request_data=request,
|
||
timeout=30,
|
||
)
|
||
if not response:
|
||
return None
|
||
if response.get("error"):
|
||
raise RuntimeError(response["error"])
|
||
return response.get("assignment")
|
||
|
||
|
||
def _publish_result(worker, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||
*_, _channel_name = _load_worker_bits()
|
||
response = worker._send_request(
|
||
publish_channel=_channel_name("manual_test:result"),
|
||
response_channel=_channel_name(f"manual_test:result:response:{worker.worker_id}"),
|
||
request_data=payload,
|
||
timeout=30,
|
||
)
|
||
return response or {"ok": False, "error": "manual result response timed out"}
|
||
|
||
|
||
def run_manual_task(executor, worker, assignment: Dict[str, Any], tester_name: str) -> Dict[str, Any]:
|
||
_, ErrorInfo, InfraError, _, TaskMonitor, _ = _load_worker_bits()
|
||
task_payload = dict(assignment.get("task_payload") or {})
|
||
task_payload.setdefault("app_name", assignment.get("app_name") or assignment["package_name"])
|
||
task_payload.setdefault("package_name", assignment["package_name"])
|
||
task_payload["task_key"] = f"manual_{assignment['assignment_id']}_{assignment['package_name']}"
|
||
task_payload["keep_app_installed"] = True
|
||
task_payload.setdefault("available_sources", ["local", "google_play", "apkpure"])
|
||
worker.current_task = task_payload
|
||
|
||
ctx = executor._create_task_context(task_payload)
|
||
mon = TaskMonitor(worker, ctx.task_key)
|
||
started_at = time.time()
|
||
pcap_start_status = "not_started"
|
||
traffic_synced = False
|
||
log_synced = False
|
||
|
||
try:
|
||
mon.start_task()
|
||
next_task = executor._prepare_download(ctx, mon, worker)
|
||
if next_task is not None:
|
||
raise RuntimeError("download preparation returned next task")
|
||
next_task = executor._execute_download(ctx, mon, worker)
|
||
if next_task is not None:
|
||
raise RuntimeError("download failed or returned next task")
|
||
collect_result = executor._prepare_collect(ctx, mon, worker)
|
||
if collect_result is not None:
|
||
pcap_start_status = "failed"
|
||
print("[WARN] PcapDroid 自动开启失败,请手动开启抓包后继续。")
|
||
else:
|
||
pcap_start_status = "started"
|
||
|
||
input("\n请开始手动测试应用。测试完成后按 Enter 停止抓包并上传结果...")
|
||
status_payload = _status_prompt()
|
||
try:
|
||
executor.pcap.stop_capture()
|
||
except Exception as exc:
|
||
print(f"[WARN] 停止 PcapDroid 失败: {exc}")
|
||
try:
|
||
traffic_synced = bool(executor.data.sync_latest_traffic_file(ctx.package_name))
|
||
except Exception as exc:
|
||
print(f"[WARN] 同步流量失败: {exc}")
|
||
try:
|
||
mon.finish_stage("collect", status="success" if status_payload["status_code"] == "normal" else "failed")
|
||
mon.finish_task(
|
||
"success" if status_payload["status_code"] == "normal" else "failed",
|
||
failed_stage=None if status_payload["status_code"] == "normal" else "collect",
|
||
error_type=None if status_payload["status_code"] == "normal" else "BUSINESS_ERROR/0",
|
||
error_message=status_payload["failure_reason"],
|
||
)
|
||
except Exception:
|
||
pass
|
||
return {
|
||
**status_payload,
|
||
"assignment_id": assignment["assignment_id"],
|
||
"package_name": assignment["package_name"],
|
||
"run_kind": assignment["run_kind"],
|
||
"source_run_kind": assignment.get("source_run_kind", "ranking"),
|
||
"tester_name": tester_name,
|
||
"worker_id": worker.worker_id,
|
||
"task_key": ctx.task_key,
|
||
"download_source": ctx.download_source or "",
|
||
"traffic_synced": traffic_synced,
|
||
"log_synced": log_synced,
|
||
"pcap_start_status": pcap_start_status,
|
||
"total_duration_seconds": round(time.time() - started_at, 2),
|
||
}
|
||
except Exception as exc:
|
||
return {
|
||
"assignment_id": assignment["assignment_id"],
|
||
"package_name": assignment["package_name"],
|
||
"run_kind": assignment["run_kind"],
|
||
"source_run_kind": assignment.get("source_run_kind", "ranking"),
|
||
"tester_name": tester_name,
|
||
"worker_id": worker.worker_id,
|
||
"task_key": task_payload["task_key"],
|
||
"status_code": "worker_error",
|
||
"status_label": "Worker 执行失败",
|
||
"failure_reason": str(exc),
|
||
"pcap_start_status": pcap_start_status,
|
||
"traffic_synced": traffic_synced,
|
||
"log_synced": log_synced,
|
||
"total_duration_seconds": round(time.time() - started_at, 2),
|
||
}
|
||
|
||
|
||
def main() -> int:
|
||
TestExecutor, _, _, TaskWorker, _, _ = _load_worker_bits()
|
||
tester_name = input("请输入测试人姓名: ").strip()
|
||
if not tester_name:
|
||
print("测试人姓名不能为空")
|
||
return 2
|
||
run_kind = input("请输入人工测试类型 [manual_real/manual_block] (默认 manual_real): ").strip() or "manual_real"
|
||
tier_label = input("指定档位可选,如 T1/5;直接回车不限: ").strip()
|
||
|
||
worker = TaskWorker()
|
||
worker.ip_address = worker._get_ip_address()
|
||
worker.mac_address = worker._get_mac_address()
|
||
worker.hostname = socket.gethostname()
|
||
worker.worker_id = f"{worker.ip_address}_{worker.mac_address}"
|
||
print(f"Worker ID: {worker.worker_id}")
|
||
|
||
executor = TestExecutor()
|
||
if not executor.setup():
|
||
print("设备初始化失败")
|
||
return 1
|
||
|
||
while True:
|
||
assignment = _request_assignment(worker, tester_name, run_kind, tier_label=tier_label)
|
||
if not assignment:
|
||
print("暂无人工测试任务")
|
||
return 0
|
||
print(f"\n领取任务: {assignment['app_name']} ({assignment['package_name']}) [{assignment.get('tier_label', '')}]")
|
||
result_payload = run_manual_task(executor, worker, assignment, tester_name)
|
||
response = _publish_result(worker, result_payload)
|
||
print("上传结果:", json.dumps(response, ensure_ascii=False))
|
||
again = input("继续领取下一个任务?[Y/n]: ").strip().lower()
|
||
if again == "n":
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|