aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--generat3d/formula.py33
-rw-r--r--tests/test_formula.py36
2 files changed, 57 insertions, 12 deletions
diff --git a/generat3d/formula.py b/generat3d/formula.py
index 4a6a0d9..67d25e0 100644
--- a/generat3d/formula.py
+++ b/generat3d/formula.py
@@ -68,14 +68,18 @@ class Graph:
return None
path.append(key)
total, factor = prop.value, 1.0
- for stage, ops, source in self.formulas.get(key, ()):
- r = self._run(key, ops, done, path, source)
- if r is None:
- continue
- if stage == "sum":
- total += r
- else:
- factor *= r
+ try:
+ for stage, ops, source in self.formulas.get(key, ()):
+ r = self._run(key, ops, done, path, source)
+ if r is None:
+ continue
+ if stage == "sum":
+ total += r
+ else:
+ factor *= r
+ except RecursionError:
+ # ponytail: recursive evaluation; make it iterative if real content hits this
+ raise Error("formula dependency chain too deep") from None
path.pop()
v = total * factor
if prop.clamped:
@@ -105,11 +109,16 @@ class Graph:
stack.append(_spline_op(name, stack))
else:
raise Error(f"{source}: unknown formula operation {name!r} for {key}")
- except (IndexError, KeyError, TypeError, ZeroDivisionError, ValueError):
+ if len(stack) != 1:
+ raise Error(f"{source}: malformed formula for {key}")
+ r = stack[0]
+ if not (isinstance(r, (int, float)) and not isinstance(r, bool)
+ and math.isfinite(r)):
+ raise Error(f"{source}: malformed formula for {key}")
+ return r
+ except (IndexError, KeyError, TypeError, ZeroDivisionError, ValueError,
+ OverflowError):
raise Error(f"{source}: malformed formula for {key}") from None
- if len(stack) != 1:
- raise Error(f"{source}: malformed formula for {key}")
- return stack[0]
def _spline_op(name, stack):
diff --git a/tests/test_formula.py b/tests/test_formula.py
index 8074e5a..72f41ea 100644
--- a/tests/test_formula.py
+++ b/tests/test_formula.py
@@ -94,3 +94,39 @@ class GraphTest(unittest.TestCase):
self.g.add_formula("b", [val(1), val(2)], source="f")
with self.assertRaisesRegex(Error, "f: malformed formula for b"):
self.g.evaluate()
+
+ def test_non_numeric_result_is_fatal(self):
+ self.g.add_formula("b", [val("x")], source="f")
+ with self.assertRaisesRegex(Error, "f: malformed formula for b"):
+ self.g.evaluate()
+
+ def test_overflow_is_fatal(self):
+ self.g.add_formula("b", [val(10**400), val(0.5), {"op": "mult"}], source="f")
+ with self.assertRaisesRegex(Error, "f: malformed formula for b"):
+ self.g.evaluate()
+
+ def test_nan_result_is_fatal(self):
+ self.g.add_formula("b", [val(float("nan"))], source="f")
+ with self.assertRaisesRegex(Error, "f: malformed formula for b"):
+ self.g.evaluate()
+
+ def test_deep_chain_is_fatal(self):
+ g = Graph()
+ n = 2000
+ for i in range(n):
+ g.add_prop(f"p{i}")
+ for i in range(n - 1):
+ g.add_formula(f"p{i}", [push(f"p{i + 1}")])
+ with self.assertRaisesRegex(Error, "too deep"):
+ g.evaluate()
+
+ def test_diamond_dependency(self):
+ g = Graph()
+ g.add_prop("a", 1.0)
+ g.add_prop("b")
+ g.add_prop("c")
+ g.add_prop("d")
+ g.add_formula("b", [push("a")])
+ g.add_formula("c", [push("a"), push("a"), {"op": "add"}])
+ g.add_formula("d", [push("b"), push("c"), {"op": "add"}])
+ self.assertEqual(g.evaluate()["d"], 3)