autool-dispatcher/tests/test_analytics_new_schema.py
2026-06-17 19:50:39 +08:00

242 lines
8.3 KiB
Python

import json
from analytics import AnalyticsService, _make_task_key
from redis_task_distribute import RedisTaskDispatcher
class FakeRedis:
def __init__(self):
self.lists = {}
self.hashes = {}
self.sets = {}
def delete(self, key):
self.lists.pop(key, None)
self.hashes.pop(key, None)
self.sets.pop(key, None)
def lpush(self, key, value):
self.lists.setdefault(key, []).insert(0, value)
def rpush(self, key, value):
self.lists.setdefault(key, []).append(value)
def rpop(self, key):
values = self.lists.setdefault(key, [])
return values.pop() if values else None
def lrange(self, key, start, end):
values = self.lists.get(key, [])
stop = None if end == -1 else end + 1
return values[start:stop]
def llen(self, key):
return len(self.lists.get(key, []))
def lpos(self, key, value):
try:
return self.lists.get(key, []).index(value)
except ValueError:
return None
def lrem(self, key, count, value):
values = self.lists.get(key, [])
original_len = len(values)
self.lists[key] = [item for item in values if item != value]
return original_len - len(self.lists[key])
def hset(self, key, field, value):
self.hashes.setdefault(key, {})[field] = value
def hget(self, key, field):
return self.hashes.get(key, {}).get(field)
def hgetall(self, key):
return dict(self.hashes.get(key, {}))
def hvals(self, key):
return list(self.hashes.get(key, {}).values())
def hlen(self, key):
return len(self.hashes.get(key, {}))
def hdel(self, key, field):
self.hashes.get(key, {}).pop(field, None)
def sadd(self, key, value):
self.sets.setdefault(key, set()).add(value)
def srem(self, key, value):
self.sets.setdefault(key, set()).discard(value)
def scard(self, key):
return len(self.sets.get(key, set()))
def smembers(self, key):
return set(self.sets.get(key, set()))
def scan_iter(self, match=None):
return iter(())
class FakeMonitor:
def set_worker_state(self, *args, **kwargs):
return None
def set_failure_bucket(self, *args, **kwargs):
return None
class FakeWorker:
def __init__(self, analytics, dispatcher, worker_id="fake-worker"):
self.analytics = analytics
self.dispatcher = dispatcher
self.worker_id = worker_id
def report(
self,
*,
app_name,
package_name,
status,
failure_type="",
num_nodes=12,
self_traffic_bytes=600,
server_traffic_bytes=300,
total_traffic_bytes=1000,
model_flow_count=0,
):
summary = {
"app_name": app_name,
"app_magic_label": f"magic:{package_name}",
"latest_task_key": _make_task_key(app_name, package_name),
"latest_status": status,
"latest_worker_id": self.worker_id,
"latest_failure_type": failure_type,
"latest_task_detail": failure_type,
"collection_status_reason": failure_type or "success",
"collection_task_type": "new_app",
"duration_seconds": 42,
"droidbot_steps": 30,
"gui_agent_steps": 12,
"num_nodes": num_nodes,
"total_traffic_bytes": total_traffic_bytes,
"self_traffic_bytes": self_traffic_bytes,
"server_traffic_bytes": server_traffic_bytes,
"unrecognized_traffic_bytes": max(0, total_traffic_bytes - self_traffic_bytes - server_traffic_bytes),
"model_flow_count": model_flow_count,
"model_traffic_bytes": model_flow_count * 100,
}
self.analytics.repo.replace_package_snapshot(package_name, summary, [], [], [])
persisted = self.analytics.repo.get_collection_row(package_name)
self.dispatcher._handle_analytics_snapshot(package_name, persisted)
return persisted
def make_dispatcher(redis_conn, analytics):
dispatcher = RedisTaskDispatcher.__new__(RedisTaskDispatcher)
dispatcher.redis = redis_conn
dispatcher.analytics = analytics
dispatcher.worker_inventory = {}
dispatcher.managed_worker_ids = set()
dispatcher.only_managed_workers_can_dispatch = False
dispatcher.worker_online_timeout = 300
dispatcher.task_routing_rules = {"package_name": {}, "task_key": {}}
dispatcher.monitor = FakeMonitor()
dispatcher.notifier = None
dispatcher._apk_registry = None
dispatcher._minio_storage = None
return dispatcher
def task_status(redis_conn, task_key):
payload = redis_conn.hget("task:status", task_key)
return json.loads(payload) if payload else {}
def test_repository_initializes_new_tables_without_views(tmp_path):
analytics = AnalyticsService(db_path=str(tmp_path / "analytics.sqlite3"), start_worker=False)
with analytics.repo._connect() as connection:
names = {
row["name"]
for row in connection.execute("SELECT name FROM sqlite_master WHERE type IN ('table', 'view')")
}
assert "app_catalog" in names
assert "collection_task" in names
assert "app_collect_summary" not in names
assert "v_collection_latest" not in names
analytics.replace_catalog_from_rows(
[{"app_name": "Tagged App", "package_name": "com.example.tagged"}]
)
assert analytics.set_incremental_batch_tag_for_packages(["com.example.tagged"], "batch-a") == 1
assert "batch-a" in analytics.repo.get_collection_row("com.example.tagged")["incremental_batch_tags"]
assert analytics.clear_incremental_batch_tags("batch-a") == 1
assert "batch-a" not in analytics.repo.get_collection_row("com.example.tagged")["incremental_batch_tags"]
def test_fake_worker_reports_drive_new_schema_and_redis_states(tmp_path):
analytics = AnalyticsService(db_path=str(tmp_path / "analytics.sqlite3"), start_worker=False)
analytics.replace_catalog_from_rows(
[
{"app_name": "Good App", "package_name": "com.example.good", "downloads": "100K"},
{"app_name": "Gone App", "package_name": "com.example.gone", "downloads": "1K"},
{"app_name": "Retry App", "package_name": "com.example.retry", "downloads": "2M"},
{"app_name": "Model App", "package_name": "com.example.model", "downloads": "3M"},
]
)
redis_conn = FakeRedis()
dispatcher = make_dispatcher(redis_conn, analytics)
assert dispatcher.load_tasks_from_app_summary() == 4
worker = FakeWorker(analytics, dispatcher)
good = worker.report(app_name="Good App", package_name="com.example.good", status="success")
gone = worker.report(
app_name="Gone App",
package_name="com.example.gone",
status="failed",
failure_type="APP_ERROR/3",
num_nodes=0,
total_traffic_bytes=0,
self_traffic_bytes=0,
server_traffic_bytes=0,
)
retry = worker.report(
app_name="Retry App",
package_name="com.example.retry",
status="failed",
failure_type="DOWNLOAD_ERROR/5",
num_nodes=0,
total_traffic_bytes=0,
self_traffic_bytes=0,
server_traffic_bytes=0,
)
model = worker.report(
app_name="Model App",
package_name="com.example.model",
status="success",
model_flow_count=2,
)
assert good["collection_status"] == "qualified"
assert gone["collection_status"] == "failed_terminal"
assert retry["collection_status"] == "pending"
assert model["model_eligible"] == 1
good_key = _make_task_key("Good App", "com.example.good")
gone_key = _make_task_key("Gone App", "com.example.gone")
retry_key = _make_task_key("Retry App", "com.example.retry")
assert task_status(redis_conn, good_key)["status"] == "qualified"
assert good_key in redis_conn.smembers("task:completed")
assert task_status(redis_conn, gone_key)["status"] == "failed"
assert gone_key in redis_conn.smembers("task:failed")
assert task_status(redis_conn, retry_key)["status"] == "pending"
assert retry_key in redis_conn.lrange("task:queue:default", 0, -1)
assert dispatcher.refresh_tasks_from_app_summary() == 1
assert _make_task_key("Model App", "com.example.model") + "_model" in redis_conn.lrange("task:queue:low", 0, -1)