refactor: 平台改为代码内置 provider 架构,一次性迁移现有数据 - 新增 app/providers:BalanceProvider 基类 + DeepSeek 适配器 + 注册表 - platforms 表去除 method/url/headers/body/balance_path,新增 provider_id - 现有数据库已一次性迁移(备份 data/monitor.db.bak-v1),不保留迁移工具 - 平台 UI 改为选择内置提供方;fetcher/monitor 走 provider 构建请求与解析 - 表达式引擎(运算符/函数)随架构保留,供 provider 内部使用 - 测试更新至 provider 模式,共 92 个
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""余额表达式引擎测试:运算符、函数、兼容性与安全。"""
|
||||
|
||||
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'")
|
||||
Reference in New Issue
Block a user