diff --git a/Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs b/Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs
index 5ebbe576b1..4a259de619 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs
@@ -80,6 +80,20 @@ public bool IsCyclicReference(object expectation)
public ITraceWriter TraceWriter { get; set; }
+ ///
+ /// This method ensures that tracing starts with a fresh state when invoked.
+ ///
+ internal void ResetTracing()
+ {
+ // SMELL: We need to ensure that if tracing is enabled using the built-in internal writer,
+ // we start with a fresh instance of InternalTraceWriter. We can't add extend ITraceWriter
+ // as that would be a breaking change.
+ if (TraceWriter is InternalTraceWriter)
+ {
+ TraceWriter = new InternalTraceWriter();
+ }
+ }
+
public override string ToString()
{
return Invariant($"{{Path=\"{CurrentNode.Description}\"}}");
diff --git a/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs b/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
index 230952a4aa..eb9d77dd21 100644
--- a/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
+++ b/Src/FluentAssertions/Equivalency/EquivalencyValidator.cs
@@ -13,6 +13,8 @@ public class EquivalencyValidator : IEquivalencyValidator
public void AssertEquality(Comparands comparands, EquivalencyValidationContext context)
{
+ context.ResetTracing();
+
using var scope = new AssertionScope();
scope.AssumeSingleCaller();
diff --git a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
index 52c2cacff2..ce7806e085 100644
--- a/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
+++ b/Src/FluentAssertions/Equivalency/Matching/MustMatchByNameRule.cs
@@ -34,13 +34,13 @@ public IMember Match(IMember expectedMember, object subject, INode parent, IEqui
if (subjectMember is null)
{
Execute.Assertion.FailWith(
- $"Expectation has {expectedMember.Description} that the other object does not have.");
+ "Expectation has {0} that the other object does not have.", expectedMember.Description.AsNonFormattable());
}
else if (options.IgnoreNonBrowsableOnSubject && !subjectMember.IsBrowsable)
{
Execute.Assertion.FailWith(
- $"Expectation has {expectedMember.Description} that is non-browsable in the other object, and non-browsable " +
- "members on the subject are ignored with the current configuration");
+ "Expectation has {0} that is non-browsable in the other object, and non-browsable " +
+ "members on the subject are ignored with the current configuration", expectedMember.Description.AsNonFormattable());
}
else
{
diff --git a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
index 3246847d7b..d33e97d240 100644
--- a/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
+++ b/Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyAssertionOptions.cs
@@ -643,7 +643,7 @@ public TSelf ComparingByValue(Type type)
///
public TSelf WithTracing(ITraceWriter writer = null)
{
- TraceWriter = writer ?? new StringBuilderTraceWriter();
+ TraceWriter = writer ?? new InternalTraceWriter();
return (TSelf)this;
}
diff --git a/Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs b/Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs
index f7bae2311d..273da55f43 100644
--- a/Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs
@@ -37,6 +37,10 @@ public string[] SelectClosestMatchFor(object key = null)
}
KeyValuePair[] bestResultSets = GetBestResultSets();
+ if (bestResultSets.Length == 0)
+ {
+ return [];
+ }
KeyValuePair bestMatch = Array.Find(bestResultSets, r => r.Key.Equals(key));
@@ -50,6 +54,11 @@ public string[] SelectClosestMatchFor(object key = null)
private KeyValuePair[] GetBestResultSets()
{
+ if (set.Values.Count == 0)
+ {
+ return [];
+ }
+
int fewestFailures = set.Values.Min(r => r.Length);
return set.Where(r => r.Value.Length == fewestFailures).ToArray();
}
diff --git a/Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs
index 4c3609bccd..9cdde8f9e1 100644
--- a/Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs
@@ -71,7 +71,8 @@ private bool ExecuteAssertion(Comparands comparands, IEquivalencyValidationConte
bool subjectIsValidType =
AssertionScope.Current
.ForCondition(subjectIsNull || comparands.Subject.GetType().IsSameOrInherits(typeof(TSubject)))
- .FailWith("Expected " + context.CurrentNode.Description + " from subject to be a {0}{reason}, but found a {1}.",
+ .FailWith("Expected {0} from subject to be a {1}{reason}, but found a {2}.",
+ context.CurrentNode.Description.AsNonFormattable(),
typeof(TSubject), comparands.Subject?.GetType());
bool expectationIsNull = comparands.Expectation is null;
@@ -80,7 +81,8 @@ private bool ExecuteAssertion(Comparands comparands, IEquivalencyValidationConte
AssertionScope.Current
.ForCondition(expectationIsNull || comparands.Expectation.GetType().IsSameOrInherits(typeof(TSubject)))
.FailWith(
- "Expected " + context.CurrentNode.Description + " from expectation to be a {0}{reason}, but found a {1}.",
+ "Expected {0} from expectation to be a {1}{reason}, but found a {2}.",
+ context.CurrentNode.Description.AsNonFormattable(),
typeof(TSubject), comparands.Expectation?.GetType());
if (subjectIsValidType && expectationIsValidType)
diff --git a/Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs b/Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs
index 274f6a7c3e..326176fc07 100644
--- a/Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs
@@ -27,7 +27,8 @@ public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationCon
string expectationName = GetDisplayNameForEnumComparison(comparands.Expectation, expectationsUnderlyingValue);
return new FailReason(
- $"Expected {{context:enum}} to be equivalent to {expectationName}{{reason}}, but found {{0}}.",
+ "Expected {context:enum} to be equivalent to {0}{reason}, but found {1}.",
+ expectationName.AsNonFormattable(),
comparands.Subject);
});
@@ -65,7 +66,8 @@ private static void HandleByValue(Comparands comparands, Reason reason)
string expectationName = GetDisplayNameForEnumComparison(comparands.Expectation, expectationsUnderlyingValue);
return new FailReason(
- $"Expected {{context:enum}} to equal {expectationName} by value{{reason}}, but found {subjectsName}.");
+ "Expected {context:enum} to equal {0} by value{reason}, but found {1}.",
+ expectationName.AsNonFormattable(), subjectsName.AsNonFormattable());
});
}
@@ -86,7 +88,8 @@ private static void HandleByName(Comparands comparands, Reason reason)
string expectationName = GetDisplayNameForEnumComparison(comparands.Expectation, expectationsUnderlyingValue);
return new FailReason(
- $"Expected {{context:enum}} to equal {expectationName} by name{{reason}}, but found {subjectsName}.");
+ "Expected {context:enum} to equal {0} by name{reason}, but found {1}.",
+ expectationName.AsNonFormattable(), subjectsName.AsNonFormattable());
});
}
@@ -100,7 +103,7 @@ private static string GetDisplayNameForEnumComparison(object o, decimal? v)
string typePart = o.GetType().Name;
string namePart = o.ToString().Replace(", ", "|", StringComparison.Ordinal);
string valuePart = v.Value.ToString(CultureInfo.InvariantCulture);
- return $"{typePart}.{namePart} {{{{value: {valuePart}}}}}";
+ return $"{typePart}.{namePart} {{value: {valuePart}}}";
}
private static decimal? ExtractDecimal(object o)
diff --git a/Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs b/Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs
index ce7f0f5ef9..6f9d58ba81 100644
--- a/Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs
+++ b/Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs
@@ -44,7 +44,7 @@ private static bool ValidateAgainstNulls(Comparands comparands, INode currentNod
if (onlyOneNull)
{
AssertionScope.Current.FailWith(
- $"Expected {currentNode.Description} to be {{0}}{{reason}}, but found {{1}}.", expected, subject);
+ "Expected {0} to be {1}{reason}, but found {2}.", currentNode.Description.AsNonFormattable(), expected, subject);
return false;
}
@@ -61,7 +61,7 @@ private static bool ValidateSubjectIsString(Comparands comparands, INode current
return
AssertionScope.Current
- .FailWith($"Expected {currentNode} to be {{0}}, but found {{1}}.",
+ .FailWith("Expected {0} to be {1}, but found {2}.", currentNode.AsNonFormattable(),
comparands.RuntimeType, comparands.Subject.GetType());
}
}
diff --git a/Src/FluentAssertions/Equivalency/Tracing/InternalTraceWriter.cs b/Src/FluentAssertions/Equivalency/Tracing/InternalTraceWriter.cs
new file mode 100644
index 0000000000..6c3b2d8b51
--- /dev/null
+++ b/Src/FluentAssertions/Equivalency/Tracing/InternalTraceWriter.cs
@@ -0,0 +1,3 @@
+namespace FluentAssertions.Equivalency.Tracing;
+
+internal sealed class InternalTraceWriter : StringBuilderTraceWriter;
diff --git a/Src/FluentAssertions/Equivalency/Tracing/StringBuilderTraceWriter.cs b/Src/FluentAssertions/Equivalency/Tracing/StringBuilderTraceWriter.cs
index cb1047defc..9253729a08 100644
--- a/Src/FluentAssertions/Equivalency/Tracing/StringBuilderTraceWriter.cs
+++ b/Src/FluentAssertions/Equivalency/Tracing/StringBuilderTraceWriter.cs
@@ -30,7 +30,7 @@ private void WriteLine(string trace)
{
foreach (string traceLine in trace.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries))
{
- builder.Append(new string(' ', depth * 2)).AppendLine(traceLine);
+ builder.Append(' ', depth * 2).AppendLine(traceLine);
}
}
diff --git a/Src/FluentAssertions/Execution/AssertionScope.cs b/Src/FluentAssertions/Execution/AssertionScope.cs
index 71a9a32f41..2348564751 100644
--- a/Src/FluentAssertions/Execution/AssertionScope.cs
+++ b/Src/FluentAssertions/Execution/AssertionScope.cs
@@ -210,7 +210,14 @@ public AssertionScope WithExpectation(string message, params object[] args)
string actualReason = localReason?.Invoke() ?? string.Empty;
string identifier = GetIdentifier();
- return messageBuilder.Build(message, args, actualReason, contextData, identifier, fallbackIdentifier);
+ // As the message builder will aggressively remove trailing whitespace, we need to add it back if the message ends with a space
+ string padding = string.Empty;
+ if (message.EndsWith(" ", StringComparison.Ordinal))
+ {
+ padding = " ";
+ }
+
+ return messageBuilder.Build(message, args, actualReason, contextData, identifier, fallbackIdentifier).TrimEnd() + padding;
};
return this;
diff --git a/Src/FluentAssertions/Execution/MessageBuilder.cs b/Src/FluentAssertions/Execution/MessageBuilder.cs
index 27f1b82e2a..f7ecd891c5 100644
--- a/Src/FluentAssertions/Execution/MessageBuilder.cs
+++ b/Src/FluentAssertions/Execution/MessageBuilder.cs
@@ -41,6 +41,19 @@ public string Build(string message, object[] messageArgs, string reason, Context
message = FormatArgumentPlaceholders(message, messageArgs);
+ message = TrimLineEndings(message);
+
+ return message;
+ }
+
+ private static string TrimLineEndings(string message)
+ {
+ string[] lines = message.Split([Environment.NewLine], StringSplitOptions.None);
+ if (lines.Length > 1)
+ {
+ return string.Join(Environment.NewLine, lines.Select(line => line.TrimEnd()));
+ }
+
return message;
}
diff --git a/Src/FluentAssertions/Execution/StringExtensions.cs b/Src/FluentAssertions/Execution/StringExtensions.cs
new file mode 100644
index 0000000000..ced4cf28d8
--- /dev/null
+++ b/Src/FluentAssertions/Execution/StringExtensions.cs
@@ -0,0 +1,24 @@
+namespace FluentAssertions.Execution;
+
+internal static class StringExtensions
+{
+ ///
+ /// Can be used
+ ///
+ ///
+ ///
+ public static WithoutFormattingWrapper AsNonFormattable(this string value)
+ {
+ return new WithoutFormattingWrapper(value);
+ }
+
+ ///
+ /// Can be used
+ ///
+ ///
+ ///
+ public static WithoutFormattingWrapper AsNonFormattable(this object value)
+ {
+ return new WithoutFormattingWrapper(value?.ToString());
+ }
+}
diff --git a/Src/FluentAssertions/Execution/WithoutFormattingWrapper.cs b/Src/FluentAssertions/Execution/WithoutFormattingWrapper.cs
new file mode 100644
index 0000000000..3c482a645f
--- /dev/null
+++ b/Src/FluentAssertions/Execution/WithoutFormattingWrapper.cs
@@ -0,0 +1,11 @@
+using FluentAssertions.Formatting;
+
+namespace FluentAssertions.Execution;
+
+///
+/// Wrapper to tell the not to apply any value formatters on this string.
+///
+internal class WithoutFormattingWrapper(string value)
+{
+ public override string ToString() => value;
+}
diff --git a/Src/FluentAssertions/Formatting/FormattedObjectGraph.cs b/Src/FluentAssertions/Formatting/FormattedObjectGraph.cs
index b668e290b8..1150616bb0 100644
--- a/Src/FluentAssertions/Formatting/FormattedObjectGraph.cs
+++ b/Src/FluentAssertions/Formatting/FormattedObjectGraph.cs
@@ -121,7 +121,7 @@ private void AppendSafely(string line)
throw new MaxLinesExceededException();
}
- lines.Add(line);
+ lines.Add(line.TrimEnd());
}
///
@@ -217,8 +217,10 @@ public void InsertLineOrFragment(string fragment)
}
else
{
- parentGraph.lines[startingLineCount] = parentGraph.lines[startingLineCount]
- .Insert(startingLineBuilderIndex, InsertNewLineIntoFragment(fragment));
+ string parentGraphLine = parentGraph.lines[startingLineCount];
+
+ parentGraph.lines[startingLineCount] = parentGraphLine
+ .Insert(Math.Min(startingLineBuilderIndex, parentGraphLine.Length), InsertNewLineIntoFragment(fragment));
}
}
@@ -226,10 +228,10 @@ private string InsertNewLineIntoFragment(string fragment)
{
if (StartingLineHasBeenAddedTo())
{
- return fragment + Environment.NewLine + MakeWhitespace(parentGraph.indentation + 1);
+ return fragment.TrimEnd() + Environment.NewLine + MakeWhitespace(parentGraph.indentation + 1);
}
- return fragment;
+ return fragment.TrimEnd();
}
private bool StartingLineHasBeenAddedTo() => parentGraph.lines[startingLineCount].Length > startingLineBuilderIndex;
diff --git a/Src/FluentAssertions/Formatting/Formatter.cs b/Src/FluentAssertions/Formatting/Formatter.cs
index 206d2b16d3..210880dabc 100644
--- a/Src/FluentAssertions/Formatting/Formatter.cs
+++ b/Src/FluentAssertions/Formatting/Formatter.cs
@@ -20,6 +20,7 @@ public static class Formatter
private static readonly List DefaultFormatters = new()
{
+ new PassthroughValueFormatter(),
new XmlReaderValueFormatter(),
new XmlNodeFormatter(),
new AttributeBasedFormatter(),
diff --git a/Src/FluentAssertions/Formatting/PassthroughValueFormatter.cs b/Src/FluentAssertions/Formatting/PassthroughValueFormatter.cs
new file mode 100644
index 0000000000..7fdaf36e58
--- /dev/null
+++ b/Src/FluentAssertions/Formatting/PassthroughValueFormatter.cs
@@ -0,0 +1,17 @@
+using FluentAssertions.Execution;
+
+namespace FluentAssertions.Formatting;
+
+///
+/// Ensures that any value wrapped in a
+/// is passed through as-is.
+///
+internal class PassthroughValueFormatter : IValueFormatter
+{
+ public bool CanHandle(object value) => value is WithoutFormattingWrapper;
+
+ public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild)
+ {
+ formattedGraph.AddFragment(((WithoutFormattingWrapper)value).ToString());
+ }
+}
diff --git a/Src/FluentAssertions/Formatting/PredicateLambdaExpressionValueFormatter.cs b/Src/FluentAssertions/Formatting/PredicateLambdaExpressionValueFormatter.cs
index 2528b91b33..23858ec787 100644
--- a/Src/FluentAssertions/Formatting/PredicateLambdaExpressionValueFormatter.cs
+++ b/Src/FluentAssertions/Formatting/PredicateLambdaExpressionValueFormatter.cs
@@ -42,7 +42,7 @@ private static Expression ReduceConstantSubExpressions(Expression expression)
{
return new ConstantSubExpressionReductionVisitor().Visit(expression);
}
- catch (InvalidOperationException)
+ catch (Exception e) when (e is InvalidOperationException or NotSupportedException)
{
// Fallback if we make an invalid rewrite of the expression.
return expression;
diff --git a/Src/FluentAssertions/Primitives/StringEqualityStrategy.cs b/Src/FluentAssertions/Primitives/StringEqualityStrategy.cs
index 211c8595cb..a864ff1c24 100644
--- a/Src/FluentAssertions/Primitives/StringEqualityStrategy.cs
+++ b/Src/FluentAssertions/Primitives/StringEqualityStrategy.cs
@@ -42,8 +42,8 @@ public void ValidateAgainstMismatch(IAssertionScope assertion, string subject, s
assertion.FailWith(
ExpectationDescription + "the same string{reason}, but they differ " + locationDescription + ":" +
- Environment.NewLine
- + GetMismatchSegmentForLongStrings(subject, expected, indexOfMismatch) + ".");
+ Environment.NewLine + "{0}.",
+ GetMismatchSegmentForLongStrings(subject, expected, indexOfMismatch).AsNonFormattable());
}
else if (ValidateAgainstLengthDifferences(assertion, subject, expected))
{
diff --git a/Src/FluentAssertions/Specialized/ExceptionAssertions.cs b/Src/FluentAssertions/Specialized/ExceptionAssertions.cs
index 90c0957002..20340ad826 100644
--- a/Src/FluentAssertions/Specialized/ExceptionAssertions.cs
+++ b/Src/FluentAssertions/Specialized/ExceptionAssertions.cs
@@ -286,10 +286,7 @@ public static void Execute(IEnumerable messages, string expectation, str
foreach (string failure in results.SelectClosestMatchFor())
{
- string replacedCurlyBraces =
- failure.EscapePlaceholders();
-
- AssertionScope.Current.FailWith(replacedCurlyBraces);
+ AssertionScope.Current.FailWith("{0}", failure.AsNonFormattable());
}
}
}
diff --git a/Tests/FSharp.Specs/FSharp.Specs.fsproj b/Tests/FSharp.Specs/FSharp.Specs.fsproj
index d1bb7629f6..f03fd81b86 100644
--- a/Tests/FSharp.Specs/FSharp.Specs.fsproj
+++ b/Tests/FSharp.Specs/FSharp.Specs.fsproj
@@ -2,7 +2,7 @@
net6.0
- 6.0
+ 8.0
diff --git a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.cs
index 49c2f4b692..cb40d2d8b1 100644
--- a/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Collections/CollectionAssertionSpecs.cs
@@ -71,7 +71,7 @@ public void When_the_collection_is_not_ordered_according_to_the_subsequent_ascen
// Assert
action.Should().Throw()
- .WithMessage("Expected collection * to be ordered \"by Item2\"*");
+ .WithMessage("Expected collection*to be ordered \"by Item2\"*");
}
[Fact]
@@ -163,7 +163,7 @@ public void When_the_collection_is_not_ordered_according_to_the_subsequent_desce
// Assert
action.Should().Throw()
- .WithMessage("Expected collection * to be ordered \"by Item2\"*");
+ .WithMessage("Expected collection*to be ordered \"by Item2\"*");
}
[Fact]
diff --git a/Tests/FluentAssertions.Specs/Collections/Data/DataRowCollectionAssertionSpecs.cs b/Tests/FluentAssertions.Specs/Collections/Data/DataRowCollectionAssertionSpecs.cs
index 6780b8a2c5..ecca8b73e7 100644
--- a/Tests/FluentAssertions.Specs/Collections/Data/DataRowCollectionAssertionSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Collections/Data/DataRowCollectionAssertionSpecs.cs
@@ -367,7 +367,7 @@ public void When_collection_is_compared_with_null_it_should_fail()
// Assert
action.Should().Throw().WithMessage(
- "Expected nullDataRows to not have the same count as * because we care, but found .");
+ "Expected nullDataRows to not have the same count as*because we care, but found .");
}
[Fact]
@@ -516,7 +516,7 @@ public void When_collection_does_not_contain_equivalent_row_it_should_fail()
// Assert
action.Should().Throw().WithMessage(
- "Expected dataTable.Rows * to contain equivalent of System.Data.DataRow* because we care.*");
+ "Expected dataTable.Rows*to contain equivalent of System.Data.DataRow*because we care.*");
}
}
@@ -580,7 +580,7 @@ public void When_collection_contains_equivalent_row_it_should_fail()
// Assert
action.Should().Throw()
- .WithMessage("Expected dataTable.Rows * not to contain equivalent of System.Data.DataRow* because we " +
+ .WithMessage("Expected dataTable.Rows*not to contain equivalent of System.Data.DataRow*because we " +
"care, but found one at index 1.*");
}
diff --git a/Tests/FluentAssertions.Specs/ConfigurationSpecs.cs b/Tests/FluentAssertions.Specs/ConfigurationSpecs.cs
index 36cbc680ef..0fc06da88a 100644
--- a/Tests/FluentAssertions.Specs/ConfigurationSpecs.cs
+++ b/Tests/FluentAssertions.Specs/ConfigurationSpecs.cs
@@ -2,6 +2,7 @@
using System.Threading.Tasks;
using FluentAssertions.Common;
using Xunit;
+using Xunit.Sdk;
namespace FluentAssertions.Specs;
@@ -29,6 +30,31 @@ public void When_concurrently_accessing_current_Configuration_no_exception_shoul
// Assert
act.Should().NotThrow();
}
+
+ [Fact]
+ public void Tracing_must_be_safe_when_executed_concurrently()
+ {
+ try
+ {
+ // Arrange
+ AssertionOptions.AssertEquivalencyUsing(e => e.WithTracing());
+
+ Parallel.For(1, 10_000, (_, _) =>
+ {
+ try
+ {
+ new { A = "a" }.Should().BeEquivalentTo(new { A = "b" });
+ }
+ catch (XunitException)
+ {
+ }
+ });
+ }
+ finally
+ {
+ AssertionOptions.AssertEquivalencyUsing(_ => new());
+ }
+ }
}
// Due to tests that call Configuration.Current
diff --git a/Tests/FluentAssertions.Specs/Execution/AssertionScope.MessageFormatingSpecs.cs b/Tests/FluentAssertions.Specs/Execution/AssertionScope.MessageFormatingSpecs.cs
index 53119f1f6e..e399d30000 100644
--- a/Tests/FluentAssertions.Specs/Execution/AssertionScope.MessageFormatingSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Execution/AssertionScope.MessageFormatingSpecs.cs
@@ -511,4 +511,37 @@ public void Message_should_contain_the_reason_as_defined_with_arguments()
act.Should().Throw()
.WithMessage("Expected because reasons");
}
+
+ [Theory]
+ [InlineData("{0}{0}", "\"foo\"\"foo\"")]
+ [InlineData("{{0}}{0}", "{0}\"foo\"")]
+ [InlineData("{0}{{0}}", "\"foo\"{0}")]
+ [InlineData("{{{0}}}{0}", "{\"foo\"}\"foo\"")]
+ [InlineData("{0}{{{0}}}", "\"foo\"{\"foo\"}")]
+ public void Can_handle_escaped_braces(string format, string expected)
+ {
+ Execute.Assertion
+ .Invoking(e => e.FailWith(format, "foo"))
+ .Should().Throw().WithMessage(expected);
+ }
+
+ [Theory]
+ [InlineData("{")]
+ [InlineData("}")]
+ [InlineData("{}")]
+ [InlineData("{0}")]
+ [InlineData("{{0}}")]
+ public void Can_handle_more_braces_in_dictionary_keys(string key)
+ {
+ // Arrange
+ var subject = new Dictionary { [key] = "" };
+ var expectation = new Dictionary { [key] = null };
+
+ // Act
+ var act = () => expectation.Should().BeEquivalentTo(subject);
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage($"Expected expectation[{key}] to be \"\", but found .*");
+ }
}
diff --git a/Tests/FluentAssertions.Specs/Formatting/FormatterSpecs.cs b/Tests/FluentAssertions.Specs/Formatting/FormatterSpecs.cs
index c50ea1a523..f814630108 100644
--- a/Tests/FluentAssertions.Specs/Formatting/FormatterSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Formatting/FormatterSpecs.cs
@@ -211,32 +211,32 @@ public void When_the_object_is_a_generic_type_without_custom_string_representati
act.Should().Throw()
.WithMessage(
"""
- Expected stuff to be equal to
+ Expected stuff to be equal to
{
FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]]
{
- Children = {1, 2, 3, 4},
- Description = "Stuff_1",
+ Children = {1, 2, 3, 4},
+ Description = "Stuff_1",
StuffId = 1
- },
+ },
FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]]
{
- Children = {1, 2, 3, 4},
- Description = "WRONG_DESCRIPTION",
+ Children = {1, 2, 3, 4},
+ Description = "WRONG_DESCRIPTION",
StuffId = 2
}
- }, but
+ }, but
{
FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]]
{
- Children = {1, 2, 3, 4},
- Description = "Stuff_1",
+ Children = {1, 2, 3, 4},
+ Description = "Stuff_1",
StuffId = 1
- },
+ },
FluentAssertions.Specs.Formatting.FormatterSpecs+Stuff`1[[System.Int32*]]
{
- Children = {1, 2, 3, 4},
- Description = "Stuff_2",
+ Children = {1, 2, 3, 4},
+ Description = "Stuff_2",
StuffId = 2
}
} differs at index 1.
@@ -258,9 +258,9 @@ public void When_the_object_is_a_user_defined_type_it_should_show_the_name_on_th
"""
Expected stuff to be , but found FluentAssertions.Specs.Formatting.FormatterSpecs+StuffRecord
{
- RecordChildren = {10, 20, 30, 40},
- RecordDescription = "description",
- RecordId = 42,
+ RecordChildren = {10, 20, 30, 40},
+ RecordDescription = "description",
+ RecordId = 42,
SingleChild = FluentAssertions.Specs.Formatting.FormatterSpecs+ChildRecord
{
ChildRecordId = 24
@@ -293,18 +293,18 @@ public void When_the_object_is_an_anonymous_type_it_should_show_the_properties_r
act.Should().Throw()
.Which.Message.Should().Be(
"""
- Expected stuff to be
+ Expected stuff to be
{
- Children = {10, 20, 30, 40},
- SingleChild =
+ Children = {10, 20, 30, 40},
+ SingleChild =
{
ChildId = 4
}
- }, but found
+ }, but found
{
- Children = {1, 2, 3, 4},
- Description = "absent",
- SingleChild =
+ Children = {1, 2, 3, 4},
+ Description = "absent",
+ SingleChild =
{
ChildId = 8
}
@@ -347,11 +347,11 @@ public void When_the_object_is_a_list_of_anonymous_type_it_should_show_the_prope
act.Should().Throw()
.Which.Message.Should().StartWith(
"""
- Expected stuff to be a collection with 1 item(s), but
+ Expected stuff to be a collection with 1 item(s), but
{
{
Description = "absent"
- },
+ },
{
Description = "absent"
}
@@ -360,11 +360,11 @@ contains 1 item(s) more than
{
{
- ComplexChildren =
+ ComplexChildren =
{
{
Property = "hello"
- },
+ },
{
Property = "goodbye"
}
@@ -405,15 +405,15 @@ public void When_the_object_is_a_tuple_it_should_show_the_properties_recursively
act.Should().Throw()
.Which.Message.Should().Be(
"""
- Expected stuff to be equal to
+ Expected stuff to be equal to
{
- Item1 = 2,
- Item2 = "WRONG_DESCRIPTION",
+ Item1 = 2,
+ Item2 = "WRONG_DESCRIPTION",
Item3 = {4, 5, 6, 7}
- }, but found
+ }, but found
{
- Item1 = 1,
- Item2 = "description",
+ Item1 = 1,
+ Item2 = "description",
Item3 = {1, 2, 3, 4}
}.
""");
@@ -441,14 +441,14 @@ public void When_the_object_is_a_record_it_should_show_the_properties_recursivel
act.Should().Throw()
.Which.Message.Should().Be(
"""
- Expected stuff to be
+ Expected stuff to be
{
RecordDescription = "WRONG_DESCRIPTION"
}, but found FluentAssertions.Specs.Formatting.FormatterSpecs+StuffRecord
{
- RecordChildren = {4, 5, 6, 7},
- RecordDescription = "descriptive",
- RecordId = 9,
+ RecordChildren = {4, 5, 6, 7},
+ RecordDescription = "descriptive",
+ RecordId = 9,
SingleChild = FluentAssertions.Specs.Formatting.FormatterSpecs+ChildRecord
{
ChildRecordId = 80
@@ -914,7 +914,7 @@ public void
string result = Formatter.ToString(subject, new FormattingOptions { UseLineBreaks = true });
// Assert
- result.Should().Contain($"FluentAssertions.Specs.Formatting.FormatterSpecs+A, {Environment.NewLine}");
+ result.Should().Contain($"FluentAssertions.Specs.Formatting.FormatterSpecs+A,{Environment.NewLine}");
result.Should().Contain($"FluentAssertions.Specs.Formatting.FormatterSpecs+B{Environment.NewLine}");
}
@@ -1195,11 +1195,36 @@ public void When_defining_a_custom_enumerable_value_formatter_it_should_respect_
str.Should().Match(Environment.NewLine +
"{*FluentAssertions*FormatterSpecs+CustomClass" + Environment.NewLine +
" {" + Environment.NewLine +
- " IntProperty = 1, " + Environment.NewLine +
+ " IntProperty = 1," + Environment.NewLine +
" StringProperty = " + Environment.NewLine +
" },*…1 more…*}*");
}
+ [Fact]
+ public void Can_render_an_empty_nested_collection_without_crashing()
+ {
+ // Arrange
+ IReadOnlyCollection> x =
+ [
+ [],
+ [new SomeObject(42)]
+ ];
+
+ // Act
+ string result = Formatter.ToString(x, new FormattingOptions
+ {
+ MaxDepth = 5,
+ MaxLines = 100,
+ UseLineBreaks = false
+ });
+
+ // Assert
+ result.Should().NotBeEmpty();
+ }
+
+ // ReSharper disable once NotAccessedPositionalProperty.Local
+ private record SomeObject(int Bar);
+
private class SingleItemValueFormatter : EnumerableValueFormatter
{
protected override int MaxItems => 1;
diff --git a/Tests/FluentAssertions.Specs/Formatting/PredicateLambdaExpressionValueFormatterSpecs.cs b/Tests/FluentAssertions.Specs/Formatting/PredicateLambdaExpressionValueFormatterSpecs.cs
index b70ce9c4e5..fcf58bfc23 100644
--- a/Tests/FluentAssertions.Specs/Formatting/PredicateLambdaExpressionValueFormatterSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Formatting/PredicateLambdaExpressionValueFormatterSpecs.cs
@@ -88,6 +88,7 @@ public void When_condition_contains_extension_method_then_extension_method_must_
result.Should().Be("a.TextIsNotBlank() AndAlso (a.Number >= 0) AndAlso (a.Number <= 1000)");
}
+#pragma warning disable RCS1196 // Do not call Contains as extension method. This is to exercise first-class spans
[Fact]
public void When_condition_contains_linq_extension_method_then_extension_method_must_be_formatted()
{
@@ -95,12 +96,28 @@ public void When_condition_contains_linq_extension_method_then_extension_method_
var allowed = new[] { 1, 2, 3 };
// Act
- string result = Format(a => allowed.Contains(a));
+ string result = Format(a => Enumerable.Contains(allowed, a));
// Assert
result.Should().Be("value(System.Int32[]).Contains(a)");
}
+#if NET6_0_OR_GREATER
+ [Fact]
+ public void Methods_using_ReadOnlySpan_can_be_formatted()
+ {
+ // Arrange
+ int[] allowed = [1, 2, 3];
+
+ // Act
+ string result = Format(a => MemoryExtensions.Contains(allowed, a));
+
+ // Assert
+ result.Should().Match("*.Contains(a)");
+ }
+#endif
+#pragma warning restore RCS1196
+
[Fact]
public void Formatting_a_lifted_binary_operator()
{
diff --git a/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs b/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs
index 1f2369981a..78f891a335 100644
--- a/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs
+++ b/Tests/FluentAssertions.Specs/Primitives/ReferenceTypeAssertionsSpecs.cs
@@ -43,10 +43,10 @@ public void When_two_different_objects_are_expected_to_be_the_same_it_should_fai
.Should().Throw()
.WithMessage(
"""
- Expected subject to refer to
+ Expected subject to refer to
{
UserName = "JohnDoe"
- } because they are the same, but found
+ } because they are the same, but found
{
Name = "John Doe"
}.
@@ -61,7 +61,7 @@ public void When_a_derived_class_has_longer_formatting_than_the_base_class()
act.Should().Throw()
.WithMessage(
"""
- Expected subject to be empty, but found at least one item
+ Expected subject to be empty, but found at least one item
{
FluentAssertions.Specs.Primitives.Complex
{
diff --git a/Tests/FluentAssertions.Specs/Primitives/StringAssertionSpecs.Be.cs b/Tests/FluentAssertions.Specs/Primitives/StringAssertionSpecs.Be.cs
index 6b158cc2b3..4e11444004 100644
--- a/Tests/FluentAssertions.Specs/Primitives/StringAssertionSpecs.Be.cs
+++ b/Tests/FluentAssertions.Specs/Primitives/StringAssertionSpecs.Be.cs
@@ -351,6 +351,38 @@ public void Mismatches_in_multiline_text_includes_the_line_number()
↑ (expected).
""");
}
+
+ [Fact]
+ public void When_string_contains_opening_brace_it_should_not_throw_format_exception()
+ {
+ // Arrange - create a string longer than the default truncation length (80 chars)
+ // with an opening brace to trigger the full details display which had the bug
+ const int lengthToTriggerFullDetails = 80;
+ var actual = "{" + new string('x', lengthToTriggerFullDetails);
+ var expected = "";
+
+ // Act
+ Action act = () => actual.Should().Be(expected);
+
+ // Assert - should throw XunitException, not FormatException
+ act.Should().Throw()
+ .WithMessage("Expected*to be the same string*");
+ }
+
+ [Fact]
+ public void When_long_string_contains_braces_it_should_display_properly()
+ {
+ // Arrange
+ const string subject = "public class Test { public void Method() { var x = 1; } }";
+ const string expected = "public class Test { public void Method() { var x = 2; } }";
+
+ // Act
+ Action act = () => subject.Should().Be(expected);
+
+ // Assert
+ act.Should().Throw()
+ .WithMessage("Expected*to be the same string*");
+ }
}
public class NotBe
diff --git a/docs/_pages/releases.md b/docs/_pages/releases.md
index 2d3f7526cb..6e11e199b8 100644
--- a/docs/_pages/releases.md
+++ b/docs/_pages/releases.md
@@ -7,6 +7,20 @@ sidebar:
nav: "sidebar"
---
+## 7.2.2
+
+### Fixes
+
+* Fix a formatting exception when {} is used as a dictionary key - [#3029](https://github.com/fluentassertions/fluentassertions/pull/3029)
+* Ensured `WithTracing` is safe when used with `BeEquivalentTo` globally - [#3067](https://github.com/fluentassertions/fluentassertions/pull/3067)
+* Fixed a formatting exception when comparing strings containing braces - [#3151](https://github.com/fluentassertions/fluentassertions/pull/3151)
+
+## 7.2.1
+
+### Fixes
+
+* Fixed a crash while formatting a collection with nested empty collections - [#3150](https://github.com/fluentassertions/fluentassertions/pull/3150)
+
## 7.2.0
### Fixes