Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QND7Co43B24zK2P937rdxt
98 lines
4.0 KiB
Python
98 lines
4.0 KiB
Python
"""249-1 B08 거리이정표 자동 그림 = 자식 프로세스 캡처(사용자 148번).
|
|
|
|
서버(uvicorn · Windows) = WindowsSelectorEventLoopPolicy → 스레드 안 sync_playwright 가
|
|
드라이버 하위 프로세스를 못 만듦. 같은 정책 · 스레드에서:
|
|
① 옛 길(같은 프로세스 shoot) = 「브라우저 못 띄움」 재현
|
|
② make_route_image(자식 파이썬 프로세스) = 실제 Edge 로 PNG 생김 · 키 값 응답 0
|
|
③ 자식 시간 넘김 · 자식 죽음 = 까닭 한 줄
|
|
카카오 SDK 대신 단색 페이지(이 파일을 자식 스크립트로 · page_html 만 바꿈) — 네트워크 없음.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import sys
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from B08_Quantity import B08_Quantity_Milestone_Capture as cap # noqa: E402
|
|
|
|
HTML = (
|
|
'<!doctype html><body style="margin:0"><div id="m" style="width:1200px;height:800px;'
|
|
'background:#E60046"></div><script>window.__done = true</script></body>'
|
|
)
|
|
KEY = "fake-js-key-249"
|
|
LINES = [[[35.6866, 127.9095], [35.66, 127.85]]]
|
|
MARKS = [{"name": "거창군청", "lon": 127.9095, "lat": 35.6866}]
|
|
|
|
if __name__ == "__main__": # 자식 프로세스 — 카카오 페이지만 단색 페이지로
|
|
cap.page_html = lambda key, lines, marks, view=None: HTML
|
|
print(json.dumps(cap.capture(json.loads(sys.stdin.buffer.read()))))
|
|
sys.exit(0)
|
|
|
|
import pytest # noqa: E402
|
|
|
|
pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="Windows Selector 루프 재현")
|
|
|
|
|
|
def in_server_thread(fn):
|
|
"""uvicorn 처럼 Selector 정책을 건 채 스레드(asyncio.to_thread 자리)에서 fn()."""
|
|
got = {}
|
|
|
|
def run():
|
|
try:
|
|
got["value"] = fn()
|
|
except BaseException as error: # noqa: BLE001 — 시험에서 그대로 봄
|
|
got["error"] = error
|
|
|
|
before = asyncio.get_event_loop_policy()
|
|
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
try:
|
|
job = threading.Thread(target=run)
|
|
job.start()
|
|
job.join(60)
|
|
finally:
|
|
asyncio.set_event_loop_policy(before)
|
|
return got
|
|
|
|
|
|
def test_옛_길_같은_프로세스는_못_띄움():
|
|
pytest.importorskip("playwright.sync_api")
|
|
got = in_server_thread(lambda: cap.shoot(HTML, cap.DEFAULT_ORIGIN))
|
|
assert isinstance(got.get("error"), cap.CaptureError)
|
|
assert str(got["error"]) == cap.NO_BROWSER
|
|
|
|
|
|
def test_자식_프로세스로_PNG_생김(tmp_path, monkeypatch):
|
|
pytest.importorskip("playwright.sync_api")
|
|
monkeypatch.setattr(cap, "CHILD", __file__)
|
|
monkeypatch.setenv(cap.KAKAO_JS, KEY)
|
|
monkeypatch.delenv(cap.ORIGIN_KEY, raising=False)
|
|
out = tmp_path / "auto"
|
|
got = in_server_thread(lambda: cap.make_route_image(out, "r1", LINES, MARKS))
|
|
assert got.get("value") == {"saved": True, "reason": None}, got
|
|
png = (out / "r1.png").read_bytes()
|
|
assert png[:8] == b"\x89PNG\r\n\x1a\n" and len(png) > 1000
|
|
assert sorted(p.name for p in out.iterdir()) == ["r1.png"] # .new 남지 않음
|
|
|
|
|
|
def test_자식_시간_넘김_죽음_까닭(tmp_path, monkeypatch):
|
|
monkeypatch.setenv(cap.KAKAO_JS, KEY)
|
|
tail = " — [그림 변경]으로 넣으세요."
|
|
head = "경로 그림 자동 캡처 실패 — "
|
|
hang = tmp_path / "hang.py"
|
|
hang.write_text("import time\ntime.sleep(30)\n", encoding="utf-8")
|
|
monkeypatch.setattr(cap, "CHILD", str(hang))
|
|
monkeypatch.setattr(cap, "CHILD_TIMEOUT_S", 1)
|
|
got = cap.make_route_image(tmp_path / "auto", "r1", LINES, MARKS)
|
|
assert got == {"saved": False, "reason": head + "지도 그림이 20초 안에 안 끝남" + tail}
|
|
dead = tmp_path / "dead.py"
|
|
dead.write_text(f"raise SystemExit('boom {KEY}')\n", encoding="utf-8")
|
|
monkeypatch.setattr(cap, "CHILD", str(dead))
|
|
got = cap.make_route_image(tmp_path / "auto", "r1", LINES, MARKS)
|
|
assert got == {"saved": False, "reason": head + cap.NO_BROWSER + tail}
|
|
assert not (tmp_path / "auto").exists()
|