367 lines
18 KiB
C#
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 }
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|