test_p3_unit_edge.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """P3 unit edge/exception/empty-value tests (engineering rules).
  2. Covers boundary, exception-input and empty/zero-value paths for the P3
  3. platform batch modules, complementing the integration-level tests.
  4. Run: python scripts/test_p3_unit_edge.py (exit 0 = PASS)
  5. All behaviour below was confirmed by probing the actual implementation;
  6. no behaviour is assumed.
  7. """
  8. import os
  9. import sys
  10. import tempfile
  11. _BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  12. sys.path.insert(0, _BASE)
  13. _TMP = os.path.join(tempfile.mkdtemp(), "edge.db")
  14. os.environ["AFM_DB_PATH"] = _TMP
  15. os.environ["KIMI_API_KEY"] = ""
  16. # ---------------------------------------------------------------- strategies
  17. from src.afmcore.strategies import ( # noqa: E402
  18. get_strategy, normalize_method, register_strategy, is_registered,
  19. )
  20. from src.afmcore.strategies.full_factorial import FullFactorialStrategy # noqa: E402
  21. from src.afmcore.strategies.lhs import LHSStrategy # noqa: E402
  22. # [S1] registry exception paths
  23. try:
  24. register_strategy("", object)
  25. raise SystemExit("empty kind should raise ValueError")
  26. except ValueError:
  27. pass
  28. try:
  29. register_strategy("x", 123)
  30. raise SystemExit("non-subclass should raise TypeError")
  31. except TypeError:
  32. pass
  33. try:
  34. get_strategy("no-such-strategy")
  35. raise SystemExit("unknown kind should raise KeyError")
  36. except KeyError:
  37. pass
  38. assert is_registered("full_factorial") and is_registered("lhs") and is_registered("adaptive")
  39. print("[S1] registry exception paths OK")
  40. # [S2] normalize_method
  41. assert normalize_method(None) == "full_factorial"
  42. assert normalize_method("") == "full_factorial"
  43. assert normalize_method("active_learning") == "adaptive"
  44. assert normalize_method("constrained") == "adaptive"
  45. assert normalize_method(" ADAPTIVE ") == "adaptive"
  46. assert normalize_method("ABC") == "abc" # unknown kept lowercase, reported by caller
  47. print("[S2] normalize_method OK")
  48. # [S3] FullFactorial empty / batching / converged
  49. ff_empty = FullFactorialStrategy(points=[])
  50. assert ff_empty.select_next() == []
  51. assert ff_empty.is_converged() is True
  52. assert ff_empty.next_batch_ready() is False
  53. ff = FullFactorialStrategy(points=[{"x": 1}, {"x": 2}, {"x": 3}, {"x": 4}, {"x": 5}], batch_size=2)
  54. b1 = ff.select_next()
  55. b2 = ff.select_next()
  56. b3 = ff.select_next()
  57. assert [len(b1), len(b2), len(b3)] == [2, 2, 1], (b1, b2, b3)
  58. assert ff.is_converged() is True
  59. assert ff.select_next() == [] # exhausted
  60. print("[S3] FullFactorial empty/batch/converged OK")
  61. # [S4] LHS empty vs normal
  62. lhs_empty = LHSStrategy(parameters=[], n_samples=4)
  63. assert lhs_empty.select_next() == []
  64. lhs = LHSStrategy(
  65. parameters=[
  66. {"name": "a", "min_value": 1, "max_value": 2},
  67. {"name": "b", "min_value": 10, "max_value": 20},
  68. ],
  69. n_samples=3,
  70. )
  71. pts = lhs.select_next()
  72. assert len(pts) == 3, pts
  73. for p in pts:
  74. assert p["point_id"] is not None
  75. assert set(p["params"].keys()) == {"a", "b"}
  76. print("[S4] LHS empty/normal OK")
  77. # [S5] batch_size boundary
  78. ff0 = FullFactorialStrategy(points=[{"x": 1}, {"x": 2}], batch_size=0)
  79. assert ff0.batch_size == 1 # max(1, 0)
  80. print("[S5] batch_size boundary OK")
  81. # --------------------------------------------------------------- orchestrator
  82. sys.path.insert(0, os.path.join(_BASE, "web", "backend"))
  83. from app.database import init_db # noqa: E402
  84. init_db()
  85. from app.services.strategy_orchestrator import AdaptiveOrchestrator # noqa: E402
  86. from app.services.task_manager import get_task_manager # noqa: E402
  87. from app.services.adaptive_loop import AdaptiveLoop # noqa: E402
  88. tm = get_task_manager()
  89. orch = AdaptiveOrchestrator(state_dir=os.path.join(tempfile.mkdtemp(), "loops"))
  90. # [O1] start_loop empty parameters -> ValueError
  91. try:
  92. orch.start_loop("e1", parameters=[])
  93. raise SystemExit("start with empty parameters should raise ValueError")
  94. except ValueError:
  95. pass
  96. # [O2] duplicate loop -> ValueError
  97. orch.start_loop("dup", parameters=[{"name": "airgap_mm", "min_value": 1, "max_value": 2}])
  98. try:
  99. orch.start_loop("dup", parameters=[{"name": "airgap_mm", "min_value": 1, "max_value": 2}])
  100. raise SystemExit("duplicate loop should raise ValueError")
  101. except ValueError:
  102. pass
  103. # [O3] advance/status on missing loop -> ValueError
  104. for fn in (orch.advance_loop, orch.get_loop_status):
  105. try:
  106. fn("missing-loop")
  107. raise SystemExit("missing loop should raise ValueError")
  108. except ValueError:
  109. pass
  110. # [O4] advance while batch still running -> no error, stays running
  111. res = orch.start_loop(
  112. "l1",
  113. parameters=[{"name": "airgap_mm", "min_value": 1, "max_value": 2}],
  114. total_budget=8, batch_size=4, initial_samples=4,
  115. )
  116. view = orch.advance_loop("l1")
  117. assert view["phase"] == "running", view
  118. assert view.get("message") == "batch still running", view
  119. print("[O1-O4] orchestrator boundary/exception paths OK")
  120. # [O5] AdaptiveLoop.submit_batch_to_executor before init -> RuntimeError
  121. loop = AdaptiveLoop(user_requirement="probe")
  122. try:
  123. loop.submit_batch_to_executor()
  124. raise SystemExit("submit before init should raise RuntimeError")
  125. except RuntimeError:
  126. pass
  127. print("[O5] AdaptiveLoop submit-before-init raises RuntimeError OK")
  128. # ----------------------------------------------------------------- task_manager
  129. # [T1] get_task on missing id -> None
  130. assert tm.get_task("no-such-task") is None
  131. # [T2] create_task with empty parameters -> still creates a task (no crash)
  132. t = tm.create_task(plan_id=None, plan_data={}, parameters=[])
  133. assert t.get("task_id"), t
  134. print("[T1-T2] task_manager empty/missing paths OK")
  135. print("\nALL P3 UNIT EDGE/EXCEPTION/EMPTY TESTS PASSED")