test_p4_schema.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. """P4-M1 regression: single-source plan schema (src.plan_schema) + web wiring.
  2. Run: python scripts/test_p4_schema.py (exit 0 = PASS)
  3. Covers:
  4. 1. src.plan_schema.parse_plan / validate_plan_dict:
  5. - happy path (values + start/stop/step vars, topology, strategy)
  6. - alias tolerance (min_value/max_value -> start/stop)
  7. - empty / zero-value inputs
  8. - malformed input (non-dict, bad structure) -> structured error
  9. - require_model_path stage (draft vs execution)
  10. - point generation consistency
  11. 2. Web wiring: POST /api/plans rejects invalid plan_data (400), accepts
  12. valid draft (201); PUT rejects invalid plan_data (400). Uses FastAPI
  13. TestClient on an isolated temp SQLite DB. No real Motor-CAD.
  14. """
  15. import os
  16. import sys
  17. import tempfile
  18. _ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  19. _BACKEND = os.path.join(_ROOT, "web", "backend")
  20. _DB = os.path.join(tempfile.mkdtemp(prefix="p4schema_"), "web.db")
  21. os.environ["AFM_DB_PATH"] = _DB
  22. os.environ["KIMI_API_KEY"] = "" # hermetic: no AI calls
  23. sys.path.insert(0, _BACKEND)
  24. sys.path.insert(0, _ROOT)
  25. from src.plan_schema import ( # noqa: E402
  26. parse_plan, validate_plan_dict, SimulationPlan,
  27. )
  28. VALID_VARS = [
  29. {"name": "airgap_mm", "display_name": "Airgap", "unit": "mm",
  30. "start": 0.8, "stop": 2.0, "step": 0.1},
  31. {"name": "Magnet_Arc_[ED]", "values": [120.0, 130.0, 140.0]},
  32. ]
  33. VALID_FPS = [{"name": "Outer_Rotor_Diameter", "value": 200.0,
  34. "category": "Geometry"}]
  35. def make_plan(**over):
  36. d = {
  37. "plan_version": "2.0",
  38. "topology": "SSSR",
  39. "model_path": "models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot",
  40. "fixed_params": VALID_FPS,
  41. "variables": VALID_VARS,
  42. "cases": [{"id": "default", "name": "Default"}],
  43. "search_strategy": {"method": "adaptive", "batch_size": 4,
  44. "max_solver_calls": 80},
  45. "acceptance_criteria": {"hard_constraints": ["efficiency_pct >= 92"]},
  46. }
  47. d.update(over)
  48. return d
  49. # ---------------- 1) happy path + point generation ----------------
  50. p = parse_plan(make_plan())
  51. assert p.topology == "SSSR"
  52. assert len(p.variables) == 2
  53. assert p.variables[0].get_values()[0] == 0.8
  54. assert len(p.variables[0].get_values()) == 13 # 0.8..2.0 step 0.1
  55. ok, errs = validate_plan_dict(make_plan())
  56. assert ok, errs
  57. pts, names = p.generate_points()
  58. assert len(pts) == 13 * 3 and set(names) == {"airgap_mm", "Magnet_Arc_[ED]"}
  59. print("[1] happy path + point generation OK (39 points)")
  60. # ---------------- 2) alias tolerance ----------------
  61. p2 = parse_plan(make_plan(variables=[
  62. {"name": "airgap_mm", "min_value": 0.8, "max_value": 2.0, "step": 0.1}]))
  63. assert p2.variables[0].get_values()[0] == 0.8, p2.variables[0].get_values()
  64. print("[2] min_value/max_value alias -> start/stop OK")
  65. # ---------------- 3) empty / zero-value inputs ----------------
  66. ok3, errs3 = validate_plan_dict(make_plan(variables=[]))
  67. assert not ok3 and any("scan variable" in e for e in errs3), errs3
  68. ok3b, errs3b = validate_plan_dict({})
  69. assert not ok3b, errs3b
  70. ok3c, errs3c = validate_plan_dict(make_plan(variables=[
  71. {"name": "x", "start": 5.0, "stop": 1.0, "step": 0.5}]))
  72. assert not ok3c and any("no values" in e for e in errs3c), errs3c
  73. print("[3] empty / zero-value inputs rejected OK")
  74. # ---------------- 4) malformed input ----------------
  75. ok4, errs4 = validate_plan_dict("not-a-dict")
  76. assert not ok4 and any("malformed" in e for e in errs4), errs4
  77. ok4b, errs4b = validate_plan_dict(make_plan(variables=[{"no_name": 1}]))
  78. assert not ok4b, errs4b # KeyError on name -> malformed
  79. print("[4] malformed input -> structured error OK")
  80. # ---------------- 5) require_model_path stage ----------------
  81. ok5, _ = validate_plan_dict(
  82. make_plan(model_path=""), require_model_path=False)
  83. assert ok5, "draft stage must not require a model"
  84. ok5b, errs5b = validate_plan_dict(make_plan(model_path=""))
  85. assert not ok5b and any("model_path" in e for e in errs5b)
  86. print("[5] require_model_path stage OK")
  87. # ---------------- 6) unsupported topology / strategy ----------------
  88. ok6, errs6 = validate_plan_dict(make_plan(topology="NOPE"))
  89. assert not ok6 and any("topology" in e for e in errs6), errs6
  90. ok6b, errs6b = validate_plan_dict(
  91. make_plan(search_strategy={"method": "no_such_method"}))
  92. assert not ok6b and any("strategy" in e for e in errs6b), errs6b
  93. print("[6] unsupported topology/strategy rejected OK")
  94. # ---------------- 7) web wiring (TestClient) ----------------
  95. from fastapi.testclient import TestClient # noqa: E402
  96. from app.database import init_db as web_init_db # noqa: E402
  97. web_init_db()
  98. from app.main import app # noqa: E402
  99. client = TestClient(app)
  100. # create project
  101. r = client.post("/api/projects", json={"name": "t", "topology": "SSSR"})
  102. assert r.status_code == 201, r.text
  103. pid = r.json()["id"]
  104. # invalid plan_data -> 400
  105. bad = make_plan(variables=[])
  106. r = client.post("/api/plans", json={"project_id": pid, "name": "bad",
  107. "plan_data": bad})
  108. assert r.status_code == 400, r.status_code
  109. assert "Invalid plan_data" in r.json()["detail"], r.text
  110. # valid draft (no model path) -> 201
  111. good = make_plan(model_path="")
  112. r = client.post("/api/plans", json={"project_id": pid, "name": "good",
  113. "plan_data": good})
  114. assert r.status_code == 201, r.text
  115. plan_id = r.json()["id"]
  116. # invalid update -> 400
  117. r = client.put("/api/plans/%d" % plan_id,
  118. json={"plan_data": make_plan(variables=[])})
  119. assert r.status_code == 400, r.status_code
  120. # valid update -> 200
  121. r = client.put("/api/plans/%d" % plan_id,
  122. json={"plan_data": make_plan(model_path="")})
  123. assert r.status_code == 200, r.text
  124. # download round-trips through the same schema
  125. r = client.get("/api/plans/%d/download" % plan_id)
  126. assert r.status_code == 200
  127. assert r.json()["plan_data"]["variables"]
  128. print("[7] web create/update validation wired OK (400 on invalid)")
  129. print("\nALL P4-M1 SCHEMA TESTS PASSED")