Skip to content

InvalidOperationException while asserting Expressions with custom EqualityComparer #2175

@ghost

Description

Description

The class FluentAssertions.Formatting.PredicateLambdaExpressionValueFormatter throws an exception when an assertion fails between two lambda expressions that initialize their members from a parameter.

Reproduction Steps

using FluentAssertions;
using System.Linq.Expressions;
using Xunit;

namespace FluentAssertionTests;

public class FluentAssertionExceptionTest
{
    [Fact]
    public void BeEquivalentTo_WithEqualityComparer_ShouldNotThrowInvalidOperationException()
    {
        // Arrange
        Expression a = (string arg) => new TestItem { Value = arg };
        Expression b = (string arg) => new TestItem { Value = arg };

        // Act
        a.Should().BeEquivalentTo(b, options => options.Using(new ExpressionEqualityComparer()));
    }

    private record TestItem
    {
        public string? Value { get; set; }
    }

    private class ExpressionEqualityComparer : IEqualityComparer<Expression>
    {
        public bool Equals(Expression? x, Expression? y) => false;

        public int GetHashCode(Expression obj) => default;
    }
}

Expected behavior

Xunit.Sdk.XunitException
Expected a to be equal to TestItem { Value =  arg } according to "FluentAssertionTests.FluentAssertionExceptionTest+ExpressionEqualityComparer", but TestItem { Value =  arg  } was not.

Actual behavior

System.InvalidOperationException
When called from 'VisitMemberInit', rewriting a node of type 'System.Linq.Expressions.NewExpression' must return a non-null value of the same type. Alternatively, override 'VisitMemberInit' and change it to not visit children of this type.
   at System.Linq.Expressions.ExpressionVisitor.VisitAndConvert[T](T node, String callerName)
   at System.Linq.Expressions.ExpressionVisitor.VisitMemberInit(MemberInitExpression node)
   at FluentAssertions.Formatting.PredicateLambdaExpressionValueFormatter.ConstantSubExpressionReductionVisitor.Visit(Expression node)
   at FluentAssertions.Formatting.PredicateLambdaExpressionValueFormatter.ReduceConstantSubExpressions(Expression expression)
   at FluentAssertions.Formatting.PredicateLambdaExpressionValueFormatter.Format(Object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild)
   at FluentAssertions.Formatting.Formatter.Format(Object value, FormattedObjectGraph output, FormattingContext context, FormatChild formatChild)
   at FluentAssertions.Formatting.Formatter.ToString(Object value, FormattingOptions options)
   at FluentAssertions.Execution.MessageBuilder.<FormatArgumentPlaceholders>b__6_0(Object a)
   at System.Linq.Enumerable.SelectArrayIterator`2.ToArray()
   at FluentAssertions.Execution.MessageBuilder.FormatArgumentPlaceholders(String failureMessage, Object[] failureArgs)
   at FluentAssertions.Execution.MessageBuilder.Build(String message, Object[] messageArgs, String reason, ContextDataItems contextData, String identifier, String fallbackIdentifier)
   at FluentAssertions.Execution.AssertionScope.<>c__DisplayClass38_0.<FailWith>b__0()
   at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
   at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
   at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args)
   at FluentAssertions.Execution.GivenSelector`1.FailWith(String message, Object[] args)
   at FluentAssertions.Equivalency.Steps.EqualityComparerEquivalencyStep`1.Handle(Comparands comparands, IEquivalencyValidationContext context, IEquivalencyValidator nestedValidator)
   at FluentAssertions.Equivalency.Steps.RunAllUserStepsEquivalencyStep.Handle(Comparands comparands, IEquivalencyValidationContext context, IEquivalencyValidator nestedValidator)
   at FluentAssertions.Equivalency.EquivalencyValidator.RunStepsUntilEquivalencyIsProven(Comparands comparands, IEquivalencyValidationContext context)
   at FluentAssertions.Equivalency.EquivalencyValidator.RecursivelyAssertEquality(Comparands comparands, IEquivalencyValidationContext context)
   at FluentAssertions.Equivalency.EquivalencyValidator.AssertEquality(Comparands comparands, EquivalencyValidationContext context)
   at FluentAssertions.Primitives.ObjectAssertions`2.BeEquivalentTo[TExpectation](TExpectation expectation, Func`2 config, String because, Object[] becauseArgs)
   at FluentAssertionTests.FluentAssertionExceptionTest.BeEquivalentTo_WithEqualityComparer_ShouldNotThrowInvalidOperationException()

Regression?

No response

Known Workarounds

Create a custom ValueFormatter to ignore the default PredicateLambdaExpressionValueFormatter:

public class FluentAssertionExceptionTest
{
    public FluentAssertionExceptionTest()
    {
        Formatter.AddFormatter(new LambdaExpressionFormatter());
    }

    private class LambdaExpressionFormatter : DefaultValueFormatter
    {
        public override bool CanHandle(object value) => value is LambdaExpression;
    }

    ...
}

Configuration

.NET 7.0.100
FluentAssertions 6.10.0

Other information

The exception seems to be because the NewExpression under an InitMemberExpression doesn't contain any ParameterExpression and is considered a constant.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions