#include "catch.hpp" #include #include "Formula.h" TEST_CASE("Formula compiles a constant expression", "[formula]") { const Formula f = Formula::compile("42"); REQUIRE(f.isValid()); REQUIRE(f.evaluate(0.0) == Approx(42.0)); REQUIRE(f.evaluate(100.0) == Approx(42.0)); } TEST_CASE("Formula evaluates a linear expression in x", "[formula]") { // Matches world.toml [waves].threat_rate_formula default. const Formula f = Formula::compile("1*x - 30"); REQUIRE(f.evaluate(0.0) == Approx(-30.0)); REQUIRE(f.evaluate(30.0) == Approx(0.0)); REQUIRE(f.evaluate(60.0) == Approx(30.0)); } TEST_CASE("Formula evaluates a polynomial in x", "[formula]") { const Formula f = Formula::compile("2 + 3*x*x"); REQUIRE(f.evaluate(0.0) == Approx(2.0)); REQUIRE(f.evaluate(2.0) == Approx(14.0)); REQUIRE(f.evaluate(4.0) == Approx(50.0)); } TEST_CASE("Formula retains its source string", "[formula]") { const std::string source = "10 + x / 5"; const Formula f = Formula::compile(source); REQUIRE(f.source() == source); } TEST_CASE("Formula throws on malformed source", "[formula]") { REQUIRE_THROWS_AS(Formula::compile("1 +"), std::runtime_error); REQUIRE_THROWS_AS(Formula::compile(""), std::runtime_error); REQUIRE_THROWS_AS(Formula::compile("unknown_var"), std::runtime_error); } TEST_CASE("Formula survives move construction", "[formula]") { // tinyexpr bakes the bound variable's address into the compiled tree. // Formula keeps x on the heap so that address stays valid across moves. Formula original = Formula::compile("x * 2"); Formula moved(std::move(original)); REQUIRE(moved.evaluate(21.0) == Approx(42.0)); } TEST_CASE("Formula survives move assignment", "[formula]") { Formula a = Formula::compile("x + 1"); Formula b = Formula::compile("x + 100"); b = std::move(a); REQUIRE(b.evaluate(5.0) == Approx(6.0)); } TEST_CASE("Default-constructed Formula is invalid and throws on evaluate", "[formula]") { const Formula f; REQUIRE_FALSE(f.isValid()); REQUIRE_THROWS_AS(f.evaluate(0.0), std::runtime_error); }