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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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)); } /// 辅助:单条件 tokens 序列化为 JSON。 private static string BuildTokens(ConditionItem item) { return JsonConvert.SerializeObject(new ConditionExpressionConfig { tokens = new System.Collections.Generic.List { new ConditionToken { type = "condition", condition = item } } }); } } }