PyJHora 只在子进程里计算请求年及后两年。对话、校正和其余章节保持不导入时的结果。结果指纹不算这一章。 Windows 自测:隐私标记、导入扫描、结果指纹和 S7 退出码 0。阅读页、英文和年运章在此之前已通过。虚构盘 0 中英一起最高 59 秒。 Co-Authored-By: Grok (agent loop)
241 lines
9.7 KiB
Python
241 lines
9.7 KiB
Python
"""Issue #64: skip useless report subprocesses and observe the heavy-compute gate."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import logging
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from scripts import api_heavy_compute_gate as gate
|
||
from scripts.annual_pyjhora_replay import (
|
||
_run_isolated_annual_replay,
|
||
_run_isolated_tajaka_year_lord,
|
||
reset_jhora_import_cache,
|
||
)
|
||
from scripts.jyotish_engine import (
|
||
_oracle_progress_snapshot,
|
||
reset_oracle_progress_snapshot_cache,
|
||
)
|
||
from scripts.tajika import (
|
||
_resolve_external_pyjhora_python,
|
||
reset_external_pyjhora_python_cache,
|
||
)
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
FIXTURES = ROOT / "tests" / "fixtures" / "api-chart-subprocess-gate"
|
||
|
||
|
||
class _Place:
|
||
name = "Delhi"
|
||
latitude = 28.6139
|
||
longitude = 77.209
|
||
timezone = 5.5
|
||
elevation = 0
|
||
|
||
|
||
_PROFILE = {"ayanamsa": "LAHIRI", "node_mode": "mean"}
|
||
|
||
|
||
def _without_hash(payload: dict) -> dict:
|
||
body = dict(payload)
|
||
body.pop("request_hash", None)
|
||
return body
|
||
|
||
|
||
def test_no_jhora_tajaka_skips_subprocess_and_matches_golden(monkeypatch) -> None:
|
||
reset_jhora_import_cache()
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay._jhora_importable", lambda: False)
|
||
|
||
def _forbid_subprocess(*_args, **_kwargs):
|
||
raise AssertionError("subprocess.run must not run when jhora cannot be imported")
|
||
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _forbid_subprocess)
|
||
result = _run_isolated_tajaka_year_lord(_PROFILE, 2447893.0, _Place(), 30)
|
||
golden = json.loads((FIXTURES / "tajaka-no-jhora.json").read_text(encoding="utf-8"))
|
||
assert result["request_hash"]
|
||
assert _without_hash(result) == golden
|
||
|
||
|
||
def test_no_jhora_annual_skips_subprocess_and_matches_golden(monkeypatch) -> None:
|
||
reset_jhora_import_cache()
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay._jhora_importable", lambda: False)
|
||
|
||
def _forbid_subprocess(*_args, **_kwargs):
|
||
raise AssertionError("subprocess.run must not run when jhora cannot be imported")
|
||
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _forbid_subprocess)
|
||
result = _run_isolated_annual_replay(_PROFILE, 2447893.0, _Place(), 30)
|
||
golden = json.loads((FIXTURES / "annual-no-jhora.json").read_text(encoding="utf-8"))
|
||
assert result["request_hash"]
|
||
assert _without_hash(result) == golden
|
||
|
||
|
||
def test_jhora_available_still_uses_subprocess(monkeypatch) -> None:
|
||
# 原值:_jhora_importable 为真时两个隔离函数各起一次子进程(calls == 2)
|
||
# 新值:产品调用不传 research 时,即使可导入也不起子进程、返回 golden;传 research=True 时仍起子进程(calls == 2)
|
||
# 原因:#93 红线 1:装包后旧回放不得进年运章
|
||
reset_jhora_import_cache()
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay._jhora_importable", lambda: True)
|
||
calls = {"n": 0}
|
||
|
||
class _Completed:
|
||
returncode = 0
|
||
stdout = json.dumps({"status": "partial_verified", "settings": {}})
|
||
|
||
def _fake_run(*_args, **_kwargs):
|
||
calls["n"] += 1
|
||
return _Completed()
|
||
|
||
def _forbid_subprocess(*_args, **_kwargs):
|
||
raise AssertionError("product replay must not spawn a child")
|
||
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _forbid_subprocess)
|
||
tajaka = _run_isolated_tajaka_year_lord(_PROFILE, 2447893.0, _Place(), 30)
|
||
annual = _run_isolated_annual_replay(_PROFILE, 2447893.0, _Place(), 30)
|
||
assert calls["n"] == 0
|
||
tajaka_golden = json.loads((FIXTURES / "tajaka-no-jhora.json").read_text(encoding="utf-8"))
|
||
annual_golden = json.loads((FIXTURES / "annual-no-jhora.json").read_text(encoding="utf-8"))
|
||
assert _without_hash(tajaka) == tajaka_golden
|
||
assert _without_hash(annual) == annual_golden
|
||
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay.subprocess.run", _fake_run)
|
||
_run_isolated_tajaka_year_lord(_PROFILE, 2447893.0, _Place(), 30, research=True)
|
||
_run_isolated_annual_replay(_PROFILE, 2447893.0, _Place(), 30, research=True)
|
||
assert calls["n"] == 2
|
||
|
||
|
||
def test_product_replay_calls_a_four_arg_stand_in(monkeypatch) -> None:
|
||
"""The 200-chart sweep replaces the runners with four-argument functions."""
|
||
from scripts.annual_pyjhora_replay import (
|
||
_no_jhora_annual_replay,
|
||
_no_jhora_tajaka_replay,
|
||
replay_pyjhora_annual,
|
||
)
|
||
|
||
calls = []
|
||
|
||
def blocked(profile, birth_julian_day, place, age):
|
||
calls.append(age)
|
||
settings = {"ayanamsa": "LAHIRI", "node_mode": "mean"}
|
||
result = _no_jhora_tajaka_replay(settings) if len(calls) == 1 else _no_jhora_annual_replay(settings)
|
||
result["request_hash"] = "stand-in"
|
||
return result
|
||
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay._run_isolated_tajaka_year_lord", blocked)
|
||
monkeypatch.setattr("scripts.annual_pyjhora_replay._run_isolated_annual_replay", blocked)
|
||
result = replay_pyjhora_annual(_PROFILE, birth_julian_day=2447893.0, place=_Place(), age=30)
|
||
assert calls == [30, 30]
|
||
assert result["status"] == "blocked"
|
||
|
||
|
||
def test_pyjhora_interpreter_lookup_is_cached(monkeypatch) -> None:
|
||
reset_external_pyjhora_python_cache()
|
||
calls = {"n": 0}
|
||
|
||
def _missing():
|
||
calls["n"] += 1
|
||
return None
|
||
|
||
monkeypatch.setattr("scripts.tajika._lookup_external_pyjhora_python", _missing)
|
||
assert _resolve_external_pyjhora_python() is None
|
||
assert _resolve_external_pyjhora_python() is None
|
||
assert calls["n"] == 1
|
||
|
||
|
||
def test_oracle_progress_snapshot_reuses_until_the_file_changes(monkeypatch) -> None:
|
||
reset_oracle_progress_snapshot_cache()
|
||
calls = {"n": 0}
|
||
real_run = __import__("subprocess").run
|
||
|
||
def _counting_run(*args, **kwargs):
|
||
calls["n"] += 1
|
||
return real_run(*args, **kwargs)
|
||
|
||
monkeypatch.setattr("subprocess.run", _counting_run)
|
||
first = _oracle_progress_snapshot()
|
||
after_first = calls["n"]
|
||
assert after_first >= 1
|
||
second = _oracle_progress_snapshot()
|
||
assert calls["n"] == after_first
|
||
assert second == first
|
||
|
||
oracle = ROOT / "references" / "oracle" / "dasha_shadbala_oracle_cases.json"
|
||
stat = oracle.stat()
|
||
later = stat.st_mtime + 5
|
||
try:
|
||
oracle.touch()
|
||
import os
|
||
os.utime(oracle, (later, later))
|
||
third = _oracle_progress_snapshot()
|
||
assert calls["n"] > after_first
|
||
assert third["scope"] == first["scope"]
|
||
finally:
|
||
os_utime = __import__("os").utime
|
||
os_utime(oracle, (stat.st_atime, stat.st_mtime))
|
||
reset_oracle_progress_snapshot_cache()
|
||
|
||
|
||
def test_chart_overview_gate_is_off_by_default(monkeypatch) -> None:
|
||
monkeypatch.delenv(gate.ENV_GATE_CHART_OVERVIEW, raising=False)
|
||
gate.reset_heavy_compute_gate(limit=1)
|
||
held = gate.acquire_heavy_compute_slot("/api/high_rigor_workflow")
|
||
assert held is not None
|
||
assert gate.acquire_heavy_compute_slot_for_request("/api/chart", {"year": 1990}) is None
|
||
gate.release_heavy_compute_slot(held)
|
||
assert gate.heavy_compute_status()["in_use"] == 0
|
||
gate.reset_heavy_compute_gate()
|
||
|
||
|
||
def test_chart_overview_gate_only_sync_unskipped_requests(monkeypatch) -> None:
|
||
monkeypatch.setenv(gate.ENV_GATE_CHART_OVERVIEW, "1")
|
||
# 原值:reset(limit=1) 后第二次立刻 HeavyComputeBusy
|
||
# 新值:wait_seconds=0
|
||
# 原因:默认改为短排队;本测试仍锁「名额被占时同步排盘被拒绝」,等待行为不在这里
|
||
gate.reset_heavy_compute_gate(limit=1, wait_seconds=0)
|
||
body = {"year": 1990, "lat": 28.6, "lon": 77.2}
|
||
slot = gate.acquire_heavy_compute_slot_for_request("/api/chart", body)
|
||
assert slot is not None
|
||
assert gate.heavy_compute_status() == {"limit": 1, "in_use": 1}
|
||
with pytest.raises(gate.HeavyComputeBusy):
|
||
gate.acquire_heavy_compute_slot_for_request("/api/chart", {"year": 1991})
|
||
gate.release_heavy_compute_slot(slot)
|
||
assert gate.heavy_compute_status()["in_use"] == 0
|
||
assert gate.acquire_heavy_compute_slot_for_request(
|
||
"/api/chart", {"skip_vedastro_main_entry_overview": True}
|
||
) is None
|
||
assert gate.acquire_heavy_compute_slot_for_request("/api/chart", {"async": True}) is None
|
||
assert gate.acquire_heavy_compute_slot_for_request("/api/chart", {"enqueue": 1}) is None
|
||
assert "/api/chart" not in gate.HEAVY_COMPUTE_PATHS
|
||
gate.reset_heavy_compute_gate()
|
||
|
||
|
||
def test_busy_log_names_holders_and_not_the_body(caplog) -> None:
|
||
# 原值:reset(limit=1) 后拒绝日志立刻出现
|
||
# 新值:wait_seconds=0
|
||
# 原因:默认会先等 8 秒;本测试仍锁日志只有路径、没有请求正文
|
||
gate.reset_heavy_compute_gate(limit=1, wait_seconds=0)
|
||
held = gate.acquire_heavy_compute_slot("/api/professional_report_reference")
|
||
assert held is not None
|
||
secret_body = {"birth_date": "1990-01-01", "name": "must-not-appear"}
|
||
with caplog.at_level(logging.WARNING, logger="jyotish.heavy_compute"):
|
||
with pytest.raises(gate.HeavyComputeBusy):
|
||
gate.acquire_heavy_compute_slot_for_request("/api/professional_report_reference", secret_body)
|
||
text = caplog.text
|
||
assert "/api/professional_report_reference" in text
|
||
assert "1990-01-01" not in text
|
||
assert "must-not-appear" not in text
|
||
gate.release_heavy_compute_slot(held)
|
||
assert gate.heavy_compute_status()["in_use"] == 0
|
||
gate.reset_heavy_compute_gate()
|
||
|
||
|
||
def test_warm_consultation_still_skips_the_slot() -> None:
|
||
gate.reset_heavy_compute_gate(limit=1)
|
||
assert gate.acquire_heavy_compute_slot_for_request(
|
||
"/api/consultation_workflow", {"consult_card_warm_only": True}
|
||
) is None
|
||
assert gate.heavy_compute_status()["in_use"] == 0
|
||
gate.reset_heavy_compute_gate()
|