fix(content): 蝉妈妈导出标签页崩溃自愈与 cookie 文件格式对齐
导出点击后 tab 死亡(渲染进程被清理)时:页面级死亡先同 context 重开 新页(facade 新增 reopen_page),context 也死亡才整套重建驱动,按 profile → cookie 文件 → 账密顺序恢复会话,两种自愈各限一次防死循环。 cookie 文件改存裸列表,与 vault 同步及适配器 BrowserCookieStore 三方 格式对齐(字典格式会被适配器拒绝加载);adapters 暴露 is_browser_closed_error 供模块识别目标已关闭类异常。
This commit is contained in:
1 parent
6cd563eab0
commit
7b305b0b12
4 files changed
+325
-42
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
@@ -33,7 +35,7 @@ def test_refresh_then_export_waits_once_after_all_accounts(monkeypatch):
|
||||
monkeypatch.setattr(chanmama, "export_video_data", lambda _driver, mtime: events.append(("export", mtime)) or "x.xlsx")
|
||||
monkeypatch.setattr(chanmama, "parse_chanmama_excel", lambda _path: ([{"title": "video"}], None))
|
||||
|
||||
records, summaries = chanmama.refresh_then_export_accounts(object(), urls, wait_seconds=600)
|
||||
records, summaries, _driver = chanmama.refresh_then_export_accounts(object(), urls, wait_seconds=600)
|
||||
|
||||
assert len(records) == 2
|
||||
assert [item["clicked"] for item in summaries] == [2, 0]
|
||||
@@ -54,7 +56,7 @@ def test_refresh_then_export_skips_wait_when_nothing_needs_refresh(monkeypatch):
|
||||
monkeypatch.setattr(chanmama, "_max_excel_mtime", lambda: 0.0)
|
||||
monkeypatch.setattr(chanmama, "export_video_data", lambda *_args: None)
|
||||
|
||||
records, _ = chanmama.refresh_then_export_accounts(object(), ["account-1"], wait_seconds=600)
|
||||
records, _, _driver = chanmama.refresh_then_export_accounts(object(), ["account-1"], wait_seconds=600)
|
||||
|
||||
assert records == []
|
||||
assert sleeps == []
|
||||
@@ -159,7 +161,6 @@ def test_missing_get_data_button_does_not_trigger_refresh_wait(monkeypatch):
|
||||
chanmama.refresh_then_export_accounts(object(), ["account-1"], wait_seconds=600)
|
||||
assert sleeps == []
|
||||
|
||||
|
||||
def test_create_driver_uses_runtime_cdp_profile_and_download_bindings(monkeypatch, tmp_path):
|
||||
calls = []
|
||||
expected = object()
|
||||
@@ -183,7 +184,9 @@ def test_create_driver_uses_runtime_cdp_profile_and_download_bindings(monkeypatc
|
||||
"profile_dir": str(tmp_path / "profile"),
|
||||
"download_dir": str(tmp_path / "downloads"),
|
||||
"cdp_url": "http://127.0.0.1:22002",
|
||||
"stealthy": True,
|
||||
"stealthy": False,
|
||||
"real_chrome": True,
|
||||
"extra_flags": ["--no-proxy-server"],
|
||||
}]
|
||||
|
||||
|
||||
@@ -206,8 +209,10 @@ def test_create_driver_starts_scrapling_without_cdp(monkeypatch, tmp_path):
|
||||
|
||||
assert chanmama.create_driver() is expected
|
||||
assert calls[0]["cdp_url"] is None
|
||||
assert calls[0]["stealthy"] is True
|
||||
assert calls[0]["real_chrome"] is False
|
||||
# 蝉妈妈 WAF 拉黑代理出口且压制 stealthy 补丁,必须真 Chrome + 直连。
|
||||
assert calls[0]["stealthy"] is False
|
||||
assert calls[0]["real_chrome"] is True
|
||||
assert calls[0]["extra_flags"] == ["--no-proxy-server"]
|
||||
|
||||
|
||||
def test_create_driver_falls_back_when_bound_cdp_is_not_listening(
|
||||
@@ -241,8 +246,8 @@ def test_create_driver_falls_back_when_bound_cdp_is_not_listening(
|
||||
"http://127.0.0.1:22002",
|
||||
None,
|
||||
]
|
||||
assert all(call["stealthy"] is True for call in calls)
|
||||
assert calls[1]["real_chrome"] is False
|
||||
assert all(call["stealthy"] is False for call in calls)
|
||||
assert all(call["real_chrome"] is True for call in calls)
|
||||
|
||||
|
||||
def test_create_driver_keeps_explicit_remote_cdp_fail_closed(
|
||||
@@ -761,7 +766,7 @@ def test_refresh_then_export_relogin_on_expired_session(monkeypatch):
|
||||
chanmama, "parse_chanmama_excel", lambda _path: ([{"title": "v"}], None)
|
||||
)
|
||||
|
||||
records, summaries = chanmama.refresh_then_export_accounts(
|
||||
records, summaries, _driver = chanmama.refresh_then_export_accounts(
|
||||
object(), ["account-1", "account-2"], wait_seconds=0
|
||||
)
|
||||
|
||||
@@ -791,7 +796,7 @@ def test_refresh_then_export_no_relogin_for_other_errors(monkeypatch):
|
||||
chanmama, "export_video_data", lambda _driver, mtime: None
|
||||
)
|
||||
|
||||
records, _summaries = chanmama.refresh_then_export_accounts(
|
||||
records, _summaries, _driver = chanmama.refresh_then_export_accounts(
|
||||
object(), ["account-1"], wait_seconds=0
|
||||
)
|
||||
|
||||
@@ -889,3 +894,192 @@ def test_wait_video_list_ready_tolerates_transient_script_errors(monkeypatch):
|
||||
return True
|
||||
|
||||
chanmama._wait_video_list_ready(FlakyDriver(), 2) # 不抛即通过
|
||||
|
||||
|
||||
def test_wait_for_exported_excel_raises_when_browser_closed_without_file(monkeypatch):
|
||||
"""浏览器被外部关闭且文件未落盘: 立刻抛出关闭异常,不再空等满超时。"""
|
||||
|
||||
class _DeadDriver:
|
||||
def execute_script(self, _script, *_args):
|
||||
raise RuntimeError(
|
||||
"Page.goto: Target page, context or browser has been closed"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(chanmama.time, "time", lambda: 1000.0)
|
||||
monkeypatch.setattr(
|
||||
chanmama.time,
|
||||
"sleep",
|
||||
lambda _seconds: pytest.fail("浏览器已死不应继续轮询等待"),
|
||||
)
|
||||
monkeypatch.setattr(chanmama, "_find_new_excel_after", lambda _pre_mtime: None)
|
||||
|
||||
with pytest.raises(RuntimeError, match="closed"):
|
||||
chanmama._wait_for_exported_excel(0.0, driver=_DeadDriver())
|
||||
|
||||
|
||||
def test_wait_for_exported_excel_returns_file_if_landed_before_close(monkeypatch):
|
||||
"""关闭前下载已完整落盘: 仍返回文件路径,不因浏览器死亡误报失败。"""
|
||||
|
||||
class _DeadDriver:
|
||||
def execute_script(self, _script, *_args):
|
||||
raise RuntimeError(
|
||||
"Download.save_as: Target page, context or browser has been closed"
|
||||
)
|
||||
|
||||
landed = None
|
||||
|
||||
def fake_find(_pre_mtime):
|
||||
return landed
|
||||
|
||||
def fake_complete(path):
|
||||
return True
|
||||
|
||||
monkeypatch.setattr(chanmama.time, "time", lambda: 1000.0)
|
||||
monkeypatch.setattr(chanmama, "_find_new_excel_after", fake_find)
|
||||
monkeypatch.setattr(chanmama, "_download_complete", fake_complete)
|
||||
|
||||
landed = "/downloads/关闭前已落盘.xlsx"
|
||||
assert chanmama._wait_for_exported_excel(0.0, driver=_DeadDriver()) == landed
|
||||
|
||||
|
||||
def test_refresh_then_export_rebuilds_browser_when_closed_mid_run(monkeypatch):
|
||||
"""导出中途浏览器被外部关闭: 重建驱动+恢复会话后重试当前博主一次。"""
|
||||
events = []
|
||||
navigate_calls = []
|
||||
dead_driver = object()
|
||||
live_driver = object()
|
||||
|
||||
def fake_navigate(current, url):
|
||||
navigate_calls.append((current, url))
|
||||
if current is dead_driver and len(navigate_calls) == 3:
|
||||
# 刷新阶段两个博主都成功后,导出阶段第一次导航即发现浏览器死亡
|
||||
raise RuntimeError(
|
||||
"蝉妈妈目标页加载失败: x.html; Page.goto: "
|
||||
"Target page, context or browser has been closed"
|
||||
)
|
||||
|
||||
monkeypatch.setattr(chanmama, "navigate_to_target", fake_navigate)
|
||||
monkeypatch.setattr(
|
||||
chanmama,
|
||||
"refresh_zero_exposure_videos",
|
||||
lambda _driver: {
|
||||
"clicked": 0, "pending": 0, "zero_without_button": 0,
|
||||
"unconfirmed": 0, "pages": 1,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(chanmama.time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(chanmama, "_max_excel_mtime", lambda: 0.0)
|
||||
monkeypatch.setattr(
|
||||
chanmama, "export_video_data", lambda _driver, mtime: "x.xlsx"
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
chanmama, "parse_chanmama_excel", lambda _path: ([{"title": "v"}], None)
|
||||
)
|
||||
monkeypatch.setattr(chanmama, "verify_login", lambda _driver: True)
|
||||
monkeypatch.setattr(
|
||||
chanmama, "create_driver", lambda: events.append("rebuild") or live_driver
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
chanmama, "login",
|
||||
lambda _driver: events.append("login") or True,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
chanmama, "load_cookies",
|
||||
lambda _driver: (_ for _ in ()).throw(
|
||||
AssertionError("profile 直接恢复,不应走 cookie 文件")
|
||||
),
|
||||
)
|
||||
|
||||
class _DeadDriver:
|
||||
def quit(self):
|
||||
events.append("quit-dead")
|
||||
|
||||
dead_driver = _DeadDriver()
|
||||
|
||||
records, _summaries, final_driver = chanmama.refresh_then_export_accounts(
|
||||
dead_driver, ["account-1", "account-2"], wait_seconds=0
|
||||
)
|
||||
|
||||
assert events == ["quit-dead", "rebuild"] # 旧驱动清理+重建一次
|
||||
assert final_driver is live_driver # 返回重建后的驱动供收尾关闭
|
||||
assert len(records) == 2 # 两个博主最终都导出成功
|
||||
# 第 3 次导航(导出博主 1)发现死亡,其后所有导航都在重建后的驱动上
|
||||
assert all(current is live_driver for current, _ in navigate_calls[3:])
|
||||
|
||||
|
||||
def test_refresh_then_export_rebuilds_only_once(monkeypatch):
|
||||
"""环境持续杀浏览器时只重建一次,后续博主直接失败不再死循环。"""
|
||||
rebuilds = []
|
||||
|
||||
def always_closed_navigate(_driver, _url):
|
||||
raise RuntimeError(
|
||||
"Page.goto: Target page, context or browser has been closed"
|
||||
)
|
||||
|
||||
class _Driver:
|
||||
def quit(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr(chanmama, "navigate_to_target", always_closed_navigate)
|
||||
monkeypatch.setattr(
|
||||
chanmama, "create_driver", lambda: rebuilds.append(1) or _Driver()
|
||||
)
|
||||
monkeypatch.setattr(chanmama, "verify_login", lambda _driver: True)
|
||||
monkeypatch.setattr(chanmama.time, "sleep", lambda _seconds: None)
|
||||
|
||||
records, _summaries, _driver = chanmama.refresh_then_export_accounts(
|
||||
_Driver(), ["account-1", "account-2"], wait_seconds=0
|
||||
)
|
||||
|
||||
assert len(rebuilds) == 1
|
||||
assert records == []
|
||||
|
||||
|
||||
def test_save_cookies_writes_adapter_list_format(monkeypatch, tmp_path):
|
||||
"""cookie 文件与 vault/适配器共用,必须写裸列表而不是字典。"""
|
||||
cookie_file = tmp_path / "cookies.json"
|
||||
monkeypatch.setattr(chanmama, "COOKIE_FILE", str(cookie_file))
|
||||
|
||||
class _Driver:
|
||||
@staticmethod
|
||||
def get_cookies():
|
||||
return [{"name": "session", "domain": ".chanmama.com"}]
|
||||
|
||||
assert chanmama.save_cookies(_Driver()) is True
|
||||
payload = json.loads(cookie_file.read_text(encoding="utf-8"))
|
||||
assert payload == [{"name": "session", "domain": ".chanmama.com"}]
|
||||
|
||||
|
||||
def test_load_cookies_accepts_list_and_legacy_dict(monkeypatch, tmp_path):
|
||||
cookie_file = tmp_path / "cookies.json"
|
||||
|
||||
injected = []
|
||||
|
||||
class _Driver:
|
||||
@staticmethod
|
||||
def get(_url):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def add_cookie(cookie):
|
||||
injected.append(cookie)
|
||||
|
||||
monkeypatch.setattr(chanmama, "COOKIE_FILE", str(cookie_file))
|
||||
monkeypatch.setattr(chanmama.time, "sleep", lambda _seconds: None)
|
||||
|
||||
cookie_file.write_text(
|
||||
json.dumps([{"name": "a", "domain": ".chanmama.com"}]),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert chanmama.load_cookies(_Driver()) is True
|
||||
assert [c["name"] for c in injected] == ["a"]
|
||||
|
||||
injected.clear()
|
||||
cookie_file.write_text(
|
||||
json.dumps(
|
||||
{"cookies": [{"name": "b", "domain": ".chanmama.com"}], "timestamp": time.time()}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert chanmama.load_cookies(_Driver()) is True
|
||||
assert [c["name"] for c in injected] == ["b"]
|
||||
Reference in new issue
Block a user