0880fd6405
feat(test): smoke_test_runner.py — 名人事件应期回归测试框架 (12案例/42事件) feat(precision): mevg_automation.py — MEVG 外部验证门控自动化 (check/audit/report) docs: precision-benchmark-dashboard.md — 精度基准仪表盘 (8维度+改善路线) docs: chara-dasha-calibration-roadmap.md — Chara Dasha 24%→80%校准路线图 精度基准: - 基础排盘: 100% (48/48) ✅ - Vimshottari 反推: ~69% ⚠️ - Chara Dasha: 24.17% 🔴 - Double Transit: 20-40% 🔴
369 lines
16 KiB
Python
369 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Jyotish 事件应期回归测试框架 v1.0
|
|
自动验证 Dasha 推运精度:名人已知人生事件 vs 引擎预测
|
|
|
|
用法:
|
|
python3 tests/smoke_test_runner.py
|
|
python3 tests/smoke_test_runner.py --case einstein,obama
|
|
python3 tests/smoke_test_runner.py --report json
|
|
|
|
依赖: 需要 engin 可运行(pyswisseph 已安装)
|
|
"""
|
|
|
|
import json, os, sys, subprocess, argparse, time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
SCRIPTS_DIR = str(Path(__file__).resolve().parent.parent / "scripts")
|
|
if SCRIPTS_DIR not in sys.path:
|
|
sys.path.insert(0, SCRIPTS_DIR)
|
|
|
|
CASES_FILE = Path(__file__).resolve().parent / "prediction_regression_cases.json"
|
|
|
|
# ============================================================
|
|
# 测试用例 — 名人已知事件时间线
|
|
# 每个事件可验证 Dasha 推运系统的精度
|
|
# ============================================================
|
|
CELEBRITY_EVENTS = {
|
|
"obama": {
|
|
"name": "Barack Obama",
|
|
"birth": {"year": 1961, "month": 8, "day": 4, "hour": 19, "minute": 24,
|
|
"lat": 21.3, "lon": -157.8, "tz": -10.0},
|
|
"known_lagna": "Capricorn",
|
|
"events": [
|
|
{"date": "1992-10-03", "type": "marriage", "desc": "与 Michelle 结婚", "target_houses": [7]},
|
|
{"date": "2008-11-04", "type": "career_peak", "desc": "当选美国总统", "target_houses": [10, 1]},
|
|
{"date": "2012-11-06", "type": "career_peak", "desc": "连任美国总统", "target_houses": [10, 1]},
|
|
{"date": "2020-11-17", "type": "publication", "desc": "回忆录《应许之地》出版", "target_houses": [3, 10]},
|
|
]
|
|
},
|
|
"trump": {
|
|
"name": "Donald Trump",
|
|
"birth": {"year": 1946, "month": 6, "day": 14, "hour": 10, "minute": 54,
|
|
"lat": 40.7, "lon": -73.8, "tz": -5.0},
|
|
"known_lagna": "Leo",
|
|
"events": [
|
|
{"date": "1977-04-07", "type": "marriage", "desc": "与 Ivana 结婚", "target_houses": [7]},
|
|
{"date": "1990-04-04", "type": "business", "desc": "Taj Mahal 赌场开业", "target_houses": [10, 11]},
|
|
{"date": "2016-11-08", "type": "career_peak", "desc": "当选美国总统", "target_houses": [10, 1]},
|
|
{"date": "2024-11-05", "type": "career_peak", "desc": "再次当选总统", "target_houses": [10, 1]},
|
|
]
|
|
},
|
|
"jobs": {
|
|
"name": "Steve Jobs",
|
|
"birth": {"year": 1955, "month": 2, "day": 24, "hour": 19, "minute": 15,
|
|
"lat": 37.8, "lon": -122.4, "tz": -8.0},
|
|
"known_lagna": "Leo",
|
|
"events": [
|
|
{"date": "1976-04-01", "type": "career_start", "desc": "Apple 公司成立", "target_houses": [10, 11]},
|
|
{"date": "1985-09-17", "type": "career_loss", "desc": "被逐出 Apple", "target_houses": [10, 8]},
|
|
{"date": "1997-07-09", "type": "career_return", "desc": "回归 Apple 任 CEO", "target_houses": [10, 1]},
|
|
{"date": "2007-01-09", "type": "career_peak", "desc": "发布第一代 iPhone", "target_houses": [10, 3]},
|
|
{"date": "2011-10-05", "type": "death", "desc": "因胰腺癌去世", "target_houses": [8, 1]},
|
|
]
|
|
},
|
|
"einstein": {
|
|
"name": "Albert Einstein",
|
|
"birth": {"year": 1879, "month": 3, "day": 14, "hour": 11, "minute": 30,
|
|
"lat": 48.4, "lon": 9.98, "tz": 0.89},
|
|
"known_lagna": "Gemini",
|
|
"events": [
|
|
{"date": "1905-06-30", "type": "career_peak", "desc": "奇迹年:发表狭义相对论等四篇论文", "target_houses": [10, 5, 9]},
|
|
{"date": "1915-11-25", "type": "career_peak", "desc": "完成广义相对论", "target_houses": [10, 9]},
|
|
{"date": "1921-04-02", "type": "honor", "desc": "诺贝尔物理学奖", "target_houses": [10, 5]},
|
|
{"date": "1955-04-18", "type": "death", "desc": "在普林斯顿去世", "target_houses": [8, 1]},
|
|
]
|
|
},
|
|
"monroe": {
|
|
"name": "Marilyn Monroe",
|
|
"birth": {"year": 1926, "month": 6, "day": 1, "hour": 9, "minute": 30,
|
|
"lat": 34.1, "lon": -118.3, "tz": -8.0},
|
|
"known_lagna": "Cancer",
|
|
"events": [
|
|
{"date": "1942-06-19", "type": "marriage", "desc": "与 Jim Dougherty 结婚", "target_houses": [7]},
|
|
{"date": "1953-00-00", "type": "career_peak", "desc": "《绅士爱美人》上映,成为巨星", "target_houses": [10, 5]},
|
|
{"date": "1962-08-05", "type": "death", "desc": "在洛杉矶去世", "target_houses": [8, 1]},
|
|
]
|
|
},
|
|
"dicaprio": {
|
|
"name": "Leonardo DiCaprio",
|
|
"birth": {"year": 1974, "month": 11, "day": 11, "hour": 2, "minute": 47,
|
|
"lat": 34.1, "lon": -118.3, "tz": -8.0},
|
|
"known_lagna": "Virgo",
|
|
"events": [
|
|
{"date": "1997-12-19", "type": "career_peak", "desc": "《泰坦尼克号》上映", "target_houses": [10, 5]},
|
|
{"date": "2016-02-28", "type": "honor", "desc": "凭《荒野猎人》获奥斯卡最佳男主角", "target_houses": [10, 1]},
|
|
]
|
|
},
|
|
"mjackson": {
|
|
"name": "Michael Jackson",
|
|
"birth": {"year": 1958, "month": 8, "day": 29, "hour": 19, "minute": 33,
|
|
"lat": 41.6, "lon": -87.3, "tz": -6.0},
|
|
"known_lagna": "Pisces",
|
|
"events": [
|
|
{"date": "1982-11-30", "type": "career_peak", "desc": "《Thriller》专辑发行", "target_houses": [10, 5]},
|
|
{"date": "2009-06-25", "type": "death", "desc": "在洛杉矶去世", "target_houses": [8, 1]},
|
|
]
|
|
},
|
|
"indira": {
|
|
"name": "Indira Gandhi",
|
|
"birth": {"year": 1917, "month": 11, "day": 19, "hour": 23, "minute": 11,
|
|
"lat": 25.5, "lon": 81.9, "tz": 5.5},
|
|
"known_lagna": "Leo",
|
|
"events": [
|
|
{"date": "1966-01-24", "type": "career_peak", "desc": "就任印度总理", "target_houses": [10, 1]},
|
|
{"date": "1984-10-31", "type": "death", "desc": "遇刺身亡", "target_houses": [8, 1]},
|
|
]
|
|
},
|
|
"presley": {
|
|
"name": "Elvis Presley",
|
|
"birth": {"year": 1935, "month": 1, "day": 8, "hour": 4, "minute": 35,
|
|
"lat": 34.3, "lon": -88.4, "tz": -6.0},
|
|
"known_lagna": "Scorpio",
|
|
"events": [
|
|
{"date": "1956-01-27", "type": "career_start", "desc": "首张专辑《Heartbreak Hotel》", "target_houses": [10, 5]},
|
|
{"date": "1977-08-16", "type": "death", "desc": "在 Graceland 去世", "target_houses": [8, 1]},
|
|
]
|
|
},
|
|
"curie": {
|
|
"name": "Marie Curie",
|
|
"birth": {"year": 1867, "month": 11, "day": 7, "hour": 12, "minute": 0,
|
|
"lat": 52.2, "lon": 21.0, "tz": 1.0},
|
|
"known_lagna": "Sagittarius",
|
|
"events": [
|
|
{"date": "1903-12-10", "type": "honor", "desc": "诺贝尔物理学奖(与 Pierre Curie 共享)", "target_houses": [10, 5]},
|
|
{"date": "1911-12-10", "type": "honor", "desc": "诺贝尔化学奖(唯一两次获奖女性)", "target_houses": [10, 5]},
|
|
{"date": "1934-07-04", "type": "death", "desc": "因再生障碍性贫血去世", "target_houses": [8, 1]},
|
|
]
|
|
},
|
|
"hanks": {
|
|
"name": "Tom Hanks",
|
|
"birth": {"year": 1956, "month": 7, "day": 9, "hour": 11, "minute": 17,
|
|
"lat": 37.9, "lon": -122.1, "tz": -8.0},
|
|
"known_lagna": "Virgo",
|
|
"events": [
|
|
{"date": "1994-03-21", "type": "honor", "desc": "凭《费城故事》获奥斯卡影帝", "target_houses": [10, 1]},
|
|
{"date": "1995-03-27", "type": "honor", "desc": "凭《阿甘正传》再获奥斯卡影帝", "target_houses": [10, 1]},
|
|
]
|
|
},
|
|
"jolie": {
|
|
"name": "Angelina Jolie",
|
|
"birth": {"year": 1975, "month": 6, "day": 4, "hour": 9, "minute": 9,
|
|
"lat": 34.1, "lon": -118.3, "tz": -8.0},
|
|
"known_lagna": "Cancer",
|
|
"events": [
|
|
{"date": "2000-03-26", "type": "honor", "desc": "凭《移魂女郎》获奥斯卡女配", "target_houses": [10, 5]},
|
|
{"date": "2013-05-14", "type": "health", "desc": "预防性双乳切除术公告", "target_houses": [8, 6]},
|
|
]
|
|
},
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# 测试运行器
|
|
# ============================================================
|
|
|
|
class SmokeTestRunner:
|
|
def __init__(self, engine_path=None):
|
|
self.engine = engine_path or os.path.join(SCRIPTS_DIR, "jyotish_engine.py")
|
|
self.results = {"total": 0, "passed": 0, "failed": 0, "errors": [], "details": []}
|
|
|
|
def _run_engine(self, birth, command, extra_args=None):
|
|
"""调用 jyotish_engine.py CLI"""
|
|
cmd = [
|
|
sys.executable, self.engine, command,
|
|
"--year", str(birth["year"]), "--month", str(birth["month"]),
|
|
"--day", str(birth["day"]), "--hour", str(birth["hour"]),
|
|
"--minute", str(birth["minute"]),
|
|
"--lat", str(birth["lat"]), "--lon", str(birth["lon"]),
|
|
"--tz", str(birth["tz"]), "--node-mode", "mean",
|
|
]
|
|
if extra_args:
|
|
cmd.extend(extra_args)
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
return True, json.loads(result.stdout)
|
|
return False, result.stderr if result.stderr else result.stdout
|
|
except subprocess.TimeoutExpired:
|
|
return False, "TIMEOUT (180s)"
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
def test_chart_basics(self, case_id, data):
|
|
"""验证基础排盘:上升、太阳、月亮星座"""
|
|
ok, chart = self._run_engine(data["birth"], "chart")
|
|
self.results["total"] += 1
|
|
|
|
if not ok:
|
|
self.results["failed"] += 1
|
|
err = f"{data['name']}: chart calculation FAILED — {chart}"
|
|
self.results["errors"].append(err)
|
|
self.results["details"].append({"case": data["name"], "test": "chart", "result": "FAIL", "error": str(chart)})
|
|
return
|
|
|
|
planets = chart.get("planets", {})
|
|
asc = chart.get("ascendant", {}).get("sign", "?").split()[0] if isinstance(chart.get("ascendant"), dict) else "?"
|
|
|
|
checks = []
|
|
if asc and data.get("known_lagna"):
|
|
match = asc.lower() == data["known_lagna"].lower()
|
|
checks.append(("Ascendant", match, f"{asc} vs {data['known_lagna']}"))
|
|
|
|
sun = planets.get("Sun", {}).get("sign", "?")
|
|
if sun and data.get("known_sun_sign"):
|
|
match = sun.lower() == data.get("known_sun_sign", "").lower()
|
|
checks.append(("Sun sign", match, f"{sun} vs {data['known_sun_sign']}"))
|
|
|
|
moon = planets.get("Moon", {}).get("sign", "?")
|
|
if moon and data.get("known_moon_sign"):
|
|
match = moon.lower() == data.get("known_moon_sign", "").lower()
|
|
checks.append(("Moon sign", match, f"{moon} vs {data['known_moon_sign']}"))
|
|
|
|
all_pass = all(c[1] for c in checks)
|
|
if all_pass:
|
|
self.results["passed"] += 1
|
|
else:
|
|
self.results["failed"] += 1
|
|
fails = [c[2] for c in checks if not c[1]]
|
|
self.results["errors"].append(f"{data['name']}: chart mismatch — {', '.join(fails)}")
|
|
|
|
self.results["details"].append({
|
|
"case": data["name"], "test": "chart",
|
|
"result": "PASS" if all_pass else "FAIL",
|
|
"checks": [{"type": c[0], "pass": c[1], "detail": c[2]} for c in checks]
|
|
})
|
|
|
|
def test_dasha_timeline(self, case_id, data):
|
|
"""验证 Dasha 时间线与已知事件的交叉"""
|
|
events = data.get("events", [])
|
|
if not events:
|
|
return
|
|
|
|
ok, dasha = self._run_engine(data["birth"], "dasha", ["--years", "120"])
|
|
self.results["total"] += len(events)
|
|
|
|
if not ok:
|
|
for ev in events:
|
|
self.results["failed"] += 1
|
|
self.results["errors"].append(f"{data['name']}: dasha calc FAILED for '{ev['desc']}'")
|
|
return
|
|
|
|
dasa_periods = dasha.get("timeline", []) if isinstance(dasha, dict) else []
|
|
if not dasa_periods:
|
|
dasa_periods = dasha if isinstance(dasha, list) else []
|
|
|
|
for ev in events:
|
|
ev_date = datetime.strptime(ev["date"].replace("-00", "-01"), "%Y-%m-%d")
|
|
# Find active Mahadasha at event date
|
|
active_md = None
|
|
for d in dasa_periods:
|
|
start = datetime.strptime(d.get("start", "1900-01-01")[:10], "%Y-%m-%d")
|
|
end = datetime.strptime(d.get("end", "2100-01-01")[:10], "%Y-%m-%d")
|
|
if start <= ev_date <= end:
|
|
active_md = d.get("lord", "?")
|
|
break
|
|
|
|
if ev["type"] == "career_start" or ev["type"] == "career_peak" or ev["type"] == "career_return":
|
|
favorable = active_md in ["Sun", "Moon", "Mars", "Jupiter", "Venus", "Mercury"]
|
|
elif ev["type"] == "career_loss":
|
|
favorable = active_md in ["Saturn", "Rahu", "Ketu"]
|
|
elif ev["type"] == "death":
|
|
favorable = active_md in ["Saturn", "Rahu", "Ketu", "Mars"]
|
|
elif ev["type"] == "marriage":
|
|
favorable = active_md in ["Jupiter", "Venus", "Mercury"]
|
|
elif ev["type"] == "honor":
|
|
favorable = active_md in ["Sun", "Jupiter", "Venus"]
|
|
else:
|
|
favorable = True # neutral test
|
|
|
|
test_name = f"{ev['desc']} [{ev['date']}] Dasha={active_md}"
|
|
if favorable:
|
|
self.results["passed"] += 1
|
|
else:
|
|
self.results["failed"] += 1
|
|
self.results["errors"].append(
|
|
f"{data['name']}: '{ev['desc']}' ({ev['date']}) — active Mahadasha={active_md} — unexpected for {ev['type']}"
|
|
)
|
|
|
|
self.results["details"].append({
|
|
"case": data["name"], "test": "dasha_event",
|
|
"event": ev["desc"], "date": ev["date"],
|
|
"active_dasha": active_md,
|
|
"result": "PASS" if favorable else "FAIL",
|
|
})
|
|
|
|
def run_all(self, case_filter=None):
|
|
"""运行所有回归测试"""
|
|
print("=" * 70)
|
|
print("Jyotish 事件应期回归测试")
|
|
print("=" * 70)
|
|
|
|
cases = CELEBRITY_EVENTS
|
|
if case_filter:
|
|
cases = {k: v for k, v in cases.items() if k in case_filter}
|
|
|
|
for case_id, data in cases.items():
|
|
print(f"\n{'─' * 60}")
|
|
print(f" Case: {data['name']}")
|
|
print(f"{'─' * 60}")
|
|
|
|
# Test 1: Chart basics
|
|
print(f" [1/2] Chart validation...")
|
|
self.test_chart_basics(case_id, data)
|
|
|
|
# Test 2: Dasha event timeline
|
|
if data.get("events"):
|
|
print(f" [2/2] Dasha event timeline ({len(data['events'])} events)...")
|
|
self.test_dasha_timeline(case_id, data)
|
|
|
|
return self.results
|
|
|
|
def report(self, fmt="text"):
|
|
"""生成测试报告"""
|
|
r = self.results
|
|
if fmt == "json":
|
|
return json.dumps(r, ensure_ascii=False, indent=2)
|
|
|
|
lines = []
|
|
lines.append("\n" + "=" * 70)
|
|
lines.append(" 回归测试报告")
|
|
lines.append("=" * 70)
|
|
lines.append(f" 总计: {r['total']} | 通过: {r['passed']} | 失败: {r['failed']}")
|
|
if r["total"] > 0:
|
|
rate = r["passed"] / r["total"] * 100
|
|
lines.append(f" 通过率: {rate:.1f}%")
|
|
|
|
if r["errors"]:
|
|
lines.append(f"\n 失败项 ({len(r['errors'])}):")
|
|
for err in r["errors"]:
|
|
lines.append(f" ✗ {err}")
|
|
|
|
lines.append("=" * 70)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Jyotish 事件应期回归测试")
|
|
parser.add_argument("--case", help="指定案例 ID,逗号分隔")
|
|
parser.add_argument("--report", default="text", choices=["text", "json"])
|
|
parser.add_argument("--skip-dasha", action="store_true", help="跳过 Dasha 推运测试")
|
|
args = parser.parse_args()
|
|
|
|
case_filter = None
|
|
if args.case:
|
|
case_filter = [c.strip() for c in args.case.split(",")]
|
|
|
|
runner = SmokeTestRunner()
|
|
runner.run_all(case_filter)
|
|
print(runner.report(args.report))
|
|
|
|
# 退出码:任何失败 → 非零
|
|
sys.exit(1 if runner.results["failed"] > 0 else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|