diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md index 0962e9e..faa7db9 100644 --- a/BREAKING_CHANGES.md +++ b/BREAKING_CHANGES.md @@ -23,7 +23,7 @@ You can read the original problem specification and discussion [here](https://g If your project has been impacted by this particular breaking change, you might consider reevaluate the correctness of the affected tests. -## Error Messages +## Error Message Quotes Some error messages surrounded actual values in double quotes. Others surrounded the values in single quotes. In version 3.0.0 *all* values are surrounded in single quotes. @@ -33,7 +33,27 @@ Consistency. ### Fix -Amend all effected tests to expect single quotes instead of double quotes. +Amend any affected tests to expect single quotes instead of double quotes. + +## Error Message Lambda Expression + +In error messages, lambda expressions arguments are now surrounded in a pair of parentheses. For example: + + ... to pass the given condition (model => (model.Property1 != null)) + +will now look like this: + + ... to pass the given condition ((model) => (model.Property1 != null)) + +As you can see, the argument called `model` is now surrounded in parentheses. + +###Reason + +FluentMVCTesting now uses [ExpressionToString](https://github.com/JakeGinnivan/ExpressionToString) to humanize expression trees. ExpressionToString surrounds arguments in parentheses. + +###Fix + +Amend any affected tests to expect lambda expression arguments to be surrounded in parentheses. # Version 2.0.0 diff --git a/TestStack.FluentMVCTesting.Tests/Internal/ExpressionInspectorTests.cs b/TestStack.FluentMVCTesting.Tests/Internal/ExpressionInspectorTests.cs deleted file mode 100644 index a27155a..0000000 --- a/TestStack.FluentMVCTesting.Tests/Internal/ExpressionInspectorTests.cs +++ /dev/null @@ -1,174 +0,0 @@ -using NUnit.Framework; -using System; -using System.Linq.Expressions; -using TestStack.FluentMVCTesting.Internal; - -namespace TestStack.FluentMVCTesting.Tests.Internal -{ - [TestFixture] - public class ExpressionInspectorShould - { - [Test] - public void Correctly_parse_equality_comparison_with_string_operands() - { - Expression> func = text => text == "any"; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("text => (text == \"any\")", actual); - } - - [Test] - public void Correctly_parse_equality_comparison_with_int_operands() - { - Expression> func = number => number == 5; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("number => (number == 5)", actual); - } - - [Test] - public void Correctly_parse_equality_comparison_with_property_operand() - { - Expression> func = post => post.Title == "A"; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("post => (post.Title == \"A\")", actual); - } - - [Test] - public void Correctly_parse_equality_comparison_with_two_property_operands() - { - Expression> func = post => - post.Title == post.Slug; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("post => (post.Title == post.Slug)", actual); - } - - [Test] - public void Correctly_parse_equality_comparison_with_captured_constant_operand() - { - const int Number = 5; - Expression> func = number => number == Number; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual(string.Concat("number => (number == ", Number, ")"), actual); - } - - [Test] - public void Correctly_parse_inequality_comparison() - { - Expression> func = number => number != 5; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("number => (number != 5)", actual); - } - - [Test] - public void Correctly_parse_relational_comparison() - { - Expression> func = number => number < 5; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("number => (number < 5)", actual); - } - - [Test] - public void Correctly_parse_expression_whose_source_text_contains_parentheses() - { - Expression> func = post => post.Title.Contains(""); - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("post => post.Title.Contains(\"\")", actual); - } - - [Test] - public void Correctly_parse_null_coalescing_operator() - { - Expression> func = - text => text == ("" ?? "a"); - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("text => (text == (\"\" ?? \"a\"))", actual); - } - - [Test] - public void Correctly_parse_conditional_or_operator() - { - Expression> func = - text => text == "any" || text.Length == 3; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("text => ((text == \"any\") || (text.Length == 3))", actual); - } - - [Test] - public void Correctly_parse_two_conditional_or_operators() - { - Expression> func = - text => text == "any" || text.Length == 3 || text.Length == 9; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual( - "text => (((text == \"any\") || (text.Length == 3)) || (text.Length == 9))", actual); - } - - [Test] - public void Correctly_parse_conditional_and_operator() - { - Expression> func = - text => text == "any" && text.Length == 3; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("text => ((text == \"any\") && (text.Length == 3))", actual); - } - - [Test] - public void Correctly_parse_logical_and_operator() - { - Expression> func = - text => text == "any" & text.Length == 3; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("text => ((text == \"any\") & (text.Length == 3))", actual); - } - - [Test] - public void Correctly_parse_logical_or_operator() - { - Expression> func = - text => text == "any" | text.Length == 3; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("text => ((text == \"any\") | (text.Length == 3))", actual); - } - - [Test] - public void Not_mistake_property_called_OrElse_for_conditional_or_operator() - { - Expression> func = - post => post.Title == "" || post.OrElse == ""; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("post => ((post.Title == \"\") || (post.OrElse == \"\"))", actual); - } - - [Test] - public void Correctly_parse_logical_xor_operator() - { - Expression> func = - text => text == "any" ^ text.Length == 3; - ExpressionInspector sut = new ExpressionInspector(); - var actual = sut.Inspect(func); - Assert.AreEqual("text => ((text == \"any\") ^ (text.Length == 3))", actual); - } - } - - public class PostViewModel - { - public string Title { get; set; } - public string Slug { get; set; } - - public string OrElse { get; set; } - } -} \ No newline at end of file diff --git a/TestStack.FluentMVCTesting.Tests/TestStack.FluentMVCTesting.Tests.csproj b/TestStack.FluentMVCTesting.Tests/TestStack.FluentMVCTesting.Tests.csproj index 2db7429..5129022 100644 --- a/TestStack.FluentMVCTesting.Tests/TestStack.FluentMVCTesting.Tests.csproj +++ b/TestStack.FluentMVCTesting.Tests/TestStack.FluentMVCTesting.Tests.csproj @@ -89,7 +89,6 @@ - diff --git a/TestStack.FluentMVCTesting.Tests/ViewResultTestTests.cs b/TestStack.FluentMVCTesting.Tests/ViewResultTestTests.cs index 9da2a2c..885699d 100644 --- a/TestStack.FluentMVCTesting.Tests/ViewResultTestTests.cs +++ b/TestStack.FluentMVCTesting.Tests/ViewResultTestTests.cs @@ -72,7 +72,7 @@ public void Check_for_invalid_model_using_predicate() var exception = Assert.Throws(() => _viewResultTest.WithModel(m => m.Property1 == null) ); - Assert.That(exception.Message, Is.EqualTo(string.Format("Expected view model {{\"Property1\":\"{0}\",\"Property2\":{1}}} to pass the given condition (m => (m.Property1 == null)), but it failed.", _model.Property1, _model.Property2))); + Assert.That(exception.Message, Is.EqualTo(string.Format("Expected view model {{\"Property1\":\"{0}\",\"Property2\":{1}}} to pass the given condition ((m) => (m.Property1 == null)), but it failed.", _model.Property1, _model.Property2))); } [Test] @@ -81,7 +81,7 @@ public void Check_for_invalid_model_using_predicate_with_conditional_or() var exception = Assert.Throws(() => _viewResultTest.WithModel(m => m.Property1 == null || m.Property2 == 1) ); - Assert.That(exception.Message, Is.EqualTo(string.Format("Expected view model {{\"Property1\":\"{0}\",\"Property2\":{1}}} to pass the given condition (m => ((m.Property1 == null) || (m.Property2 == 1))), but it failed.", _model.Property1, _model.Property2))); + Assert.That(exception.Message, Is.EqualTo(string.Format("Expected view model {{\"Property1\":\"{0}\",\"Property2\":{1}}} to pass the given condition ((m) => ((m.Property1 == null) || (m.Property2 == 1))), but it failed.", _model.Property1, _model.Property2))); } [Test] @@ -91,7 +91,18 @@ public void Check_for_invalid_model_using_predicate_with_primitive_operand() var exception = Assert.Throws(() => _viewResultTest.WithModel(m => m == "ab") ); - Assert.That(exception.Message, Is.EqualTo(string.Format("Expected view model \"{0}\" to pass the given condition (m => (m == \"ab\")), but it failed.", _viewResult.ViewData.Model))); + Assert.That(exception.Message, Is.EqualTo(string.Format("Expected view model \"{0}\" to pass the given condition ((m) => (m == \"ab\")), but it failed.", _viewResult.ViewData.Model))); + } + + [Test] + public void Check_for_invalid_model_using_predicate_with_captured_var_operand() + { + var capturedOuterVar = "ab"; + _viewResult.ViewData.Model = "abc"; + var exception = Assert.Throws(() => + _viewResultTest.WithModel(m => m == capturedOuterVar) + ); + Assert.That(exception.Message, Is.EqualTo(string.Format("Expected view model \"{0}\" to pass the given condition ((m) => (m == capturedOuterVar)), but it failed.", _viewResult.ViewData.Model))); } [Test] diff --git a/TestStack.FluentMvcTesting/App_Packages/ExpressionStringBuilder.0.9.2/ExpressionStringBuilder.cs b/TestStack.FluentMvcTesting/App_Packages/ExpressionStringBuilder.0.9.2/ExpressionStringBuilder.cs new file mode 100644 index 0000000..9ef5f2a --- /dev/null +++ b/TestStack.FluentMvcTesting/App_Packages/ExpressionStringBuilder.0.9.2/ExpressionStringBuilder.cs @@ -0,0 +1,283 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using System.Text; + +// ReSharper disable CheckNamespace +namespace ExpressionToString +{ + class ExpressionStringBuilder : ExpressionVisitor + { + // ReSharper disable InconsistentNaming + private readonly StringBuilder builder = new StringBuilder(); + private readonly bool trimLongArgumentList; + bool skipDot; + + private ExpressionStringBuilder(bool trimLongArgumentList) + { + this.trimLongArgumentList = trimLongArgumentList; + } + + /// + /// A nicely formatted ToString of an expression + /// + /// The expression to format + /// If true will replace large (>3) argument lists with an elipsis + /// + public static string ToString(Expression expression, bool trimLongArgumentList = false) + { + var visitor = new ExpressionStringBuilder(trimLongArgumentList); + visitor.Visit(expression); + var s = visitor.builder.ToString(); + return s; + } + + + protected override Expression VisitLambda(Expression node) + { + if (node.Parameters.Any()) + { + Out("("); + Out(String.Join(",", node.Parameters.Select(n => n.Name))); + Out(") => "); + } + Visit(node.Body); + return node; + } + + protected override Expression VisitInvocation(InvocationExpression node) + { + var visitInvocation = base.VisitInvocation(node); + Out("()"); + return visitInvocation; + } + + protected override Expression VisitBinary(BinaryExpression node) + { + Out("("); + Visit(node.Left); + Out(" "); + Out(ToString(node.NodeType)); + Out(" "); + Visit(node.Right); + Out(")"); + return node; + } + + protected override Expression VisitParameter(ParameterExpression node) + { + Out(node.Name); + return node; + } + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Expression.NodeType == ExpressionType.Constant) + { + Visit(node.Expression); + if (skipDot) + { + skipDot = false; + Out(node.Member.Name); + } + else + Out("." + node.Member.Name); + } + else + { + Visit(node.Expression); + Out("." + node.Member.Name); + } + + return node; + } + + private static bool CheckIfAnonymousType(Type type) + { + // hack: the only way to detect anonymous types right now + var isDefined = type.IsDefined(typeof(CompilerGeneratedAttribute), false); + return isDefined + && (type.IsGenericType && type.Name.Contains("AnonymousType") || type.Name.Contains("DisplayClass")) + && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$")); + } + + protected override Expression VisitConstant(ConstantExpression node) + { + if (CheckIfAnonymousType(node.Type)) + { + skipDot = true; + return node; + } + if (node.Value == null) + { + Out("null"); + } + else + { + var stringValue = node.Value as string; + if (stringValue != null) + { + Out("\"" + stringValue + "\""); + } + else + { + if (node.Value is bool) + { + Out(node.Value.ToString().ToLower()); + } + else + { + var valueToString = node.Value.ToString(); + var type = node.Value.GetType(); + if (type.FullName != valueToString) + { + Out(valueToString); + } + else + { + skipDot = true; + } + } + } + } + + return node; + } + + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Convert) + { + Visit(node.Operand); + return node; + } + if (node.NodeType == ExpressionType.Not) + { + Out("!"); + Visit(node.Operand); + return node; + } + if (node.NodeType == ExpressionType.TypeAs) + { + Out("("); + Visit(node.Operand); + Out(" As " + node.Type.Name + ")"); + return node; + } + + return base.VisitUnary(node); + } + + protected override Expression VisitNew(NewExpression node) + { + Out("new " + node.Type.Name + "("); + VisitArguments(node.Arguments.ToArray()); + Out(")"); + return node; + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + Visit(node.Object); + + if (!skipDot && !node.Method.IsStatic) + { + Out("."); + skipDot = false; + } + Out(node.Method.Name + "("); + var args = node.Arguments.ToArray(); + if (args.Length > 3 && trimLongArgumentList) + { + Out("..."); + } + else + { + VisitArguments(args); + } + + Out(")"); + return node; + } + + private void VisitArguments(Expression[] arguments) + { + int argindex = 0; + while (argindex < arguments.Length) + { + Visit(arguments[argindex]); + argindex++; + + if (argindex < arguments.Length) + { + Out(", "); + } + } + } + + protected override Expression VisitConditional(ConditionalExpression node) + { + Out("IIF("); + Visit(node.Test); + Out(", "); + Visit(node.IfTrue); + Out(", "); + Visit(node.IfFalse); + Out(")"); + return node; + } + + private static string ToString(ExpressionType type) + { + switch (type) + { + case ExpressionType.Add: + return "+"; + case ExpressionType.And: + return "&"; + case ExpressionType.AndAlso: + return "&&"; + case ExpressionType.Divide: + return "/"; + case ExpressionType.Equal: + return "=="; + case ExpressionType.GreaterThan: + return ">"; + case ExpressionType.GreaterThanOrEqual: + return ">="; + case ExpressionType.LessThan: + return "<"; + case ExpressionType.LessThanOrEqual: + return "<="; + case ExpressionType.Modulo: + return "%"; + case ExpressionType.Multiply: + return "*"; + case ExpressionType.Negate: + return "-"; + case ExpressionType.Not: + return "!"; + case ExpressionType.NotEqual: + return "!="; + case ExpressionType.Or: + return "|"; + case ExpressionType.OrElse: + return "||"; + case ExpressionType.Subtract: + return "-"; + case ExpressionType.Coalesce: + return "??"; + case ExpressionType.ExclusiveOr: + return "^"; + default: + throw new NotImplementedException(); + } + } + + private void Out(string s) + { + builder.Append(s); + } + } +} diff --git a/TestStack.FluentMvcTesting/Internal/ExpressionInspector.cs b/TestStack.FluentMvcTesting/Internal/ExpressionInspector.cs deleted file mode 100644 index 4637792..0000000 --- a/TestStack.FluentMvcTesting/Internal/ExpressionInspector.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Linq.Expressions; - -namespace TestStack.FluentMVCTesting.Internal -{ - internal class ExpressionInspector - { - internal string Inspect(LambdaExpression expression) - { - return expression.ToString() - .Replace(" OrElse ", " || ") - .Replace(" AndAlso ", " && ") - .Replace(" Or ", " | ") - .Replace(" And ", " & "); - } - } -} \ No newline at end of file diff --git a/TestStack.FluentMvcTesting/Properties/AssemblyInfo.cs b/TestStack.FluentMvcTesting/Properties/AssemblyInfo.cs index 7fe02d7..7e935da 100644 --- a/TestStack.FluentMvcTesting/Properties/AssemblyInfo.cs +++ b/TestStack.FluentMvcTesting/Properties/AssemblyInfo.cs @@ -34,6 +34,3 @@ // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: InternalsVisibleTo("TestStack.FluentMVCTesting.Tests")] -[assembly: InternalsVisibleTo("TestStack.FluentMVCTesting.Mvc3.Tests")] -[assembly: InternalsVisibleTo("TestStack.FluentMVCTesting.Mvc4.Tests")] diff --git a/TestStack.FluentMvcTesting/TestStack.FluentMVCTesting.csproj b/TestStack.FluentMvcTesting/TestStack.FluentMVCTesting.csproj index 5789a34..b9925ec 100644 --- a/TestStack.FluentMvcTesting/TestStack.FluentMVCTesting.csproj +++ b/TestStack.FluentMvcTesting/TestStack.FluentMVCTesting.csproj @@ -76,6 +76,7 @@ + @@ -83,7 +84,6 @@ - diff --git a/TestStack.FluentMvcTesting/ViewResultTest.cs b/TestStack.FluentMvcTesting/ViewResultTest.cs index ab52506..74db677 100644 --- a/TestStack.FluentMvcTesting/ViewResultTest.cs +++ b/TestStack.FluentMvcTesting/ViewResultTest.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using System.Web.Helpers; using System.Web.Mvc; -using TestStack.FluentMVCTesting.Internal; +using ExpressionToString; namespace TestStack.FluentMVCTesting { @@ -47,9 +47,8 @@ public ModelTest WithModel(Expression> predic var model = _viewResult.Model as TModel; - var inspector = new ExpressionInspector(); var modelLex = Json.Encode(model); - var predicateLex = inspector.Inspect(predicate); + var predicateLex = ExpressionStringBuilder.ToString(predicate); var compiledPredicate = predicate.Compile(); if (!compiledPredicate(model)) diff --git a/TestStack.FluentMvcTesting/packages.config b/TestStack.FluentMvcTesting/packages.config index 2b0a9f5..fbce3fe 100644 --- a/TestStack.FluentMvcTesting/packages.config +++ b/TestStack.FluentMvcTesting/packages.config @@ -1,5 +1,6 @@  +