Skip to content

Be() throws System.FormatException when expected value contains "{" #3147

@ernestasju

Description

@ernestasju

Description

Hi,

I was writing tests for my code formatter and came across this weird behavior - FluentAssertions use string.Format without sanitizing the values (but that is not optimal solution IMHO).

It is kind of annoying:

This works just fine:

// Ok
[Fact]
public void FormatParameters_MultpleParameters_MovedParametersToTheirOwnLines()
{
    _formatter.Format("""
        codeunit 0 C
        {
            procedure Nop(X: Integer; Y: Integer; Z: Integer)
            begin
            end;
        }
        """).Should().Be("""
        codeunit 0 C
        {
            procedure Nop(
                X: Integer;
                Y: Integer;
                Z: Integer)
            begin
            end;
        }
        """);
}

This throws throws System.FormatException.

[Fact]
public void FormatParameters_MultipleParameters_ShrinksUnnecessaryWhitespace()
{
    _formatter.Format("""
        codeunit 0 C
        {
            procedure Nop(

        // X
        X: Integer;

        // Y
        Y: Integer;

        // Z
        Z: Integer)
            begin
            end;
        }
        """).Should().Be("""
        codeunit 0 C            
        {
            procedure Nop(
                // X
                X: Integer;
                // Y
                Y: Integer;
                // Z
                Z: Integer)
            begin
            end;
        }
        """;
}

Thanks

Reproduction Steps

using FluentAssertions;
using Xunit.Sdk;

namespace DemonCore.Tests;

public class DemonCoreTests
{
    [Fact]
    public void NotOk()
    {
        DemonCore(80).Should().Throw<XunitException>(); // Be() throws System.FormatException
    }

    [Fact]
    public void Ok()
    {
        DemonCore(79).Should().Throw<XunitException>(); // Be() does not throw but fails to format something.
        /*
        Actual assertion message if we just run `DemonCore(79)();`.
    
        **WARNING** failure message 'Expected actual to be a match with the expectation, but it differs at index 0:
           ↓ (actual)
          "{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
          ""
           ↑ (expected)' could not be formatted with string.Format
           at System.Text.ValueStringBuilder.AppendFormatHelper(IFormatProvider provider, String format, ReadOnlySpan`1 args)
           at System.String.FormatHelper(IFormatProvider provider, String format, ReadOnlySpan`1 args)
           at System.String.Format(IFormatProvider provider, String format, Object[] args)
           at FluentAssertions.Execution.FailureMessageFormatter.FormatArgumentPlaceholders(String failureMessage, Object[] failureArgs)
        */
    }

    private static Action DemonCore(int number) =>
        () =>
        {
            var actual = "{" + new string('x', number);
            var expected = "";
            actual.Should().Be(expected);
        };
}
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.4" />
    <PackageReference Include="FluentAssertions" Version="8.8.0" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
    <PackageReference Include="xunit" Version="2.9.3" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>

</Project>

P.S.: The fact that it fails around 79-80 is funny.

Expected behavior

Be should throw only XunitException for string comparisons - strings are either equal or not equal.

IMHO, building final assertion message incrementally using multiple string.Format calls feels too hacky.
Probably, a better approach would be to create some tree-ish type and walking over it to build the final string. But I'm not regularly programming in C# so don't listen to me. :)

Actual behavior

In reproduction tests:

  • The NotOk test fails - the actual string is "{" and then 80+ "x"s and it is compared to an empty string.
  • The Ok test succeeds but it has string.Format error in the assertion message - the actual string is "{" and 79 "x"s.

Full dotnet test result:

PS C:\MY\NotWork\UpdateAlWorkspaceThing> dotnet test
Restore complete (0,5s)
  UpdateAlWorkspaceThing.Tests net10.0 succeeded (0,1s) → UpdateAlWorkspaceThing.Tests\bin\Debug\net10.0\UpdateAlWorkspaceThing.Tests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.5+1b188a7b0a (64-bit .NET 10.0.2)
[xUnit.net 00:00:00.05]   Discovering: UpdateAlWorkspaceThing.Tests
[xUnit.net 00:00:00.08]   Discovered:  UpdateAlWorkspaceThing.Tests
[xUnit.net 00:00:00.10]   Starting:    UpdateAlWorkspaceThing.Tests
     Warning:
     The component "Fluent Assertions" is governed by the rules defined in the Xceed License Agreement and
     the Xceed Fluent Assertions Community License. You may use Fluent Assertions free of charge for
     non-commercial use only. An active subscription is required to use Fluent Assertions for commercial use.
     Please contact Xceed Sales mailto:sales@xceed.com to acquire a subscription at a very low cost.
     A paid commercial license supports the development and continued increasing support of
     Fluent Assertions users under both commercial and community licenses. Help us
     keep Fluent Assertions at the forefront of unit testing.
     For more information, visit https://xceed.com/products/unit-testing/fluent-assertions/
[xUnit.net 00:00:00.15]     UpdateAlWorkspaceThing.Tests.DemonCoreTests.NotOk [FAIL]
[xUnit.net 00:00:00.15]       Expected a <Xunit.Sdk.XunitException> to be thrown, but found <System.FormatException>:
[xUnit.net 00:00:00.15]       System.FormatException: Input string was not in a correct format. Failure to parse near offset 59. Expected an ASCII digit.
[xUnit.net 00:00:00.15]          at System.Text.StringBuilder.AppendFormat(IFormatProvider provider, String format, ReadOnlySpan`1 args)
[xUnit.net 00:00:00.15]          at System.Text.StringBuilder.AppendFormat(IFormatProvider provider, String format, Object[] args)
[xUnit.net 00:00:00.15]          at FluentAssertions.Primitives.StringEqualityStrategy.GetMismatchSegmentForLongStrings(String subject, String expected, Int32 firstIndexOfMismatch)
[xUnit.net 00:00:00.15]          at FluentAssertions.Primitives.StringEqualityStrategy.AssertForEquality(AssertionChain assertionChain, String subject, String expected)
[xUnit.net 00:00:00.15]          at FluentAssertions.Primitives.StringValidator.Validate(String subject, String expected)
[xUnit.net 00:00:00.15]          at FluentAssertions.Primitives.StringAssertions`1.Be(String expected, String because, Object[] becauseArgs)
[xUnit.net 00:00:00.15]          at UpdateAlWorkspaceThing.Tests.DemonCoreTests.<>c__DisplayClass2_0.<DemonCore>b__0() in C:\MY\NotWork\UpdateAlWorkspaceThing\UpdateAlWorkspaceThing.Tests\CustomFormatterTests.cs:line 25
[xUnit.net 00:00:00.15]          at FluentAssertions.Specialized.ActionAssertions.InvokeSubject()
[xUnit.net 00:00:00.15]          at FluentAssertions.Specialized.DelegateAssertions`2.InvokeSubjectWithInterception().
[xUnit.net 00:00:00.15]       Stack Trace:
[xUnit.net 00:00:00.16]            at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
[xUnit.net 00:00:00.16]            at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message)
[xUnit.net 00:00:00.16]            at FluentAssertions.Execution.AssertionScope.AddPreFormattedFailure(String formattedFailureMessage)
[xUnit.net 00:00:00.16]            at FluentAssertions.Execution.AssertionChain.FailWith(Func`1 getFailureReason)
[xUnit.net 00:00:00.16]            at FluentAssertions.Execution.AssertionChain.FailWith(Func`1 getFailureReason)
[xUnit.net 00:00:00.16]            at FluentAssertions.Execution.AssertionChain.FailWith(String message, Object[] args)
[xUnit.net 00:00:00.16]            at FluentAssertions.Specialized.DelegateAssertionsBase`2.<>c__DisplayClass8_0`1.<ThrowInternal>b__0(AssertionChain chain)
[xUnit.net 00:00:00.16]            at FluentAssertions.Execution.AssertionChain.WithExpectation(String message, Action`1 chain, Object[] args)
[xUnit.net 00:00:00.16]            at FluentAssertions.Execution.AssertionChain.WithExpectation(String message, Object arg1, Action`1 chain)
[xUnit.net 00:00:00.16]            at FluentAssertions.Specialized.DelegateAssertionsBase`2.ThrowInternal[TException](Exception exception, String because, Object[] becauseArgs)
[xUnit.net 00:00:00.16]            at FluentAssertions.Specialized.DelegateAssertions`2.Throw[TException](String because, Object[] becauseArgs)
[xUnit.net 00:00:00.16]         C:\MY\NotWork\UpdateAlWorkspaceThing\UpdateAlWorkspaceThing.Tests\CustomFormatterTests.cs(11,0): at UpdateAlWorkspaceThing.Tests.DemonCoreTests.NotOk()
[xUnit.net 00:00:00.16]            at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
[xUnit.net 00:00:00.16]            at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
[xUnit.net 00:00:00.16]   Finished:    UpdateAlWorkspaceThing.Tests
  UpdateAlWorkspaceThing.Tests test net10.0 failed with 1 error(s) (0,7s)
    C:\MY\NotWork\UpdateAlWorkspaceThing\UpdateAlWorkspaceThing.Tests\CustomFormatterTests.cs(11): error TESTERROR:
      UpdateAlWorkspaceThing.Tests.DemonCoreTests.NotOk (6ms): Error Message: Expected a <Xunit.Sdk.XunitException> to be thrown, but found <System.FormatExcep
      tion>:
      System.FormatException: Input string was not in a correct format. Failure to parse near offset 59. Expected an ASCII digit.
         at System.Text.StringBuilder.AppendFormat(IFormatProvider provider, String format, ReadOnlySpan`1 args)
         at System.Text.StringBuilder.AppendFormat(IFormatProvider provider, String format, Object[] args)
         at FluentAssertions.Primitives.StringEqualityStrategy.GetMismatchSegmentForLongStrings(String subject, String expected, Int32 firstIndexOfMismatch)
         at FluentAssertions.Primitives.StringEqualityStrategy.AssertForEquality(AssertionChain assertionChain, String subject, String expected)
         at FluentAssertions.Primitives.StringValidator.Validate(String subject, String expected)
         at FluentAssertions.Primitives.StringAssertions`1.Be(String expected, String because, Object[] becauseArgs)
         at UpdateAlWorkspaceThing.Tests.DemonCoreTests.<>c__DisplayClass2_0.<DemonCore>b__0() in C:\MY\NotWork\UpdateAlWorkspaceThing\UpdateAlWorkspaceThing.T
      ests\CustomFormatterTests.cs:line 25
         at FluentAssertions.Specialized.ActionAssertions.InvokeSubject()
         at FluentAssertions.Specialized.DelegateAssertions`2.InvokeSubjectWithInterception().
      Stack Trace:
         at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
         at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message)
         at FluentAssertions.Execution.AssertionScope.AddPreFormattedFailure(String formattedFailureMessage)
         at FluentAssertions.Execution.AssertionChain.FailWith(Func`1 getFailureReason)
         at FluentAssertions.Execution.AssertionChain.FailWith(Func`1 getFailureReason)
         at FluentAssertions.Execution.AssertionChain.FailWith(String message, Object[] args)
         at FluentAssertions.Specialized.DelegateAssertionsBase`2.<>c__DisplayClass8_0`1.<ThrowInternal>b__0(AssertionChain chain)
         at FluentAssertions.Execution.AssertionChain.WithExpectation(String message, Action`1 chain, Object[] args)
         at FluentAssertions.Execution.AssertionChain.WithExpectation(String message, Object arg1, Action`1 chain)
         at FluentAssertions.Specialized.DelegateAssertionsBase`2.ThrowInternal[TException](Exception exception, String because, Object[] becauseArgs)
         at FluentAssertions.Specialized.DelegateAssertions`2.Throw[TException](String because, Object[] becauseArgs)
         at UpdateAlWorkspaceThing.Tests.DemonCoreTests.NotOk() in C:\MY\NotWork\UpdateAlWorkspaceThing\UpdateAlWorkspaceThing.Tests\CustomFormatterTests.cs:li
      ne 11
         at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
         at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Test summary: total: 2; failed: 1; succeeded: 1; skipped: 0; duration: 0,7s
Build failed with 1 error(s) in 1,5s

Regression?

Partial - downgrading to 8.7.0 makes both tests pass, but it still has the string.Format error in the assertion message:

**WARNING** failure message 'Expected actual to be the same string, but they differ at index 0:
   ↓ (actual)
  "{xxxxxxxxxxxxxxxxxxx…"
  ""
   ↑ (expected).' could not be formatted with string.Format
   at System.Text.ValueStringBuilder.AppendFormatHelper(IFormatProvider provider, String format, ReadOnlySpan`1 args)
   at System.String.FormatHelper(IFormatProvider provider, String format, ReadOnlySpan`1 args)
   at System.String.Format(IFormatProvider provider, String format, Object[] args)
   at FluentAssertions.Execution.FailureMessageFormatter.FormatArgumentPlaceholders(String failureMessage, Object[] failureArgs)

The goal is not to make exception disappear. It is to fix the incorrect behavior.

Known Workarounds

Replacing "{" with "{{" before comparing.

Configuration

  • FluentAssertions v8.8.0
  • .NET 10.0

Other information

No response

Are you willing to help with a pull-request?

No

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions