FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Fix formatting exception when comparing strings containing braces by Copilot · Pull Request #3151 · fluentassertions/fluentassertions · GitHub

Fix formatting exception when comparing strings containing braces - #3151

Merged
dennisdoomen merged 3 commits into
mainfrom
copilot/fix-system-format-exception
Jan 26, 2026
Merged

Fix formatting exception when comparing strings containing braces#3151
dennisdoomen merged 3 commits into
mainfrom
copilot/fix-system-format-exception

Conversation

Copilot AI commented Jan 21, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Be() assertion throws FormatException instead of test failure when comparing strings containing { or } characters.

Root Cause

StringEqualityStrategy.GetMismatchSegmentForLongStrings used StringBuilder.AppendFormat with an interpolated string literal containing user values:

sb.AppendFormat(CultureInfo.InvariantCulture,
    $"""
     Full expectation:
     {expected.RenderAsIndentedBlock().AsNonFormattable()},
     
     Full subject:
     {subject.RenderAsIndentedBlock().AsNonFormattable()}
     """);

This causes double-interpretation: string interpolation inserts user values first, then AppendFormat interprets the result as a format string. Braces in user strings are mistaken for format placeholders.

Changes

  • Use string.Create() to build the interpolated string, then Append() the result
  • Remove unnecessary .AsNonFormattable() calls since we no longer use AppendFormat
string fullDetails = string.Create(CultureInfo.InvariantCulture,
    $"""
     Full expectation:
     {expected.RenderAsIndentedBlock()},
     
     Full subject:
     {subject.RenderAsIndentedBlock()}
     """);
sb.Append(fullDetails);

This ensures user strings are never reinterpreted as format strings.

Original prompt

This section details on the original issue you should resolve

<issue_title>Be() throws System.FormatException when expected value contains "{"</issue_title>
<issue_description>### 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.Tes...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes fluentassertions/fluentassertions#3147

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for you](https://github.com/fluentassertions/fluentassertions/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: dennisdoomen <572734+dennisdoomen@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix System.FormatException when value contains "{" Fix FormatException when comparing strings containing braces Jan 21, 2026
Copilot AI requested a review from dennisdoomen January 21, 2026 19:41

github-actions Bot commented Jan 21, 2026
edited
Loading

Copy link
Copy Markdown

Test Results

    37 files  ± 0      37 suites  ±0   2m 43s ⏱️ -22s
 6 290 tests + 2   6 290 ✅ + 2  0 💤 ±0  0 ❌ ±0 
39 086 runs  +12  39 086 ✅ +12  0 💤 ±0  0 ❌ ±0 

Results for commit e773f85. ± Comparison against base commit 4762446.

This pull request removes 10 and adds 10 tests. Note that renamed tests count towards both.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Primitives.StringAssertionSpecs+Be ‑ When_long_string_contains_braces_it_should_display_properly
FluentAssertions.Specs.Primitives.StringAssertionSpecs+Be ‑ When_string_contains_opening_brace_it_should_not_throw_format_exception
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HaveLength ‑ When_a_throwing_stream_should_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+HavePosition ‑ When_a_throwing_stream_should_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHaveLength ‑ When_a_throwing_stream_should_not_have_a_length_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetLengthExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)
FluentAssertions.Specs.Streams.StreamAssertionSpecs+NotHavePosition ‑ When_a_throwing_stream_should_not_have_a_position_it_should_fail(exception: System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'GetPositionExceptionMessage'.)

♻️ This comment has been updated with latest results.

coveralls commented Jan 21, 2026
edited
Loading

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 21300676482

Details

  • 4 of 4 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.001%) to 97.185%

Totals
Change from base Build 20931548150: 0.001%
Covered Lines: 12851
Relevant Lines: 13070

💛 - Coveralls

github-actions Bot commented Jan 21, 2026
edited
Loading

Copy link
Copy Markdown

Qodana for .NET

It seems all right 👌

No new problems were found according to the checks applied

💡 Qodana analysis was run in the pull request mode: only the changed files were checked
☁️ View the detailed Qodana report

Contact Qodana team

Contact us at qodana-support@jetbrains.com

dennisdoomen marked this pull request as ready for review January 23, 2026 20:45
Co-authored-by: dennisdoomen <572734+dennisdoomen@users.noreply.github.com>
dennisdoomen force-pushed the copilot/fix-system-format-exception branch from 233a12c to e773f85 Compare January 23, 2026 20:47
dennisdoomen requested a review from jnyrup January 23, 2026 20:49
jnyrup linked an issue Jan 25, 2026 that may be closed by this pull request

if (IncludeFullDetails && wasTruncated)
{
sb.AppendFormat(CultureInfo.InvariantCulture,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

If we polyfill StringBuilder.Append in StringBuilderExtensions we can avoid the intermediate string.

sb.Append(CultureInfo.InvariantCulture,
    $"""


        Full expectation:

        {expected.RenderAsIndentedBlock()},

        Full subject:

        {subject.RenderAsIndentedBlock()}

        """);

For StringBuilderExtensions

public static StringBuilder AppendLine(this StringBuilder stringBuilder, IFormatProvider provider, FormattableString formattable) =>
    stringBuilder.AppendLine(string.Create(provider, formattable));

public static StringBuilder Append(this StringBuilder stringBuilder, IFormatProvider provider, FormattableString formattable) =>
    stringBuilder.Append(string.Create(provider, formattable));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Is that a suggestion? And why is that important? This code is only executed for a failing test and when the full details are requested.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

It's a suggestion and not blocking for approving the PR.

dennisdoomen changed the title Fix FormatException when comparing strings containing braces Fix formatting exception when comparing strings containing braces Jan 26, 2026
dennisdoomen merged commit 60b430a into main Jan 26, 2026
14 checks passed
dennisdoomen deleted the copilot/fix-system-format-exception branch January 26, 2026 06:29
jnyrup mentioned this pull request Feb 15, 2026
9 tasks
This was referenced Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants


Back | FazBrowse Home | New Git URL