135 lines
4.4 KiB
Python
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'")
|