Files
physicalCombinatorics/tests/test_formula.py
Andrew Simonson 3429bce8d0 let domains author their own pass-2 estimator as formulas, not Python
Pass 2 previously only knew one estimator: a hardcoded physics model that
matches dimensions literally named platform/actuator/energy_storage. Any
domain outside that shape (e.g. archery) got all-zero estimates and failed
every combo. Domains can now declare free variables and per-metric formulas
as data instead; a safe AST-based evaluator (engine/formula.py, no eval())
resolves declared entity properties via dep(key, constraint_type) and
generalizes the existing hand-nested mass-budget search into an N-variable
recursive optimizer. Fully additive -- the legacy platform/actuator/
energy_storage path is untouched and still runs unchanged for every domain
that declares no formulas.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 21:28:15 -05:00

109 lines
3.9 KiB
Python

"""Tests for the safe formula evaluator."""
import math
import pytest
from physcom.engine.formula import FormulaError, compile_formula, evaluate_formula
class TestArithmetic:
def test_constant(self):
assert evaluate_formula(compile_formula("42"), {}) == 42.0
def test_basic_ops(self):
assert evaluate_formula(compile_formula("2 + 3 * 4"), {}) == 14.0
assert evaluate_formula(compile_formula("(2 + 3) * 4"), {}) == 20.0
assert evaluate_formula(compile_formula("10 / 4"), {}) == 2.5
assert evaluate_formula(compile_formula("2 ** 3"), {}) == 8.0
def test_unary_minus(self):
assert evaluate_formula(compile_formula("-5 + 2"), {}) == -3.0
def test_variable_lookup(self):
result = evaluate_formula(compile_formula("mass * 2"), {"mass": 3.0})
assert result == 6.0
def test_unknown_variable_raises(self):
with pytest.raises(FormulaError):
evaluate_formula(compile_formula("unknown_var"), {})
def test_division_by_zero_raises_formula_error(self):
with pytest.raises(FormulaError):
evaluate_formula(compile_formula("1 / 0"), {})
class TestFunctions:
def test_default_math_functions(self):
assert evaluate_formula(compile_formula("sqrt(16)"), {}) == 4.0
assert evaluate_formula(compile_formula("max(1, 2, 3)"), {}) == 3.0
assert evaluate_formula(compile_formula("min(1, 2, 3)"), {}) == 1.0
assert evaluate_formula(compile_formula("abs(-5)"), {}) == 5.0
assert evaluate_formula(compile_formula("exp(0)"), {}) == 1.0
assert math.isclose(evaluate_formula(compile_formula("log(exp(1))"), {}), 1.0)
def test_custom_injected_function(self):
formula = compile_formula('dep("power_density", "provides")')
result = evaluate_formula(
formula, {}, functions={"dep": lambda key, constraint_type: 99.0}
)
assert result == 99.0
def test_unknown_function_raises(self):
with pytest.raises(FormulaError):
evaluate_formula(compile_formula("unknown_fn(1)"), {})
def test_string_constant_passthrough_to_function(self):
formula = compile_formula('dep("mass")')
result = evaluate_formula(formula, {}, functions={"dep": lambda key: len(key)})
assert result == 4.0
class TestSecurity:
@pytest.mark.parametrize("source", [
"__import__('os').system('echo hi')",
"().__class__",
"[1, 2, 3]",
"{1: 2}",
"{1, 2}",
"(x for x in [1])",
"lambda: 1",
"1 if True else 0",
"1 == 1",
"x.__class__",
"x[0]",
"(lambda: 1)()",
"1; 2",
])
def test_disallowed_constructs_rejected(self, source):
with pytest.raises(FormulaError):
compile_formula(source)
def test_unregistered_function_name_never_executes(self):
"""exec/eval/__import__ etc. parse as ordinary Call nodes -- the
actual guarantee is that no function name is callable unless it's
explicitly in DEFAULT_FUNCTIONS or caller-supplied, checked at
evaluate time, not that the bare name is rejected at compile time."""
with pytest.raises(FormulaError):
evaluate_formula(compile_formula("exec('1')"), {})
def test_dunder_name_rejected(self):
with pytest.raises(FormulaError):
compile_formula("__builtins__")
def test_indirect_call_rejected(self):
with pytest.raises(FormulaError):
compile_formula("(a + b)(1)")
def test_invalid_syntax_raises_formula_error(self):
with pytest.raises(FormulaError):
compile_formula("2 +")
def test_boolean_constant_rejected(self):
with pytest.raises(FormulaError):
compile_formula("True")
def test_large_exponent_overflows_cleanly_not_hangs(self):
with pytest.raises(FormulaError):
evaluate_formula(compile_formula("9 ** 9 ** 9 ** 9"), {})