"""余额表达式引擎测试:运算符、函数、兼容性与安全。""" import pytest from app.expr import ExprError, evaluate_balance DATA = { "data": { "balance": "1.50", "total": 100, "used": 40, "fee": 2.5, }, "list": [ {"x": 1, "y": 10}, {"x": 2, "y": 20}, ], "balances": [1, 2, 3], "nested": {"a": {"b": 6}}, } class TestPlainPath: def test_dot_path(self): assert evaluate_balance({"data": {"balance": 12.5}}, "data.balance") == 12.5 def test_array_index(self): assert evaluate_balance(DATA, "list[0].x") == 1 def test_dollar_prefix(self): assert evaluate_balance(DATA, "$.data.total") == 100 def test_string_number(self): assert evaluate_balance(DATA, "data.balance") == 1.5 def test_missing_path(self): with pytest.raises(ExprError, match="路径不存在"): evaluate_balance(DATA, "data.nope") def test_index_out_of_range(self): with pytest.raises(ExprError, match="数组索引越界"): evaluate_balance(DATA, "list[5].x") class TestOperators: def test_division(self): assert evaluate_balance(DATA, "data.total / 100") == 1.0 def test_addition(self): assert evaluate_balance(DATA, "data.total + data.used") == 140 def test_priority(self): assert evaluate_balance(DATA, "data.total + data.used * 2") == 180 assert evaluate_balance(DATA, "(data.total + data.used) * 2") == 280 def test_floor_div_and_mod(self): assert evaluate_balance(DATA, "data.total // 30") == 3 assert evaluate_balance(DATA, "data.total % 30") == 10 def test_power(self): assert evaluate_balance(DATA, "2 ** 3 * 5") == 40 def test_unary_minus(self): assert evaluate_balance(DATA, "-data.total") == -100 assert evaluate_balance(DATA, "data.total - -data.used") == 140 def test_float_result(self): assert evaluate_balance(DATA, "data.total / 8") == 12.5 class TestFunctions: def test_float(self): assert evaluate_balance(DATA, "float(data.balance)") == 1.5 def test_int(self): assert evaluate_balance(DATA, "int(data.total / 3)") == 33 def test_abs(self): assert evaluate_balance(DATA, "abs(data.used - data.total)") == 60 def test_round_one_arg(self): assert evaluate_balance(DATA, "round(data.fee * 3)") == 8 def test_round_two_args(self): assert evaluate_balance(DATA, "round(data.fee, 1)") == 2.5 assert evaluate_balance(DATA, "round(3.14159, 2)") == 3.14 def test_sum_multi_args(self): assert evaluate_balance(DATA, "sum(data.total, data.used, data.fee)") == 142.5 def test_sum_array(self): assert evaluate_balance(DATA, "sum(balances)") == 6 def test_min_max(self): assert evaluate_balance(DATA, "min(data.total, data.used)") == 40 assert evaluate_balance(DATA, "max(data.total, data.used)") == 100 assert evaluate_balance(DATA, "min(balances)") == 1 assert evaluate_balance(DATA, "max(balances)") == 3 def test_len(self): assert evaluate_balance(DATA, "len(balances)") == 3 def test_nested_call(self): assert evaluate_balance(DATA, "round(abs(data.used - data.total) / 3, 1)") == 20.0 class TestErrors: def test_unknown_function(self): with pytest.raises(ExprError, match="不支持的函数"): evaluate_balance(DATA, "eval(data.balance)") def test_syntax_error(self): with pytest.raises(ExprError): evaluate_balance(DATA, "data.total +") with pytest.raises(ExprError): evaluate_balance(DATA, "(data.total") def test_bad_arity(self): with pytest.raises(ExprError, match="float"): evaluate_balance(DATA, "float()") with pytest.raises(ExprError, match="round"): evaluate_balance(DATA, "round(data.total, 2, 3)") def test_division_by_zero(self): with pytest.raises((ZeroDivisionError, ExprError)): evaluate_balance(DATA, "data.total / 0") def test_non_numeric_result(self): with pytest.raises(ExprError, match="不是数字"): evaluate_balance(DATA, "sum(list)") # 数组元素是对象,无法转数字 def test_string_literal_rejected(self): with pytest.raises(ExprError): evaluate_balance(DATA, "data.total + 'abc'")