"""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 = ( '
' ) 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()