Files

135 lines
4.4 KiB
Python

"""余额表达式引擎测试:运算符、函数、兼容性与安全。"""
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'")