# -*- coding: utf-8 -*- """monitor runner —— 尤其是抖音那条(不走子进程的)路的运行状态流转。 这条路的地位特殊:它不经过 ``crawler_manager``,所以爬虫那套「退出码 / 日志尾巴」的 约定它一个都不沾。凡是写在那里面的东西,这条路都得单独有一份。 """ import asyncio import pytest import pytest_asyncio from sqlalchemy import select from tools.time_util import get_current_timestamp from api.monitor import db as monitor_db from api.monitor import runner as runner_module from api.monitor.models import ( MODE_CREATOR, RUN_RUNNING, MonitorRun, MonitorTarget, MonitorTask, ) @pytest_asyncio.fixture async def db(tmp_path): monitor_db.set_sqlite_path(tmp_path / "monitor.db") await monitor_db.init_db() yield monitor_db await monitor_db.dispose_engine() async def _make_douyin_task() -> int: async with monitor_db.get_session() as session: now = get_current_timestamp() task = MonitorTask( name="dy", platform="dy", mode=MODE_CREATOR, enabled=True, interval_minutes=360, max_notes_count=20, enable_comments=False, max_comments_count=20, run_timeout_seconds=3600, notify_enabled=False, notify_failures=False, created_at=now, updated_at=now, ) session.add(task) await session.flush() session.add( MonitorTarget( task_id=task.id, kind=MODE_CREATOR, external_id="MS4w-sec", xsec_token="", xsec_source="", raw_value="MS4w-sec", label="x", enabled=True, created_at=now, ) ) return task.id class TestDouyinRunStatus: @pytest.mark.asyncio async def test_the_run_is_marked_running_before_collecting(self, db, monkeypatch): """**采集开始之前**,run 就必须已经是 running。 这一行原先只写在爬虫那条分支里,于是抖音路上 run 一直停在 pending —— 一旦中途 出事(异常、或进程被重启),界面上就是一个永远「排队中」的幽灵,而且 recover() 当时也只收 running、够不着它。 """ task_id = await _make_douyin_task() seen = {} async def fake_collect(out_dir, **kwargs): async with monitor_db.get_session() as session: run = await session.scalar(select(MonitorRun).order_by(MonitorRun.id)) seen["status"] = run.status return { "notes": 0, "comments": 0, "errors": ["故意失败"], "jsonl_dir": str(out_dir), } monkeypatch.setattr(runner_module.douyin_fetch, "collect", fake_collect) await runner_module.execute_task(task_id, trigger="manual") assert seen["status"] == RUN_RUNNING @pytest.mark.asyncio async def test_a_hanging_collect_does_not_leave_the_run_running(self, db, monkeypatch): """进程内那条路也要有超时。 爬虫那条靠 ``run_and_wait(timeout=...)`` 兜底,这条路没有子进程、没人管 —— 里面任何一次卡住(实测过 ``page.evaluate`` 打在一个卡死的标签页上不返回)都会让 run 永远停在「运行中」,界面上看起来就是任务卡死了。 """ task_id = await _make_douyin_task() async with monitor_db.get_session() as session: task = await session.get(MonitorTask, task_id) task.run_timeout_seconds = 1 # 把超时压到 1 秒,别让测试真等 async def hanging_collect(out_dir, **kwargs): await asyncio.sleep(60) raise AssertionError("不该走到这里") monkeypatch.setattr(runner_module.douyin_fetch, "collect", hanging_collect) await runner_module.execute_task(task_id, trigger="manual") async with monitor_db.get_session() as session: run = await session.scalar(select(MonitorRun).order_by(MonitorRun.id)) assert run.status != RUN_RUNNING assert "超时" in (run.error_message or "") or "超过" in (run.error_message or "")