Enable all TriggerTree tests.
Add null as an expression value. Add variable and infix printing to expressions.
This commit is contained in:
Родитель
f81b3df21b
Коммит
f6ea3e2812
|
@ -102,6 +102,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Expre
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Dialogs.Declarative.Tests", "tests\Microsoft.Bot.Builder.Dialogs.Declarative.Tests\Microsoft.Bot.Builder.Dialogs.Declarative.Tests.csproj", "{D5E70443-4BA2-42ED-992A-010268440B08}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.AI.TriggerTrees.Tests", "tests\Microsoft.Bot.Builder.AI.TriggerTrees.Tests\Microsoft.Bot.Builder.AI.TriggerTrees.Tests.csproj", "{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU
|
||||
|
@ -461,6 +463,14 @@ Global
|
|||
{D5E70443-4BA2-42ED-992A-010268440B08}.Documentation|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D5E70443-4BA2-42ED-992A-010268440B08}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D5E70443-4BA2-42ED-992A-010268440B08}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Documentation|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -513,6 +523,7 @@ Global
|
|||
{8DC1257B-7650-40EB-97A2-C1CBA306DA6A} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
|
||||
{FB2EA804-158C-4654-AD60-A2105AC366FF} = {4269F3C3-6B42-419B-B64A-3E6DC0F1574A}
|
||||
{D5E70443-4BA2-42ED-992A-010268440B08} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
|
||||
{A9E5DD02-E633-46DC-B702-2ABA1AAC2851} = {AD743B78-D61F-4FBF-B620-FA83CE599A50}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16}
|
||||
|
|
|
@ -218,8 +218,8 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
{
|
||||
if (soFar == RelationshipType.Equal)
|
||||
{
|
||||
var shortIgnores = shorterClause.Children.Where(p => p.Type != TriggerTree.Ignore);
|
||||
var longIgnores = longerClause.Children.Where(p => p.Type != TriggerTree.Ignore);
|
||||
var shortIgnores = shorterClause.Children.Where(p => p.Type == TriggerTree.Ignore);
|
||||
var longIgnores = longerClause.Children.Where(p => p.Type == TriggerTree.Ignore);
|
||||
var swapped = false;
|
||||
if (longIgnores.Count() < shortIgnores.Count())
|
||||
{
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
/// <summary>
|
||||
/// All of the most specific triggers that contain the <see cref="Clause"/> in this node.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Trigger> Triggers => _triggers;
|
||||
public IReadOnlyList<Trigger> Triggers => _triggers;
|
||||
|
||||
/// <summary>
|
||||
/// All triggers that contain the <see cref="Clause"/> in this node.
|
||||
|
@ -40,7 +40,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
/// <summary>
|
||||
/// Specialized children of this node.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Node> Specializations => _specializations;
|
||||
public IReadOnlyList<Node> Specializations => _specializations;
|
||||
|
||||
/// <summary>
|
||||
/// The logical conjunction this node represents.
|
||||
|
@ -71,6 +71,13 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
|
||||
internal Node(Clause clause, TriggerTree tree, Trigger trigger = null)
|
||||
{
|
||||
// In order to debug:
|
||||
// 1) Enable Count and VerifyTree
|
||||
// 2) Run your scenario
|
||||
// 3) You will most likely get a beak on the error.
|
||||
// 4) Enable TraceTree and set it hear to get the trace before count
|
||||
// Node._count has the global count for breakpointd
|
||||
// ShowTrace = _count > 280000;
|
||||
Clause = clause;
|
||||
Tree = tree;
|
||||
if (trigger != null)
|
||||
|
@ -87,18 +94,18 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
{
|
||||
predicate = child.Children[0];
|
||||
}
|
||||
children.Add(child);
|
||||
children.Add(predicate);
|
||||
}
|
||||
if (children.Any())
|
||||
{
|
||||
Expression = Expression.MakeExpression(ExpressionType.And, null, children.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
Expression = Expression.ConstantExpression(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Expression == null)
|
||||
{
|
||||
Expression = Expression.ConstantExpression(true);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
@ -108,7 +115,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
return builder.ToString();
|
||||
}
|
||||
|
||||
public void ToString(StringBuilder builder, int indent = 0)
|
||||
public void ToString(StringBuilder builder, int indent = 0)
|
||||
=> Clause.ToString(builder, indent);
|
||||
|
||||
/// <summary>
|
||||
|
@ -197,7 +204,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
{
|
||||
_allTriggers.Add(trigger);
|
||||
var add = true;
|
||||
for (var i = 0; i < _triggers.Count(); )
|
||||
for (var i = 0; i < _triggers.Count();)
|
||||
{
|
||||
var existing = _triggers[i];
|
||||
var reln = trigger.Relationship(existing, Tree.Comparers);
|
||||
|
@ -246,9 +253,6 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
case RelationshipType.Specializes:
|
||||
{
|
||||
triggerNode.AddSpecialization(this);
|
||||
#if DEBUG
|
||||
Debug.Assert(triggerNode.CheckInvariants());
|
||||
#endif
|
||||
op = Operation.Inserted;
|
||||
}
|
||||
break;
|
||||
|
@ -309,7 +313,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
{
|
||||
_specializations.Add(triggerNode);
|
||||
#if DEBUG
|
||||
Debug.Assert(triggerNode.CheckInvariants());
|
||||
Debug.Assert(CheckInvariants());
|
||||
#endif
|
||||
op = Operation.Added;
|
||||
}
|
||||
|
@ -332,6 +336,8 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
var reln = Relationship(child);
|
||||
Debug.Assert(reln == RelationshipType.Generalizes);
|
||||
}
|
||||
|
||||
// Siblings should be incomparable
|
||||
for (var i = 0; i < _specializations.Count; ++i)
|
||||
{
|
||||
var first = _specializations[i];
|
||||
|
@ -342,6 +348,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
Debug.Assert(reln == RelationshipType.Incomparable);
|
||||
}
|
||||
}
|
||||
|
||||
// Triggers should be incomparable
|
||||
for (var i = 0; i < _triggers.Count(); ++i)
|
||||
{
|
||||
|
@ -355,6 +362,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All triggers should all be found in triggers
|
||||
for (var i = 0; i < _allTriggers.Count(); ++i)
|
||||
{
|
||||
|
@ -437,6 +445,10 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
added = true;
|
||||
#if TraceTree
|
||||
if (Node.ShowTrace) Debug.WriteLine("Added as specialization");
|
||||
|
||||
#endif
|
||||
#if DEBUG
|
||||
Debug.Assert(CheckInvariants());
|
||||
#endif
|
||||
}
|
||||
return added;
|
||||
|
@ -510,7 +522,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
if (!found)
|
||||
{
|
||||
var (value, error) = Expression.TryEvaluate(state);
|
||||
if (error != null && value is bool match && match && Triggers.Any())
|
||||
if (error == null && value is bool match && match && Triggers.Any())
|
||||
{
|
||||
matches.Add(this);
|
||||
found = true;
|
||||
|
|
|
@ -174,7 +174,10 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
}
|
||||
}
|
||||
|
||||
private Expression MakeExpression(string type, Expression expression) => Expression.MakeExpression(type, null, expression.Children);
|
||||
private Expression ReplaceExpression(string type, Expression expression) => Expression.MakeExpression(type, TriggerTree.LookupFunction(type), expression.Children);
|
||||
|
||||
private Expression MakeExpression(string type, Expression expression) => Expression.MakeExpression(type, TriggerTree.LookupFunction(type), expression);
|
||||
|
||||
|
||||
// Push not down to leaves using De Morgan's rule
|
||||
private Expression PushDownNot(Expression expression, bool inNot)
|
||||
|
@ -213,33 +216,37 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
case ExpressionType.LessThan:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = MakeExpression(ExpressionType.GreaterThanOrEqual, expression);
|
||||
newExpr = ReplaceExpression(ExpressionType.GreaterThanOrEqual, expression);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.LessThanOrEqual:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = MakeExpression(ExpressionType.GreaterThan, expression);
|
||||
newExpr = ReplaceExpression(ExpressionType.GreaterThan, expression);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Equal:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = MakeExpression(ExpressionType.NotEqual, expression);
|
||||
newExpr = ReplaceExpression(ExpressionType.NotEqual, expression);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.GreaterThanOrEqual:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = MakeExpression(ExpressionType.LessThan, expression);
|
||||
newExpr = ReplaceExpression(ExpressionType.LessThan, expression);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.GreaterThan:
|
||||
if (inNot)
|
||||
{
|
||||
newExpr = MakeExpression(ExpressionType.LessThanOrEqual, expression);
|
||||
newExpr = ReplaceExpression(ExpressionType.LessThanOrEqual, expression);
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Exists:
|
||||
// Rewrite exists(x) -> x != null
|
||||
newExpr = Expression.MakeExpression(inNot ? ExpressionType.Equal : ExpressionType.NotEqual, null, expression.Children[0], Expression.ConstantExpression(null));
|
||||
break;
|
||||
case TriggerTree.Optional:
|
||||
case TriggerTree.Ignore:
|
||||
// Pass through optional/ignore
|
||||
|
@ -385,6 +392,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
if (reln == RelationshipType.Equal)
|
||||
{
|
||||
_clauses.RemoveAt(j);
|
||||
--j;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -441,8 +449,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
&& cnst.Value is string str
|
||||
&& str == variable)
|
||||
{
|
||||
cnst.Value = binding;
|
||||
newExpr = cnst;
|
||||
newExpr = Expression.Accessor(binding);
|
||||
changed = true;
|
||||
}
|
||||
else
|
||||
|
@ -566,6 +573,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
}
|
||||
}
|
||||
}
|
||||
clause.Children = predicates.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ namespace Microsoft.Bot.Builder.AI.TriggerTrees
|
|||
ExpressionEvaluator eval;
|
||||
if (type == Optional || type == Ignore)
|
||||
{
|
||||
eval = new ExpressionEvaluator(null, ReturnType.Object, BuiltInFunctions.ValidateUnaryBoolean);
|
||||
eval = new ExpressionEvaluator(null, ReturnType.Boolean, BuiltInFunctions.ValidateUnaryBoolean);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -133,6 +133,10 @@ namespace Microsoft.Bot.Builder.Expressions.Parser
|
|||
{
|
||||
result = Expression.ConstantExpression(true);
|
||||
}
|
||||
else if (symbol == "null")
|
||||
{
|
||||
result = Expression.ConstantExpression(null);
|
||||
}
|
||||
else if (IsShortHandExpression(symbol))
|
||||
{
|
||||
result = MakeShortHandExpression(symbol);
|
||||
|
|
|
@ -268,7 +268,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
public static string VerifyBoolean(object value, Expression expression)
|
||||
{
|
||||
string error = null;
|
||||
if (!(value is Boolean))
|
||||
if (!(value is bool))
|
||||
{
|
||||
error = $"{expression} is not a boolean.";
|
||||
}
|
||||
|
@ -414,7 +414,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
/// <returns>Information about expression type.</returns>
|
||||
public static ExpressionEvaluator Lookup(string type)
|
||||
{
|
||||
if (!_functions.TryGetValue(type, out ExpressionEvaluator eval))
|
||||
if (!_functions.TryGetValue(type, out var eval))
|
||||
{
|
||||
throw new ArgumentException($"{type} does not have a built-in expression evaluator.");
|
||||
}
|
||||
|
@ -444,7 +444,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
{
|
||||
object value = null;
|
||||
string error = null;
|
||||
object instance = state;
|
||||
var instance = state;
|
||||
var children = expression.Children;
|
||||
if (children.Length == 2)
|
||||
{
|
||||
|
@ -477,7 +477,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
{
|
||||
if (idxValue is int idx)
|
||||
{
|
||||
int count = -1;
|
||||
var count = -1;
|
||||
if (inst is Array arr)
|
||||
{
|
||||
count = arr.Length;
|
||||
|
@ -531,7 +531,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
(result, error) = child.TryEvaluate(state);
|
||||
if (error == null)
|
||||
{
|
||||
if (!(result is Boolean bresult))
|
||||
if (!(result is bool bresult))
|
||||
{
|
||||
error = $"{child} is not boolean";
|
||||
break;
|
||||
|
@ -559,7 +559,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
(result, error) = child.TryEvaluate(state);
|
||||
if (error == null)
|
||||
{
|
||||
if (!(result is Boolean bresult))
|
||||
if (!(result is bool bresult))
|
||||
{
|
||||
error = $"{child} is not boolean";
|
||||
break;
|
||||
|
@ -668,7 +668,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
{ ExpressionType.Divide,
|
||||
new ExpressionEvaluator(ApplySequence(args => args[0] / args[1],
|
||||
(value, expression) => {
|
||||
string error = VerifyNumber(value, expression);
|
||||
var error = VerifyNumber(value, expression);
|
||||
if (error == null && Convert.ToDouble(value) == 0.0)
|
||||
{
|
||||
error = $"Cannot divide by 0 from {expression}";
|
||||
|
@ -692,7 +692,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
{ ExpressionType.LessThanOrEqual, Comparison(args => args[0] <= args[1]) },
|
||||
{ ExpressionType.Equal,
|
||||
new ExpressionEvaluator(Apply(args => args[0] == args[1]), ReturnType.Boolean, ValidateBinary) },
|
||||
{ ExpressionType.NotEqual,
|
||||
{ ExpressionType.NotEqual,
|
||||
new ExpressionEvaluator(Apply(args => args[0] != args[1]), ReturnType.Boolean, ValidateBinary) },
|
||||
{ ExpressionType.GreaterThan,Comparison(args => args[0] > args[1]) },
|
||||
{ ExpressionType.GreaterThanOrEqual, Comparison(args => args[0] >= args[1]) },
|
||||
|
|
|
@ -34,7 +34,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
Evaluator.ReturnType =
|
||||
(value is string ? ReturnType.String
|
||||
: value.IsNumber() ? ReturnType.Number
|
||||
: value is Boolean ? ReturnType.Boolean
|
||||
: value is bool ? ReturnType.Boolean
|
||||
: ReturnType.Object);
|
||||
_value = value;
|
||||
}
|
||||
|
@ -44,7 +44,11 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Value is string str)
|
||||
if (Value == null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
else if (Value is string str)
|
||||
{
|
||||
return $"'{Value}'";
|
||||
}
|
||||
|
|
|
@ -70,15 +70,12 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
/// <summary>
|
||||
/// Expected result of evaluating expression.
|
||||
/// </summary>
|
||||
public ReturnType ReturnType { get { return Evaluator.ReturnType; } }
|
||||
public ReturnType ReturnType => Evaluator.ReturnType;
|
||||
|
||||
/// <summary>
|
||||
/// Validate immediate expression.
|
||||
/// </summary>
|
||||
public void Validate()
|
||||
{
|
||||
Evaluator.ValidateExpression(this);
|
||||
}
|
||||
public void Validate() => Evaluator.ValidateExpression(this);
|
||||
|
||||
/// <summary>
|
||||
/// Recursively validate the expression tree.
|
||||
|
@ -103,30 +100,63 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
=> Evaluator.TryEvaluate(this, state);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(Type);
|
||||
}
|
||||
|
||||
protected string ToString(string name)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(Type);
|
||||
builder.Append('(');
|
||||
var first = true;
|
||||
foreach (var child in Children)
|
||||
// Special support for memory paths
|
||||
if (Type == ExpressionType.Accessor)
|
||||
{
|
||||
if (first)
|
||||
var prop = (Children[0] as Constant).Value;
|
||||
if (Children.Count() == 1)
|
||||
{
|
||||
first = false;
|
||||
builder.Append(prop);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(", ");
|
||||
builder.Append(Children[1].ToString());
|
||||
builder.Append('.');
|
||||
builder.Append(prop);
|
||||
}
|
||||
|
||||
builder.Append(child.ToString());
|
||||
}
|
||||
builder.Append(')');
|
||||
else if (Type == ExpressionType.Element)
|
||||
{
|
||||
builder.Append(Children[0].ToString());
|
||||
builder.Append('[');
|
||||
builder.Append(Children[1].ToString());
|
||||
builder.Append(']');
|
||||
}
|
||||
else
|
||||
{
|
||||
var infix = Type.Length > 0 && !char.IsLetter(Type[0]) && Children.Count() >= 2;
|
||||
if (!infix)
|
||||
{
|
||||
builder.Append(Type);
|
||||
}
|
||||
builder.Append('(');
|
||||
var first = true;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (infix)
|
||||
{
|
||||
builder.Append(' ');
|
||||
builder.Append(Type);
|
||||
builder.Append(' ');
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(", ");
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append(child.ToString());
|
||||
}
|
||||
builder.Append(')');
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
|
|
|
@ -16,19 +16,17 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
/// <param name="value">Value to check.</param>
|
||||
/// <returns>True if numeric type.</returns>
|
||||
public static bool IsNumber(this object value)
|
||||
{
|
||||
return value is sbyte
|
||||
|| value is byte
|
||||
|| value is short
|
||||
|| value is ushort
|
||||
|| value is int
|
||||
|| value is uint
|
||||
|| value is long
|
||||
|| value is ulong
|
||||
|| value is float
|
||||
|| value is double
|
||||
|| value is decimal;
|
||||
}
|
||||
=> value is sbyte
|
||||
|| value is byte
|
||||
|| value is short
|
||||
|| value is ushort
|
||||
|| value is int
|
||||
|| value is uint
|
||||
|| value is long
|
||||
|| value is ulong
|
||||
|| value is float
|
||||
|| value is double
|
||||
|| value is decimal;
|
||||
|
||||
/// <summary>
|
||||
/// Test an object to see if it is an integer type.
|
||||
|
@ -36,16 +34,14 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
/// <param name="value">Value to check.</param>
|
||||
/// <returns>True if numeric type.</returns>
|
||||
public static bool IsInteger(this object value)
|
||||
{
|
||||
return value is sbyte
|
||||
|| value is byte
|
||||
|| value is short
|
||||
|| value is ushort
|
||||
|| value is int
|
||||
|| value is uint
|
||||
|| value is long
|
||||
|| value is ulong;
|
||||
}
|
||||
=> value is sbyte
|
||||
|| value is byte
|
||||
|| value is short
|
||||
|| value is ushort
|
||||
|| value is int
|
||||
|| value is uint
|
||||
|| value is long
|
||||
|| value is ulong;
|
||||
|
||||
/// <summary>
|
||||
/// Do a deep equality between expressions.
|
||||
|
@ -55,7 +51,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
/// <returns>True if expressions are the same.</returns>
|
||||
public static bool DeepEquals(this Expression expr, Expression other)
|
||||
{
|
||||
bool eq = true;
|
||||
var eq = true;
|
||||
if (expr != null && other != null)
|
||||
{
|
||||
eq = expr.Type == other.Type;
|
||||
|
@ -65,7 +61,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
{
|
||||
var val = ((Constant)expr).Value;
|
||||
var otherVal = ((Constant)other).Value;
|
||||
eq = val.Equals(otherVal);
|
||||
eq = val == otherVal || (val != null && val.Equals(otherVal));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -186,7 +182,7 @@ namespace Microsoft.Bot.Builder.Expressions
|
|||
}
|
||||
else if (instance is JObject jobj)
|
||||
{
|
||||
if (jobj.TryGetValue(property, out JToken jtoken))
|
||||
if (jobj.TryGetValue(property, out var jtoken))
|
||||
{
|
||||
if (jtoken is JArray jarray)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,567 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.Bot.Builder.AI.TriggerTrees;
|
||||
using Microsoft.Bot.Builder.Expressions;
|
||||
|
||||
namespace Microsoft.TriggerTreeTests
|
||||
{
|
||||
public class Comparison
|
||||
{
|
||||
public string Type;
|
||||
public object Value;
|
||||
public Comparison(string type, object value)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public class ExpressionInfo
|
||||
{
|
||||
public Expression Expression;
|
||||
public Dictionary<string, Comparison> Bindings = new Dictionary<string, Comparison>();
|
||||
public List<Quantifier> Quantifiers = new List<Quantifier>();
|
||||
|
||||
public ExpressionInfo(Expression expression)
|
||||
{
|
||||
Expression = expression;
|
||||
}
|
||||
|
||||
public ExpressionInfo(Expression expression, string name, object value, string type)
|
||||
{
|
||||
Expression = expression;
|
||||
Bindings.Add(name, new Comparison(type, value));
|
||||
}
|
||||
|
||||
public ExpressionInfo(Expression expression, Dictionary<string, Comparison> bindings, List<Quantifier> quantifiers = null)
|
||||
{
|
||||
Expression = expression;
|
||||
Bindings = bindings;
|
||||
if (quantifiers != null)
|
||||
{
|
||||
Quantifiers = quantifiers;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => Expression.ToString();
|
||||
}
|
||||
|
||||
public class TriggerInfo
|
||||
{
|
||||
public Expression Trigger;
|
||||
public Dictionary<string, object> Bindings = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
public class Generator
|
||||
{
|
||||
public Random Rand;
|
||||
|
||||
public class SimpleValues
|
||||
{
|
||||
public int Int = 1;
|
||||
public double Double = 2.0;
|
||||
public string String = "3";
|
||||
public object Object = null;
|
||||
|
||||
public SimpleValues()
|
||||
{ }
|
||||
|
||||
public SimpleValues(int integer)
|
||||
{
|
||||
Int = integer;
|
||||
}
|
||||
|
||||
public SimpleValues(double number)
|
||||
{
|
||||
Double = number;
|
||||
}
|
||||
|
||||
public SimpleValues(object obj)
|
||||
{
|
||||
Object = obj;
|
||||
}
|
||||
|
||||
public bool Test(int? value) => value.HasValue && Int == value;
|
||||
|
||||
public bool Test(double? value) => value.HasValue && Double == value;
|
||||
|
||||
public bool Test(string value) => value != null && String == value;
|
||||
|
||||
public bool Test(SimpleValues value) => Int == value.Int && Double == value.Double && String == value.String && Object.Equals(value.Object);
|
||||
|
||||
public static bool Test(SimpleValues obj, int? value) => value.HasValue && obj.Int == value;
|
||||
|
||||
public static bool Test(SimpleValues obj, double? value) => value.HasValue && obj.Double == value;
|
||||
|
||||
public static bool Test(SimpleValues obj, string value) => value != null && obj.String == value;
|
||||
|
||||
public static bool Test(SimpleValues obj, object other) => other != null && obj.Object.Equals(other);
|
||||
}
|
||||
|
||||
public Generator(int seed = 0)
|
||||
{
|
||||
Rand = new Random(seed);
|
||||
}
|
||||
|
||||
private static readonly string[] comparisons = new string[] {
|
||||
ExpressionType.LessThan, ExpressionType.LessThanOrEqual,
|
||||
ExpressionType.Equal,
|
||||
// TODO: null values are always not equal ExpressionType.NotEqual,
|
||||
ExpressionType.GreaterThanOrEqual, ExpressionType.GreaterThan };
|
||||
|
||||
/* Predicates */
|
||||
|
||||
public Expression GenerateString(int length) => Expression.ConstantExpression(RandomString(length));
|
||||
|
||||
public string RandomString(int length)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
for (var i = 0; i < length; ++i)
|
||||
{
|
||||
builder.Append(((char)('a' + Rand.Next(26))));
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private const double DoubleEpsilon = 0.000001;
|
||||
|
||||
private int AdjustValue(int value, string type)
|
||||
{
|
||||
var result = value;
|
||||
const int epsilon = 1;
|
||||
switch (type)
|
||||
{
|
||||
case ExpressionType.LessThan: result += epsilon; break;
|
||||
case ExpressionType.NotEqual: result += epsilon; break;
|
||||
case ExpressionType.GreaterThan: result -= epsilon; break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private double AdjustValue(double value, string type)
|
||||
{
|
||||
var result = value;
|
||||
switch (type)
|
||||
{
|
||||
case ExpressionType.LessThan: result += DoubleEpsilon; break;
|
||||
case ExpressionType.NotEqual: result += DoubleEpsilon; break;
|
||||
case ExpressionType.GreaterThan: result -= DoubleEpsilon; break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public ExpressionInfo GenerateSimpleComparison(string name)
|
||||
{
|
||||
Expression expression = null;
|
||||
object value = null;
|
||||
var type = RandomChoice<string>(comparisons);
|
||||
switch (Rand.Next(2))
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
value = Rand.Next();
|
||||
expression = Expression.MakeExpression(
|
||||
type,
|
||||
null,
|
||||
Expression.Accessor(name),
|
||||
Expression.ConstantExpression(AdjustValue((int)value, type)));
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
{
|
||||
value = Rand.NextDouble();
|
||||
expression = Expression.MakeExpression(
|
||||
type,
|
||||
null,
|
||||
Expression.Accessor(name),
|
||||
Expression.ConstantExpression(AdjustValue((double)value, type)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return new ExpressionInfo(expression, name, value, type);
|
||||
}
|
||||
|
||||
public ExpressionInfo GenerateHasValueComparison(string name)
|
||||
{
|
||||
Expression expression = null;
|
||||
object value = null;
|
||||
switch (Rand.Next(3))
|
||||
{
|
||||
case 0:
|
||||
expression = Expression.MakeExpression(ExpressionType.Exists, null, Expression.Accessor(name));
|
||||
value = Rand.Next();
|
||||
break;
|
||||
case 1:
|
||||
expression = Expression.MakeExpression(ExpressionType.Exists, null, Expression.Accessor(name));
|
||||
value = Rand.NextDouble();
|
||||
break;
|
||||
case 2:
|
||||
expression = Expression.MakeExpression(ExpressionType.NotEqual, null, Expression.Accessor(name), Expression.ConstantExpression(null));
|
||||
value = RandomString(5);
|
||||
break;
|
||||
}
|
||||
return new ExpressionInfo(expression, name, value, ExpressionType.Not);
|
||||
}
|
||||
|
||||
public List<ExpressionInfo> GeneratePredicates(int n, string nameBase)
|
||||
{
|
||||
var expressions = new List<ExpressionInfo>();
|
||||
for (var i = 0; i < n; ++i)
|
||||
{
|
||||
var name = $"{nameBase}{i}";
|
||||
var selection = RandomWeighted(new double[] { 1.0, 1.0 });
|
||||
switch (selection)
|
||||
{
|
||||
case 0: expressions.Add(GenerateSimpleComparison(name)); break;
|
||||
case 1: expressions.Add(GenerateHasValueComparison(name)); break;
|
||||
}
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
public List<ExpressionInfo> GenerateConjunctions(List<ExpressionInfo> predicates, int numConjunctions, int minClause, int maxClause)
|
||||
{
|
||||
var conjunctions = new List<ExpressionInfo>();
|
||||
for (var i = 0; i < numConjunctions; ++i)
|
||||
{
|
||||
var clauses = minClause + Rand.Next(maxClause - minClause);
|
||||
var expressions = new List<ExpressionInfo>();
|
||||
var used = new List<int>();
|
||||
for (var j = 0; j < clauses; ++j)
|
||||
{
|
||||
int choice;
|
||||
do
|
||||
{
|
||||
choice = Rand.Next(predicates.Count);
|
||||
} while (used.Contains(choice));
|
||||
expressions.Add(predicates[choice]);
|
||||
used.Add(choice);
|
||||
}
|
||||
var conjunction = Binary(ExpressionType.And, expressions, out var bindings);
|
||||
conjunctions.Add(new ExpressionInfo(conjunction, bindings));
|
||||
}
|
||||
return conjunctions;
|
||||
}
|
||||
|
||||
public List<ExpressionInfo> GenerateDisjunctions(List<ExpressionInfo> predicates, int numDisjunctions, int minClause, int maxClause)
|
||||
{
|
||||
var disjunctions = new List<ExpressionInfo>();
|
||||
for (var i = 0; i < numDisjunctions; ++i)
|
||||
{
|
||||
var clauses = minClause + Rand.Next(maxClause - minClause);
|
||||
var expressions = new List<ExpressionInfo>();
|
||||
var used = new List<int>();
|
||||
for (var j = 0; j < clauses; ++j)
|
||||
{
|
||||
int choice;
|
||||
do
|
||||
{
|
||||
choice = Rand.Next(predicates.Count);
|
||||
} while (used.Contains(choice));
|
||||
expressions.Add(predicates[choice]);
|
||||
used.Add(choice);
|
||||
}
|
||||
var disjunction = Binary(ExpressionType.Or, expressions, out var bindings);
|
||||
disjunctions.Add(new ExpressionInfo(disjunction, bindings));
|
||||
}
|
||||
return disjunctions;
|
||||
}
|
||||
|
||||
public List<ExpressionInfo> GenerateOptionals(List<ExpressionInfo> predicates, int numOptionals, int minClause, int maxClause)
|
||||
{
|
||||
var optionals = new List<ExpressionInfo>();
|
||||
for (var i = 0; i < numOptionals; ++i)
|
||||
{
|
||||
var clauses = minClause + Rand.Next(maxClause - minClause);
|
||||
var expressions = new List<ExpressionInfo>();
|
||||
var used = new List<int>();
|
||||
for (var j = 0; j < clauses; ++j)
|
||||
{
|
||||
int choice;
|
||||
do
|
||||
{
|
||||
choice = Rand.Next(predicates.Count);
|
||||
} while (used.Contains(choice));
|
||||
var predicate = predicates[choice];
|
||||
if (j == 0)
|
||||
{
|
||||
var optional = Expression.MakeExpression(TriggerTree.Optional, null, predicate.Expression);
|
||||
if (Rand.NextDouble() < 0.25)
|
||||
{
|
||||
optional = Expression.NotExpression(optional);
|
||||
}
|
||||
expressions.Add(new ExpressionInfo(optional, predicate.Bindings));
|
||||
}
|
||||
else
|
||||
{
|
||||
expressions.Add(predicate);
|
||||
}
|
||||
used.Add(choice);
|
||||
}
|
||||
var conjunction = Binary(ExpressionType.And, expressions, out var bindings);
|
||||
optionals.Add(new ExpressionInfo(conjunction, bindings));
|
||||
}
|
||||
return optionals;
|
||||
}
|
||||
|
||||
public Expression Binary(string type, IEnumerable<ExpressionInfo> expressions,
|
||||
out Dictionary<string, Comparison> bindings)
|
||||
{
|
||||
bindings = MergeBindings(expressions);
|
||||
Expression binaryExpression = null;
|
||||
foreach (var info in expressions)
|
||||
{
|
||||
if (binaryExpression == null)
|
||||
{
|
||||
binaryExpression = info.Expression;
|
||||
}
|
||||
else
|
||||
{
|
||||
binaryExpression = Expression.MakeExpression(type, null, binaryExpression, info.Expression);
|
||||
}
|
||||
}
|
||||
return binaryExpression;
|
||||
}
|
||||
|
||||
public IEnumerable<Expression> Predicates(IEnumerable<ExpressionInfo> expressions)
|
||||
{
|
||||
foreach (var info in expressions)
|
||||
{
|
||||
yield return info.Expression;
|
||||
}
|
||||
}
|
||||
|
||||
private int SplitMemory(string mem, out string baseName)
|
||||
{
|
||||
var i = 0;
|
||||
for (; i < mem.Length; ++i)
|
||||
{
|
||||
if (char.IsDigit(mem[i]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
baseName = mem.Substring(0, i);
|
||||
return int.Parse(mem.Substring(i));
|
||||
}
|
||||
|
||||
public List<ExpressionInfo> GenerateQuantfiers(List<ExpressionInfo> predicates, int numExpressions, int maxVariable, int maxExpansion, int maxQuantifiers)
|
||||
{
|
||||
var result = new List<ExpressionInfo>();
|
||||
var allBindings = MergeBindings(predicates);
|
||||
var allTypes = VariablesByType(allBindings);
|
||||
for (var exp = 0; exp < numExpressions; ++exp)
|
||||
{
|
||||
var expression = RandomChoice(predicates);
|
||||
var info = new ExpressionInfo(expression.Expression);
|
||||
var numQuants = 1 + Rand.Next(maxQuantifiers - 1);
|
||||
var chosen = new HashSet<string>();
|
||||
var maxBase = Math.Min(expression.Bindings.Count, numQuants);
|
||||
for (var quant = 0; quant < maxBase; ++quant)
|
||||
{
|
||||
KeyValuePair<string, Comparison> baseBinding;
|
||||
// Can only map each expression variable once in a quantifier
|
||||
do
|
||||
{
|
||||
baseBinding = expression.Bindings.ElementAt(Rand.Next(expression.Bindings.Count));
|
||||
} while (chosen.Contains(baseBinding.Key));
|
||||
chosen.Add(baseBinding.Key);
|
||||
SplitMemory(baseBinding.Key, out var baseName);
|
||||
var mappings = new List<string>();
|
||||
var expansion = 1 + Rand.Next(maxExpansion - 1);
|
||||
for (var i = 0; i < expansion; ++i)
|
||||
{
|
||||
if (i == 0)
|
||||
{
|
||||
mappings.Add($"{baseBinding.Key}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var mapping = RandomChoice<string>(allTypes[baseBinding.Value.Value.GetType()]);
|
||||
if (!mappings.Contains(mapping))
|
||||
{
|
||||
mappings.Add(mapping);
|
||||
}
|
||||
}
|
||||
}
|
||||
var any = Rand.NextDouble() < 0.5;
|
||||
if (any)
|
||||
{
|
||||
var mem = RandomChoice(mappings);
|
||||
if (!info.Bindings.ContainsKey(mem))
|
||||
{
|
||||
info.Bindings.Add(mem, baseBinding.Value);
|
||||
}
|
||||
info.Quantifiers.Add(new Quantifier(baseBinding.Key, QuantifierType.Any, mappings));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var mapping in mappings)
|
||||
{
|
||||
if (!info.Bindings.ContainsKey(mapping))
|
||||
{
|
||||
info.Bindings.Add(mapping, baseBinding.Value);
|
||||
}
|
||||
}
|
||||
info.Quantifiers.Add(new Quantifier(baseBinding.Key, QuantifierType.All, mappings));
|
||||
}
|
||||
}
|
||||
result.Add(info);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Comparison NotValue(Comparison comparison)
|
||||
{
|
||||
var value = comparison.Value;
|
||||
var type = value.GetType();
|
||||
var isNot = false;
|
||||
if (type != typeof(int) && type != typeof(double) && type != typeof(string))
|
||||
{
|
||||
throw new Exception($"Unsupported type {type}");
|
||||
}
|
||||
switch (comparison.Type)
|
||||
{
|
||||
case ExpressionType.LessThanOrEqual:
|
||||
case ExpressionType.LessThan:
|
||||
{
|
||||
if (type == typeof(int)) value = (int)value + 1;
|
||||
else if (type == typeof(double)) value = (double)value + DoubleEpsilon;
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Equal:
|
||||
{
|
||||
if (type == typeof(int)) value = (int)value - 1;
|
||||
else if (type == typeof(double)) value = (double)value - DoubleEpsilon;
|
||||
}
|
||||
break;
|
||||
case ExpressionType.NotEqual:
|
||||
{
|
||||
if (type == typeof(int)) value = (int)value - 1;
|
||||
else if (type == typeof(double)) value = (double)value - DoubleEpsilon;
|
||||
}
|
||||
break;
|
||||
case ExpressionType.GreaterThanOrEqual:
|
||||
case ExpressionType.GreaterThan:
|
||||
{
|
||||
if (type == typeof(int)) value = (int)value - 1;
|
||||
else if (type == typeof(double)) value = (double)value - DoubleEpsilon;
|
||||
}
|
||||
break;
|
||||
case ExpressionType.Not:
|
||||
{
|
||||
isNot = true;
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
return isNot ? null : new Comparison(comparison.Type, value);
|
||||
}
|
||||
|
||||
public IEnumerable<ExpressionInfo> GenerateNots(IList<ExpressionInfo> predicates, int numNots)
|
||||
{
|
||||
for (var i = 0; i < numNots; ++i)
|
||||
{
|
||||
var expr = RandomChoice(predicates);
|
||||
var bindings = new Dictionary<string, Comparison>();
|
||||
foreach (var binding in expr.Bindings)
|
||||
{
|
||||
var comparison = NotValue(binding.Value);
|
||||
if (comparison != null)
|
||||
{
|
||||
bindings.Add(binding.Key, comparison);
|
||||
}
|
||||
}
|
||||
yield return new ExpressionInfo(Expression.NotExpression(expr.Expression), bindings, expr.Quantifiers);
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<Type, List<string>> VariablesByType(Dictionary<string, Comparison> bindings)
|
||||
{
|
||||
var result = new Dictionary<Type, List<string>>();
|
||||
foreach (var binding in bindings)
|
||||
{
|
||||
var type = binding.Value.Value.GetType();
|
||||
if (!result.ContainsKey(type))
|
||||
{
|
||||
result.Add(type, new List<string>());
|
||||
}
|
||||
result[type].Add(binding.Key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Dictionary<string, Comparison> MergeBindings(IEnumerable<ExpressionInfo> expressions)
|
||||
{
|
||||
var bindings = new Dictionary<string, Comparison>();
|
||||
foreach (var info in expressions)
|
||||
{
|
||||
foreach (var binding in info.Bindings)
|
||||
{
|
||||
bindings[binding.Key] = binding.Value;
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
public T RandomChoice<T>(IList<T> choices) => choices[Rand.Next(choices.Count)];
|
||||
|
||||
public class WeightedChoice<T>
|
||||
{
|
||||
public double Weight = 0.0;
|
||||
public T Choice = default(T);
|
||||
}
|
||||
|
||||
public T RandomWeighted<T>(IEnumerable<WeightedChoice<T>> choices)
|
||||
{
|
||||
var totalWeight = 0.0;
|
||||
foreach (var choice in choices)
|
||||
{
|
||||
totalWeight += choice.Weight;
|
||||
}
|
||||
var selection = Rand.NextDouble() * totalWeight;
|
||||
var soFar = 0.0;
|
||||
var result = default(T);
|
||||
foreach (var choice in choices)
|
||||
{
|
||||
if (soFar <= selection)
|
||||
{
|
||||
soFar += choice.Weight;
|
||||
result = choice.Choice;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public int RandomWeighted(IReadOnlyList<double> weights)
|
||||
{
|
||||
var totalWeight = 0.0;
|
||||
foreach (var weight in weights)
|
||||
{
|
||||
totalWeight += weight;
|
||||
}
|
||||
var selection = Rand.NextDouble() * totalWeight;
|
||||
var soFar = 0.0;
|
||||
var result = 0;
|
||||
for (var i = 0; i < weights.Count; ++i)
|
||||
{
|
||||
if (soFar <= selection)
|
||||
{
|
||||
soFar += weights[i];
|
||||
result = i;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0-preview-20180816-01" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\libraries\Microsoft.Bot.Builder.AI.TriggerTrees\Microsoft.Bot.Builder.AI.TriggerTrees.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Microsoft.Bot.Builder.AI.TriggerTrees;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.Bot.Builder.Expressions;
|
||||
using Microsoft.Bot.Builder.Expressions.Parser;
|
||||
|
||||
namespace Microsoft.TriggerTreeTests
|
||||
{
|
||||
[TestClass]
|
||||
public class Tests
|
||||
{
|
||||
private Generator _generator;
|
||||
|
||||
private Trigger VerifyAddTrigger(TriggerTree tree, Expression expression, object action)
|
||||
{
|
||||
var trigger = tree.AddTrigger(expression, action);
|
||||
VerifyTree(tree);
|
||||
return trigger;
|
||||
}
|
||||
|
||||
private void VerifyTree(TriggerTree tree)
|
||||
{
|
||||
var badNode = tree.VerifyTree();
|
||||
Assert.AreEqual(null, badNode);
|
||||
}
|
||||
|
||||
public Tests()
|
||||
{
|
||||
_generator = new Generator();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestRoot()
|
||||
{
|
||||
var tree = new TriggerTree();
|
||||
tree.AddTrigger("true", "root");
|
||||
var matches = tree.Matches(new Dictionary<string, object>());
|
||||
Assert.AreEqual(1, matches.Count());
|
||||
Assert.AreEqual("root", matches.First().Triggers.First().Action);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestIgnore()
|
||||
{
|
||||
var tree = new TriggerTree();
|
||||
tree.AddTrigger("ignore(!exists(foo)) && exists(blah)", 1);
|
||||
tree.AddTrigger("exists(blah) && ignore(!exists(foo2)) && woof == 3", 2);
|
||||
tree.AddTrigger("exists(blah) && woof == 3", 3);
|
||||
tree.AddTrigger("exists(blah) && woof == 3 && ignore(!exists(foo2))", 2);
|
||||
var frame = new Dictionary<string, object> { { "blah", 1 }, { "woof", 3 } };
|
||||
var matches = tree.Matches(frame).ToList();
|
||||
Assert.AreEqual(2, matches.Count);
|
||||
Assert.AreEqual(1, matches[0].AllTriggers.Count());
|
||||
Assert.AreEqual(1, matches[1].AllTriggers.Count());
|
||||
Assert.AreEqual(3, matches[1].AllTriggers.First().Action);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestTree()
|
||||
{
|
||||
var numPredicates = 100;
|
||||
var numSingletons = 50;
|
||||
var numConjunctions = 100;
|
||||
var numDisjunctions = 100;
|
||||
var numOptionals = 100;
|
||||
var numQuantifiers = 100;
|
||||
var numNots = 100;
|
||||
|
||||
var minClause = 2;
|
||||
var maxClause = 4;
|
||||
var maxExpansion = 3;
|
||||
var maxQuantifiers = 3;
|
||||
var singletons = _generator.GeneratePredicates(numPredicates, "mem");
|
||||
var tree = new TriggerTree();
|
||||
var predicates = new List<ExpressionInfo>(singletons);
|
||||
|
||||
// Add singletons
|
||||
foreach (var predicate in singletons.Take(numSingletons))
|
||||
{
|
||||
tree.AddTrigger(predicate.Expression, predicate.Bindings);
|
||||
}
|
||||
Assert.AreEqual(numSingletons, tree.TotalTriggers);
|
||||
|
||||
// Add conjunctions and test matches
|
||||
var conjunctions = _generator.GenerateConjunctions(predicates, numConjunctions, minClause, maxClause);
|
||||
foreach (var conjunction in conjunctions)
|
||||
{
|
||||
var memory = new Dictionary<string, object>();
|
||||
foreach (var binding in conjunction.Bindings)
|
||||
{
|
||||
memory.Add(binding.Key, binding.Value.Value);
|
||||
}
|
||||
var trigger = tree.AddTrigger(conjunction.Expression, conjunction.Bindings);
|
||||
var matches = tree.Matches(memory);
|
||||
Assert.IsTrue(matches.Count() == 1);
|
||||
var first = matches.First().Clause;
|
||||
foreach (var match in matches)
|
||||
{
|
||||
Assert.AreEqual(RelationshipType.Equal, first.Relationship(match.Clause, tree.Comparers));
|
||||
}
|
||||
}
|
||||
Assert.AreEqual(numSingletons + numConjunctions, tree.TotalTriggers);
|
||||
|
||||
// Add disjunctions
|
||||
predicates.AddRange(conjunctions);
|
||||
var disjunctions = _generator.GenerateDisjunctions(predicates, numDisjunctions, minClause, maxClause);
|
||||
foreach (var disjunction in disjunctions)
|
||||
{
|
||||
tree.AddTrigger(disjunction.Expression, disjunction.Bindings);
|
||||
}
|
||||
Assert.AreEqual(numSingletons + numConjunctions + numDisjunctions, tree.TotalTriggers);
|
||||
|
||||
var all = new List<ExpressionInfo>(predicates);
|
||||
all.AddRange(disjunctions);
|
||||
|
||||
// Add optionals
|
||||
var optionals = _generator.GenerateOptionals(all, numOptionals, minClause, maxClause);
|
||||
foreach(var optional in optionals)
|
||||
{
|
||||
tree.AddTrigger(optional.Expression, optional.Bindings);
|
||||
}
|
||||
Assert.AreEqual(numSingletons + numConjunctions + numDisjunctions + numOptionals, tree.TotalTriggers);
|
||||
all.AddRange(optionals);
|
||||
|
||||
// Add quantifiers
|
||||
var quantified = _generator.GenerateQuantfiers(all, numQuantifiers, maxClause, maxExpansion, maxQuantifiers);
|
||||
foreach(var expr in quantified)
|
||||
{
|
||||
tree.AddTrigger(expr.Expression, expr.Bindings, expr.Quantifiers.ToArray());
|
||||
}
|
||||
Assert.AreEqual(numSingletons + numConjunctions + numDisjunctions + numOptionals + numQuantifiers, tree.TotalTriggers);
|
||||
all.AddRange(quantified);
|
||||
|
||||
var nots = _generator.GenerateNots(all, numNots);
|
||||
foreach(var expr in nots)
|
||||
{
|
||||
tree.AddTrigger(expr.Expression, expr.Bindings, expr.Quantifiers.ToArray());
|
||||
}
|
||||
Assert.AreEqual(numSingletons + numConjunctions + numDisjunctions + numOptionals + numQuantifiers + numNots, tree.TotalTriggers);
|
||||
|
||||
VerifyTree(tree);
|
||||
|
||||
// Test matches
|
||||
foreach (var predicate in predicates)
|
||||
{
|
||||
var memory = new Dictionary<string, object>();
|
||||
foreach (var binding in predicate.Bindings)
|
||||
{
|
||||
memory.Add(binding.Key, binding.Value.Value);
|
||||
}
|
||||
var matches = tree.Matches(memory).ToList();
|
||||
for (var i = 0; i < matches.Count; ++i)
|
||||
{
|
||||
var first = matches[i];
|
||||
for (var j = i + 1; j < matches.Count; ++j)
|
||||
{
|
||||
var second = matches[j];
|
||||
var reln = first.Relationship(second);
|
||||
Assert.AreEqual(RelationshipType.Incomparable, reln);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tree.GenerateGraph("tree.dot");
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче