Files
zszq-trs/UnitTestProject/Helpers/ConditionEvaluatorTests.cs
T

367 lines
18 KiB
C#

using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using YLErp.Helpers;
namespace YLErp.Helpers.Tests
{
[TestClass]
public class ConditionEvaluatorTests
{
private static ConditionContext CreateContext(double notional = 0, string tradeType = "", int? userId = null, int? initGroupId = null, string processCategory = "")
{
return new ConditionContext
{
Trade = new DBModels.trade
{
StockEqvNotional = notional,
TradeType = tradeType
},
UserId = userId,
InitGroupId = initGroupId,
ProcessCategory = processCategory
};
}
[TestMethod]
public void Evaluate_EmptyOrNullCondition_ReturnsFalse()
{
Assert.IsFalse(ConditionEvaluator.Evaluate(null, CreateContext()));
Assert.IsFalse(ConditionEvaluator.Evaluate("", CreateContext()));
Assert.IsFalse(ConditionEvaluator.Evaluate(" ", CreateContext()));
}
[TestMethod]
public void Evaluate_InvalidJson_ReturnsFalse()
{
Assert.IsFalse(ConditionEvaluator.Evaluate("not a json", CreateContext()));
Assert.IsFalse(ConditionEvaluator.Evaluate("{\"tokens\": [", CreateContext()));
}
[TestMethod]
public void Evaluate_TokenSingleCondition_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken
{
type = "condition",
condition = new ConditionItem { field = "notional", op = ">", value = 100 }
}
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
}
[TestMethod]
public void Evaluate_TokenAnd_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 100 } },
new ConditionToken { type = "operator", connector = "and" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "<=", value = 500 } }
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50)));
}
[TestMethod]
public void Evaluate_TokenOr_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "<", value = 100 } },
new ConditionToken { type = "operator", connector = "or" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 500 } }
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50)));
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 600)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 300)));
}
[TestMethod]
public void Evaluate_TokenWithParentheses_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "lparen" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 100 } },
new ConditionToken { type = "operator", connector = "or" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "<", value = 50 } },
new ConditionToken { type = "rparen" },
new ConditionToken { type = "operator", connector = "and" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "tradeType", op = "==", value = "香草" } }
}
});
// (200>100 or 200<50) and tradeType=="香草" => true
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200, tradeType: "香草")));
// (30>100 or 30<50) and tradeType=="雪球" => false
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 30, tradeType: "雪球")));
// (80>100 or 80<50) and tradeType=="香草" => false
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 80, tradeType: "香草")));
}
[TestMethod]
public void Evaluate_MixedAndOr_PriorityAndOverOr()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 100 } },
new ConditionToken { type = "operator", connector = "or" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "<", value = 50 } },
new ConditionToken { type = "operator", connector = "and" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "tradeType", op = "==", value = "香草" } }
}
});
// A or (B and C) — and 优先级高于 or
// 200>100 -> true,无需计算右侧
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200, tradeType: "")));
// 30>100=false, 30<50=true, 香草==香草=true -> false or (true and true) = true
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 30, tradeType: "香草")));
// 80>100=false, 80<50=false, 雪球==香草=false -> false or (false and false) = false
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 80, tradeType: "雪球")));
}
[TestMethod]
public void Evaluate_InitGroup_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "initGroup", op = "==", value = 5 } }
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, new ConditionContext { InitGroupId = 5 }));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, new ConditionContext { InitGroupId = 3 }));
}
[TestMethod]
public void Evaluate_ProcessCategory_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "processCategory", op = "==", value = "CloseProcess" } }
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(processCategory: "CloseProcess")));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(processCategory: "TradeProcess")));
}
[TestMethod]
public void Evaluate_TradeTypeStringComparison_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "tradeType", op = "!=", value = "雪球" } }
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(tradeType: "香草")));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(tradeType: "雪球")));
}
[TestMethod]
public void Evaluate_IncompleteParentheses_DoesNotThrow()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "lparen" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">", value = 100 } }
}
});
// Parser tolerates missing closing parenthesis and returns the value inside
var result = ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200));
Assert.IsTrue(result);
}
[TestMethod]
public void Evaluate_NotEquals_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "!=", value = 100 } }
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
}
[TestMethod]
public void Evaluate_GreaterOrEqual_Works()
{
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = ">=", value = 100 } }
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 99)));
}
// ===== 字母 op 标识符(前端改用 gt/lt/gte/lte/eq/neq 规避 > < 编码问题)=====
[TestMethod]
public void Evaluate_AlphaOp_GreaterThan_Works()
{
var condition = BuildTokens(new ConditionItem { field = "notional", op = "gt", value = 100 });
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50)));
}
[TestMethod]
public void Evaluate_AlphaOp_LessThanOrEqual_Works()
{
var condition = BuildTokens(new ConditionItem { field = "notional", op = "lte", value = 100 });
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 101)));
}
[TestMethod]
public void Evaluate_AlphaOp_EqualAndNotEqual_Works()
{
var eqCond = BuildTokens(new ConditionItem { field = "tradeType", op = "eq", value = "香草" });
Assert.IsTrue(ConditionEvaluator.Evaluate(eqCond, CreateContext(tradeType: "香草")));
Assert.IsFalse(ConditionEvaluator.Evaluate(eqCond, CreateContext(tradeType: "雪球")));
var neqCond = BuildTokens(new ConditionItem { field = "tradeType", op = "neq", value = "雪球" });
Assert.IsTrue(ConditionEvaluator.Evaluate(neqCond, CreateContext(tradeType: "香草")));
Assert.IsFalse(ConditionEvaluator.Evaluate(neqCond, CreateContext(tradeType: "雪球")));
}
[TestMethod]
public void Evaluate_AlphaOp_CaseInsensitive_Works()
{
// 大写字母 op 也应识别(归一化为小写)
var condition = BuildTokens(new ConditionItem { field = "notional", op = "GTE", value = 100 });
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 100)));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 99)));
}
[TestMethod]
public void Evaluate_MixedAlphaAndSymbolOp_Works()
{
// 字母 op 与符号 op 混用:notional gt 100 and tradeType == 香草
var condition = JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = new ConditionItem { field = "notional", op = "gt", value = 100 } },
new ConditionToken { type = "operator", connector = "and" },
new ConditionToken { type = "condition", condition = new ConditionItem { field = "tradeType", op = "==", value = "香草" } }
}
});
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200, tradeType: "香草")));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 50, tradeType: "香草")));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, CreateContext(notional: 200, tradeType: "雪球")));
}
// ===== 需求①触发条件字段:initialNotional(期初)/ currentNotional(本次)=====
[TestMethod]
public void Evaluate_InitialNotional_UsesOriginalStockEqvNotional()
{
// initialNotional 取 trade.OriginalStockEqvNotional(与 StockEqvNotional 是不同字段)
var condition = BuildTokens(new ConditionItem { field = "initialNotional", op = "gte", value = 1000000 });
var ctx = new ConditionContext
{
Trade = new DBModels.trade
{
StockEqvNotional = 500, // 当前份额,不应被 initialNotional 使用
OriginalStockEqvNotional = 2000000 // 期初名义本金
}
};
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, ctx));
var ctxBelow = new ConditionContext
{
Trade = new DBModels.trade { OriginalStockEqvNotional = 500000 }
};
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, ctxBelow));
}
[TestMethod]
public void Evaluate_CurrentNotional_UsesContextValue()
{
// currentNotional 取 ConditionContext.CurrentNotional(了结场景由 trade_cash 取绝对值传入)
var condition = BuildTokens(new ConditionItem { field = "currentNotional", op = "gt", value = 1000000 });
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 5000000 }));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 500000 }));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = null }));
}
[TestMethod]
public void Evaluate_CurrentNotional_AbsoluteValueSemantics()
{
// 需求:平仓500万,本次交易名义本金按绝对值判断。
// 调用方应传 Math.Abs 后的正值(BuildTriggerContext 已处理),这里验证传入正值即可。
var condition = BuildTokens(new ConditionItem { field = "currentNotional", op = "gte", value = 5000000 });
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 5000000 }));
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 8000000 }));
Assert.IsFalse(ConditionEvaluator.Evaluate(condition, new ConditionContext { CurrentNotional = 4999999 }));
}
[TestMethod]
public void Evaluate_InitialNotional_AbsoluteValueStored()
{
// OriginalStockEqvNotional 的 setter 已做 Math.Abs,负值存入会变正
var condition = BuildTokens(new ConditionItem { field = "initialNotional", op = "gt", value = 100 });
var ctx = new ConditionContext
{
Trade = new DBModels.trade { OriginalStockEqvNotional = -500 } // setter 归一化为 500
};
Assert.IsTrue(ConditionEvaluator.Evaluate(condition, ctx));
}
/// <summary>辅助:单条件 tokens 序列化为 JSON。</summary>
private static string BuildTokens(ConditionItem item)
{
return JsonConvert.SerializeObject(new ConditionExpressionConfig
{
tokens = new System.Collections.Generic.List<ConditionToken>
{
new ConditionToken { type = "condition", condition = item }
}
});
}
}
}