From 4ca5226d486471eb90012ff17d80a9d62d812af1 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Wed, 21 Jan 2026 20:21:39 +0100 Subject: [PATCH 1/9] Prevent a crash while formatting a collection with nested empty subcollections - Prevents potential IndexOutOfRangeException in `FormattedObjectGraph` by using `Math.Min` for string insertion. - Improves formatting consistency of test expectations by removing unnecessary whitespace. - Adds a test to ensure rendering of empty nested collections does not crash. --- .output.txt | 309 ++++++++++++++++++ .../Execution/AssertionScope.cs | 9 +- .../Execution/MessageBuilder.cs | 13 + .../Formatting/FormattedObjectGraph.cs | 12 +- .../Collections/CollectionAssertionSpecs.cs | 4 +- .../Data/DataRowCollectionAssertionSpecs.cs | 6 +- .../Formatting/FormatterSpecs.cs | 101 +++--- .../ReferenceTypeAssertionsSpecs.cs | 6 +- docs/_pages/releases.md | 6 + 9 files changed, 414 insertions(+), 52 deletions(-) create mode 100644 .output.txt diff --git a/.output.txt b/.output.txt new file mode 100644 index 0000000000..514ba918f1 --- /dev/null +++ b/.output.txt @@ -0,0 +1,309 @@ +commit 150574ae2224eef38012a132adfe1e0c4f785d26 (HEAD -> cleanup-stack-trace, origin/cleanup-stack-trace) +Author: Dennis Doomen +Date: Thu Jan 29 08:22:33 2026 +0100 + Hide the FluentAssertions code from the stack trace +Src/FluentAssertions/AggregateExceptionExtractor.cs +Src/FluentAssertions/AndConstraint.cs +Src/FluentAssertions/AndWhichConstraint.cs +Src/FluentAssertions/AssertionConfiguration.cs +Src/FluentAssertions/AssertionEngine.cs +Src/FluentAssertions/AssertionExtensions.cs +Src/FluentAssertions/AsyncAssertionsExtensions.cs +Src/FluentAssertions/AtLeast.cs +Src/FluentAssertions/AtMost.cs +Src/FluentAssertions/CallerIdentification/IParsingStrategy.cs +Src/FluentAssertions/CallerIdentification/MultiLineCommentParsingStrategy.cs +Src/FluentAssertions/CallerIdentification/ParsingState.cs +Src/FluentAssertions/CallerIdentification/QuotesParsingStrategy.cs +Src/FluentAssertions/CallerIdentification/SemicolonParsingStrategy.cs +Src/FluentAssertions/CallerIdentification/ShouldCallParsingStrategy.cs +Src/FluentAssertions/CallerIdentification/SingleLineCommentParsingStrategy.cs +Src/FluentAssertions/CallerIdentification/StatementParser.cs +Src/FluentAssertions/CallerIdentifier.cs +Src/FluentAssertions/Collections/GenericCollectionAssertions.cs +Src/FluentAssertions/Collections/GenericDictionaryAssertions.cs +Src/FluentAssertions/Collections/MaximumMatching/Element.cs +Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingProblem.cs +Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolution.cs +Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolver.cs +Src/FluentAssertions/Collections/MaximumMatching/Predicate.cs +Src/FluentAssertions/Collections/StringCollectionAssertions.cs +Src/FluentAssertions/Collections/WhoseValueConstraint.cs +Src/FluentAssertions/Common/CSharpAccessModifierExtensions.cs +Src/FluentAssertions/Common/Clock.cs +Src/FluentAssertions/Common/DateTimeExtensions.cs +Src/FluentAssertions/Common/EnumerableExtensions.cs +Src/FluentAssertions/Common/ExceptionExtensions.cs +Src/FluentAssertions/Common/ExpressionExtensions.cs +Src/FluentAssertions/Common/Guard.cs +Src/FluentAssertions/Common/IClock.cs +Src/FluentAssertions/Common/ITimer.cs +Src/FluentAssertions/Common/IntegerExtensions.cs +Src/FluentAssertions/Common/Iterator.cs +Src/FluentAssertions/Common/KeyValuePairCollectionExtensions.cs +Src/FluentAssertions/Common/MemberPath.cs +Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs +Src/FluentAssertions/Common/MethodInfoExtensions.cs +Src/FluentAssertions/Common/ObjectExtensions.cs +Src/FluentAssertions/Common/PropertyInfoExtensions.cs +Src/FluentAssertions/Common/StartTimer.cs +Src/FluentAssertions/Common/StopwatchTimer.cs +Src/FluentAssertions/Common/StringExtensions.cs +Src/FluentAssertions/Common/TimeOnlyExtensions.cs +Src/FluentAssertions/Common/TypeExtensions.cs +Src/FluentAssertions/Common/TypeReflector.cs +Src/FluentAssertions/Common/ValueFormatterDetectionMode.cs +Src/FluentAssertions/Configuration/GlobalConfiguration.cs +Src/FluentAssertions/Configuration/GlobalEquivalencyOptions.cs +Src/FluentAssertions/Configuration/GlobalFormattingOptions.cs +Src/FluentAssertions/CustomAssertionAttribute.cs +Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs +Src/FluentAssertions/Disposable.cs +Src/FluentAssertions/EnumAssertionsExtensions.cs +Src/FluentAssertions/Equivalency/AssertionChainExtensions.cs +Src/FluentAssertions/Equivalency/Comparands.cs +Src/FluentAssertions/Equivalency/ConversionSelector.cs +Src/FluentAssertions/Equivalency/Digit.cs +Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs +Src/FluentAssertions/Equivalency/EquivalencyOptions.cs +Src/FluentAssertions/Equivalency/EquivalencyPlan.cs +Src/FluentAssertions/Equivalency/EquivalencyStep.cs +Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs +Src/FluentAssertions/Equivalency/EquivalencyValidator.cs +Src/FluentAssertions/Equivalency/Execution/CollectionMemberOptionsDecorator.cs +Src/FluentAssertions/Equivalency/Execution/CyclicReferenceDetector.cs +Src/FluentAssertions/Equivalency/Execution/ObjectInfo.cs +Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs +Src/FluentAssertions/Equivalency/Field.cs +Src/FluentAssertions/Equivalency/Inlining/ActionBasedInlineAssertion.cs +Src/FluentAssertions/Equivalency/Inlining/ConditionBasedInlineAssertion.cs +Src/FluentAssertions/Equivalency/Inlining/InlineEquivalencyStep.cs +Src/FluentAssertions/Equivalency/JsonProperty.cs +Src/FluentAssertions/Equivalency/MemberFactory.cs +Src/FluentAssertions/Equivalency/MemberSelectionContext.cs +Src/FluentAssertions/Equivalency/MemberVisibility.cs +Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs +Src/FluentAssertions/Equivalency/MultiDimensionalArrayEquivalencyStep.cs +Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs +Src/FluentAssertions/Equivalency/Node.cs +Src/FluentAssertions/Equivalency/OrderingRuleCollection.cs +Src/FluentAssertions/Equivalency/Property.cs +Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPredicateSelectionRule.cs +Src/FluentAssertions/Equivalency/Selection/ExcludeNonBrowsableMembersRule.cs +Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyOptions.cs +Src/FluentAssertions/Equivalency/Steps/AssertionContext.cs +Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs +Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/AutoConversionStep.cs +Src/FluentAssertions/Equivalency/Steps/DateAndTimeEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/DictionaryEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/DictionaryInterfaceInfo.cs +Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs +Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidator.cs +Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidatorExtensions.cs +Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/EquivalencyValidationContextExtensions.cs +Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/GenericEnumerableEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/JsonConversionStep.cs +Src/FluentAssertions/Equivalency/Steps/ReferenceEqualityEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/RunAllUserStepsEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/SimpleEqualityEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/TypeEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/ValueTypeEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/XAttributeEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/XDocumentEquivalencyStep.cs +Src/FluentAssertions/Equivalency/Steps/XElementEquivalencyStep.cs +Src/FluentAssertions/Equivalency/SubjectInfoExtensions.cs +Src/FluentAssertions/Equivalency/Typing/AlwaysBeStrictTypingRule.cs +Src/FluentAssertions/Equivalency/Typing/PredicateBasedTypingRule.cs +Src/FluentAssertions/EventRaisingExtensions.cs +Src/FluentAssertions/Events/EventAssertions.cs +Src/FluentAssertions/Events/EventHandlerFactory.cs +Src/FluentAssertions/Events/EventMetadata.cs +Src/FluentAssertions/Events/EventMonitor.cs +Src/FluentAssertions/Events/EventMonitorOptions.cs +Src/FluentAssertions/Events/EventRecorder.cs +Src/FluentAssertions/Events/FilteredEventRecording.cs +Src/FluentAssertions/Events/IMonitor.cs +Src/FluentAssertions/Events/OccurredEvent.cs +Src/FluentAssertions/Events/RecordedEvent.cs +Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs +Src/FluentAssertions/Exactly.cs +Src/FluentAssertions/ExceptionAssertionsExtensions.cs +Src/FluentAssertions/Execution/AssertionChain.cs +Src/FluentAssertions/Execution/AssertionFailedException.cs +Src/FluentAssertions/Execution/AssertionScope.cs +Src/FluentAssertions/Execution/CollectingAssertionStrategy.cs +Src/FluentAssertions/Execution/ContextDataDictionary.cs +Src/FluentAssertions/Execution/Continuation.cs +Src/FluentAssertions/Execution/ContinuationOfGiven.cs +Src/FluentAssertions/Execution/DefaultAssertionStrategy.cs +Src/FluentAssertions/Execution/FailReason.cs +Src/FluentAssertions/Execution/FailureMessageFormatter.cs +Src/FluentAssertions/Execution/FallbackTestFramework.cs +Src/FluentAssertions/Execution/GivenSelector.cs +Src/FluentAssertions/Execution/GivenSelectorExtensions.cs +Src/FluentAssertions/Execution/IAssertionStrategy.cs +Src/FluentAssertions/Execution/ITestFramework.cs +Src/FluentAssertions/Execution/LateBoundTestFramework.cs +Src/FluentAssertions/Execution/MSTestFrameworkV2.cs +Src/FluentAssertions/Execution/MSTestFrameworkV4.cs +Src/FluentAssertions/Execution/MSpecFramework.cs +Src/FluentAssertions/Execution/NUnitTestFramework.cs +Src/FluentAssertions/Execution/Reason.cs +Src/FluentAssertions/Execution/StringExtensions.cs +Src/FluentAssertions/Execution/SubjectIdentificationBuilder.cs +Src/FluentAssertions/Execution/TUnitFramework.cs +Src/FluentAssertions/Execution/TestFrameworkFactory.cs +Src/FluentAssertions/Execution/WithoutFormattingWrapper.cs +Src/FluentAssertions/Execution/XUnitTestFramework.cs +Src/FluentAssertions/Extensibility/AssertionEngineInitializerAttribute.cs +Src/FluentAssertions/Extensions/FluentDateTimeExtensions.cs +Src/FluentAssertions/Extensions/FluentTimeSpanExtensions.cs +Src/FluentAssertions/Extensions/OccurrenceConstraintExtensions.cs +Src/FluentAssertions/FluentActions.cs +Src/FluentAssertions/FluentAssertions.csproj +Src/FluentAssertions/Formatting/DateTimeOffsetValueFormatter.cs +Src/FluentAssertions/Formatting/DecimalValueFormatter.cs +Src/FluentAssertions/Formatting/DictionaryValueFormatter.cs +Src/FluentAssertions/Formatting/DoubleValueFormatter.cs +Src/FluentAssertions/Formatting/EnumValueFormatter.cs +Src/FluentAssertions/Formatting/EnumerableExtensions.cs +Src/FluentAssertions/Formatting/EnumerableValueFormatter.cs +Src/FluentAssertions/Formatting/ExceptionValueFormatter.cs +Src/FluentAssertions/Formatting/ExpressionValueFormatter.cs +Src/FluentAssertions/Formatting/FormatChild.cs +Src/FluentAssertions/Formatting/Formatter.cs +Src/FluentAssertions/Formatting/FormattingContext.cs +Src/FluentAssertions/Formatting/FormattingOptions.cs +Src/FluentAssertions/Formatting/IValueFormatter.cs +Src/FluentAssertions/Formatting/Int16ValueFormatter.cs +Src/FluentAssertions/Formatting/Int32ValueFormatter.cs +Src/FluentAssertions/Formatting/Int64ValueFormatter.cs +Src/FluentAssertions/Formatting/MaxLinesExceededException.cs +Src/FluentAssertions/Formatting/MultidimensionalArrayFormatter.cs +Src/FluentAssertions/Formatting/NullValueFormatter.cs +Src/FluentAssertions/Formatting/PredicateLambdaExpressionValueFormatter.cs +Src/FluentAssertions/Formatting/PropertyInfoFormatter.cs +Src/FluentAssertions/Formatting/SByteValueFormatter.cs +Src/FluentAssertions/Formatting/SingleValueFormatter.cs +Src/FluentAssertions/Formatting/StringValueFormatter.cs +Src/FluentAssertions/Formatting/TaskFormatter.cs +Src/FluentAssertions/Formatting/TimeSpanValueFormatter.cs +Src/FluentAssertions/Formatting/UInt16ValueFormatter.cs +Src/FluentAssertions/Formatting/UInt32ValueFormatter.cs +Src/FluentAssertions/Formatting/UInt64ValueFormatter.cs +Src/FluentAssertions/Formatting/XDocumentValueFormatter.cs +Src/FluentAssertions/Formatting/XElementValueFormatter.cs +Src/FluentAssertions/Formatting/XmlReaderValueFormatter.cs +Src/FluentAssertions/JsonAssertionExtensions.cs +Src/FluentAssertions/LessThan.cs +Src/FluentAssertions/License.cs +Src/FluentAssertions/MoreThan.cs +Src/FluentAssertions/Numeric/ByteAssertions.cs +Src/FluentAssertions/Numeric/DecimalAssertions.cs +Src/FluentAssertions/Numeric/DoubleAssertions.cs +Src/FluentAssertions/Numeric/Int16Assertions.cs +Src/FluentAssertions/Numeric/Int32Assertions.cs +Src/FluentAssertions/Numeric/Int64Assertions.cs +Src/FluentAssertions/Numeric/NullableByteAssertions.cs +Src/FluentAssertions/Numeric/NullableDecimalAssertions.cs +Src/FluentAssertions/Numeric/NullableInt16Assertions.cs +Src/FluentAssertions/Numeric/NullableInt32Assertions.cs +Src/FluentAssertions/Numeric/NullableInt64Assertions.cs +Src/FluentAssertions/Numeric/NullableSByteAssertions.cs +Src/FluentAssertions/Numeric/NullableSingleAssertions.cs +Src/FluentAssertions/Numeric/NullableUInt16Assertions.cs +Src/FluentAssertions/Numeric/NullableUInt32Assertions.cs +Src/FluentAssertions/Numeric/NullableUInt64Assertions.cs +Src/FluentAssertions/Numeric/NumericAssertionsBase.cs +Src/FluentAssertions/Numeric/SByteAssertions.cs +Src/FluentAssertions/Numeric/SingleAssertions.cs +Src/FluentAssertions/Numeric/UInt16Assertions.cs +Src/FluentAssertions/Numeric/UInt32Assertions.cs +Src/FluentAssertions/Numeric/UInt64Assertions.cs +Src/FluentAssertions/NumericAssertionsExtensions.cs +Src/FluentAssertions/ObjectAssertionsExtensions.cs +Src/FluentAssertions/OccurrenceConstraint.cs +Src/FluentAssertions/Polyfill/StringBuilderExtensions.cs +Src/FluentAssertions/Polyfill/StringExtensions.cs +Src/FluentAssertions/Polyfill/SystemExtensions.cs +Src/FluentAssertions/Primitives/BooleanAssertions.cs +Src/FluentAssertions/Primitives/DateOnlyAssertions.cs +Src/FluentAssertions/Primitives/DateTimeAssertions.cs +Src/FluentAssertions/Primitives/DateTimeOffsetAssertions.cs +Src/FluentAssertions/Primitives/DateTimeOffsetRangeAssertions.cs +Src/FluentAssertions/Primitives/DateTimeRangeAssertions.cs +Src/FluentAssertions/Primitives/EnumAssertions.cs +Src/FluentAssertions/Primitives/GuidAssertions.cs +Src/FluentAssertions/Primitives/NullableBooleanAssertions.cs +Src/FluentAssertions/Primitives/NullableDateOnlyAssertions.cs +Src/FluentAssertions/Primitives/NullableDateTimeAssertions.cs +Src/FluentAssertions/Primitives/NullableDateTimeOffsetAssertions.cs +Src/FluentAssertions/Primitives/NullableEnumAssertions.cs +Src/FluentAssertions/Primitives/NullableGuidAssertions.cs +Src/FluentAssertions/Primitives/NullableSimpleTimeSpanAssertions.cs +Src/FluentAssertions/Primitives/NullableTimeOnlyAssertions.cs +Src/FluentAssertions/Primitives/SimpleTimeSpanAssertions.cs +Src/FluentAssertions/Primitives/StringAssertions.cs +Src/FluentAssertions/Primitives/StringContainsStrategy.cs +Src/FluentAssertions/Primitives/StringEndStrategy.cs +Src/FluentAssertions/Primitives/StringEqualityStrategy.cs +Src/FluentAssertions/Primitives/StringStartStrategy.cs +Src/FluentAssertions/Primitives/StringValidator.cs +Src/FluentAssertions/Primitives/StringValidatorSupportingNull.cs +Src/FluentAssertions/Primitives/StringWildcardMatchingStrategy.cs +Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs +Src/FluentAssertions/Primitives/TimeSpanPredicate.cs +Src/FluentAssertions/Specialized/ActionAssertions.cs +Src/FluentAssertions/Specialized/DelegateAssertions.cs +Src/FluentAssertions/Specialized/DelegateAssertionsBase.cs +Src/FluentAssertions/Specialized/ExceptionAssertions.cs +Src/FluentAssertions/Specialized/ExecutionTime.cs +Src/FluentAssertions/Specialized/ExecutionTimeAssertions.cs +Src/FluentAssertions/Specialized/FunctionAssertions.cs +Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs +Src/FluentAssertions/Specialized/JsonNodeAssertions.cs +Src/FluentAssertions/Specialized/JsonValueExtensions.cs +Src/FluentAssertions/Specialized/MemberExecutionTime.cs +Src/FluentAssertions/Specialized/NonGenericAsyncFunctionAssertions.cs +Src/FluentAssertions/Specialized/TaskCompletionSourceAssertions.cs +Src/FluentAssertions/Specialized/TaskCompletionSourceAssertionsBase.cs +Src/FluentAssertions/Streams/BufferedStreamAssertions.cs +Src/FluentAssertions/Streams/StreamAssertions.cs +Src/FluentAssertions/TypeEnumerableExtensions.cs +Src/FluentAssertions/TypeExtensions.cs +Src/FluentAssertions/Types/AllTypes.cs +Src/FluentAssertions/Types/AssemblyAssertions.cs +Src/FluentAssertions/Types/ConstructorInfoAssertions.cs +Src/FluentAssertions/Types/MemberInfoAssertions.cs +Src/FluentAssertions/Types/MethodBaseAssertions.cs +Src/FluentAssertions/Types/MethodInfoAssertions.cs +Src/FluentAssertions/Types/MethodInfoSelector.cs +Src/FluentAssertions/Types/MethodInfoSelectorAssertions.cs +Src/FluentAssertions/Types/PropertyInfoAssertions.cs +Src/FluentAssertions/Types/PropertyInfoSelector.cs +Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs +Src/FluentAssertions/Types/TypeAssertions.cs +Src/FluentAssertions/Types/TypeSelector.cs +Src/FluentAssertions/Types/TypeSelectorAssertions.cs +Src/FluentAssertions/Value.cs +Src/FluentAssertions/Xml/Equivalency/AttributeData.cs +Src/FluentAssertions/Xml/Equivalency/Failure.cs +Src/FluentAssertions/Xml/Equivalency/Node.cs +Src/FluentAssertions/Xml/Equivalency/XmlIterator.cs +Src/FluentAssertions/Xml/Equivalency/XmlReaderValidator.cs +Src/FluentAssertions/Xml/XAttributeAssertions.cs +Src/FluentAssertions/Xml/XDocumentAssertions.cs +Src/FluentAssertions/Xml/XElementAssertions.cs +Src/FluentAssertions/Xml/XmlElementAssertions.cs +Src/FluentAssertions/Xml/XmlNodeAssertions.cs +Src/FluentAssertions/Xml/XmlNodeFormatter.cs +Src/FluentAssertions/XmlAssertionExtensions.cs +Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt +Tests/FluentAssertions.Specs/Exceptions/OuterExceptionSpecs.cs +Tests/FluentAssertions.Specs/StackTraceHiddenSpecs.cs \ No newline at end of file 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/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/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/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/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/docs/_pages/releases.md b/docs/_pages/releases.md index 2d3f7526cb..3d5307376a 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -7,6 +7,12 @@ sidebar: nav: "sidebar" --- +## 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 From 6c1a39889bc56f63da992dd234364e22771cb111 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sat, 21 Feb 2026 13:12:39 +0100 Subject: [PATCH 2/9] Remove .output.txt --- .output.txt | 309 ---------------------------------------------------- 1 file changed, 309 deletions(-) delete mode 100644 .output.txt diff --git a/.output.txt b/.output.txt deleted file mode 100644 index 514ba918f1..0000000000 --- a/.output.txt +++ /dev/null @@ -1,309 +0,0 @@ -commit 150574ae2224eef38012a132adfe1e0c4f785d26 (HEAD -> cleanup-stack-trace, origin/cleanup-stack-trace) -Author: Dennis Doomen -Date: Thu Jan 29 08:22:33 2026 +0100 - Hide the FluentAssertions code from the stack trace -Src/FluentAssertions/AggregateExceptionExtractor.cs -Src/FluentAssertions/AndConstraint.cs -Src/FluentAssertions/AndWhichConstraint.cs -Src/FluentAssertions/AssertionConfiguration.cs -Src/FluentAssertions/AssertionEngine.cs -Src/FluentAssertions/AssertionExtensions.cs -Src/FluentAssertions/AsyncAssertionsExtensions.cs -Src/FluentAssertions/AtLeast.cs -Src/FluentAssertions/AtMost.cs -Src/FluentAssertions/CallerIdentification/IParsingStrategy.cs -Src/FluentAssertions/CallerIdentification/MultiLineCommentParsingStrategy.cs -Src/FluentAssertions/CallerIdentification/ParsingState.cs -Src/FluentAssertions/CallerIdentification/QuotesParsingStrategy.cs -Src/FluentAssertions/CallerIdentification/SemicolonParsingStrategy.cs -Src/FluentAssertions/CallerIdentification/ShouldCallParsingStrategy.cs -Src/FluentAssertions/CallerIdentification/SingleLineCommentParsingStrategy.cs -Src/FluentAssertions/CallerIdentification/StatementParser.cs -Src/FluentAssertions/CallerIdentifier.cs -Src/FluentAssertions/Collections/GenericCollectionAssertions.cs -Src/FluentAssertions/Collections/GenericDictionaryAssertions.cs -Src/FluentAssertions/Collections/MaximumMatching/Element.cs -Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingProblem.cs -Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolution.cs -Src/FluentAssertions/Collections/MaximumMatching/MaximumMatchingSolver.cs -Src/FluentAssertions/Collections/MaximumMatching/Predicate.cs -Src/FluentAssertions/Collections/StringCollectionAssertions.cs -Src/FluentAssertions/Collections/WhoseValueConstraint.cs -Src/FluentAssertions/Common/CSharpAccessModifierExtensions.cs -Src/FluentAssertions/Common/Clock.cs -Src/FluentAssertions/Common/DateTimeExtensions.cs -Src/FluentAssertions/Common/EnumerableExtensions.cs -Src/FluentAssertions/Common/ExceptionExtensions.cs -Src/FluentAssertions/Common/ExpressionExtensions.cs -Src/FluentAssertions/Common/Guard.cs -Src/FluentAssertions/Common/IClock.cs -Src/FluentAssertions/Common/ITimer.cs -Src/FluentAssertions/Common/IntegerExtensions.cs -Src/FluentAssertions/Common/Iterator.cs -Src/FluentAssertions/Common/KeyValuePairCollectionExtensions.cs -Src/FluentAssertions/Common/MemberPath.cs -Src/FluentAssertions/Common/MemberPathSegmentEqualityComparer.cs -Src/FluentAssertions/Common/MethodInfoExtensions.cs -Src/FluentAssertions/Common/ObjectExtensions.cs -Src/FluentAssertions/Common/PropertyInfoExtensions.cs -Src/FluentAssertions/Common/StartTimer.cs -Src/FluentAssertions/Common/StopwatchTimer.cs -Src/FluentAssertions/Common/StringExtensions.cs -Src/FluentAssertions/Common/TimeOnlyExtensions.cs -Src/FluentAssertions/Common/TypeExtensions.cs -Src/FluentAssertions/Common/TypeReflector.cs -Src/FluentAssertions/Common/ValueFormatterDetectionMode.cs -Src/FluentAssertions/Configuration/GlobalConfiguration.cs -Src/FluentAssertions/Configuration/GlobalEquivalencyOptions.cs -Src/FluentAssertions/Configuration/GlobalFormattingOptions.cs -Src/FluentAssertions/CustomAssertionAttribute.cs -Src/FluentAssertions/CustomAssertionsAssemblyAttribute.cs -Src/FluentAssertions/Disposable.cs -Src/FluentAssertions/EnumAssertionsExtensions.cs -Src/FluentAssertions/Equivalency/AssertionChainExtensions.cs -Src/FluentAssertions/Equivalency/Comparands.cs -Src/FluentAssertions/Equivalency/ConversionSelector.cs -Src/FluentAssertions/Equivalency/Digit.cs -Src/FluentAssertions/Equivalency/EqualityStrategyProvider.cs -Src/FluentAssertions/Equivalency/EquivalencyOptions.cs -Src/FluentAssertions/Equivalency/EquivalencyPlan.cs -Src/FluentAssertions/Equivalency/EquivalencyStep.cs -Src/FluentAssertions/Equivalency/EquivalencyValidationContext.cs -Src/FluentAssertions/Equivalency/EquivalencyValidator.cs -Src/FluentAssertions/Equivalency/Execution/CollectionMemberOptionsDecorator.cs -Src/FluentAssertions/Equivalency/Execution/CyclicReferenceDetector.cs -Src/FluentAssertions/Equivalency/Execution/ObjectInfo.cs -Src/FluentAssertions/Equivalency/Execution/ObjectReference.cs -Src/FluentAssertions/Equivalency/Field.cs -Src/FluentAssertions/Equivalency/Inlining/ActionBasedInlineAssertion.cs -Src/FluentAssertions/Equivalency/Inlining/ConditionBasedInlineAssertion.cs -Src/FluentAssertions/Equivalency/Inlining/InlineEquivalencyStep.cs -Src/FluentAssertions/Equivalency/JsonProperty.cs -Src/FluentAssertions/Equivalency/MemberFactory.cs -Src/FluentAssertions/Equivalency/MemberSelectionContext.cs -Src/FluentAssertions/Equivalency/MemberVisibility.cs -Src/FluentAssertions/Equivalency/MemberVisibilityExtensions.cs -Src/FluentAssertions/Equivalency/MultiDimensionalArrayEquivalencyStep.cs -Src/FluentAssertions/Equivalency/NestedExclusionOptionBuilder.cs -Src/FluentAssertions/Equivalency/Node.cs -Src/FluentAssertions/Equivalency/OrderingRuleCollection.cs -Src/FluentAssertions/Equivalency/Property.cs -Src/FluentAssertions/Equivalency/Selection/ExcludeMemberByPredicateSelectionRule.cs -Src/FluentAssertions/Equivalency/Selection/ExcludeNonBrowsableMembersRule.cs -Src/FluentAssertions/Equivalency/SelfReferenceEquivalencyOptions.cs -Src/FluentAssertions/Equivalency/Steps/AssertionContext.cs -Src/FluentAssertions/Equivalency/Steps/AssertionResultSet.cs -Src/FluentAssertions/Equivalency/Steps/AssertionRuleEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/AutoConversionStep.cs -Src/FluentAssertions/Equivalency/Steps/DateAndTimeEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/DictionaryEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/DictionaryInterfaceInfo.cs -Src/FluentAssertions/Equivalency/Steps/EnumEqualityStep.cs -Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidator.cs -Src/FluentAssertions/Equivalency/Steps/EnumerableEquivalencyValidatorExtensions.cs -Src/FluentAssertions/Equivalency/Steps/EqualityComparerEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/EquivalencyValidationContextExtensions.cs -Src/FluentAssertions/Equivalency/Steps/GenericDictionaryEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/GenericEnumerableEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/JsonConversionStep.cs -Src/FluentAssertions/Equivalency/Steps/ReferenceEqualityEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/RunAllUserStepsEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/SimpleEqualityEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/StringEqualityEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/StructuralEqualityEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/TypeEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/ValueTypeEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/XAttributeEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/XDocumentEquivalencyStep.cs -Src/FluentAssertions/Equivalency/Steps/XElementEquivalencyStep.cs -Src/FluentAssertions/Equivalency/SubjectInfoExtensions.cs -Src/FluentAssertions/Equivalency/Typing/AlwaysBeStrictTypingRule.cs -Src/FluentAssertions/Equivalency/Typing/PredicateBasedTypingRule.cs -Src/FluentAssertions/EventRaisingExtensions.cs -Src/FluentAssertions/Events/EventAssertions.cs -Src/FluentAssertions/Events/EventHandlerFactory.cs -Src/FluentAssertions/Events/EventMetadata.cs -Src/FluentAssertions/Events/EventMonitor.cs -Src/FluentAssertions/Events/EventMonitorOptions.cs -Src/FluentAssertions/Events/EventRecorder.cs -Src/FluentAssertions/Events/FilteredEventRecording.cs -Src/FluentAssertions/Events/IMonitor.cs -Src/FluentAssertions/Events/OccurredEvent.cs -Src/FluentAssertions/Events/RecordedEvent.cs -Src/FluentAssertions/Events/ThreadSafeSequenceGenerator.cs -Src/FluentAssertions/Exactly.cs -Src/FluentAssertions/ExceptionAssertionsExtensions.cs -Src/FluentAssertions/Execution/AssertionChain.cs -Src/FluentAssertions/Execution/AssertionFailedException.cs -Src/FluentAssertions/Execution/AssertionScope.cs -Src/FluentAssertions/Execution/CollectingAssertionStrategy.cs -Src/FluentAssertions/Execution/ContextDataDictionary.cs -Src/FluentAssertions/Execution/Continuation.cs -Src/FluentAssertions/Execution/ContinuationOfGiven.cs -Src/FluentAssertions/Execution/DefaultAssertionStrategy.cs -Src/FluentAssertions/Execution/FailReason.cs -Src/FluentAssertions/Execution/FailureMessageFormatter.cs -Src/FluentAssertions/Execution/FallbackTestFramework.cs -Src/FluentAssertions/Execution/GivenSelector.cs -Src/FluentAssertions/Execution/GivenSelectorExtensions.cs -Src/FluentAssertions/Execution/IAssertionStrategy.cs -Src/FluentAssertions/Execution/ITestFramework.cs -Src/FluentAssertions/Execution/LateBoundTestFramework.cs -Src/FluentAssertions/Execution/MSTestFrameworkV2.cs -Src/FluentAssertions/Execution/MSTestFrameworkV4.cs -Src/FluentAssertions/Execution/MSpecFramework.cs -Src/FluentAssertions/Execution/NUnitTestFramework.cs -Src/FluentAssertions/Execution/Reason.cs -Src/FluentAssertions/Execution/StringExtensions.cs -Src/FluentAssertions/Execution/SubjectIdentificationBuilder.cs -Src/FluentAssertions/Execution/TUnitFramework.cs -Src/FluentAssertions/Execution/TestFrameworkFactory.cs -Src/FluentAssertions/Execution/WithoutFormattingWrapper.cs -Src/FluentAssertions/Execution/XUnitTestFramework.cs -Src/FluentAssertions/Extensibility/AssertionEngineInitializerAttribute.cs -Src/FluentAssertions/Extensions/FluentDateTimeExtensions.cs -Src/FluentAssertions/Extensions/FluentTimeSpanExtensions.cs -Src/FluentAssertions/Extensions/OccurrenceConstraintExtensions.cs -Src/FluentAssertions/FluentActions.cs -Src/FluentAssertions/FluentAssertions.csproj -Src/FluentAssertions/Formatting/DateTimeOffsetValueFormatter.cs -Src/FluentAssertions/Formatting/DecimalValueFormatter.cs -Src/FluentAssertions/Formatting/DictionaryValueFormatter.cs -Src/FluentAssertions/Formatting/DoubleValueFormatter.cs -Src/FluentAssertions/Formatting/EnumValueFormatter.cs -Src/FluentAssertions/Formatting/EnumerableExtensions.cs -Src/FluentAssertions/Formatting/EnumerableValueFormatter.cs -Src/FluentAssertions/Formatting/ExceptionValueFormatter.cs -Src/FluentAssertions/Formatting/ExpressionValueFormatter.cs -Src/FluentAssertions/Formatting/FormatChild.cs -Src/FluentAssertions/Formatting/Formatter.cs -Src/FluentAssertions/Formatting/FormattingContext.cs -Src/FluentAssertions/Formatting/FormattingOptions.cs -Src/FluentAssertions/Formatting/IValueFormatter.cs -Src/FluentAssertions/Formatting/Int16ValueFormatter.cs -Src/FluentAssertions/Formatting/Int32ValueFormatter.cs -Src/FluentAssertions/Formatting/Int64ValueFormatter.cs -Src/FluentAssertions/Formatting/MaxLinesExceededException.cs -Src/FluentAssertions/Formatting/MultidimensionalArrayFormatter.cs -Src/FluentAssertions/Formatting/NullValueFormatter.cs -Src/FluentAssertions/Formatting/PredicateLambdaExpressionValueFormatter.cs -Src/FluentAssertions/Formatting/PropertyInfoFormatter.cs -Src/FluentAssertions/Formatting/SByteValueFormatter.cs -Src/FluentAssertions/Formatting/SingleValueFormatter.cs -Src/FluentAssertions/Formatting/StringValueFormatter.cs -Src/FluentAssertions/Formatting/TaskFormatter.cs -Src/FluentAssertions/Formatting/TimeSpanValueFormatter.cs -Src/FluentAssertions/Formatting/UInt16ValueFormatter.cs -Src/FluentAssertions/Formatting/UInt32ValueFormatter.cs -Src/FluentAssertions/Formatting/UInt64ValueFormatter.cs -Src/FluentAssertions/Formatting/XDocumentValueFormatter.cs -Src/FluentAssertions/Formatting/XElementValueFormatter.cs -Src/FluentAssertions/Formatting/XmlReaderValueFormatter.cs -Src/FluentAssertions/JsonAssertionExtensions.cs -Src/FluentAssertions/LessThan.cs -Src/FluentAssertions/License.cs -Src/FluentAssertions/MoreThan.cs -Src/FluentAssertions/Numeric/ByteAssertions.cs -Src/FluentAssertions/Numeric/DecimalAssertions.cs -Src/FluentAssertions/Numeric/DoubleAssertions.cs -Src/FluentAssertions/Numeric/Int16Assertions.cs -Src/FluentAssertions/Numeric/Int32Assertions.cs -Src/FluentAssertions/Numeric/Int64Assertions.cs -Src/FluentAssertions/Numeric/NullableByteAssertions.cs -Src/FluentAssertions/Numeric/NullableDecimalAssertions.cs -Src/FluentAssertions/Numeric/NullableInt16Assertions.cs -Src/FluentAssertions/Numeric/NullableInt32Assertions.cs -Src/FluentAssertions/Numeric/NullableInt64Assertions.cs -Src/FluentAssertions/Numeric/NullableSByteAssertions.cs -Src/FluentAssertions/Numeric/NullableSingleAssertions.cs -Src/FluentAssertions/Numeric/NullableUInt16Assertions.cs -Src/FluentAssertions/Numeric/NullableUInt32Assertions.cs -Src/FluentAssertions/Numeric/NullableUInt64Assertions.cs -Src/FluentAssertions/Numeric/NumericAssertionsBase.cs -Src/FluentAssertions/Numeric/SByteAssertions.cs -Src/FluentAssertions/Numeric/SingleAssertions.cs -Src/FluentAssertions/Numeric/UInt16Assertions.cs -Src/FluentAssertions/Numeric/UInt32Assertions.cs -Src/FluentAssertions/Numeric/UInt64Assertions.cs -Src/FluentAssertions/NumericAssertionsExtensions.cs -Src/FluentAssertions/ObjectAssertionsExtensions.cs -Src/FluentAssertions/OccurrenceConstraint.cs -Src/FluentAssertions/Polyfill/StringBuilderExtensions.cs -Src/FluentAssertions/Polyfill/StringExtensions.cs -Src/FluentAssertions/Polyfill/SystemExtensions.cs -Src/FluentAssertions/Primitives/BooleanAssertions.cs -Src/FluentAssertions/Primitives/DateOnlyAssertions.cs -Src/FluentAssertions/Primitives/DateTimeAssertions.cs -Src/FluentAssertions/Primitives/DateTimeOffsetAssertions.cs -Src/FluentAssertions/Primitives/DateTimeOffsetRangeAssertions.cs -Src/FluentAssertions/Primitives/DateTimeRangeAssertions.cs -Src/FluentAssertions/Primitives/EnumAssertions.cs -Src/FluentAssertions/Primitives/GuidAssertions.cs -Src/FluentAssertions/Primitives/NullableBooleanAssertions.cs -Src/FluentAssertions/Primitives/NullableDateOnlyAssertions.cs -Src/FluentAssertions/Primitives/NullableDateTimeAssertions.cs -Src/FluentAssertions/Primitives/NullableDateTimeOffsetAssertions.cs -Src/FluentAssertions/Primitives/NullableEnumAssertions.cs -Src/FluentAssertions/Primitives/NullableGuidAssertions.cs -Src/FluentAssertions/Primitives/NullableSimpleTimeSpanAssertions.cs -Src/FluentAssertions/Primitives/NullableTimeOnlyAssertions.cs -Src/FluentAssertions/Primitives/SimpleTimeSpanAssertions.cs -Src/FluentAssertions/Primitives/StringAssertions.cs -Src/FluentAssertions/Primitives/StringContainsStrategy.cs -Src/FluentAssertions/Primitives/StringEndStrategy.cs -Src/FluentAssertions/Primitives/StringEqualityStrategy.cs -Src/FluentAssertions/Primitives/StringStartStrategy.cs -Src/FluentAssertions/Primitives/StringValidator.cs -Src/FluentAssertions/Primitives/StringValidatorSupportingNull.cs -Src/FluentAssertions/Primitives/StringWildcardMatchingStrategy.cs -Src/FluentAssertions/Primitives/TimeOnlyAssertions.cs -Src/FluentAssertions/Primitives/TimeSpanPredicate.cs -Src/FluentAssertions/Specialized/ActionAssertions.cs -Src/FluentAssertions/Specialized/DelegateAssertions.cs -Src/FluentAssertions/Specialized/DelegateAssertionsBase.cs -Src/FluentAssertions/Specialized/ExceptionAssertions.cs -Src/FluentAssertions/Specialized/ExecutionTime.cs -Src/FluentAssertions/Specialized/ExecutionTimeAssertions.cs -Src/FluentAssertions/Specialized/FunctionAssertions.cs -Src/FluentAssertions/Specialized/GenericAsyncFunctionAssertions.cs -Src/FluentAssertions/Specialized/JsonNodeAssertions.cs -Src/FluentAssertions/Specialized/JsonValueExtensions.cs -Src/FluentAssertions/Specialized/MemberExecutionTime.cs -Src/FluentAssertions/Specialized/NonGenericAsyncFunctionAssertions.cs -Src/FluentAssertions/Specialized/TaskCompletionSourceAssertions.cs -Src/FluentAssertions/Specialized/TaskCompletionSourceAssertionsBase.cs -Src/FluentAssertions/Streams/BufferedStreamAssertions.cs -Src/FluentAssertions/Streams/StreamAssertions.cs -Src/FluentAssertions/TypeEnumerableExtensions.cs -Src/FluentAssertions/TypeExtensions.cs -Src/FluentAssertions/Types/AllTypes.cs -Src/FluentAssertions/Types/AssemblyAssertions.cs -Src/FluentAssertions/Types/ConstructorInfoAssertions.cs -Src/FluentAssertions/Types/MemberInfoAssertions.cs -Src/FluentAssertions/Types/MethodBaseAssertions.cs -Src/FluentAssertions/Types/MethodInfoAssertions.cs -Src/FluentAssertions/Types/MethodInfoSelector.cs -Src/FluentAssertions/Types/MethodInfoSelectorAssertions.cs -Src/FluentAssertions/Types/PropertyInfoAssertions.cs -Src/FluentAssertions/Types/PropertyInfoSelector.cs -Src/FluentAssertions/Types/PropertyInfoSelectorAssertions.cs -Src/FluentAssertions/Types/TypeAssertions.cs -Src/FluentAssertions/Types/TypeSelector.cs -Src/FluentAssertions/Types/TypeSelectorAssertions.cs -Src/FluentAssertions/Value.cs -Src/FluentAssertions/Xml/Equivalency/AttributeData.cs -Src/FluentAssertions/Xml/Equivalency/Failure.cs -Src/FluentAssertions/Xml/Equivalency/Node.cs -Src/FluentAssertions/Xml/Equivalency/XmlIterator.cs -Src/FluentAssertions/Xml/Equivalency/XmlReaderValidator.cs -Src/FluentAssertions/Xml/XAttributeAssertions.cs -Src/FluentAssertions/Xml/XDocumentAssertions.cs -Src/FluentAssertions/Xml/XElementAssertions.cs -Src/FluentAssertions/Xml/XmlElementAssertions.cs -Src/FluentAssertions/Xml/XmlNodeAssertions.cs -Src/FluentAssertions/Xml/XmlNodeFormatter.cs -Src/FluentAssertions/XmlAssertionExtensions.cs -Tests/Approval.Tests/ApprovedApi/FluentAssertions/net6.0.verified.txt -Tests/FluentAssertions.Specs/Exceptions/OuterExceptionSpecs.cs -Tests/FluentAssertions.Specs/StackTraceHiddenSpecs.cs \ No newline at end of file From 63ba89443b91259688bc8ce9e54f8579ed25c6c6 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sat, 21 Feb 2026 13:06:57 +0100 Subject: [PATCH 3/9] Bump F# to 8.0 to compile with .NET 11 SDK This fixes the following compile error: ``` FSC : error FS3880: Language version '6.0' is out of support. The last .NET SDK supporting it is available at https://dotnet.microsoft.com/en-us/download/dotnet/10.0 ``` --- Tests/FSharp.Specs/FSharp.Specs.fsproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From d5e1ef525bc7e7d213b5315591ad068cca84479c Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Fri, 13 Feb 2026 14:53:18 +0100 Subject: [PATCH 4/9] Fix CA1830 --- .../Equivalency/Tracing/StringBuilderTraceWriter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } } From bb896c7405ba7c4d049af86cf3f35e452a1a8918 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sat, 7 Feb 2026 21:02:31 +0100 Subject: [PATCH 5/9] Fix a formatting exception when {} is used as a dictionary key --- .../Matching/MustMatchByNameRule.cs | 6 ++-- .../Steps/AssertionRuleEquivalencyStep.cs | 6 ++-- .../Equivalency/Steps/EnumEqualityStep.cs | 11 ++++--- .../Steps/StringEqualityEquivalencyStep.cs | 4 +-- .../Execution/StringExtensions.cs | 24 ++++++++++++++ .../Execution/WithoutFormattingWrapper.cs | 11 +++++++ Src/FluentAssertions/Formatting/Formatter.cs | 1 + .../Formatting/PassthroughValueFormatter.cs | 17 ++++++++++ .../Specialized/ExceptionAssertions.cs | 5 +-- .../AssertionScope.MessageFormatingSpecs.cs | 33 +++++++++++++++++++ docs/_pages/releases.md | 6 ++++ 11 files changed, 109 insertions(+), 15 deletions(-) create mode 100644 Src/FluentAssertions/Execution/StringExtensions.cs create mode 100644 Src/FluentAssertions/Execution/WithoutFormattingWrapper.cs create mode 100644 Src/FluentAssertions/Formatting/PassthroughValueFormatter.cs 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/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/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/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/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/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/docs/_pages/releases.md b/docs/_pages/releases.md index 3d5307376a..016d311492 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -7,6 +7,12 @@ 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) + ## 7.2.1 ### Fixes From 6ad1fb824226fc7c4c251c1bd2a6352533d3d0b2 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Tue, 2 Dec 2025 13:18:12 +0100 Subject: [PATCH 6/9] Fix NotSupportedException from ReduceConstantSubExpressions C# 14' "first-class spans" causes `MemoryExtensions.Contains` to be picked over `Enumerable.Contains`. `ConstantSubExpressionReductionVisitor.Visit` calls `DynamicInvoke` which then throws a `NotSupportedException` from [`RuntimeMethodInfo.ThrowNoInvokeException`](https://source.dot.net/#System.Private.CoreLib/src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs,70) For more information see https://github.com/dotnet/runtime/issues/120251 --- ...PredicateLambdaExpressionValueFormatter.cs | 2 +- ...cateLambdaExpressionValueFormatterSpecs.cs | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) 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/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() { From 97d9a201c4ec82d67160fb8334660e4eac3e5fab Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sun, 29 Jun 2025 09:28:23 +0200 Subject: [PATCH 7/9] Ensured WithTracing is safe when used with BeEquivalentTo globally --- .../EquivalencyValidationContext.cs | 14 ++++++++++ .../Equivalency/EquivalencyValidator.cs | 2 ++ ...elfReferenceEquivalencyAssertionOptions.cs | 2 +- .../Tracing/InternalTraceWriter.cs | 3 +++ .../ConfigurationSpecs.cs | 26 +++++++++++++++++++ docs/_pages/releases.md | 1 + 6 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 Src/FluentAssertions/Equivalency/Tracing/InternalTraceWriter.cs 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/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/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/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/docs/_pages/releases.md b/docs/_pages/releases.md index 016d311492..ce13397cf0 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -12,6 +12,7 @@ sidebar: ### 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) ## 7.2.1 From f3b47cb8909c7a89121cc191fac2e4c4de5beaa6 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Sat, 20 Sep 2025 19:01:44 +0200 Subject: [PATCH 8/9] Backport `AssertionResultSet` fixes from #3100 --- .../Equivalency/Steps/AssertionResultSet.cs | 9 +++++++++ 1 file changed, 9 insertions(+) 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(); } From 16e5b2d96a6ebfa1600844024362ad93489065c6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:29:42 +0100 Subject: [PATCH 9/9] Fix formatting exception when comparing strings containing braces (#3151) --- .../Primitives/StringEqualityStrategy.cs | 4 +-- .../Primitives/StringAssertionSpecs.Be.cs | 32 +++++++++++++++++++ docs/_pages/releases.md | 1 + 3 files changed, 35 insertions(+), 2 deletions(-) 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/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 ce13397cf0..6e11e199b8 100644 --- a/docs/_pages/releases.md +++ b/docs/_pages/releases.md @@ -13,6 +13,7 @@ sidebar: * 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