From 119afd57eb52829ce706d061256471f453a5efe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 12 May 2026 18:06:00 +0200 Subject: [PATCH 01/13] Add structured assertion message infrastructure (RFC 012) Introduce the foundational types and helpers for structured multi-line assertion failure messages as described in RFC 012: - EvidenceLine: labeled line record struct for evidence blocks - EvidenceBlock: collection of labeled lines with automatic alignment - StructuredAssertionMessage: builder producing the new multi-line format (prefix + summary + user message + evidence block + call-site) - AssertionValueRenderer: renders values per RFC 012 rules (null, quoted strings with escape sequences, booleans, collections as JSON arrays) - AssertFailedException: add ExpectedText/ActualText public properties - Assert: add ReportAssertFailed/ThrowAssertFailed overloads accepting StructuredAssertionMessage No existing assertion methods are changed yet - this PR only introduces the infrastructure that subsequent PRs will use to migrate each assertion method to the new format. --- .../TestFramework/Assertions/Assert.cs | 58 +++++++ .../Assertions/AssertionValueRenderer.cs | 117 +++++++++++++ .../TestFramework/Assertions/EvidenceBlock.cs | 67 +++++++ .../TestFramework/Assertions/EvidenceLine.cs | 9 + .../Assertions/StructuredAssertionMessage.cs | 129 ++++++++++++++ .../Exceptions/AssertFailedException.cs | 14 ++ .../PublicAPI/PublicAPI.Unshipped.txt | 2 + .../Assertions/AssertFailedExceptionTests.cs | 39 +++++ .../Assertions/AssertionValueRendererTests.cs | 100 +++++++++++ .../Assertions/EvidenceBlockTests.cs | 77 +++++++++ .../StructuredAssertionMessageTests.cs | 163 ++++++++++++++++++ 11 files changed, 775 insertions(+) create mode 100644 src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs create mode 100644 src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs create mode 100644 src/TestFramework/TestFramework/Assertions/EvidenceLine.cs create mode 100644 src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/EvidenceBlockTests.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 0703444ae6..7e565a24b4 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -107,6 +107,64 @@ static bool ShouldLaunchDebugger() private static AssertFailedException CreateAssertFailedException(string assertionName, string? message) => new(FormatAssertionFailed(assertionName, message)); + private static AssertFailedException CreateAssertFailedException(StructuredAssertionMessage structuredMessage) + { + AssertFailedException exception = new(structuredMessage.Format()) + { + ExpectedText = structuredMessage.ExpectedText, + ActualText = structuredMessage.ActualText, + }; + return exception; + } + + /// + /// Reports an assertion failure using a structured message. Within an , + /// the failure is collected and execution continues. Outside a scope, the failure is thrown immediately. + /// + /// + /// The structured assertion failure message. + /// +#pragma warning disable CS8763 // A method marked [DoesNotReturn] should not return + [DoesNotReturn] + [StackTraceHidden] + internal static void ReportAssertFailed(StructuredAssertionMessage structuredMessage) + { + LaunchDebuggerIfNeeded(); + AssertFailedException assertionFailedException = CreateAssertFailedException(structuredMessage); + if (AssertScope.Current is { } scope) + { + try + { + throw assertionFailedException; + } + catch (AssertFailedException ex) + { + assertionFailedException = ex; + } + + scope.AddError(assertionFailedException); + return; + } + + throw assertionFailedException; + } +#pragma warning restore CS8763 // A method marked [DoesNotReturn] should not return + + /// + /// Reports an assertion failure using a structured message and always throws, + /// even within an . + /// + /// + /// The structured assertion failure message. + /// + [DoesNotReturn] + [StackTraceHidden] + internal static void ThrowAssertFailed(StructuredAssertionMessage structuredMessage) + { + LaunchDebuggerIfNeeded(); + throw CreateAssertFailedException(structuredMessage); + } + private static string FormatAssertionFailed(string assertionName, string? message) { string failedMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName); diff --git a/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs new file mode 100644 index 0000000000..ba610f4c22 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/AssertionValueRenderer.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma warning disable IDE0046 // Convert to conditional expression + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Renders values for display in structured assertion messages following the RFC 012 value rendering rules. +/// +internal static class AssertionValueRenderer +{ + /// + /// Renders a value as a string suitable for display in the evidence block. + /// + internal static string RenderValue(object? value) + { + if (value is null) + { + return "null"; + } + + return value switch + { + string s => RenderString(s), + bool b => b ? "true" : "false", + char c => RenderChar(c), + IEnumerable enumerable when value is not string => RenderCollection(enumerable), + _ => value.ToString() ?? value.GetType().FullName ?? value.GetType().Name, + }; + } + + /// + /// Renders a string value with double quotes and escape sequences for control characters. + /// + private static string RenderString(string value) + { + StringBuilder sb = new(value.Length + 2); + sb.Append('"'); + foreach (char c in value) + { + switch (c) + { + case '"': + sb.Append("\\\""); + break; + case '\\': + sb.Append("\\\\"); + break; + case '\n': + sb.Append("\\n"); + break; + case '\r': + sb.Append("\\r"); + break; + case '\t': + sb.Append("\\t"); + break; + case '\0': + sb.Append("\\0"); + break; + default: + if (char.IsControl(c)) + { + sb.Append("\\u"); + sb.Append(((int)c).ToString("X4", CultureInfo.InvariantCulture)); + } + else + { + sb.Append(c); + } + + break; + } + } + + sb.Append('"'); + return sb.ToString(); + } + + /// + /// Renders a char value with single quotes and escape sequences. + /// + private static string RenderChar(char value) => + value switch + { + '\n' => "'\\n'", + '\r' => "'\\r'", + '\t' => "'\\t'", + '\0' => "'\\0'", + _ when char.IsControl(value) => $"'\\u{(int)value:X4}'", + _ => $"'{value}'", + }; + + /// + /// Renders a collection in JSON-style array notation. + /// + private static string RenderCollection(IEnumerable enumerable) + { + StringBuilder sb = new(); + sb.Append('['); + bool first = true; + foreach (object? item in enumerable) + { + if (!first) + { + sb.Append(", "); + } + + sb.Append(RenderValue(item)); + first = false; + } + + sb.Append(']'); + return sb.ToString(); + } +} diff --git a/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs b/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs new file mode 100644 index 0000000000..4fafbb4f8f --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/EvidenceBlock.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Represents the evidence block of a structured assertion message, containing labeled value lines +/// such as expected/actual values and assertion-specific details. +/// +internal readonly struct EvidenceBlock +{ + private readonly List _lines; + + private EvidenceBlock(List lines) + { + _lines = lines; + } + + internal static EvidenceBlock Create() => new([]); + + internal IReadOnlyList Lines => _lines; + + internal EvidenceBlock AddLine(string label, string value) + { + _lines.Add(new EvidenceLine(label, value)); + return this; + } + + /// + /// Formats the evidence block as aligned label: value lines. + /// Labels are right-padded so all values start at the same column. + /// + internal string Format() + { + if (_lines.Count == 0) + { + return string.Empty; + } + + int maxLabelLength = 0; + foreach (EvidenceLine line in _lines) + { + if (line.Label.Length > maxLabelLength) + { + maxLabelLength = line.Label.Length; + } + } + + StringBuilder sb = new(); + for (int i = 0; i < _lines.Count; i++) + { + if (i > 0) + { + sb.Append(Environment.NewLine); + } + + EvidenceLine line = _lines[i]; + + // Pad label to align values, then append ": " and value + sb.Append(line.Label.PadRight(maxLabelLength)); + sb.Append(' '); + sb.Append(line.Value); + } + + return sb.ToString(); + } +} diff --git a/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs b/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs new file mode 100644 index 0000000000..bb09f5f150 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/EvidenceLine.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Represents a single labeled line in the evidence block of a structured assertion message. +/// +internal readonly record struct EvidenceLine(string Label, string Value); diff --git a/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs b/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs new file mode 100644 index 0000000000..cfb3f0fe0a --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/StructuredAssertionMessage.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Builds a structured assertion failure message following the format: +/// +/// Assertion failed. <summary> +/// <user message> +/// +/// <evidence block> +/// +/// <call-site expression> +/// +/// +internal sealed class StructuredAssertionMessage +{ + private const string AssertionPrefix = "Assertion failed."; + + private readonly string _summary; + private readonly List _additionalSummaryLines = []; + private string? _userMessage; + private EvidenceBlock? _evidenceBlock; + private string? _callSiteExpression; + + internal StructuredAssertionMessage(string summary) + { + _summary = summary; + } + + internal string? ExpectedText { get; private set; } + + internal string? ActualText { get; private set; } + + internal StructuredAssertionMessage WithAdditionalSummaryLine(string line) + { + _additionalSummaryLines.Add(line); + return this; + } + + internal StructuredAssertionMessage WithUserMessage(string? userMessage) + { + if (!string.IsNullOrWhiteSpace(userMessage)) + { + _userMessage = userMessage; + } + + return this; + } + + internal StructuredAssertionMessage WithEvidence(EvidenceBlock evidenceBlock) + { + _evidenceBlock = evidenceBlock; + return this; + } + + internal StructuredAssertionMessage WithExpectedAndActual(string? expectedText, string? actualText) + { + ExpectedText = expectedText; + ActualText = actualText; + return this; + } + + internal StructuredAssertionMessage WithCallSiteExpression(string? callSiteExpression) + { + if (!string.IsNullOrWhiteSpace(callSiteExpression)) + { + _callSiteExpression = callSiteExpression; + } + + return this; + } + + /// + /// Formats the structured message as a multi-line string following the RFC 012 layout. + /// + internal string Format() + { + StringBuilder sb = new(); + + // Line 1: Assertion prefix + summary + sb.Append(AssertionPrefix); + if (!string.IsNullOrEmpty(_summary)) + { + sb.Append(' '); + sb.Append(_summary); + } + + // Additional summary lines + foreach (string additionalLine in _additionalSummaryLines) + { + sb.Append(Environment.NewLine); + sb.Append(additionalLine); + } + + // User message (on its own line, no label) + if (_userMessage is not null) + { + sb.Append(Environment.NewLine); + sb.Append(_userMessage); + } + + // Evidence block (separated by blank line) + if (_evidenceBlock is { } evidence) + { + string formattedEvidence = evidence.Format(); + if (!string.IsNullOrEmpty(formattedEvidence)) + { + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append(formattedEvidence); + } + } + + // Call-site expression (separated by blank line) + if (_callSiteExpression is not null) + { + sb.Append(Environment.NewLine); + sb.Append(Environment.NewLine); + sb.Append(_callSiteExpression); + } + + return sb.ToString(); + } + + /// + public override string ToString() => Format(); +} diff --git a/src/TestFramework/TestFramework/Exceptions/AssertFailedException.cs b/src/TestFramework/TestFramework/Exceptions/AssertFailedException.cs index 2bbd14c132..42a8b22397 100644 --- a/src/TestFramework/TestFramework/Exceptions/AssertFailedException.cs +++ b/src/TestFramework/TestFramework/Exceptions/AssertFailedException.cs @@ -39,6 +39,20 @@ public AssertFailedException() { } + /// + /// Gets the pre-formatted text representation of the expected value, as displayed + /// in the expected: line of the structured assertion message. Returns + /// when the assertion has no natural expected value (e.g. ). + /// + public string? ExpectedText { get; internal set; } + + /// + /// Gets the pre-formatted text representation of the actual value, as displayed + /// in the actual: line of the structured assertion message. Returns + /// when the assertion has no natural actual value. + /// + public string? ActualText { get; internal set; } + /// /// Initializes a new instance of the class. /// diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index ee81f202d3..cc143fac2e 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ #nullable enable [MSTESTEXP]static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Scope() -> System.IDisposable! +Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ActualText.get -> string? +Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException.ExpectedText.get -> string? diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs new file mode 100644 index 0000000000..62c82fd0a4 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertFailedExceptionTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public class AssertFailedExceptionTests : TestContainer +{ + public void ExpectedText_DefaultsToNull() + { + var exception = new AssertFailedException("test message"); + + exception.ExpectedText.Should().BeNull(); + } + + public void ActualText_DefaultsToNull() + { + var exception = new AssertFailedException("test message"); + + exception.ActualText.Should().BeNull(); + } + + public void ExpectedAndActualText_CanBeSet() + { + var exception = new AssertFailedException("test message") + { + ExpectedText = "42", + ActualText = "37", + }; + + exception.ExpectedText.Should().Be("42"); + exception.ActualText.Should().Be("37"); + } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs new file mode 100644 index 0000000000..dda2606d14 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertionValueRendererTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public class AssertionValueRendererTests : TestContainer +{ + public void RenderValue_Null_ReturnsNull() => + AssertionValueRenderer.RenderValue(null).Should().Be("null"); + + public void RenderValue_EmptyString_ReturnsQuotedEmpty() => + AssertionValueRenderer.RenderValue(string.Empty).Should().Be("\"\""); + + public void RenderValue_SimpleString_ReturnsQuotedString() => + AssertionValueRenderer.RenderValue("hello world").Should().Be("\"hello world\""); + + public void RenderValue_StringWithEmbeddedQuotes_EscapesQuotes() => + AssertionValueRenderer.RenderValue("she said \"hello\"").Should().Be("\"she said \\\"hello\\\"\""); + + public void RenderValue_StringWithNewline_EscapesNewline() => + AssertionValueRenderer.RenderValue("line1\nline2").Should().Be("\"line1\\nline2\""); + + public void RenderValue_StringWithCarriageReturn_EscapesCR() => + AssertionValueRenderer.RenderValue("line1\rline2").Should().Be("\"line1\\rline2\""); + + public void RenderValue_StringWithTab_EscapesTab() => + AssertionValueRenderer.RenderValue("col1\tcol2").Should().Be("\"col1\\tcol2\""); + + public void RenderValue_StringWithNullChar_EscapesNull() => + AssertionValueRenderer.RenderValue("abc\0def").Should().Be("\"abc\\0def\""); + + public void RenderValue_StringWithBackslash_EscapesBackslash() => + AssertionValueRenderer.RenderValue("path\\to\\file").Should().Be("\"path\\\\to\\\\file\""); + + public void RenderValue_WhitespaceOnlyString_ReturnsQuotedWhitespace() => + AssertionValueRenderer.RenderValue(" ").Should().Be("\" \""); + + public void RenderValue_Integer_ReturnsUnquoted() => + AssertionValueRenderer.RenderValue(42).Should().Be("42"); + + public void RenderValue_NegativeInteger_ReturnsUnquoted() => + AssertionValueRenderer.RenderValue(-7).Should().Be("-7"); + + public void RenderValue_Double_ReturnsUnquoted() => + AssertionValueRenderer.RenderValue(3.14).Should().Be("3.14"); + + public void RenderValue_BoolTrue_ReturnsLowercase() => + AssertionValueRenderer.RenderValue(true).Should().Be("true"); + + public void RenderValue_BoolFalse_ReturnsLowercase() => + AssertionValueRenderer.RenderValue(false).Should().Be("false"); + + public void RenderValue_ListOfInts_ReturnsJsonArray() + { + var list = new List { 1, 2, 3 }; + AssertionValueRenderer.RenderValue(list).Should().Be("[1, 2, 3]"); + } + + public void RenderValue_EmptyList_ReturnsEmptyBrackets() => + AssertionValueRenderer.RenderValue(new List()).Should().Be("[]"); + + public void RenderValue_ListOfStrings_ReturnsQuotedElements() + { + var list = new List { "apple", "cherry", "date" }; + AssertionValueRenderer.RenderValue(list).Should().Be("[\"apple\", \"cherry\", \"date\"]"); + } + + public void RenderValue_ListWithNull_RendersNullElement() + { + var list = new List { "apple", null, "date" }; + AssertionValueRenderer.RenderValue(list).Should().Be("[\"apple\", null, \"date\"]"); + } + + public void RenderValue_ObjectWithToString_ReturnsToString() => + AssertionValueRenderer.RenderValue(new ObjectWithCustomToString("my-object")).Should().Be("my-object"); + + public void RenderValue_Char_ReturnsSingleQuoted() => + AssertionValueRenderer.RenderValue('a').Should().Be("'a'"); + + public void RenderValue_CharNewline_ReturnsEscaped() => + AssertionValueRenderer.RenderValue('\n').Should().Be("'\\n'"); + + private sealed class ObjectWithCustomToString + { + private readonly string _value; + + public ObjectWithCustomToString(string value) + { + _value = value; + } + + public override string ToString() => _value; + } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/EvidenceBlockTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/EvidenceBlockTests.cs new file mode 100644 index 0000000000..9bc0a523a1 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/EvidenceBlockTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public class EvidenceBlockTests : TestContainer +{ + public void Format_EmptyBlock_ReturnsEmptyString() + { + var block = EvidenceBlock.Create(); + + string result = block.Format(); + + result.Should().BeEmpty(); + } + + public void Format_SingleLine_FormatsLabelAndValue() + { + EvidenceBlock block = EvidenceBlock.Create() + .AddLine("expected:", "42"); + + string result = block.Format(); + + result.Should().Be("expected: 42"); + } + + public void Format_TwoLines_AlignsLabels() + { + EvidenceBlock block = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + string result = block.Format(); + + // "expected:" is 9 chars, "actual:" is 7 chars + // "actual:" should be padded to 9 chars for alignment + string expected = "expected: 42" + Environment.NewLine + "actual: 37"; + result.Should().Be(expected); + } + + public void Format_MultipleLines_AlignsToLongestLabel() + { + EvidenceBlock block = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37") + .AddLine("ignore case:", "true") + .AddLine("culture:", "tr-TR"); + + string result = block.Format(); + + string[] lines = result.Split([Environment.NewLine], StringSplitOptions.None); + lines.Should().HaveCount(4); + lines[0].Should().Be("expected: 42"); + lines[1].Should().Be("actual: 37"); + lines[2].Should().Be("ignore case: true"); + lines[3].Should().Be("culture: tr-TR"); + } + + public void Lines_ReturnsAddedLines() + { + EvidenceBlock block = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + block.Lines.Should().HaveCount(2); + block.Lines[0].Label.Should().Be("expected:"); + block.Lines[0].Value.Should().Be("42"); + block.Lines[1].Label.Should().Be("actual:"); + block.Lines[1].Value.Should().Be("37"); + } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs new file mode 100644 index 0000000000..b30d620ca4 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/StructuredAssertionMessageTests.cs @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using AwesomeAssertions; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public class StructuredAssertionMessageTests : TestContainer +{ + public void Format_SummaryOnly_ReturnsAssertionPrefix() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + + string result = message.Format(); + + result.Should().Be("Assertion failed. Expected values to be equal."); + } + + public void Format_EmptySummary_ReturnsJustPrefix() + { + StructuredAssertionMessage message = new(string.Empty); + + string result = message.Format(); + + result.Should().Be("Assertion failed."); + } + + public void Format_WithUserMessage_ShowsMessageAfterSummary() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithUserMessage("Discount should be applied after tax"); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + "Discount should be applied after tax"; + result.Should().Be(expected); + } + + public void Format_WithEvidenceBlock_SeparatedByBlankLine() + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithEvidence(evidence); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + Environment.NewLine + + "expected: 42" + Environment.NewLine + + "actual: 37"; + result.Should().Be(expected); + } + + public void Format_WithUserMessageAndEvidence_CorrectLayout() + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithUserMessage("Discount should be applied after tax"); + message.WithEvidence(evidence); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + "Discount should be applied after tax" + Environment.NewLine + + Environment.NewLine + + "expected: 42" + Environment.NewLine + + "actual: 37"; + result.Should().Be(expected); + } + + public void Format_WithCallSiteExpression_SeparatedByBlankLine() + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithEvidence(evidence); + message.WithCallSiteExpression("Assert.AreEqual(expectedCount, actualCount)"); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + Environment.NewLine + + "expected: 42" + Environment.NewLine + + "actual: 37" + Environment.NewLine + + Environment.NewLine + + "Assert.AreEqual(expectedCount, actualCount)"; + result.Should().Be(expected); + } + + public void Format_FullMessage_CorrectLayout() + { + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", "42") + .AddLine("actual:", "37"); + + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithAdditionalSummaryLine("Values differ at position 3."); + message.WithUserMessage("Check the discount logic"); + message.WithEvidence(evidence); + message.WithCallSiteExpression("Assert.AreEqual(expected, actual)"); + + string result = message.Format(); + + string expected = "Assertion failed. Expected values to be equal." + Environment.NewLine + + "Values differ at position 3." + Environment.NewLine + + "Check the discount logic" + Environment.NewLine + + Environment.NewLine + + "expected: 42" + Environment.NewLine + + "actual: 37" + Environment.NewLine + + Environment.NewLine + + "Assert.AreEqual(expected, actual)"; + result.Should().Be(expected); + } + + public void Format_NullUserMessage_OmitsUserMessageLine() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithUserMessage(null); + + string result = message.Format(); + + result.Should().Be("Assertion failed. Expected values to be equal."); + } + + public void Format_WhitespaceUserMessage_OmitsUserMessageLine() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithUserMessage(" "); + + string result = message.Format(); + + result.Should().Be("Assertion failed. Expected values to be equal."); + } + + public void WithExpectedAndActual_SetsProperties() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + message.WithExpectedAndActual("42", "37"); + + message.ExpectedText.Should().Be("42"); + message.ActualText.Should().Be("37"); + } + + public void ToString_ReturnsSameAsFormat() + { + StructuredAssertionMessage message = new("Expected values to be equal."); + + message.ToString().Should().Be(message.Format()); + } +} From 3bbc0e914e8bd0e1092432e892cb4c599c29094e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 12 May 2026 18:16:50 +0200 Subject: [PATCH 02/13] Apply structured assertion messages to IsTrue, IsFalse, IsNull, IsNotNull - Update Assert.IsTrue/IsFalse to use StructuredAssertionMessage with evidence block showing actual value and call-site expression - Update Assert.IsNull to use StructuredAssertionMessage with evidence block showing actual value - Update Assert.IsNotNull to use StructuredAssertionMessage without evidence block (actual is always null per RFC) - Update interpolated string handlers to store condition/value for passing to the new reporting methods - Add FormatCallSiteExpression helper to Assert.cs for formatting call-site display lines - Remove unused BuildUserMessageForConditionExpression method - Update all related test expectations to match new message format --- .../TestFramework/Assertions/Assert.IsNull.cs | 41 +++++++++++----- .../TestFramework/Assertions/Assert.IsTrue.cs | 48 ++++++++++++++----- .../TestFramework/Assertions/Assert.cs | 37 ++++++++++++-- .../Assertions/AssertTests.IsNull.cs | 12 ++--- .../Assertions/AssertTests.IsTrueTests.cs | 36 +++++++------- .../Assertions/AssertTests.ScopeTests.cs | 4 +- 6 files changed, 127 insertions(+), 51 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index 00b3e1eec6..cc6ca68bf7 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -22,9 +22,11 @@ public sealed partial class Assert public readonly struct AssertIsNullInterpolatedStringHandler { private readonly StringBuilder? _builder; + private readonly object? _value; public AssertIsNullInterpolatedStringHandler(int literalLength, int formattedCount, object? value, out bool shouldAppend) { + _value = value; shouldAppend = IsNullFailing(value); if (shouldAppend) { @@ -36,8 +38,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsNullFailed(_builder.ToString()); + ReportAssertIsNullFailed(_value, _builder.ToString(), valueExpression); } } @@ -90,8 +91,7 @@ internal void ComputeAssertion(string valueExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "value", valueExpression) + " "); - ReportAssertIsNotNullFailed(_builder.ToString()); + ReportAssertIsNotNullFailed(_builder.ToString(), valueExpression); } } @@ -152,14 +152,26 @@ public static void IsNull(object? value, string? message = "", [CallerArgumentEx { if (IsNullFailing(value)) { - ReportAssertIsNullFailed(BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsNullFailed(value, message, valueExpression); } } private static bool IsNullFailing(object? value) => value is not null; - private static void ReportAssertIsNullFailed(string? message) - => ReportAssertFailed("Assert.IsNull", message); + private static void ReportAssertIsNullFailed(object? value, string? message, string valueExpression) + { + string actualValue = AssertionValueRenderer.RenderValue(value); + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("actual:", actualValue); + + StructuredAssertionMessage structured = new("Expected value to be null."); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(null, actualValue); + structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNull", valueExpression)); + + ReportAssertFailed(structured); + } /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 @@ -191,13 +203,20 @@ public static void IsNotNull([NotNull] object? value, string? message = "", [Cal { if (IsNotNullFailing(value)) { - ReportAssertIsNotNullFailed(BuildUserMessageForValueExpression(message, valueExpression)); + ReportAssertIsNotNullFailed(message, valueExpression); } } private static bool IsNotNullFailing([NotNullWhen(false)] object? value) => value is null; [DoesNotReturn] - private static void ReportAssertIsNotNullFailed(string? message) - => ReportAssertFailed("Assert.IsNotNull", message); + private static void ReportAssertIsNotNullFailed(string? message, string valueExpression) + { + // RFC: IsNotNull omits the evidence block since actual is always null + StructuredAssertionMessage structured = new("Expected value to not be null."); + structured.WithUserMessage(message); + structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotNull", valueExpression)); + + ReportAssertFailed(structured); + } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs index d1480fe701..89c5b97fe0 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -22,9 +22,11 @@ public sealed partial class Assert public readonly struct AssertIsTrueInterpolatedStringHandler { private readonly StringBuilder? _builder; + private readonly bool? _condition; public AssertIsTrueInterpolatedStringHandler(int literalLength, int formattedCount, bool? condition, out bool shouldAppend) { + _condition = condition; shouldAppend = IsTrueFailing(condition); if (shouldAppend) { @@ -36,8 +38,7 @@ internal void ComputeAssertion(string conditionExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "condition", conditionExpression) + " "); - ReportAssertIsTrueFailed(_builder.ToString()); + ReportAssertIsTrueFailed(_condition, _builder.ToString(), conditionExpression); } } @@ -74,9 +75,11 @@ internal void ComputeAssertion(string conditionExpression) public readonly struct AssertIsFalseInterpolatedStringHandler { private readonly StringBuilder? _builder; + private readonly bool? _condition; public AssertIsFalseInterpolatedStringHandler(int literalLength, int formattedCount, bool? condition, out bool shouldAppend) { + _condition = condition; shouldAppend = IsFalseFailing(condition); if (shouldAppend) { @@ -88,8 +91,7 @@ internal void ComputeAssertion(string conditionExpression) { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionSingleParameterMessage, "condition", conditionExpression) + " "); - ReportAssertIsFalseFailed(_builder.ToString()); + ReportAssertIsFalseFailed(_condition, _builder.ToString(), conditionExpression); } } @@ -150,15 +152,27 @@ public static void IsTrue([DoesNotReturnIf(false)] bool? condition, string? mess { if (IsTrueFailing(condition)) { - ReportAssertIsTrueFailed(BuildUserMessageForConditionExpression(message, conditionExpression)); + ReportAssertIsTrueFailed(condition, message, conditionExpression); } } private static bool IsTrueFailing(bool? condition) => condition is false or null; - private static void ReportAssertIsTrueFailed(string? message) - => ReportAssertFailed("Assert.IsTrue", message); + private static void ReportAssertIsTrueFailed(bool? condition, string? message, string conditionExpression) + { + string actualValue = AssertionValueRenderer.RenderValue(condition); + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("actual:", actualValue); + + StructuredAssertionMessage structured = new("Expected condition to be true."); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(null, actualValue); + structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsTrue", conditionExpression)); + + ReportAssertFailed(structured); + } /// #pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578 @@ -188,7 +202,7 @@ public static void IsFalse([DoesNotReturnIf(true)] bool? condition, string? mess { if (IsFalseFailing(condition)) { - ReportAssertIsFalseFailed(BuildUserMessageForConditionExpression(message, conditionExpression)); + ReportAssertIsFalseFailed(condition, message, conditionExpression); } } @@ -196,6 +210,18 @@ private static bool IsFalseFailing(bool? condition) => condition is true or null; [DoesNotReturn] - private static void ReportAssertIsFalseFailed(string userMessage) - => ReportAssertFailed("Assert.IsFalse", userMessage); + private static void ReportAssertIsFalseFailed(bool? condition, string? message, string conditionExpression) + { + string actualValue = AssertionValueRenderer.RenderValue(condition); + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("actual:", actualValue); + + StructuredAssertionMessage structured = new("Expected condition to be false."); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(null, actualValue); + structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsFalse", conditionExpression)); + + ReportAssertFailed(structured); + } } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 7e565a24b4..2047a93ae1 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -165,6 +165,40 @@ internal static void ThrowAssertFailed(StructuredAssertionMessage structuredMess throw CreateAssertFailedException(structuredMessage); } + /// + /// Formats a call-site expression for display at the bottom of a structured assertion message. + /// When the expression is empty or contains newlines, the expression is replaced with a placeholder. + /// + internal static string? FormatCallSiteExpression(string assertionMethodName, string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + { + return null; + } + + // If expression contains newlines (multiline constant), replace with placeholder per RFC + return expression.Contains('\n') || expression.Contains('\r') + ? null + : $"{assertionMethodName}({expression})"; + } + + /// + /// Formats a call-site expression for display at the bottom of a structured assertion message, + /// using two captured expressions. + /// + internal static string? FormatCallSiteExpression(string assertionMethodName, string expression1, string expression2) + { + if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2)) + { + return null; + } + + string arg1 = expression1.Contains('\n') || expression1.Contains('\r') ? "" : expression1; + string arg2 = expression2.Contains('\n') || expression2.Contains('\r') ? "" : expression2; + + return $"{assertionMethodName}({arg1}, {arg2})"; + } + private static string FormatAssertionFailed(string assertionName, string? message) { string failedMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName); @@ -229,9 +263,6 @@ private static string BuildUserMessageForThreeExpressions(string? format, string : $"{callerArgMessagePart} {userMessage}"; } - private static string BuildUserMessageForConditionExpression(string? format, string conditionExpression) - => BuildUserMessageForSingleExpression(format, conditionExpression, "condition"); - private static string BuildUserMessageForValueExpression(string? format, string valueExpression) => BuildUserMessageForSingleExpression(format, valueExpression, "value"); diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsNull.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsNull.cs index bd365d680a..71f219f964 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsNull.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsNull.cs @@ -16,7 +16,7 @@ public void IsNull_PassNonNull_ShouldFail() { Action action = () => Assert.IsNull(new object()); action.Should().Throw() - .WithMessage("Assert.IsNull failed. 'value' expression: 'new object()'."); + .WithMessage($"Assertion failed. Expected value to be null.{Environment.NewLine}{Environment.NewLine}actual: System.Object{Environment.NewLine}{Environment.NewLine}Assert.IsNull(new object())"); } public void IsNull_StringMessage_PassNull_ShouldPass() @@ -26,7 +26,7 @@ public void IsNull_StringMessage_PassNonNull_ShouldFail() { Action action = () => Assert.IsNull(new object(), "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsNull failed. 'value' expression: 'new object()'. User-provided message"); + .WithMessage($"Assertion failed. Expected value to be null.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: System.Object{Environment.NewLine}{Environment.NewLine}Assert.IsNull(new object())"); } public void IsNull_InterpolatedString_PassNull_ShouldPass() @@ -42,7 +42,7 @@ public async Task IsNull_InterpolatedString_PassNonNull_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsNull(new object(), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsNull failed. 'value' expression: 'new object()'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected value to be null.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: System.Object{Environment.NewLine}{Environment.NewLine}Assert.IsNull(new object())"); o.WasToStringCalled.Should().BeTrue(); } @@ -73,14 +73,14 @@ public void IsNotNull_PassNull_ShouldFail() { Action action = () => Assert.IsNotNull(null); action.Should().Throw() - .WithMessage("Assert.IsNotNull failed. 'value' expression: 'null'."); + .WithMessage($"Assertion failed. Expected value to not be null.{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(null)"); } public void IsNotNull_StringMessage_PassNonNull_ShouldFail() { Action action = () => Assert.IsNotNull(null, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsNotNull failed. 'value' expression: 'null'. User-provided message"); + .WithMessage($"Assertion failed. Expected value to not be null.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(null)"); } public async Task IsNotNull_InterpolatedString_PassNonNull_ShouldFail() @@ -89,7 +89,7 @@ public async Task IsNotNull_InterpolatedString_PassNonNull_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsNotNull(null, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsNotNull failed. 'value' expression: 'null'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected value to not be null.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(null)"); o.WasToStringCalled.Should().BeTrue(); } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsTrueTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsTrueTests.cs index a09c1416ef..a1617d47f7 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsTrueTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsTrueTests.cs @@ -12,7 +12,7 @@ public void IsFalseNullableBooleanShouldFailWithNull() bool? nullBool = null; Action action = () => Assert.IsFalse(nullBool); action.Should().Throw() - .WithMessage("Assert.IsFalse failed. 'condition' expression: 'nullBool'."); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); } public void IsFalseNullableBooleanShouldFailWithTrue() @@ -20,7 +20,7 @@ public void IsFalseNullableBooleanShouldFailWithTrue() bool? nullBool = true; Action action = () => Assert.IsFalse(nullBool); action.Should().Throw() - .WithMessage("Assert.IsFalse failed. 'condition' expression: 'nullBool'."); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); } public void IsFalseNullableBooleanShouldNotFailWithFalse() @@ -33,7 +33,7 @@ public void IsFalseBooleanShouldFailWithTrue() { Action action = () => Assert.IsFalse(true); action.Should().Throw() - .WithMessage("Assert.IsFalse failed. 'condition' expression: 'true'."); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(true)"); } public void IsFalseBooleanShouldNotFailWithFalse() @@ -44,7 +44,7 @@ public void IsFalseNullableBooleanStringMessageShouldFailWithNull() bool? nullBool = null; Action action = () => Assert.IsFalse(nullBool, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsFalse failed. 'condition' expression: 'nullBool'. User-provided message"); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); } public void IsFalseNullableBooleanStringMessageShouldFailWithTrue() @@ -52,7 +52,7 @@ public void IsFalseNullableBooleanStringMessageShouldFailWithTrue() bool? nullBool = true; Action action = () => Assert.IsFalse(nullBool, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsFalse failed. 'condition' expression: 'nullBool'. User-provided message"); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); } public void IsFalseNullableBooleanStringMessageShouldNotFailWithFalse() @@ -65,7 +65,7 @@ public void IsFalseBooleanStringMessageShouldFailWithTrue() { Action action = () => Assert.IsFalse(true, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsFalse failed. 'condition' expression: 'true'. User-provided message"); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(true)"); } public void IsFalseBooleanStringMessageShouldNotFailWithFalse() @@ -78,7 +78,7 @@ public async Task IsFalseNullableBooleanInterpolatedStringMessageShouldFailWithN DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsFalse(nullBool, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsFalse failed. 'condition' expression: 'nullBool'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); } public async Task IsFalseNullableBooleanInterpolatedStringMessageShouldFailWithTrue() @@ -88,7 +88,7 @@ public async Task IsFalseNullableBooleanInterpolatedStringMessageShouldFailWithT DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsFalse(nullBool, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsFalse failed. 'condition' expression: 'nullBool'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); } public void IsFalseNullableBooleanInterpolatedStringMessageShouldNotFailWithFalse() @@ -103,7 +103,7 @@ public async Task IsFalseBooleanInterpolatedStringMessageShouldFailWithTrue() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsFalse(true, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsFalse failed. 'condition' expression: 'true'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(true)"); } public void IsFalseBooleanInterpolatedStringMessageShouldNotFailWithFalse() @@ -114,7 +114,7 @@ public void IsTrueNullableBooleanShouldFailWithNull() bool? nullBool = null; Action action = () => Assert.IsTrue(nullBool); action.Should().Throw() - .WithMessage("Assert.IsTrue failed. 'condition' expression: 'nullBool'."); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); } public void IsTrueNullableBooleanShouldFailWithFalse() @@ -122,7 +122,7 @@ public void IsTrueNullableBooleanShouldFailWithFalse() bool? nullBool = false; Action action = () => Assert.IsTrue(nullBool); action.Should().Throw() - .WithMessage("Assert.IsTrue failed. 'condition' expression: 'nullBool'."); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); } public void IsTrueNullableBooleanShouldNotFailWithTrue() @@ -135,7 +135,7 @@ public void IsTrueBooleanShouldFailWithFalse() { Action action = () => Assert.IsTrue(false); action.Should().Throw() - .WithMessage("Assert.IsTrue failed. 'condition' expression: 'false'."); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); } public void IsTrueBooleanShouldNotFailWithTrue() @@ -146,7 +146,7 @@ public void IsTrueNullableBooleanStringMessageShouldFailWithNull() bool? nullBool = null; Action action = () => Assert.IsTrue(nullBool, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsTrue failed. 'condition' expression: 'nullBool'. User-provided message"); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); } public void IsTrueNullableBooleanStringMessageShouldFailWithFalse() @@ -154,7 +154,7 @@ public void IsTrueNullableBooleanStringMessageShouldFailWithFalse() bool? nullBool = false; Action action = () => Assert.IsTrue(nullBool, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsTrue failed. 'condition' expression: 'nullBool'. User-provided message"); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); } public void IsTrueNullableBooleanStringMessageShouldNotFailWithTrue() @@ -167,7 +167,7 @@ public void IsTrueBooleanStringMessageShouldFailWithFalse() { Action action = () => Assert.IsTrue(false, "User-provided message"); action.Should().Throw() - .WithMessage("Assert.IsTrue failed. 'condition' expression: 'false'. User-provided message"); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); } public void IsTrueBooleanStringMessageShouldNotFailWithTrue() @@ -180,7 +180,7 @@ public async Task IsTrueNullableBooleanInterpolatedStringMessageShouldFailWithNu DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsTrue(nullBool, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsTrue failed. 'condition' expression: 'nullBool'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); } public async Task IsTrueNullableBooleanInterpolatedStringMessageShouldFailWithFalse() @@ -190,7 +190,7 @@ public async Task IsTrueNullableBooleanInterpolatedStringMessageShouldFailWithFa DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsTrue(nullBool, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsTrue failed. 'condition' expression: 'nullBool'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); } public void IsTrueNullableBooleanInterpolatedStringMessageShouldNotFailWithTrue() @@ -205,7 +205,7 @@ public async Task IsTrueBooleanInterpolatedStringMessageShouldFailWithFalse() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsTrue(false, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.IsTrue failed. 'condition' expression: 'false'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); } public void IsTrueBooleanInterpolatedStringMessageShouldNotFailWithTrue() diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index 096dbb9905..c1c3f724fe 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -49,7 +49,7 @@ public void Scope_MultipleFailures_CollectsAllErrors() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); - innerException.InnerExceptions[1].Message.Should().Be("Assert.IsTrue failed. 'condition' expression: 'false'."); + innerException.InnerExceptions[1].Message.Should().Be($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); } public void Scope_AfterDispose_AssertionsThrowNormally() @@ -137,7 +137,7 @@ public void Scope_AssertIsNotNull_IsSoftFailure() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Be("Assert.IsNotNull failed. 'value' expression: 'value'."); + innerException.InnerExceptions[0].Message.Should().Be($"Assertion failed. Expected value to not be null.{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(value)"); innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); } From 13072086c9912bec0e024e58cc9cdc3046cb8328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 12 May 2026 18:32:26 +0200 Subject: [PATCH 03/13] feat: structured assertion messages for equality assertions (AreEqual/AreNotEqual) - Update generic AreEqual/AreNotEqual to use StructuredAssertionMessage - Update string AreNotEqual to use structured format - Update interpolated string handlers for generic equality assertions - Preserve expression text in numeric non-generic handler closures - Update all related test expectations to match new format --- ...ert.AreEqual.InterpolatedStringHandlers.cs | 32 +++--- .../Assertions/Assert.AreEqual.String.cs | 3 +- .../Assertions/Assert.AreEqual.cs | 98 +++++++++++++------ .../Assertions/AssertTests.AreEqualTests.cs | 63 +++--------- .../Assertions/AssertTests.ScopeTests.cs | 14 +-- 5 files changed, 100 insertions(+), 110 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs index 49db337dde..3c28a6d920 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; @@ -48,8 +48,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "expected", expectedExpression, "actual", actualExpression) + " "); - ReportAssertAreEqualFailed(_expected, _actual, _builder.ToString()); + ReportAssertAreEqualFailed(_expected, _actual, _builder.ToString(), expectedExpression, actualExpression); } } @@ -115,8 +114,7 @@ internal void ComputeAssertion(string notExpectedExpression, string actualExpres { if (_builder is not null) { - _builder.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "notExpected", notExpectedExpression, "actual", actualExpression) + " "); - ReportAssertAreNotEqualFailed(_notExpected, _actual, _builder.ToString()); + ReportAssertAreNotEqualFailed(_notExpected, _actual, _builder.ToString(), notExpectedExpression, actualExpression); } } @@ -265,7 +263,7 @@ internal void ComputeAssertion(string expectedExpression, string actualExpressio public readonly struct AssertNonGenericAreNotEqualInterpolatedStringHandler { private readonly StringBuilder? _builder; - private readonly Action? _failAction; + private readonly Action? _failAction; public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, int formattedCount, float notExpected, float actual, float delta, out bool shouldAppend) { @@ -273,7 +271,8 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => + ReportAssertAreNotEqualFailed(notExpected, actual, delta, BuildUserMessageForNotExpectedExpressionAndActualExpression(userMessage, notExpectedExpr, actualExpr)); } } @@ -283,7 +282,8 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => + ReportAssertAreNotEqualFailed(notExpected, actual, delta, BuildUserMessageForNotExpectedExpressionAndActualExpression(userMessage, notExpectedExpr, actualExpr)); } } @@ -293,7 +293,8 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => + ReportAssertAreNotEqualFailed(notExpected, actual, delta, BuildUserMessageForNotExpectedExpressionAndActualExpression(userMessage, notExpectedExpr, actualExpr)); } } @@ -303,7 +304,8 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, delta, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => + ReportAssertAreNotEqualFailed(notExpected, actual, delta, BuildUserMessageForNotExpectedExpressionAndActualExpression(userMessage, notExpectedExpr, actualExpr)); } } @@ -319,18 +321,12 @@ public AssertNonGenericAreNotEqualInterpolatedStringHandler(int literalLength, i if (shouldAppend) { _builder = new StringBuilder(literalLength + formattedCount); - _failAction = userMessage => ReportAssertAreNotEqualFailed(notExpected, actual, userMessage); + _failAction = (userMessage, notExpectedExpr, actualExpr) => ReportAssertAreNotEqualFailed(notExpected, actual, userMessage, notExpectedExpr, actualExpr); } } internal void ComputeAssertion(string notExpectedExpression, string actualExpression) - { - if (_failAction is not null) - { - _builder!.Insert(0, string.Format(CultureInfo.CurrentCulture, FrameworkMessages.CallerArgumentExpressionTwoParametersMessage, "notExpected", notExpectedExpression, "actual", actualExpression) + " "); - _failAction.Invoke(_builder!.ToString()); - } - } + => _failAction?.Invoke(_builder!.ToString(), notExpectedExpression, actualExpression); public void AppendLiteral(string value) => _builder!.Append(value); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.String.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.String.cs index aafc366e5c..37539a9d03 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.String.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.String.cs @@ -229,8 +229,7 @@ public static void AreNotEqual(string? notExpected, string? actual, bool ignoreC return; } - string userMessage = BuildUserMessageForNotExpectedExpressionAndActualExpression(message, notExpectedExpression, actualExpression); - ReportAssertAreNotEqualFailed(notExpected, actual, userMessage); + ReportAssertAreNotEqualFailed(notExpected, actual, message, notExpectedExpression, actualExpression); } #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 3142d9644f..0a2162a75c 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -131,8 +131,7 @@ public static void AreEqual(T? expected, T? actual, IEqualityComparer comp return; } - string userMessage = BuildUserMessageForExpectedExpressionAndActualExpression(message, expectedExpression, actualExpression); - ReportAssertAreEqualFailed(expected, actual, userMessage); + ReportAssertAreEqualFailed(expected, actual, message, expectedExpression, actualExpression); } private static bool AreEqualFailing(T? expected, T? actual, IEqualityComparer? comparer) @@ -237,26 +236,58 @@ private static string FormatStringDifferenceMessage(string expected, string actu } [DoesNotReturn] - private static void ReportAssertAreEqualFailed(object? expected, object? actual, string userMessage) + private static void ReportAssertAreEqualFailed(object? expected, object? actual, string? message, string expectedExpression, string actualExpression) { - string finalMessage = actual != null && expected != null && !actual.GetType().Equals(expected.GetType()) - ? string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreEqualDifferentTypesFailMsg, - userMessage, - ReplaceNulls(expected), - expected.GetType().FullName, - ReplaceNulls(actual), - actual.GetType().FullName) - : expected is string expectedString && actual is string actualString - ? FormatStringComparisonMessage(expectedString, actualString, userMessage) - : string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreEqualFailMsg, - userMessage, - ReplaceNulls(expected), - ReplaceNulls(actual)); - ReportAssertFailed("Assert.AreEqual", finalMessage); + string expectedRendered = AssertionValueRenderer.RenderValue(expected); + string actualRendered = AssertionValueRenderer.RenderValue(actual); + + string summary; + EvidenceBlock evidence; + + if (actual is not null && expected is not null && !actual.GetType().Equals(expected.GetType())) + { + summary = "Expected values to be equal, but they are of different types."; + evidence = EvidenceBlock.Create() + .AddLine("expected:", $"{expectedRendered} ({expected.GetType().FullName})") + .AddLine("actual:", $"{actualRendered} ({actual.GetType().FullName})"); + } + else if (expected is string expectedString && actual is string actualString) + { + summary = "Expected strings to be equal (case-sensitive)."; + int diffIndex = FindFirstStringDifference(expectedString, actualString); + string lengthInfo = expectedString.Length == actualString.Length + ? string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AreEqualStringDiffLengthBothMsg, expectedString.Length, diffIndex) + : string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AreEqualStringDiffLengthDifferentMsg, expectedString.Length, actualString.Length); + + evidence = EvidenceBlock.Create() + .AddLine("expected:", expectedRendered) + .AddLine("actual:", actualRendered); + + StructuredAssertionMessage stringStructured = new(summary); + stringStructured.WithAdditionalSummaryLine(lengthInfo); + stringStructured.WithUserMessage(message); + stringStructured.WithEvidence(evidence); + stringStructured.WithExpectedAndActual(expectedRendered, actualRendered); + stringStructured.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreEqual", expectedExpression, actualExpression)); + ReportAssertFailed(stringStructured); + + return; + } + else + { + summary = "Expected values to be equal."; + evidence = EvidenceBlock.Create() + .AddLine("expected:", expectedRendered) + .AddLine("actual:", actualRendered); + } + + StructuredAssertionMessage structured = new(summary); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(expectedRendered, actualRendered); + structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreEqual", expectedExpression, actualExpression)); + + ReportAssertFailed(structured); } /// @@ -347,23 +378,26 @@ public static void AreNotEqual(T? notExpected, T? actual, IEqualityComparer(T? notExpected, T? actual, IEqualityComparer? comparer) => (comparer ?? EqualityComparer.Default).Equals(notExpected!, actual!); [DoesNotReturn] - private static void ReportAssertAreNotEqualFailed(object? notExpected, object? actual, string userMessage) + private static void ReportAssertAreNotEqualFailed(object? notExpected, object? actual, string? message, string notExpectedExpression, string actualExpression) { - string finalMessage = string.Format( - CultureInfo.CurrentCulture, - FrameworkMessages.AreNotEqualFailMsg, - userMessage, - ReplaceNulls(notExpected), - ReplaceNulls(actual)); - ReportAssertFailed("Assert.AreNotEqual", finalMessage); + string actualRendered = AssertionValueRenderer.RenderValue(actual); + + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("actual:", actualRendered); + + StructuredAssertionMessage structured = new("Expected values to not be equal."); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreNotEqual", notExpectedExpression, actualExpression)); + + ReportAssertFailed(structured); } #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs index f7901cfdce..5ed835bd44 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs @@ -302,7 +302,7 @@ public void AreEqualTwoObjectsDifferentTypeShouldFail() { Action action = () => Assert.AreEqual(new object(), 1); action.Should().Throw() - .And.Message.Should().Contain("Assert.AreEqual failed. Expected:. Actual:<1 (System.Int32)>."); + .And.Message.Should().Contain("Expected values to be equal, but they are of different types."); } public void AreEqualWithTypeOverridingEqualsShouldWork() @@ -373,7 +373,7 @@ public async Task GenericAreEqual_InterpolatedString_DifferentValues_ShouldFail( DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreEqual(0, 1, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.AreEqual failed. Expected:<0>. Actual:<1>. 'expected' expression: '0', 'actual' expression: '1'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected values to be equal.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}expected: 0{Environment.NewLine}actual: 1{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(0, 1)"); o.WasToStringCalled.Should().BeTrue(); } @@ -390,7 +390,7 @@ public async Task GenericAreNotEqual_InterpolatedString_SameValues_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreNotEqual(0, 0, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assert.AreNotEqual failed. Expected any value except:<0>. Actual:<0>. 'notExpected' expression: '0', 'actual' expression: '0'. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}"); + .WithMessage($"Assertion failed. Expected values to not be equal.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: 0{Environment.NewLine}{Environment.NewLine}Assert.AreNotEqual(0, 0)"); o.WasToStringCalled.Should().BeTrue(); } @@ -1411,36 +1411,21 @@ public void AreEqualStringDifferenceAtBeginning() { Action action = () => Assert.AreEqual("baaa", "aaaa"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 4 but differ at index 0. 'expected' expression: '"baaa"', 'actual' expression: '"aaaa"'. - Expected: "baaa" - But was: "aaaa" - -----------^ - """); + .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}String lengths are both 4 but differ at index 0.{Environment.NewLine}{Environment.NewLine}expected: \"baaa\"{Environment.NewLine}actual: \"aaaa\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"baaa\", \"aaaa\")"); } public void AreEqualStringDifferenceAtEnd() { Action action = () => Assert.AreEqual("aaaa", "aaab"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 4 but differ at index 3. 'expected' expression: '"aaaa"', 'actual' expression: '"aaab"'. - Expected: "aaaa" - But was: "aaab" - --------------^ - """); + .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}String lengths are both 4 but differ at index 3.{Environment.NewLine}{Environment.NewLine}expected: \"aaaa\"{Environment.NewLine}actual: \"aaab\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aaaa\", \"aaab\")"); } public void AreEqualStringWithSpecialCharactersShouldEscape() { Action action = () => Assert.AreEqual("aa\ta", "aa a"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 4 but differ at index 2. 'expected' expression: '"aa\ta"', 'actual' expression: '"aa a"'. - Expected: "aa␉a" - But was: "aa a" - -------------^ - """); + .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}String lengths are both 4 but differ at index 2.{Environment.NewLine}{Environment.NewLine}expected: \"aa\\ta\"{Environment.NewLine}actual: \"aa a\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aa\\ta\", \"aa a\")"); } public void AreEqualLongStringsShouldTruncateAndShowContext() @@ -1450,12 +1435,8 @@ public void AreEqualLongStringsShouldTruncateAndShowContext() Action action = () => Assert.AreEqual(expected, actual); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 201 but differ at index 100. 'expected' expression: 'expected', 'actual' expression: 'actual'. - Expected: "...aaaaaaaaaaaaaaaaaabcccccccccccccccc..." - But was: "...aaaaaaaaaaaaaaaaaadcccccccccccccccc..." - --------------------------------^ - """); + .And.Message.Should().Contain("Assertion failed. Expected strings to be equal (case-sensitive).") + .And.Contain("String lengths are both 201 but differ at index 100."); } public void AreEqualStringWithCultureShouldUseEnhancedMessage() @@ -1474,48 +1455,28 @@ public void AreEqualStringWithDifferentLength() { Action action = () => Assert.AreEqual("aaaa", "aaa"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. Expected string length 4 but was 3. 'expected' expression: '"aaaa"', 'actual' expression: '"aaa"'. - Expected: "aaaa" - But was: "aaa" - --------------^ - """); + .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}Expected string length 4 but was 3.{Environment.NewLine}{Environment.NewLine}expected: \"aaaa\"{Environment.NewLine}actual: \"aaa\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aaaa\", \"aaa\")"); } public void AreEqualShorterExpectedString() { Action action = () => Assert.AreEqual("aaa", "aaab"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. Expected string length 3 but was 4. 'expected' expression: '"aaa"', 'actual' expression: '"aaab"'. - Expected: "aaa" - But was: "aaab" - --------------^ - """); + .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}Expected string length 3 but was 4.{Environment.NewLine}{Environment.NewLine}expected: \"aaa\"{Environment.NewLine}actual: \"aaab\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aaa\", \"aaab\")"); } public void AreEqualStringWithUserMessage() { Action action = () => Assert.AreEqual("aaaa", "aaab", "My custom message"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. String lengths are both 4 but differ at index 3. 'expected' expression: '"aaaa"', 'actual' expression: '"aaab"'. My custom message - Expected: "aaaa" - But was: "aaab" - --------------^ - """); + .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}String lengths are both 4 but differ at index 3.{Environment.NewLine}My custom message{Environment.NewLine}{Environment.NewLine}expected: \"aaaa\"{Environment.NewLine}actual: \"aaab\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aaaa\", \"aaab\")"); } public void AreEqualStringWithEmojis() { Action action = () => Assert.AreEqual("🥰", "aaab"); action.Should().Throw() - .WithMessage(""" - Assert.AreEqual failed. Expected string length 2 but was 4. 'expected' expression: '"🥰"', 'actual' expression: '"aaab"'. - Expected: "🥰" - But was: "aaab" - -----------^ - """); + .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}Expected string length 2 but was 4.{Environment.NewLine}{Environment.NewLine}expected: \"🥰\"{Environment.NewLine}actual: \"aaab\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"🥰\", \"aaab\")"); } public void CreateStringPreviews_DiffPointsToCorrectPlaceInNonShortenedString() diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index c1c3f724fe..dfd8e54ac1 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -28,7 +28,7 @@ public void Scope_SingleFailure_ThrowsOnDispose() Action action = () => scope.Dispose(); action.Should().Throw() - .WithMessage("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + .WithMessage($"Assertion failed. Expected values to be equal.{Environment.NewLine}{Environment.NewLine}expected: 1{Environment.NewLine}actual: 2{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(1, 2)"); } public void Scope_MultipleFailures_CollectsAllErrors() @@ -48,7 +48,7 @@ public void Scope_MultipleFailures_CollectsAllErrors() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Be("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + innerException.InnerExceptions[0].Message.Should().Be($"Assertion failed. Expected values to be equal.{Environment.NewLine}{Environment.NewLine}expected: 1{Environment.NewLine}actual: 2{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(1, 2)"); innerException.InnerExceptions[1].Message.Should().Be($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); } @@ -95,7 +95,7 @@ public void Scope_DoubleDispose_DoesNotThrowTwice() Action firstDispose = () => scope.Dispose(); firstDispose.Should().Throw() - .WithMessage("Assert.AreEqual failed. Expected:<1>. Actual:<2>. 'expected' expression: '1', 'actual' expression: '2'."); + .WithMessage($"Assertion failed. Expected values to be equal.{Environment.NewLine}{Environment.NewLine}expected: 1{Environment.NewLine}actual: 2{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(1, 2)"); // Second dispose should be a no-op Action secondDispose = () => scope.Dispose(); @@ -138,7 +138,7 @@ public void Scope_AssertIsNotNull_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be($"Assertion failed. Expected value to not be null.{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(value)"); - innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); + innerException.InnerExceptions[1].Message.Should().Contain("Assertion failed. Expected values to be equal."); } public void Scope_AssertIsInstanceOfType_IsSoftFailure() @@ -161,7 +161,7 @@ public void Scope_AssertIsInstanceOfType_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be("Assert.IsInstanceOfType failed. 'value' expression: 'value'. Expected type:. Actual type:."); - innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); + innerException.InnerExceptions[1].Message.Should().Contain("Assertion failed. Expected values to be equal."); } public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() @@ -184,7 +184,7 @@ public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be("Assert.IsExactInstanceOfType failed. 'value' expression: 'value'. Expected exact type:. Actual type:."); - innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); + innerException.InnerExceptions[1].Message.Should().Contain("Assertion failed. Expected values to be equal."); } public void Scope_AssertContainsSingle_IsSoftFailure() @@ -207,6 +207,6 @@ public void Scope_AssertContainsSingle_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be("Assert.ContainsSingle failed. Expected collection to contain exactly one element but found 3 element(s). 'collection' expression: 'items'."); - innerException.InnerExceptions[1].Message.Should().Contain("Assert.AreEqual failed."); + innerException.InnerExceptions[1].Message.Should().Contain("Assertion failed. Expected values to be equal."); } } From e90c90e1125172749c4d67b710af220767bc9be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 16:55:59 +0200 Subject: [PATCH 04/13] test: convert structured-message assertions to raw strings + full equality --- .../Assertions/AssertTests.AreEqualTests.cs | 121 ++++++++++-- .../Assertions/AssertTests.IsNull.cs | 52 +++++- .../Assertions/AssertTests.IsTrueTests.cs | 174 ++++++++++++++++-- .../Assertions/AssertTests.ScopeTests.cs | 86 ++++++++- 4 files changed, 388 insertions(+), 45 deletions(-) diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs index 5ed835bd44..7593385fb3 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs @@ -302,7 +302,15 @@ public void AreEqualTwoObjectsDifferentTypeShouldFail() { Action action = () => Assert.AreEqual(new object(), 1); action.Should().Throw() - .And.Message.Should().Contain("Expected values to be equal, but they are of different types."); + .Which.Message.Should().Be( + """ + Assertion failed. Expected values to be equal, but they are of different types. + + expected: System.Object (System.Object) + actual: 1 (System.Int32) + + Assert.AreEqual(new object(), 1) + """); } public void AreEqualWithTypeOverridingEqualsShouldWork() @@ -373,7 +381,16 @@ public async Task GenericAreEqual_InterpolatedString_DifferentValues_ShouldFail( DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreEqual(0, 1, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected values to be equal.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}expected: 0{Environment.NewLine}actual: 1{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(0, 1)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected values to be equal. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + expected: 0 + actual: 1 + + Assert.AreEqual(0, 1) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -390,7 +407,15 @@ public async Task GenericAreNotEqual_InterpolatedString_SameValues_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.AreNotEqual(0, 0, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected values to not be equal.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: 0{Environment.NewLine}{Environment.NewLine}Assert.AreNotEqual(0, 0)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected values to not be equal. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + actual: 0 + + Assert.AreNotEqual(0, 0) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -1411,21 +1436,48 @@ public void AreEqualStringDifferenceAtBeginning() { Action action = () => Assert.AreEqual("baaa", "aaaa"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}String lengths are both 4 but differ at index 0.{Environment.NewLine}{Environment.NewLine}expected: \"baaa\"{Environment.NewLine}actual: \"aaaa\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"baaa\", \"aaaa\")"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal (case-sensitive). + String lengths are both 4 but differ at index 0. + + expected: "baaa" + actual: "aaaa" + + Assert.AreEqual("baaa", "aaaa") + """); } public void AreEqualStringDifferenceAtEnd() { Action action = () => Assert.AreEqual("aaaa", "aaab"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}String lengths are both 4 but differ at index 3.{Environment.NewLine}{Environment.NewLine}expected: \"aaaa\"{Environment.NewLine}actual: \"aaab\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aaaa\", \"aaab\")"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal (case-sensitive). + String lengths are both 4 but differ at index 3. + + expected: "aaaa" + actual: "aaab" + + Assert.AreEqual("aaaa", "aaab") + """); } public void AreEqualStringWithSpecialCharactersShouldEscape() { Action action = () => Assert.AreEqual("aa\ta", "aa a"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}String lengths are both 4 but differ at index 2.{Environment.NewLine}{Environment.NewLine}expected: \"aa\\ta\"{Environment.NewLine}actual: \"aa a\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aa\\ta\", \"aa a\")"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal (case-sensitive). + String lengths are both 4 but differ at index 2. + + expected: "aa\ta" + actual: "aa a" + + Assert.AreEqual("aa\ta", "aa a") + """); } public void AreEqualLongStringsShouldTruncateAndShowContext() @@ -1435,8 +1487,16 @@ public void AreEqualLongStringsShouldTruncateAndShowContext() Action action = () => Assert.AreEqual(expected, actual); action.Should().Throw() - .And.Message.Should().Contain("Assertion failed. Expected strings to be equal (case-sensitive).") - .And.Contain("String lengths are both 201 but differ at index 100."); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal (case-sensitive). + String lengths are both 201 but differ at index 100. + + expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + actual: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + + Assert.AreEqual(expected, actual) + """); } public void AreEqualStringWithCultureShouldUseEnhancedMessage() @@ -1455,28 +1515,65 @@ public void AreEqualStringWithDifferentLength() { Action action = () => Assert.AreEqual("aaaa", "aaa"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}Expected string length 4 but was 3.{Environment.NewLine}{Environment.NewLine}expected: \"aaaa\"{Environment.NewLine}actual: \"aaa\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aaaa\", \"aaa\")"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal (case-sensitive). + Expected string length 4 but was 3. + + expected: "aaaa" + actual: "aaa" + + Assert.AreEqual("aaaa", "aaa") + """); } public void AreEqualShorterExpectedString() { Action action = () => Assert.AreEqual("aaa", "aaab"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}Expected string length 3 but was 4.{Environment.NewLine}{Environment.NewLine}expected: \"aaa\"{Environment.NewLine}actual: \"aaab\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aaa\", \"aaab\")"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal (case-sensitive). + Expected string length 3 but was 4. + + expected: "aaa" + actual: "aaab" + + Assert.AreEqual("aaa", "aaab") + """); } public void AreEqualStringWithUserMessage() { Action action = () => Assert.AreEqual("aaaa", "aaab", "My custom message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}String lengths are both 4 but differ at index 3.{Environment.NewLine}My custom message{Environment.NewLine}{Environment.NewLine}expected: \"aaaa\"{Environment.NewLine}actual: \"aaab\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"aaaa\", \"aaab\")"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal (case-sensitive). + String lengths are both 4 but differ at index 3. + My custom message + + expected: "aaaa" + actual: "aaab" + + Assert.AreEqual("aaaa", "aaab") + """); } public void AreEqualStringWithEmojis() { Action action = () => Assert.AreEqual("🥰", "aaab"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected strings to be equal (case-sensitive).{Environment.NewLine}Expected string length 2 but was 4.{Environment.NewLine}{Environment.NewLine}expected: \"🥰\"{Environment.NewLine}actual: \"aaab\"{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(\"🥰\", \"aaab\")"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected strings to be equal (case-sensitive). + Expected string length 2 but was 4. + + expected: "🥰" + actual: "aaab" + + Assert.AreEqual("🥰", "aaab") + """); } public void CreateStringPreviews_DiffPointsToCorrectPlaceInNonShortenedString() diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsNull.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsNull.cs index 71f219f964..95c5cb934a 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsNull.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsNull.cs @@ -16,7 +16,14 @@ public void IsNull_PassNonNull_ShouldFail() { Action action = () => Assert.IsNull(new object()); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be null.{Environment.NewLine}{Environment.NewLine}actual: System.Object{Environment.NewLine}{Environment.NewLine}Assert.IsNull(new object())"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected value to be null. + + actual: System.Object + + Assert.IsNull(new object()) + """); } public void IsNull_StringMessage_PassNull_ShouldPass() @@ -26,7 +33,15 @@ public void IsNull_StringMessage_PassNonNull_ShouldFail() { Action action = () => Assert.IsNull(new object(), "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to be null.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: System.Object{Environment.NewLine}{Environment.NewLine}Assert.IsNull(new object())"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected value to be null. + User-provided message + + actual: System.Object + + Assert.IsNull(new object()) + """); } public void IsNull_InterpolatedString_PassNull_ShouldPass() @@ -42,7 +57,15 @@ public async Task IsNull_InterpolatedString_PassNonNull_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsNull(new object(), $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected value to be null.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: System.Object{Environment.NewLine}{Environment.NewLine}Assert.IsNull(new object())"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected value to be null. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + actual: System.Object + + Assert.IsNull(new object()) + """); o.WasToStringCalled.Should().BeTrue(); } @@ -73,14 +96,25 @@ public void IsNotNull_PassNull_ShouldFail() { Action action = () => Assert.IsNotNull(null); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to not be null.{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(null)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected value to not be null. + + Assert.IsNotNull(null) + """); } public void IsNotNull_StringMessage_PassNonNull_ShouldFail() { Action action = () => Assert.IsNotNull(null, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected value to not be null.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(null)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected value to not be null. + User-provided message + + Assert.IsNotNull(null) + """); } public async Task IsNotNull_InterpolatedString_PassNonNull_ShouldFail() @@ -89,7 +123,13 @@ public async Task IsNotNull_InterpolatedString_PassNonNull_ShouldFail() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsNotNull(null, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected value to not be null.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(null)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected value to not be null. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + Assert.IsNotNull(null) + """); o.WasToStringCalled.Should().BeTrue(); } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsTrueTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsTrueTests.cs index a1617d47f7..2e9a6415eb 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsTrueTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsTrueTests.cs @@ -12,7 +12,14 @@ public void IsFalseNullableBooleanShouldFailWithNull() bool? nullBool = null; Action action = () => Assert.IsFalse(nullBool); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be false. + + actual: null + + Assert.IsFalse(nullBool) + """); } public void IsFalseNullableBooleanShouldFailWithTrue() @@ -20,7 +27,14 @@ public void IsFalseNullableBooleanShouldFailWithTrue() bool? nullBool = true; Action action = () => Assert.IsFalse(nullBool); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be false. + + actual: true + + Assert.IsFalse(nullBool) + """); } public void IsFalseNullableBooleanShouldNotFailWithFalse() @@ -33,7 +47,14 @@ public void IsFalseBooleanShouldFailWithTrue() { Action action = () => Assert.IsFalse(true); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(true)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be false. + + actual: true + + Assert.IsFalse(true) + """); } public void IsFalseBooleanShouldNotFailWithFalse() @@ -44,7 +65,15 @@ public void IsFalseNullableBooleanStringMessageShouldFailWithNull() bool? nullBool = null; Action action = () => Assert.IsFalse(nullBool, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be false. + User-provided message + + actual: null + + Assert.IsFalse(nullBool) + """); } public void IsFalseNullableBooleanStringMessageShouldFailWithTrue() @@ -52,7 +81,15 @@ public void IsFalseNullableBooleanStringMessageShouldFailWithTrue() bool? nullBool = true; Action action = () => Assert.IsFalse(nullBool, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be false. + User-provided message + + actual: true + + Assert.IsFalse(nullBool) + """); } public void IsFalseNullableBooleanStringMessageShouldNotFailWithFalse() @@ -65,7 +102,15 @@ public void IsFalseBooleanStringMessageShouldFailWithTrue() { Action action = () => Assert.IsFalse(true, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(true)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be false. + User-provided message + + actual: true + + Assert.IsFalse(true) + """); } public void IsFalseBooleanStringMessageShouldNotFailWithFalse() @@ -78,7 +123,15 @@ public async Task IsFalseNullableBooleanInterpolatedStringMessageShouldFailWithN DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsFalse(nullBool, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected condition to be false. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + actual: null + + Assert.IsFalse(nullBool) + """); } public async Task IsFalseNullableBooleanInterpolatedStringMessageShouldFailWithTrue() @@ -88,7 +141,15 @@ public async Task IsFalseNullableBooleanInterpolatedStringMessageShouldFailWithT DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsFalse(nullBool, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(nullBool)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected condition to be false. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + actual: true + + Assert.IsFalse(nullBool) + """); } public void IsFalseNullableBooleanInterpolatedStringMessageShouldNotFailWithFalse() @@ -103,7 +164,15 @@ public async Task IsFalseBooleanInterpolatedStringMessageShouldFailWithTrue() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsFalse(true, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected condition to be false.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: true{Environment.NewLine}{Environment.NewLine}Assert.IsFalse(true)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected condition to be false. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + actual: true + + Assert.IsFalse(true) + """); } public void IsFalseBooleanInterpolatedStringMessageShouldNotFailWithFalse() @@ -114,7 +183,14 @@ public void IsTrueNullableBooleanShouldFailWithNull() bool? nullBool = null; Action action = () => Assert.IsTrue(nullBool); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be true. + + actual: null + + Assert.IsTrue(nullBool) + """); } public void IsTrueNullableBooleanShouldFailWithFalse() @@ -122,7 +198,14 @@ public void IsTrueNullableBooleanShouldFailWithFalse() bool? nullBool = false; Action action = () => Assert.IsTrue(nullBool); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be true. + + actual: false + + Assert.IsTrue(nullBool) + """); } public void IsTrueNullableBooleanShouldNotFailWithTrue() @@ -135,7 +218,14 @@ public void IsTrueBooleanShouldFailWithFalse() { Action action = () => Assert.IsTrue(false); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be true. + + actual: false + + Assert.IsTrue(false) + """); } public void IsTrueBooleanShouldNotFailWithTrue() @@ -146,7 +236,15 @@ public void IsTrueNullableBooleanStringMessageShouldFailWithNull() bool? nullBool = null; Action action = () => Assert.IsTrue(nullBool, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be true. + User-provided message + + actual: null + + Assert.IsTrue(nullBool) + """); } public void IsTrueNullableBooleanStringMessageShouldFailWithFalse() @@ -154,7 +252,15 @@ public void IsTrueNullableBooleanStringMessageShouldFailWithFalse() bool? nullBool = false; Action action = () => Assert.IsTrue(nullBool, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be true. + User-provided message + + actual: false + + Assert.IsTrue(nullBool) + """); } public void IsTrueNullableBooleanStringMessageShouldNotFailWithTrue() @@ -167,7 +273,15 @@ public void IsTrueBooleanStringMessageShouldFailWithFalse() { Action action = () => Assert.IsTrue(false, "User-provided message"); action.Should().Throw() - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected condition to be true. + User-provided message + + actual: false + + Assert.IsTrue(false) + """); } public void IsTrueBooleanStringMessageShouldNotFailWithTrue() @@ -180,7 +294,15 @@ public async Task IsTrueNullableBooleanInterpolatedStringMessageShouldFailWithNu DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsTrue(nullBool, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: null{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected condition to be true. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + actual: null + + Assert.IsTrue(nullBool) + """); } public async Task IsTrueNullableBooleanInterpolatedStringMessageShouldFailWithFalse() @@ -190,7 +312,15 @@ public async Task IsTrueNullableBooleanInterpolatedStringMessageShouldFailWithFa DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsTrue(nullBool, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(nullBool)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected condition to be true. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + actual: false + + Assert.IsTrue(nullBool) + """); } public void IsTrueNullableBooleanInterpolatedStringMessageShouldNotFailWithTrue() @@ -205,7 +335,15 @@ public async Task IsTrueBooleanInterpolatedStringMessageShouldFailWithFalse() DateTime dateTime = DateTime.Now; Func action = async () => Assert.IsTrue(false, $"User-provided message. {o}, {o,35}, {await GetHelloStringAsync()}, {new DummyIFormattable()}, {dateTime:tt}, {dateTime,5:tt}"); (await action.Should().ThrowAsync()) - .WithMessage($"Assertion failed. Expected condition to be true.{Environment.NewLine}User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)}{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); + .Which.Message.Should().Be( + $""" + Assertion failed. Expected condition to be true. + User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} + + actual: false + + Assert.IsTrue(false) + """); } public void IsTrueBooleanInterpolatedStringMessageShouldNotFailWithTrue() diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index dfd8e54ac1..f152b7070a 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -28,7 +28,15 @@ public void Scope_SingleFailure_ThrowsOnDispose() Action action = () => scope.Dispose(); action.Should().Throw() - .WithMessage($"Assertion failed. Expected values to be equal.{Environment.NewLine}{Environment.NewLine}expected: 1{Environment.NewLine}actual: 2{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(1, 2)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """); } public void Scope_MultipleFailures_CollectsAllErrors() @@ -48,8 +56,23 @@ public void Scope_MultipleFailures_CollectsAllErrors() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Be($"Assertion failed. Expected values to be equal.{Environment.NewLine}{Environment.NewLine}expected: 1{Environment.NewLine}actual: 2{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(1, 2)"); - innerException.InnerExceptions[1].Message.Should().Be($"Assertion failed. Expected condition to be true.{Environment.NewLine}{Environment.NewLine}actual: false{Environment.NewLine}{Environment.NewLine}Assert.IsTrue(false)"); + innerException.InnerExceptions[0].Message.Should().Be( + """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """); + innerException.InnerExceptions[1].Message.Should().Be( + """ + Assertion failed. Expected condition to be true. + + actual: false + + Assert.IsTrue(false) + """); } public void Scope_AfterDispose_AssertionsThrowNormally() @@ -95,7 +118,15 @@ public void Scope_DoubleDispose_DoesNotThrowTwice() Action firstDispose = () => scope.Dispose(); firstDispose.Should().Throw() - .WithMessage($"Assertion failed. Expected values to be equal.{Environment.NewLine}{Environment.NewLine}expected: 1{Environment.NewLine}actual: 2{Environment.NewLine}{Environment.NewLine}Assert.AreEqual(1, 2)"); + .Which.Message.Should().Be( + """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """); // Second dispose should be a no-op Action secondDispose = () => scope.Dispose(); @@ -137,8 +168,21 @@ public void Scope_AssertIsNotNull_IsSoftFailure() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Be($"Assertion failed. Expected value to not be null.{Environment.NewLine}{Environment.NewLine}Assert.IsNotNull(value)"); - innerException.InnerExceptions[1].Message.Should().Contain("Assertion failed. Expected values to be equal."); + innerException.InnerExceptions[0].Message.Should().Be( + """ + Assertion failed. Expected value to not be null. + + Assert.IsNotNull(value) + """); + innerException.InnerExceptions[1].Message.Should().Be( + """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """); } public void Scope_AssertIsInstanceOfType_IsSoftFailure() @@ -161,7 +205,15 @@ public void Scope_AssertIsInstanceOfType_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be("Assert.IsInstanceOfType failed. 'value' expression: 'value'. Expected type:. Actual type:."); - innerException.InnerExceptions[1].Message.Should().Contain("Assertion failed. Expected values to be equal."); + innerException.InnerExceptions[1].Message.Should().Be( + """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """); } public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() @@ -184,7 +236,15 @@ public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be("Assert.IsExactInstanceOfType failed. 'value' expression: 'value'. Expected exact type:. Actual type:."); - innerException.InnerExceptions[1].Message.Should().Contain("Assertion failed. Expected values to be equal."); + innerException.InnerExceptions[1].Message.Should().Be( + """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """); } public void Scope_AssertContainsSingle_IsSoftFailure() @@ -207,6 +267,14 @@ public void Scope_AssertContainsSingle_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be("Assert.ContainsSingle failed. Expected collection to contain exactly one element but found 3 element(s). 'collection' expression: 'items'."); - innerException.InnerExceptions[1].Message.Should().Contain("Assertion failed. Expected values to be equal."); + innerException.InnerExceptions[1].Message.Should().Be( + """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """); } } From e03ae4d1b1da88c26e8d3600e0ae5f3c2156bbb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 17:18:40 +0200 Subject: [PATCH 05/13] fix: restore localized resx summaries and revert unrelated merge changes Replace hardcoded English summary literals in IsTrue/IsFalse/IsNull/IsNotNull/AreEqual/AreNotEqual with FrameworkMessages resource lookups. Add new resx keys for AreEqual/AreNotEqual variants. Add WithExpectedAndActual to AreNotEqual structured message. Restore unrelated files that were silently corrupted by merge auto-resolution: Platform ServiceProvider.ReplaceService and ServerTelemetry registration, .github workflow lock files and copilot-instructions, and SoftAssertionTests. --- .github/copilot-instructions.md | 9 ++++ .../workflows/address-review.agent.lock.yml | 34 +++++++------- .github/workflows/issue-arborist.lock.yml | 46 ++++++------------- .../Hosts/ServerTestHost.cs | 4 +- .../Hosts/TestHostBuilder.Modes.cs | 7 +++ .../Services/ServiceProvider.cs | 25 ++++++++++ .../Assertions/Assert.AreEqual.cs | 10 ++-- .../TestFramework/Assertions/Assert.IsNull.cs | 4 +- .../TestFramework/Assertions/Assert.IsTrue.cs | 4 +- .../Resources/FrameworkMessages.resx | 26 ++++++++++- .../Resources/xlf/FrameworkMessages.cs.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.de.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.es.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.fr.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.it.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.ja.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.ko.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.pl.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.ru.xlf | 40 ++++++++++++++++ .../Resources/xlf/FrameworkMessages.tr.xlf | 40 ++++++++++++++++ .../xlf/FrameworkMessages.zh-Hans.xlf | 40 ++++++++++++++++ .../xlf/FrameworkMessages.zh-Hant.xlf | 40 ++++++++++++++++ .../SoftAssertionTests.cs | 2 +- .../Services/ServiceProviderTests.cs | 41 +++++++++++++++++ 25 files changed, 670 insertions(+), 62 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6e506c1f3b..98071d4e33 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -44,6 +44,15 @@ When making change to resource files, you MUST: - All assertions must be written using FluentAssertions style of assertion. - When running acceptance tests, you must first run `./build.sh -pack` +## Agentic workflow guidelines + +Agentic workflows live in `.github/workflows/*.md` and `*.agent.md` and are compiled to `*.lock.yml` files via the `gh aw` GitHub CLI extension. + +- Always compile in **strict mode**. Strict mode is the default unless a workflow's frontmatter sets `strict: false`, so: + - NEVER add `strict: false` to a workflow's frontmatter. + - When in doubt, pass `--strict` explicitly to `gh aw compile` to enforce strict-mode validation across all workflows (action pinning, network config, safe-outputs, no write permissions, no deprecated fields). +- After editing any agentic workflow `.md` source (or its frontmatter), run `gh aw compile ` and commit the regenerated `.lock.yml` in the same change. NEVER hand-edit `.lock.yml` files. + ## Pull Request guidelines - Let other developers discuss their comments to your PRs, unless something sounds like a direct order to you, don't do changes. diff --git a/.github/workflows/address-review.agent.lock.yml b/.github/workflows/address-review.agent.lock.yml index 17dbf79ccd..32fd839a46 100644 --- a/.github/workflows/address-review.agent.lock.yml +++ b/.github/workflows/address-review.agent.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"3481488642f81a168b1bea60e46e43d74d372ea231a6eacc9e9d818dd631a26e","compiler_version":"v0.72.1","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"92fdb5ad7b5a060264c350b09cd488ad18f6bd17caa115882f3de69f38fecf02","compiler_version":"v0.72.1","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"d746ffe35508b1917358783b479e04febd2b8f71","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"bc56a0cad2f450c562810785ef38649c04db812a","version":"v0.72.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -71,7 +71,7 @@ jobs: activation: needs: pre_activation if: > - needs.pre_activation.outputs.activated == 'true' && (github.event.review.state != 'approved' && github.event.pull_request.head.repo.id == github.repository_id && ( + needs.pre_activation.outputs.activated == 'true' && (github.event.review.state == 'changes_requested' && github.event.pull_request.head.repo.id == github.repository_id && ( github.event.pull_request.user.login == 'copilot-swe-agent[bot]' || contains(github.event.pull_request.labels.*.name, 'copilot-autofix') )) @@ -221,23 +221,23 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_18a1555189c4b087_EOF' + cat << 'GH_AW_PROMPT_e4306c5e922bad1c_EOF' - GH_AW_PROMPT_18a1555189c4b087_EOF + GH_AW_PROMPT_e4306c5e922bad1c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_18a1555189c4b087_EOF' + cat << 'GH_AW_PROMPT_e4306c5e922bad1c_EOF' Tools: add_comment(max:3), push_to_pull_request_branch(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_18a1555189c4b087_EOF + GH_AW_PROMPT_e4306c5e922bad1c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_18a1555189c4b087_EOF' + cat << 'GH_AW_PROMPT_e4306c5e922bad1c_EOF' - GH_AW_PROMPT_18a1555189c4b087_EOF + GH_AW_PROMPT_e4306c5e922bad1c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_18a1555189c4b087_EOF' + cat << 'GH_AW_PROMPT_e4306c5e922bad1c_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -266,7 +266,7 @@ jobs: {{/if}} - GH_AW_PROMPT_18a1555189c4b087_EOF + GH_AW_PROMPT_e4306c5e922bad1c_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_prompt.md" @@ -274,12 +274,12 @@ jobs: if [ "$GITHUB_EVENT_NAME" = "issue_comment" ] && [ -n "$GH_AW_IS_PR_COMMENT" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review_comment" ] || [ "$GITHUB_EVENT_NAME" = "pull_request_review" ]; then cat "${RUNNER_TEMP}/gh-aw/prompts/pr_context_push_to_pr_branch_guidance.md" fi - cat << 'GH_AW_PROMPT_18a1555189c4b087_EOF' + cat << 'GH_AW_PROMPT_e4306c5e922bad1c_EOF' {{#runtime-import .github/workflows/shared/address-review-shared.md}} {{#runtime-import .github/workflows/shared/repo-build-setup.md}} {{#runtime-import .github/workflows/address-review.agent.md}} - GH_AW_PROMPT_18a1555189c4b087_EOF + GH_AW_PROMPT_e4306c5e922bad1c_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0 @@ -482,9 +482,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_e8c5e9997062610d_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_8222ed9b17400205_EOF' {"add_comment":{"hide_older_comments":true,"max":3},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","max":3,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"]},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_e8c5e9997062610d_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_8222ed9b17400205_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -692,7 +692,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_efee7c82956d7a03_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_4964f9ea8ee5fcd7_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -737,7 +737,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_efee7c82956d7a03_EOF + GH_AW_MCP_CONFIG_4964f9ea8ee5fcd7_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1326,7 +1326,7 @@ jobs: pre_activation: if: > - github.event.review.state != 'approved' && github.event.pull_request.head.repo.id == github.repository_id && ( + github.event.review.state == 'changes_requested' && github.event.pull_request.head.repo.id == github.repository_id && ( github.event.pull_request.user.login == 'copilot-swe-agent[bot]' || contains(github.event.pull_request.labels.*.name, 'copilot-autofix') ) diff --git a/.github/workflows/issue-arborist.lock.yml b/.github/workflows/issue-arborist.lock.yml index 90eb0f9cb4..a363148f49 100644 --- a/.github/workflows/issue-arborist.lock.yml +++ b/.github/workflows/issue-arborist.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"c6bce002c3be9f8bb217ced93ab309dbc6bcd5615d12ef52d41532ce4cf97c73","compiler_version":"v0.72.1","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"e1c4b48ef71e1319fb7b460bef4bf6bf35973b6aeab77300b6e9b8cb34a0eba5","compiler_version":"v0.72.1","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/github-script","sha":"d746ffe35508b1917358783b479e04febd2b8f71","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"bc56a0cad2f450c562810785ef38649c04db812a","version":"v0.72.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.41"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.41"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -186,20 +186,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_ace4b671ae98210e_EOF' + cat << 'GH_AW_PROMPT_8a38e705af7cdc4d_EOF' - GH_AW_PROMPT_ace4b671ae98210e_EOF + GH_AW_PROMPT_8a38e705af7cdc4d_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_ace4b671ae98210e_EOF' + cat << 'GH_AW_PROMPT_8a38e705af7cdc4d_EOF' Tools: create_issue(max:5), link_sub_issue(max:50), missing_tool, missing_data, noop - GH_AW_PROMPT_ace4b671ae98210e_EOF + GH_AW_PROMPT_8a38e705af7cdc4d_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_ace4b671ae98210e_EOF' + cat << 'GH_AW_PROMPT_8a38e705af7cdc4d_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -228,12 +228,12 @@ jobs: {{/if}} - GH_AW_PROMPT_ace4b671ae98210e_EOF + GH_AW_PROMPT_8a38e705af7cdc4d_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_ace4b671ae98210e_EOF' + cat << 'GH_AW_PROMPT_8a38e705af7cdc4d_EOF' {{#runtime-import .github/workflows/issue-arborist.md}} - GH_AW_PROMPT_ace4b671ae98210e_EOF + GH_AW_PROMPT_8a38e705af7cdc4d_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0 @@ -366,24 +366,10 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} - - name: Start DIFC Proxy - env: - GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITHUB_SERVER_URL: ${{ github.server_url }} - DIFC_PROXY_POLICY: '{"allow-only":{"min-integrity":"none","repos":"all"}}' - DIFC_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.6' - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/start_difc_proxy.sh" - env: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_ORIGINAL_GITHUB_API_URL: ${{ github.api_url }} - GH_HOST: localhost:18443 - GH_REPO: ${{ github.repository }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_API_URL: https://localhost:18443/api/v3 - GITHUB_GRAPHQL_URL: https://localhost:18443/api/graphql GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_EXTRA_CA_CERTS: /tmp/gh-aw/proxy-logs/proxy-tls/ca.crt name: Fetch issues data run: | # Create output directory @@ -398,7 +384,7 @@ jobs: -H "Authorization: Bearer ${GITHUB_TOKEN}" \ -H "Accept: application/vnd.github+json" \ --get \ - --data-urlencode "q=repo:${GH_AW_GITHUB_REPOSITORY} is:issue is:open -is:sub-issue" \ + --data-urlencode "q=repo:$GH_AW_GITHUB_REPOSITORY is:issue is:open -is:sub-issue" \ --data-urlencode "sort=created" \ --data-urlencode "order=desc" \ --data-urlencode "per_page=100" \ @@ -463,10 +449,6 @@ jobs: GH_AW_TRUSTED_USERS_VAR: ${{ vars.GH_AW_GITHUB_TRUSTED_USERS || '' }} GH_AW_APPROVAL_LABELS_VAR: ${{ vars.GH_AW_GITHUB_APPROVAL_LABELS || '' }} run: bash "${RUNNER_TEMP}/gh-aw/actions/parse_guard_list.sh" - - name: Stop DIFC Proxy - if: always() - continue-on-error: true - run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_difc_proxy.sh" - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -490,9 +472,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7bb66e2f03e7c085_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_4637bab9c74706a0_EOF' {"create_issue":{"expires":48,"group":true,"max":5,"title_prefix":"[Parent] "},"create_report_incomplete_issue":{},"link_sub_issue":{"max":50},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_7bb66e2f03e7c085_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_4637bab9c74706a0_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -709,7 +691,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_7d7f1b933958e599_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_ef5c918e31a06241_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -754,7 +736,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_7d7f1b933958e599_EOF + GH_AW_MCP_CONFIG_ef5c918e31a06241_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs index 3c8b6d80f4..c6672dfde4 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/ServerTestHost.cs @@ -29,7 +29,6 @@ internal sealed partial class ServerTestHost : CommonHost, IServerTestHost, IDis private readonly IMessageHandlerFactory _messageHandlerFactory; private readonly TestFrameworkManager _testFrameworkManager; private readonly TestHostManager _testSessionManager; - private readonly ServerTelemetry _telemetryService; private readonly IAsyncMonitor _messageMonitor; private readonly IEnvironment _environment; private readonly ILogger _logger; @@ -66,7 +65,6 @@ public ServerTestHost( _messageHandlerFactory = messageHandlerFactory; _testFrameworkManager = testFrameworkManager; _testSessionManager = testSessionManager; - _telemetryService = new ServerTelemetry(this); _clientToServerRequests = new(); _serverToClientRequests = new(); @@ -571,7 +569,7 @@ await ExecuteRequestAsync( requestExecuteStart, (DateTimeOffset)requestExecuteStop, testNodeUpdateProcessor.GetTestNodeStatistics().TotalDiscoveredTests); - await _telemetryService.LogEventAsync(isRunRequest ? TelemetryEvents.TestsRunEventName : TelemetryEvents.TestsDiscoveryEventName, metadata, cancellationToken).ConfigureAwait(false); + await ServiceProvider.GetTelemetryCollector().LogEventAsync(isRunRequest ? TelemetryEvents.TestsRunEventName : TelemetryEvents.TestsDiscoveryEventName, metadata, cancellationToken).ConfigureAwait(false); return isRunRequest ? new RunResponseArgs([.. testNodeUpdateProcessor.Artifacts]) diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.Modes.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.Modes.cs index 4b40a731b3..c77b931492 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.Modes.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.Modes.cs @@ -217,6 +217,13 @@ private async Task BuildServerTestHostAsync(BuildContext context, NamedPi (TestFrameworkManager)TestFramework!, (TestHostManager)TestHost); + // Register ServerTelemetry in the service provider so that all telemetry events in server mode + // are forwarded to the client via JSON-RPC. This replaces the NopTelemetryService registered + // earlier in SetupCommonServicesAsync. This is always registered regardless of whether platform + // telemetry is enabled or disabled, because server-mode telemetry is a protocol concern (JSON-RPC + // forwarding to the client), not a data-collection concern. + context.ServiceProvider.ReplaceService(new ServerTelemetry(serverTestHost)); + #pragma warning disable CA1416 // Preserve existing browser behavior while splitting the method. IHost actualTestHost = testControllerConnection is not null ? new TestHostControlledHost(testControllerConnection, serverTestHost, context.TestApplicationCancellationTokenSource.CancellationToken) diff --git a/src/Platform/Microsoft.Testing.Platform/Services/ServiceProvider.cs b/src/Platform/Microsoft.Testing.Platform/Services/ServiceProvider.cs index 7433e983fb..7b41dda061 100644 --- a/src/Platform/Microsoft.Testing.Platform/Services/ServiceProvider.cs +++ b/src/Platform/Microsoft.Testing.Platform/Services/ServiceProvider.cs @@ -72,6 +72,31 @@ public bool TryAddService(object service) return true; } + /// + /// Replaces the first registered service instance that is an instance of with the new service. + /// If no such service is found, the new service is added at the end of the list. + /// + /// The type of service to replace. + internal void ReplaceService(T newService) + where T : class + { + _ = newService ?? throw new ArgumentNullException(nameof(newService)); + ArgumentGuard.Ensure( + newService is not ITestFramework || AllowTestAdapterFrameworkRegistration, + nameof(newService), + PlatformResources.ServiceProviderShouldNotRegisterTestFramework); + + int index = _services.FindIndex(s => s is T); + if (index >= 0) + { + _services[index] = newService; + } + else + { + _services.Add(newService); + } + } + public IEnumerable GetServicesInternal( Type serviceType, bool stopAtFirst = false, diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 0a2162a75c..4ad50cf636 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -246,14 +246,14 @@ private static void ReportAssertAreEqualFailed(object? expected, object? actual, if (actual is not null && expected is not null && !actual.GetType().Equals(expected.GetType())) { - summary = "Expected values to be equal, but they are of different types."; + summary = FrameworkMessages.AreEqualDifferentTypesFailedSummary; evidence = EvidenceBlock.Create() .AddLine("expected:", $"{expectedRendered} ({expected.GetType().FullName})") .AddLine("actual:", $"{actualRendered} ({actual.GetType().FullName})"); } else if (expected is string expectedString && actual is string actualString) { - summary = "Expected strings to be equal (case-sensitive)."; + summary = FrameworkMessages.AreEqualStringsFailedSummary; int diffIndex = FindFirstStringDifference(expectedString, actualString); string lengthInfo = expectedString.Length == actualString.Length ? string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AreEqualStringDiffLengthBothMsg, expectedString.Length, diffIndex) @@ -275,7 +275,7 @@ private static void ReportAssertAreEqualFailed(object? expected, object? actual, } else { - summary = "Expected values to be equal."; + summary = FrameworkMessages.AreEqualFailedSummary; evidence = EvidenceBlock.Create() .AddLine("expected:", expectedRendered) .AddLine("actual:", actualRendered); @@ -387,14 +387,16 @@ private static bool AreNotEqualFailing(T? notExpected, T? actual, IEqualityCo [DoesNotReturn] private static void ReportAssertAreNotEqualFailed(object? notExpected, object? actual, string? message, string notExpectedExpression, string actualExpression) { + string notExpectedRendered = AssertionValueRenderer.RenderValue(notExpected); string actualRendered = AssertionValueRenderer.RenderValue(actual); EvidenceBlock evidence = EvidenceBlock.Create() .AddLine("actual:", actualRendered); - StructuredAssertionMessage structured = new("Expected values to not be equal."); + StructuredAssertionMessage structured = new(FrameworkMessages.AreNotEqualFailedSummary); structured.WithUserMessage(message); structured.WithEvidence(evidence); + structured.WithExpectedAndActual(notExpectedRendered, actualRendered); structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreNotEqual", notExpectedExpression, actualExpression)); ReportAssertFailed(structured); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index cc6ca68bf7..0622a82d25 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -164,7 +164,7 @@ private static void ReportAssertIsNullFailed(object? value, string? message, str EvidenceBlock evidence = EvidenceBlock.Create() .AddLine("actual:", actualValue); - StructuredAssertionMessage structured = new("Expected value to be null."); + StructuredAssertionMessage structured = new(FrameworkMessages.IsNullFailedSummary); structured.WithUserMessage(message); structured.WithEvidence(evidence); structured.WithExpectedAndActual(null, actualValue); @@ -213,7 +213,7 @@ public static void IsNotNull([NotNull] object? value, string? message = "", [Cal private static void ReportAssertIsNotNullFailed(string? message, string valueExpression) { // RFC: IsNotNull omits the evidence block since actual is always null - StructuredAssertionMessage structured = new("Expected value to not be null."); + StructuredAssertionMessage structured = new(FrameworkMessages.IsNotNullFailedSummary); structured.WithUserMessage(message); structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotNull", valueExpression)); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs index 89c5b97fe0..714f15dc02 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs @@ -165,7 +165,7 @@ private static void ReportAssertIsTrueFailed(bool? condition, string? message, s EvidenceBlock evidence = EvidenceBlock.Create() .AddLine("actual:", actualValue); - StructuredAssertionMessage structured = new("Expected condition to be true."); + StructuredAssertionMessage structured = new(FrameworkMessages.IsTrueFailedSummary); structured.WithUserMessage(message); structured.WithEvidence(evidence); structured.WithExpectedAndActual(null, actualValue); @@ -216,7 +216,7 @@ private static void ReportAssertIsFalseFailed(bool? condition, string? message, EvidenceBlock evidence = EvidenceBlock.Create() .AddLine("actual:", actualValue); - StructuredAssertionMessage structured = new("Expected condition to be false."); + StructuredAssertionMessage structured = new(FrameworkMessages.IsFalseFailedSummary); structured.WithUserMessage(message); structured.WithEvidence(evidence); structured.WithExpectedAndActual(null, actualValue); diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 90350e2434..28b976f8c3 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -408,4 +408,28 @@ Actual: {2} [STATestMethod] is not supported on non-Windows platforms. STA (Single Threaded Apartment) is a Windows-only COM threading concept. Use [OSCondition(OperatingSystems.Windows)] to skip this test on non-Windows platforms. - \ No newline at end of file + + Expected condition to be true. + + + Expected condition to be false. + + + Expected value to be null. + + + Expected value to not be null. + + + Expected values to be equal. + + + Expected values to be equal, but they are of different types. + + + Expected strings to be equal (case-sensitive). + + + Expected values to not be equal. + + diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index c8794c978c..d3a3cb98a8 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -17,6 +17,11 @@ Byla nalezena duplicitní položka:<{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Očekáváno:<{1}>. Aktuálně:<{2}>. {0} @@ -37,6 +42,11 @@ Očekáváno:<{1} ({2})>. Aktuálně:<{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ Očekávaná délka řetězce je {0}, ale byla {1}. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Nebyla očekávána žádná hodnota kromě:<{1}>. Aktuálně:<{2}>. {0} @@ -78,6 +93,11 @@ Očekáván rozdíl, který je větší jak <{3}> mezi očekávanou hodnotou <{1}> a aktuální hodnotou <{2}>. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Obě hodnoty jsou <null>. {0} @@ -272,6 +292,11 @@ Skutečnost: {2} Neplatná adresa URL lístku GitHubu + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} Hodnota {0} není v očekávaném rozsahu [{1}..{2}]. {3} @@ -317,6 +342,21 @@ Skutečnost: {2} Řetězec „{0}“ odpovídá vzoru „{1}“. {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index ed1690341f..227e443f15 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -17,6 +17,11 @@ Doppeltes Element gefunden: <{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Erwartet:<{1}>. Tatsächlich:<{2}>. {0} @@ -37,6 +42,11 @@ Erwartet:<{1} ({2})>. Tatsächlich:<{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ Die erwartete Länge der Zeichenfolge ist {0}, war aber {1}. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Es wurde ein beliebiger Wert erwartet außer:<{1}>. Tatsächlich:<{2}>. {0} @@ -78,6 +93,11 @@ Es wurde eine Differenz größer als <{3}> zwischen dem erwarteten Wert <{1}> und dem tatsächlichen Wert <{2}> erwartet. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Beide Werte sind <null>. {0} @@ -272,6 +292,11 @@ Tatsächlich: {2} Ungültige GitHub-Ticket-URL. + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} Der Wert „{0}“ liegt nicht im erwarteten Bereich [{1}..{2}]. {3} @@ -317,6 +342,21 @@ Tatsächlich: {2} Die Zeichenfolge „{0}“ stimmt mit dem Muster „{1}“ überein. {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index a3c92fbdb9..beee630934 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -17,6 +17,11 @@ Se encontró un elemento duplicado:<{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Se esperaba <{1}>, pero es <{2}>. {0} @@ -37,6 +42,11 @@ Se esperaba:<{1} ({2})>, pero es:<{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ Se esperaba una longitud de cadena {0} pero fue {1}. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Se esperaba cualquier valor excepto <{1}>, pero es <{2}>. {0} @@ -78,6 +93,11 @@ Se esperaba una diferencia mayor que <{3}> entre el valor esperado <{1}> y el valor actual <{2}>. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Ambos valores son <null>. {0} @@ -272,6 +292,11 @@ Real: {2} Dirección URL de vale de GitHub no válida + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} El valor "{0}" no está dentro del rango esperado [{1}..{2}]. {3} @@ -317,6 +342,21 @@ Real: {2} La cadena "{0}" coincide con el patrón "{1}". {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 946c62ffb2..abdcc230eb 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -17,6 +17,11 @@ Un élément dupliqué a été trouvé : <{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Attendu : <{1}>, Réel : <{2}>. {0} @@ -37,6 +42,11 @@ Attendu : <{1} ({2})>, Réel : <{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ La longueur de chaîne attendue {0} mais était {1}. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Toute valeur attendue sauf :<{1}>. Réel :<{2}>. {0} @@ -78,6 +93,11 @@ Différence attendue supérieure à <{3}> comprise entre la valeur attendue <{1}> et la valeur réelle <{2}>. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Les deux valeurs sont <null>. {0} @@ -272,6 +292,11 @@ Réel : {2} URL de ticket GitHub non valide + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} La valeur « {0} » n'est pas dans la plage attendue [{1}..{2}]. {3} @@ -317,6 +342,21 @@ Réel : {2} La chaîne '{0}' correspond au modèle '{1}'. {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 2ab47d7812..5f4262c6e8 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -17,6 +17,11 @@ Rilevato elemento duplicato:<{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Previsto:<{1}>. Effettivo:<{2}>. {0} @@ -37,6 +42,11 @@ Previsto:<{1} ({2})>. Effettivo:<{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ La lunghezza della stringa prevista è {0} ma era {1}. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Previsto qualsiasi valore tranne:<{1}>. Effettivo:<{2}>. {0} @@ -78,6 +93,11 @@ Prevista una differenza maggiore di <{3}> tra il valore previsto <{1}> e il valore effettivo <{2}>. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Entrambi i valori sono <null>. {0} @@ -272,6 +292,11 @@ Effettivo: {2} L'URL del ticket GitHub non è valido + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} Il valore '{0}' non è compreso nell'intervallo previsto [{1}, {2}]. {3} @@ -317,6 +342,21 @@ Effettivo: {2} La stringa '{0}' corrisponde al criterio '{1}'. {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index 12c82093c0..5cbbeb4c56 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -17,6 +17,11 @@ 重複する項目が見つかりました:<{1}>。{0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} <{1}> が必要ですが、<{2}> が指定されました。{0} @@ -37,6 +42,11 @@ <{1} ({2})> が必要ですが、<{3} ({4})> が指定されました。{0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ 期待される文字列の長さは {0} ですが、実際は {1} でした。 + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} <{1}> 以外の任意の値が必要ですが、<{2}> が指定されています。{0} @@ -78,6 +93,11 @@ 指定する値 <{1}> と実際の値 <{2}> との間には、<{3}> を超える差が必要です。{0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} どちらの値も<null>です。{0} @@ -272,6 +292,11 @@ Actual: {2} GitHub チケット URL が無効です + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} 値 '{0}' は予期される範囲 [{1}..{2}] 内にありません。{3} @@ -317,6 +342,21 @@ Actual: {2} 文字列 '{0}' はパターン '{1}' と一致します。{2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index a8ed4a21e8..8f33d38c61 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -17,6 +17,11 @@ 중복된 항목이 있습니다. <{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} 예상 값: <{1}>. 실제 값: <{2}>. {0} @@ -37,6 +42,11 @@ 예상 값: <{1} ({2})>. 실제 값: <{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ 문자열 길이 {0}(을)를 예상했지만 {1}입니다. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} 예상 값: <{1}>을(를) 제외한 모든 값. 실제 값: <{2}>. {0} @@ -78,6 +93,11 @@ 예상 값 <{1}>과(와) 실제 값 <{2}>의 차이가 <{3}>보다 커야 합니다. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} 두 값 모두 <null>입니다. {0} @@ -272,6 +292,11 @@ Actual: {2} 잘못된 GitHub 티켓 URL + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} '{0}' 값이 예상 범위 [{1}..{2}] 내에 있지 않습니다. {3} @@ -317,6 +342,21 @@ Actual: {2} '{0}' 문자열이 '{1}' 패턴과 일치합니다. {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index ccbabc8449..ee59e2ea50 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -17,6 +17,11 @@ Znaleziono duplikat:<{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Oczekiwana:<{1}>. Rzeczywista:<{2}>. {0} @@ -37,6 +42,11 @@ Oczekiwana:<{1} ({2})>. Rzeczywista:<{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ Oczekiwano ciągu o długości {0}, ale miał wartość {1}. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Oczekiwano dowolnej wartości za wyjątkiem:<{1}>. Rzeczywista:<{2}>. {0} @@ -78,6 +93,11 @@ Oczekiwano różnicy większej niż <{3}> pomiędzy oczekiwaną wartością <{1}> a rzeczywistą wartością <{2}>. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Obie wartości to <null>. {0} @@ -272,6 +292,11 @@ Rzeczywiste: {2} Nieprawidłowy adres URL biletu usługi GitHub + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} Wartość „{0}” nie mieści się w oczekiwanym zakresie [{1}..{2}]. {3} @@ -317,6 +342,21 @@ Rzeczywiste: {2} Ciąg „{0}” jest zgodny ze wzorcem „{1}”. {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index d9463e35f0..5ad79ecdac 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -17,6 +17,11 @@ Item duplicado encontrado:<{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Esperado:<{1}>. Real:<{2}>. {0} @@ -37,6 +42,11 @@ Esperado:<{1} ({2})>. Real:<{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ Comprimento esperado da cadeia de caracteres {0}, mas foi {1}. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Esperado qualquer valor exceto:<{1}>. Real:<{2}>. {0} @@ -78,6 +93,11 @@ Esperada uma diferença maior que <{3}> entre o valor esperado <{1}> e o valor real <{2}>. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Ambos os valores são <null>. {0} @@ -272,6 +292,11 @@ Real: {2} URL de tíquete do GitHub inválida + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} O valor '{0}' não está dentro do intervalo esperado [{1}.. {2}]. {3} @@ -317,6 +342,21 @@ Real: {2} A cadeia de caracteres “{0}” corresponde ao padrão “{1}”. {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index b21cf7dbcb..75ed1d69cc 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -17,6 +17,11 @@ Обнаружен совпадающий элемент: <{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Ожидается: <{1}>. Фактически: <{2}>. {0} @@ -37,6 +42,11 @@ Ожидается: <{1} ({2})>. Фактически: <{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ Ожидалась длина строки: {0}, фактическая длина строки: {1}. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Ожидается любое значение, кроме: <{1}>. Фактически: <{2}>. {0} @@ -78,6 +93,11 @@ Между ожидаемым значением <{1}> и фактическим значением <{2}> требуется разница более чем <{3}>. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Оба значения равны <null>. {0} @@ -272,6 +292,11 @@ Actual: {2} Недопустимый URL-адрес билета GitHub + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} Значение "{0}" не находится в пределах ожидаемого диапазона [{1}..{2}]. {3} @@ -317,6 +342,21 @@ Actual: {2} Строка "{0}" соответствует шаблону "{1}". {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index f25bf59562..35d67b3c9d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -17,6 +17,11 @@ Yinelenen öğe bulundu:<{1}>. {0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} Beklenen:<{1}>. Gerçek:<{2}>. {0} @@ -37,6 +42,11 @@ Beklenen:<{1} ({2})>. Gerçek:<{3} ({4})>. {0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ Beklenen dize uzunluğu {0} idi, ancak dize uzunluğu {1} oldu. + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} Şunun dışında bir değer bekleniyor:<{1}>. Gerçek:<{2}>. {0} @@ -78,6 +93,11 @@ Beklenen değer <{1}> ile gerçek değer <{2}> arasında, şundan büyük olan fark bekleniyor: <{3}>. {0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} Her iki değer: <null>. {0} @@ -272,6 +292,11 @@ Gerçekte olan: {2} Geçersiz GitHub anahtar URL'si + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} Değer '{0}' beklenen aralık [{1}..{2}] içinde değil. {3} @@ -317,6 +342,21 @@ Gerçekte olan: {2} '{0}' dizesi, '{1}' deseni ile eşleşiyor. {2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 349be0a344..793fa885be 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -17,6 +17,11 @@ 找到了重复项: <{1}>。{0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} 应为: <{1}>,实际为: <{2}>。{0} @@ -37,6 +42,11 @@ 应为: <{1} ({2})>,实际为: <{3} ({4})>。{0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ 字符串长度应为 {0},但为 {1}。 + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} 应为: <{1}> 以外的任意值,实际为: <{2}>。{0} @@ -78,6 +93,11 @@ 预期值 <{1}> 和实际值 <{2}> 之间的差应大于 <{3}>。{0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} 两个值均为 <null>。{0} @@ -272,6 +292,11 @@ Actual: {2} GitHub 票证 URL 无效 + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} 值 "{0}" 不在预期范围 [{1}..{2}] 内。{3} @@ -317,6 +342,21 @@ Actual: {2} 字符串 '{0}' 与模式 '{1}' 匹配。{2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 6362c9e608..27b7f8e28a 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -17,6 +17,11 @@ 找到重複的項目: <{1}>。{0} + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + Expected:<{1}>. Actual:<{2}>. {0} 預期: <{1}>。實際: <{2}>。{0} @@ -37,6 +42,11 @@ 預期: <{1} ({2})>。實際: <{3} ({4})>。{0} + + Expected values to be equal. + Expected values to be equal. + + {0}{1} {2} @@ -68,6 +78,11 @@ 預期的字串長度為 {0},但為 {1}。 + + Expected strings to be equal (case-sensitive). + Expected strings to be equal (case-sensitive). + + Expected any value except:<{1}>. Actual:<{2}>. {0} 預期任何值 (<{1}> 除外)。實際: <{2}>。{0} @@ -78,6 +93,11 @@ 預期值 <{1}> 和實際值 <{2}> 之間的預期差異大於 <{3}>。{0} + + Expected values to not be equal. + Expected values to not be equal. + + Both values are <null>. {0} 兩個值均為 <null>。{0} @@ -272,6 +292,11 @@ Actual: {2} 無效的 GitHub 票證 URL + + Expected condition to be false. + Expected condition to be false. + + Value '{0}' is not within the expected range [{1}..{2}]. {3} 值 '{0}' 不在預期的範圍 [{1}, {2}] 內。{3} @@ -317,6 +342,21 @@ Actual: {2} 字串 '{0}' 與模式 '{1}' 相符。{2} + + Expected value to not be null. + Expected value to not be null. + + + + Expected value to be null. + Expected value to be null. + + + + Expected condition to be true. + Expected condition to be true. + + The member specified ({0}) could not be found. You might need to regenerate your private accessor, diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs index ebef8904a1..13129811e4 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/SoftAssertionTests.cs @@ -77,7 +77,7 @@ public async Task ScopeWithIsNotNullSoftFailure_CollectsFailure() testHostResult.AssertExitCodeIs(ExitCode.AtLeastOneTestFailed); testHostResult.AssertOutputMatchesRegex( - """failed ScopeWithIsNotNullSoftFailure \(\d+ms\)[\s\S]+Assert\.IsNotNull failed\.[\s\S]+at UnitTest1\.ScopeWithIsNotNullSoftFailure\(\)"""); + """failed ScopeWithIsNotNullSoftFailure \(\d+ms\)[\s\S]+Assertion failed\. Expected value to not be null\.[\s\S]+at UnitTest1\.ScopeWithIsNotNullSoftFailure\(\)"""); } [TestMethod] diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/ServiceProviderTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/ServiceProviderTests.cs index b2c838a5a6..441b280a17 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/ServiceProviderTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/ServiceProviderTests.cs @@ -148,6 +148,47 @@ public void TryAddService_SameInstance_ShouldReturnFalse() Assert.IsFalse(_serviceProvider.TryAddService(instance)); } + [TestMethod] + public void ReplaceService_WhenMatchingServiceExists_ReplacesInPlace() + { + TestSessionLifetimeHandler first = new(); + TestSessionLifetimeHandler second = new(); + _serviceProvider.AddService(first); + _serviceProvider.ReplaceService(second); + + Assert.HasCount(1, _serviceProvider.Services); + Assert.AreSame(second, _serviceProvider.Services.First()); + } + + [TestMethod] + public void ReplaceService_WhenNoMatchingServiceExists_AppendsService() + { + TestSessionLifetimeHandler instance = new(); + _serviceProvider.ReplaceService(instance); + + Assert.HasCount(1, _serviceProvider.Services); + Assert.AreSame(instance, _serviceProvider.Services.First()); + } + + [TestMethod] + public void ReplaceService_ReplacesFirstMatch_PreservesOtherServices() + { + TestHostProcessLifetimeHandler other = new(); + TestSessionLifetimeHandler first = new(); + TestSessionLifetimeHandler second = new(); + _serviceProvider.AddService(other); + _serviceProvider.AddService(first); + _serviceProvider.ReplaceService(second); + + Assert.HasCount(2, _serviceProvider.Services); + Assert.AreSame(other, _serviceProvider.Services.First()); + Assert.AreSame(second, _serviceProvider.Services.Last()); + } + + [TestMethod] + public void ReplaceService_NullService_ThrowsArgumentNullException() + => Assert.ThrowsExactly(() => _serviceProvider.ReplaceService(null!)); + [TestMethod] public void GetServicesInternal_ExtensionMethod_InternalExtension_ShouldReturn() { From d9973f0809c42857e37c3a893136c760560281d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 17:39:17 +0200 Subject: [PATCH 06/13] fix: address reviewer feedback - drop misleading case-sensitive claim, equalize DoesNotReturn, restore notExpected evidence - Drop '(case-sensitive)' from AreEqualStringsFailedSummary since the generic AreEqual path also accepts case-insensitive comparers. - Add [DoesNotReturn] to ReportAssertIsTrueFailed and ReportAssertIsNullFailed for symmetry with their IsFalse/IsNotNull siblings. - Restore notExpected: line in AreNotEqual evidence so failures with custom comparers retain diagnostic info. --- .../RFCs/012-Structured-Assertion-Messages.md | 12 +++++------ .../Assertions/Assert.AreEqual.cs | 1 + .../TestFramework/Assertions/Assert.IsNull.cs | 1 + .../TestFramework/Assertions/Assert.IsTrue.cs | 1 + .../Resources/FrameworkMessages.resx | 2 +- .../Resources/xlf/FrameworkMessages.cs.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.de.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.es.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.fr.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.it.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.ja.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.ko.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.pl.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.ru.xlf | 4 ++-- .../Resources/xlf/FrameworkMessages.tr.xlf | 4 ++-- .../xlf/FrameworkMessages.zh-Hans.xlf | 4 ++-- .../xlf/FrameworkMessages.zh-Hant.xlf | 4 ++-- .../Assertions/AssertTests.AreEqualTests.cs | 21 ++++++++++--------- 19 files changed, 47 insertions(+), 43 deletions(-) diff --git a/docs/RFCs/012-Structured-Assertion-Messages.md b/docs/RFCs/012-Structured-Assertion-Messages.md index df97aa4b3b..4afcbdd63c 100644 --- a/docs/RFCs/012-Structured-Assertion-Messages.md +++ b/docs/RFCs/012-Structured-Assertion-Messages.md @@ -72,7 +72,7 @@ Examples: | Assertion | Line 1 | | --------- | ------ | | `AreEqual` (int) | `Assertion failed. Expected values to be equal.` | -| `AreEqual` (string) | `Assertion failed. Expected strings to be equal (case-sensitive).` | +| `AreEqual` (string) | `Assertion failed. Expected strings to be equal.` | | `IsTrue` | `Assertion failed. Expected condition to be true.` | | `IsNull` | `Assertion failed. Expected value to be null.` | | `IsInstanceOfType` | `Assertion failed. Expected value to be of type String (or derived).` | @@ -270,7 +270,7 @@ Assert.AreEqual(expectedCount, actualCount) ### Assert.AreEqual (strings, with user message) ```text -Assertion failed. Expected strings to be equal (case-sensitive). +Assertion failed. Expected strings to be equal. Strings have same length (11) but differ at 1 location(s). First difference at index 7. The greeting should include the user's full name @@ -317,7 +317,7 @@ Assert.ThrowsExactly(() => Validate(input)) ### Assert.AreEqual (large strings) ```text -Assertion failed. Expected strings to be equal (case-sensitive). +Assertion failed. Expected strings to be equal. Strings have different lengths (expected: 50000, actual: 49997) and differ at 1 location(s). First difference at index 1042. expected: @@ -382,7 +382,7 @@ Assert.AreEqual( Output — the multiline expression is replaced with `` in the call-site: ```text -Assertion failed. Expected strings to be equal (case-sensitive). +Assertion failed. Expected strings to be equal. Strings differ at 1 location(s). First difference at index 22. expected: "{\n \"name\": \"Alice\",\n \"age\": 30\n}" @@ -429,7 +429,7 @@ expected: 42 actual: 37 ``` -Note: When the generic `AreEqual` overload is called with `T = string` (without `ignoreCase`/`culture` parameters), the message **auto-detects the string type** and uses the string-specific format (`"Expected strings to be equal (case-sensitive)."`) with full string diff diagnostics. The generic overload defaults to case-sensitive ordinal comparison, which is exactly what the string-specific format conveys. Developers writing `Assert.AreEqual("expected", actual)` get string diagnostics without needing to know about the string-specific overload. +Note: When the generic `AreEqual` overload is called with `T = string` (without `ignoreCase`/`culture` parameters), the message **auto-detects the string type** and uses the string-specific format (`"Expected strings to be equal."`) with full string diff diagnostics. The generic overload defaults to case-sensitive ordinal comparison, which is exactly what the string-specific format conveys. Developers writing `Assert.AreEqual("expected", actual)` get string diagnostics without needing to know about the string-specific overload. #### Assert.AreEqual (with delta) @@ -446,7 +446,7 @@ Note: The `delta` overload exists for `float`, `double`, `decimal`, and `long`. #### Assert.AreEqual (string, case-sensitive) ```text -Assertion failed. Expected strings to be equal (case-sensitive). +Assertion failed. Expected strings to be equal. Strings have same length (11) but differ at 1 location(s). First difference at index 7. expected: "hello world" diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 4ad50cf636..e0f48197ce 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -391,6 +391,7 @@ private static void ReportAssertAreNotEqualFailed(object? notExpected, object? a string actualRendered = AssertionValueRenderer.RenderValue(actual); EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("notExpected:", notExpectedRendered) .AddLine("actual:", actualRendered); StructuredAssertionMessage structured = new(FrameworkMessages.AreNotEqualFailedSummary); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs index 0622a82d25..3b3ce70ea7 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs @@ -158,6 +158,7 @@ public static void IsNull(object? value, string? message = "", [CallerArgumentEx private static bool IsNullFailing(object? value) => value is not null; + [DoesNotReturn] private static void ReportAssertIsNullFailed(object? value, string? message, string valueExpression) { string actualValue = AssertionValueRenderer.RenderValue(value); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs index 714f15dc02..b1855fc375 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs @@ -159,6 +159,7 @@ public static void IsTrue([DoesNotReturnIf(false)] bool? condition, string? mess private static bool IsTrueFailing(bool? condition) => condition is false or null; + [DoesNotReturn] private static void ReportAssertIsTrueFailed(bool? condition, string? message, string conditionExpression) { string actualValue = AssertionValueRenderer.RenderValue(condition); diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 28b976f8c3..89176a6d39 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -427,7 +427,7 @@ Actual: {2} Expected values to be equal, but they are of different types. - Expected strings to be equal (case-sensitive). + Expected strings to be equal. Expected values to not be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index d3a3cb98a8..0b3c487ee6 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index 227e443f15..52fd1fd99c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index beee630934..6c4863631e 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index abdcc230eb..4e67c9ff07 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 5f4262c6e8..f1e144acf0 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index 5cbbeb4c56..32be9ec001 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index 8f33d38c61..8e56c23aed 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index ee59e2ea50..b247b17726 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index 5ad79ecdac..53c56f32cd 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 75ed1d69cc..a1f97c493d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 35d67b3c9d..7f077a57af 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 793fa885be..94571c08c5 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 27b7f8e28a..35a2bfe7e5 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -79,8 +79,8 @@ - Expected strings to be equal (case-sensitive). - Expected strings to be equal (case-sensitive). + Expected strings to be equal. + Expected strings to be equal. diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs index 7593385fb3..f4c9a6ac7c 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -412,7 +412,8 @@ public async Task GenericAreNotEqual_InterpolatedString_SameValues_ShouldFail() Assertion failed. Expected values to not be equal. User-provided message. DummyClassTrackingToStringCalls, DummyClassTrackingToStringCalls, Hello, DummyIFormattable.ToString(), {string.Format(null, "{0:tt}", dateTime)}, {string.Format(null, "{0,5:tt}", dateTime)} - actual: 0 + notExpected: 0 + actual: 0 Assert.AreNotEqual(0, 0) """); @@ -1438,7 +1439,7 @@ public void AreEqualStringDifferenceAtBeginning() action.Should().Throw() .Which.Message.Should().Be( """ - Assertion failed. Expected strings to be equal (case-sensitive). + Assertion failed. Expected strings to be equal. String lengths are both 4 but differ at index 0. expected: "baaa" @@ -1454,7 +1455,7 @@ public void AreEqualStringDifferenceAtEnd() action.Should().Throw() .Which.Message.Should().Be( """ - Assertion failed. Expected strings to be equal (case-sensitive). + Assertion failed. Expected strings to be equal. String lengths are both 4 but differ at index 3. expected: "aaaa" @@ -1470,7 +1471,7 @@ public void AreEqualStringWithSpecialCharactersShouldEscape() action.Should().Throw() .Which.Message.Should().Be( """ - Assertion failed. Expected strings to be equal (case-sensitive). + Assertion failed. Expected strings to be equal. String lengths are both 4 but differ at index 2. expected: "aa\ta" @@ -1489,7 +1490,7 @@ public void AreEqualLongStringsShouldTruncateAndShowContext() action.Should().Throw() .Which.Message.Should().Be( """ - Assertion failed. Expected strings to be equal (case-sensitive). + Assertion failed. Expected strings to be equal. String lengths are both 201 but differ at index 100. expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" @@ -1517,7 +1518,7 @@ public void AreEqualStringWithDifferentLength() action.Should().Throw() .Which.Message.Should().Be( """ - Assertion failed. Expected strings to be equal (case-sensitive). + Assertion failed. Expected strings to be equal. Expected string length 4 but was 3. expected: "aaaa" @@ -1533,7 +1534,7 @@ public void AreEqualShorterExpectedString() action.Should().Throw() .Which.Message.Should().Be( """ - Assertion failed. Expected strings to be equal (case-sensitive). + Assertion failed. Expected strings to be equal. Expected string length 3 but was 4. expected: "aaa" @@ -1549,7 +1550,7 @@ public void AreEqualStringWithUserMessage() action.Should().Throw() .Which.Message.Should().Be( """ - Assertion failed. Expected strings to be equal (case-sensitive). + Assertion failed. Expected strings to be equal. String lengths are both 4 but differ at index 3. My custom message @@ -1566,7 +1567,7 @@ public void AreEqualStringWithEmojis() action.Should().Throw() .Which.Message.Should().Be( """ - Assertion failed. Expected strings to be equal (case-sensitive). + Assertion failed. Expected strings to be equal. Expected string length 2 but was 4. expected: "🥰" From 62c39f5fa52affa8cdcc4ce24b8bc06a06673b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 18:25:04 +0200 Subject: [PATCH 07/13] Address expert reviewer feedback (H1, M2, L3, N1) --- ...ert.AreEqual.InterpolatedStringHandlers.cs | 2 +- .../Assertions/Assert.AreEqual.cs | 16 ++++++++------- .../TestFramework/Assertions/Assert.cs | 20 +++++++++++++------ .../Assertions/AssertTests.AreEqualTests.cs | 8 +++++--- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs index 3c28a6d920..a887cc5fac 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.InterpolatedStringHandlers.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.ComponentModel; diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 9bb0436ffd..8fcf611d4d 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -248,8 +248,10 @@ private static void ReportAssertAreEqualFailed(object? expected, object? actual, { summary = FrameworkMessages.AreEqualDifferentTypesFailedSummary; evidence = EvidenceBlock.Create() - .AddLine("expected:", $"{expectedRendered} ({expected.GetType().FullName})") - .AddLine("actual:", $"{actualRendered} ({actual.GetType().FullName})"); + .AddLine("expected:", expectedRendered) + .AddLine("expected type:", expected.GetType().FullName!) + .AddLine("actual:", actualRendered) + .AddLine("actual type:", actual.GetType().FullName!); } else if (expected is string expectedString && actual is string actualString) { @@ -268,7 +270,7 @@ private static void ReportAssertAreEqualFailed(object? expected, object? actual, stringStructured.WithUserMessage(message); stringStructured.WithEvidence(evidence); stringStructured.WithExpectedAndActual(expectedRendered, actualRendered); - stringStructured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreEqual", expectedExpression, actualExpression)); + stringStructured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreEqual", expectedExpression, "expected", actualExpression, "actual")); ReportAssertFailed(stringStructured); return; @@ -285,7 +287,7 @@ private static void ReportAssertAreEqualFailed(object? expected, object? actual, structured.WithUserMessage(message); structured.WithEvidence(evidence); structured.WithExpectedAndActual(expectedRendered, actualRendered); - structured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreEqual", expectedExpression, actualExpression)); + structured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreEqual", expectedExpression, "expected", actualExpression, "actual")); ReportAssertFailed(structured); } @@ -397,8 +399,8 @@ private static void ReportAssertAreNotEqualFailed(object? notExpected, object? a StructuredAssertionMessage structured = new(FrameworkMessages.AreNotEqualFailedSummary); structured.WithUserMessage(message); structured.WithEvidence(evidence); - structured.WithExpectedAndActual(notExpectedRendered, actualRendered); - structured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreNotEqual", notExpectedExpression, actualExpression)); + structured.WithExpectedAndActual($"not {notExpectedRendered}", actualRendered); + structured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreNotEqual", notExpectedExpression, "notExpected", actualExpression, "actual")); ReportAssertFailed(structured); } diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 44185a34e0..aee792a131 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -343,13 +343,21 @@ internal static string ReplaceNulls(object? input) /// /// Formats a call-site expression like Assert.MethodName(expression1, expression2). - /// Returns if either expression is empty or contains a line break. + /// When either expression is empty the call-site is omitted. When an expression contains + /// newlines (multiline constant) it is replaced with a <paramName> placeholder. /// - private static string? FormatBinaryCallSiteExpression(string methodName, string expression1, string expression2) - => string.IsNullOrEmpty(expression1) || string.IsNullOrEmpty(expression2) - || expression1.IndexOfAny(['\n', '\r']) >= 0 || expression2.IndexOfAny(['\n', '\r']) >= 0 - ? null - : $"{methodName}({expression1}, {expression2})"; + private static string? FormatBinaryCallSiteExpression(string methodName, string expression1, string paramName1, string expression2, string paramName2) + { + if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2)) + { + return null; + } + + string arg1 = expression1.IndexOfAny(['\n', '\r']) >= 0 ? $"<{paramName1}>" : expression1; + string arg2 = expression2.IndexOfAny(['\n', '\r']) >= 0 ? $"<{paramName2}>" : expression2; + + return $"{methodName}({arg1}, {arg2})"; + } private static int CompareInternal(string? expected, string? actual, bool ignoreCase, CultureInfo culture) #pragma warning disable CA1309 // Use ordinal string comparison diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs index f4c9a6ac7c..2f96464361 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using AwesomeAssertions; @@ -306,8 +306,10 @@ public void AreEqualTwoObjectsDifferentTypeShouldFail() """ Assertion failed. Expected values to be equal, but they are of different types. - expected: System.Object (System.Object) - actual: 1 (System.Int32) + expected: System.Object + expected type: System.Object + actual: 1 + actual type: System.Int32 Assert.AreEqual(new object(), 1) """); From 1b7bc4a3f60ed389e6291bbfa4afe28cb83defab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 18:37:01 +0200 Subject: [PATCH 08/13] Address expert reviewer iteration 2 (N1 IsNullOrEmpty, N2 nullable Type.FullName, L1+L3 regression tests) --- .../Assertions/Assert.AreEqual.cs | 4 +- .../TestFramework/Assertions/Assert.cs | 2 +- .../Assertions/AssertTests.AreEqualTests.cs | 46 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index 8fcf611d4d..b4fea5a3c3 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -249,9 +249,9 @@ private static void ReportAssertAreEqualFailed(object? expected, object? actual, summary = FrameworkMessages.AreEqualDifferentTypesFailedSummary; evidence = EvidenceBlock.Create() .AddLine("expected:", expectedRendered) - .AddLine("expected type:", expected.GetType().FullName!) + .AddLine("expected type:", expected.GetType().FullName ?? expected.GetType().Name) .AddLine("actual:", actualRendered) - .AddLine("actual type:", actual.GetType().FullName!); + .AddLine("actual type:", actual.GetType().FullName ?? actual.GetType().Name); } else if (expected is string expectedString && actual is string actualString) { diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index aee792a131..b67fa2b582 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -348,7 +348,7 @@ internal static string ReplaceNulls(object? input) /// private static string? FormatBinaryCallSiteExpression(string methodName, string expression1, string paramName1, string expression2, string paramName2) { - if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2)) + if (string.IsNullOrEmpty(expression1) || string.IsNullOrEmpty(expression2)) { return null; } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs index 2f96464361..49cbf86bb1 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs @@ -315,6 +315,52 @@ public void AreEqualTwoObjectsDifferentTypeShouldFail() """); } + public void AreNotEqual_PopulatesExpectedAndActualTextWithNotPrefix() + { + Action action = () => Assert.AreNotEqual(0, 0); + AssertFailedException ex = action.Should().Throw().Which; + ex.ExpectedText.Should().Be("not 0"); + ex.ActualText.Should().Be("0"); + ex.Data["assert.expected"].Should().Be("not 0"); + ex.Data["assert.actual"].Should().Be("0"); + } + + public void AreEqual_MultilineExpectedExpression_UsesPlaceholderInCallSite() + { + Action action = () => Assert.AreEqual( + """ + line1 + line2 + """, + "different"); + action.Should().Throw() + .Which.Message.Should().EndWith("Assert.AreEqual(, \"different\")"); + } + + public void AreEqual_MultilineActualExpression_UsesPlaceholderInCallSite() + { + Action action = () => Assert.AreEqual( + "different", + """ + line1 + line2 + """); + action.Should().Throw() + .Which.Message.Should().EndWith("Assert.AreEqual(\"different\", )"); + } + + public void AreNotEqual_MultilineNotExpectedExpression_UsesPlaceholderInCallSite() + { + string value = "x"; + Action action = () => Assert.AreNotEqual( + """ + x + """, + value); + action.Should().Throw() + .Which.Message.Should().EndWith("Assert.AreNotEqual(, value)"); + } + public void AreEqualWithTypeOverridingEqualsShouldWork() { var a = new TypeOverridesEquals(); From ae0742c9df5ed9b9068178ecd562b51525b94c9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 18:56:57 +0200 Subject: [PATCH 09/13] Address expert reviewer iteration 3 (L1 rename test, L2 add AreNotEqual full-message test, N4 cache GetType, N5 hoist line break chars, N6 consolidate string branch) --- .../Assertions/Assert.AreEqual.cs | 26 +++++++++---------- .../TestFramework/Assertions/Assert.cs | 6 +++-- .../Assertions/AssertTests.AreEqualTests.cs | 18 ++++++++++++- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs index b4fea5a3c3..52aa780d9d 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. namespace Microsoft.VisualStudio.TestTools.UnitTesting; @@ -243,37 +243,30 @@ private static void ReportAssertAreEqualFailed(object? expected, object? actual, string summary; EvidenceBlock evidence; + string? additionalSummaryLine = null; if (actual is not null && expected is not null && !actual.GetType().Equals(expected.GetType())) { + Type expectedType = expected.GetType(); + Type actualType = actual.GetType(); summary = FrameworkMessages.AreEqualDifferentTypesFailedSummary; evidence = EvidenceBlock.Create() .AddLine("expected:", expectedRendered) - .AddLine("expected type:", expected.GetType().FullName ?? expected.GetType().Name) + .AddLine("expected type:", expectedType.FullName ?? expectedType.Name) .AddLine("actual:", actualRendered) - .AddLine("actual type:", actual.GetType().FullName ?? actual.GetType().Name); + .AddLine("actual type:", actualType.FullName ?? actualType.Name); } else if (expected is string expectedString && actual is string actualString) { summary = FrameworkMessages.AreEqualStringsFailedSummary; int diffIndex = FindFirstStringDifference(expectedString, actualString); - string lengthInfo = expectedString.Length == actualString.Length + additionalSummaryLine = expectedString.Length == actualString.Length ? string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AreEqualStringDiffLengthBothMsg, expectedString.Length, diffIndex) : string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AreEqualStringDiffLengthDifferentMsg, expectedString.Length, actualString.Length); evidence = EvidenceBlock.Create() .AddLine("expected:", expectedRendered) .AddLine("actual:", actualRendered); - - StructuredAssertionMessage stringStructured = new(summary); - stringStructured.WithAdditionalSummaryLine(lengthInfo); - stringStructured.WithUserMessage(message); - stringStructured.WithEvidence(evidence); - stringStructured.WithExpectedAndActual(expectedRendered, actualRendered); - stringStructured.WithCallSiteExpression(FormatBinaryCallSiteExpression("Assert.AreEqual", expectedExpression, "expected", actualExpression, "actual")); - ReportAssertFailed(stringStructured); - - return; } else { @@ -284,6 +277,11 @@ private static void ReportAssertAreEqualFailed(object? expected, object? actual, } StructuredAssertionMessage structured = new(summary); + if (additionalSummaryLine is not null) + { + structured.WithAdditionalSummaryLine(additionalSummaryLine); + } + structured.WithUserMessage(message); structured.WithEvidence(evidence); structured.WithExpectedAndActual(expectedRendered, actualRendered); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index b67fa2b582..dc39295d30 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -341,6 +341,8 @@ internal static string ReplaceNulls(object? input) ? null : $"{methodName}({expression})"; + private static readonly char[] s_lineBreakChars = ['\n', '\r']; + /// /// Formats a call-site expression like Assert.MethodName(expression1, expression2). /// When either expression is empty the call-site is omitted. When an expression contains @@ -353,8 +355,8 @@ internal static string ReplaceNulls(object? input) return null; } - string arg1 = expression1.IndexOfAny(['\n', '\r']) >= 0 ? $"<{paramName1}>" : expression1; - string arg2 = expression2.IndexOfAny(['\n', '\r']) >= 0 ? $"<{paramName2}>" : expression2; + string arg1 = expression1.IndexOfAny(s_lineBreakChars) >= 0 ? $"<{paramName1}>" : expression1; + string arg2 = expression2.IndexOfAny(s_lineBreakChars) >= 0 ? $"<{paramName2}>" : expression2; return $"{methodName}({arg1}, {arg2})"; } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs index 49cbf86bb1..213fc4b2a7 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreEqualTests.cs @@ -325,6 +325,21 @@ public void AreNotEqual_PopulatesExpectedAndActualTextWithNotPrefix() ex.Data["assert.actual"].Should().Be("0"); } + public void AreNotEqual_FailsWithStructuredMessage() + { + Action action = () => Assert.AreNotEqual(0, 0); + action.Should().Throw() + .Which.Message.Should().Be( + """ + Assertion failed. Expected values to not be equal. + + notExpected: 0 + actual: 0 + + Assert.AreNotEqual(0, 0) + """); + } + public void AreEqual_MultilineExpectedExpression_UsesPlaceholderInCallSite() { Action action = () => Assert.AreEqual( @@ -1529,7 +1544,8 @@ String lengths are both 4 but differ at index 2. """); } - public void AreEqualLongStringsShouldTruncateAndShowContext() + // Long-string truncation is intentionally not yet implemented; documents the current full-string render. + public void AreEqualLongStringsShowsFullStrings() { string expected = new string('a', 100) + "b" + new string('c', 100); string actual = new string('a', 100) + "d" + new string('c', 100); From 46a001652e89c3978f973ed804873d98c9d587a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 19:03:01 +0200 Subject: [PATCH 10/13] Fix SA1311/IDE1006: rename s_lineBreakChars to LineBreakChars --- src/TestFramework/TestFramework/Assertions/Assert.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index dc39295d30..02653a5854 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -341,7 +341,7 @@ internal static string ReplaceNulls(object? input) ? null : $"{methodName}({expression})"; - private static readonly char[] s_lineBreakChars = ['\n', '\r']; + private static readonly char[] LineBreakChars = ['\n', '\r']; /// /// Formats a call-site expression like Assert.MethodName(expression1, expression2). @@ -355,8 +355,8 @@ internal static string ReplaceNulls(object? input) return null; } - string arg1 = expression1.IndexOfAny(s_lineBreakChars) >= 0 ? $"<{paramName1}>" : expression1; - string arg2 = expression2.IndexOfAny(s_lineBreakChars) >= 0 ? $"<{paramName2}>" : expression2; + string arg1 = expression1.IndexOfAny(LineBreakChars) >= 0 ? $"<{paramName1}>" : expression1; + string arg2 = expression2.IndexOfAny(LineBreakChars) >= 0 ? $"<{paramName2}>" : expression2; return $"{methodName}({arg1}, {arg2})"; } From af815379e7417def90a7f9ff3bd976b665bad198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 19:27:43 +0200 Subject: [PATCH 11/13] Address PR review: use IsNullOrWhiteSpace in FormatBinaryCallSiteExpression; clarify RFC case-sensitivity note --- docs/RFCs/012-Structured-Assertion-Messages.md | 2 +- src/TestFramework/TestFramework/Assertions/Assert.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/RFCs/012-Structured-Assertion-Messages.md b/docs/RFCs/012-Structured-Assertion-Messages.md index 4afcbdd63c..c34a4631b1 100644 --- a/docs/RFCs/012-Structured-Assertion-Messages.md +++ b/docs/RFCs/012-Structured-Assertion-Messages.md @@ -429,7 +429,7 @@ expected: 42 actual: 37 ``` -Note: When the generic `AreEqual` overload is called with `T = string` (without `ignoreCase`/`culture` parameters), the message **auto-detects the string type** and uses the string-specific format (`"Expected strings to be equal."`) with full string diff diagnostics. The generic overload defaults to case-sensitive ordinal comparison, which is exactly what the string-specific format conveys. Developers writing `Assert.AreEqual("expected", actual)` get string diagnostics without needing to know about the string-specific overload. +Note: When the generic `AreEqual` overload is called with `T = string` (without `ignoreCase`/`culture` parameters), the message **auto-detects the string type** and uses the string-specific format (`"Expected strings to be equal."`) with full string diff diagnostics. The generic overload defaults to ordinal equality (`EqualityComparer.Default`), which is case-sensitive. Developers writing `Assert.AreEqual("expected", actual)` get string diagnostics without needing to know about the string-specific overload. #### Assert.AreEqual (with delta) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 02653a5854..fc44da62f1 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -350,7 +350,7 @@ internal static string ReplaceNulls(object? input) /// private static string? FormatBinaryCallSiteExpression(string methodName, string expression1, string paramName1, string expression2, string paramName2) { - if (string.IsNullOrEmpty(expression1) || string.IsNullOrEmpty(expression2)) + if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2)) { return null; } From 6ead0ebabb63a5c2ca05a334297119ce360a6eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= <113402882+Evangelink@users.noreply.github.com> Date: Fri, 15 May 2026 14:37:50 +0200 Subject: [PATCH 12/13] Address PR review: drop `case-sensitive'' from RFC string section headings; extract AreEqual(1,2) test message into shared const; update OutputTests for structured AreEqual message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../RFCs/012-Structured-Assertion-Messages.md | 8 +- .../OutputTests.cs | 3 +- .../Assertions/AssertTests.ScopeTests.cs | 82 +++++-------------- 3 files changed, 25 insertions(+), 68 deletions(-) diff --git a/docs/RFCs/012-Structured-Assertion-Messages.md b/docs/RFCs/012-Structured-Assertion-Messages.md index c34a4631b1..2fda0cab4d 100644 --- a/docs/RFCs/012-Structured-Assertion-Messages.md +++ b/docs/RFCs/012-Structured-Assertion-Messages.md @@ -443,7 +443,7 @@ delta: 0.001 Note: The `delta` overload exists for `float`, `double`, `decimal`, and `long`. All four types use the same message format. The rendered precision follows the type’s default `ToString()` formatting. -#### Assert.AreEqual (string, case-sensitive) +#### Assert.AreEqual (string) ```text Assertion failed. Expected strings to be equal. @@ -453,10 +453,10 @@ expected: "hello world" actual: "hello wrold" ``` -#### Assert.AreEqual (string, case-insensitive with culture) +#### Assert.AreEqual (string, with ignoreCase and culture) ```text -Assertion failed. Expected strings to be equal (case-insensitive). +Assertion failed. Expected strings to be equal. Strings have different lengths (expected: 6, actual: 8) and differ at 1 location(s). First difference at index 6. expected: "straße" @@ -465,7 +465,7 @@ ignore case: true culture: de-DE ``` -Note: Under `de-DE` culture with case-insensitive comparison, `"straße"` and `"STRASSE"` are considered equal (ß expands to SS). The example above shows a genuinely failing comparison where the actual string has additional content beyond the case-equivalent portion. +Note: Under `de-DE` culture with case-insensitive comparison, `"straße"` and `"STRASSE"` are considered equal (ß expands to SS). The example above shows a genuinely failing comparison where the actual string has additional content beyond the case-equivalent portion. The structured-message format itself does not change between case-sensitive and case-insensitive overloads — the summary line is the same and the `ignore case:` / `culture:` evidence lines are added only when those parameters are supplied. #### Assert.AreNotEqual (generic) diff --git a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs index 762ba5d867..87fb0846a9 100644 --- a/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs +++ b/test/IntegrationTests/MSTest.Acceptance.IntegrationTests/OutputTests.cs @@ -17,7 +17,8 @@ public async Task DetailedOutputIsAsExpected(string tfm) TestHostResult testHostResult = await testHost.ExecuteAsync("--output detailed", cancellationToken: TestContext.CancellationToken); // Assert - testHostResult.AssertOutputContains("Assert.AreEqual failed. Expected:<1>. Actual:<2>."); + testHostResult.AssertOutputContains("Assertion failed. Expected values to be equal."); + testHostResult.AssertOutputContains("Assert.AreEqual(1, 2)"); testHostResult.AssertOutputContains(""" Standard output Console message diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs index 76c9dd2937..f31d0d52a4 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ScopeTests.cs @@ -7,6 +7,18 @@ namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; public partial class AssertTests { + // Shared expected message for the structured failure produced by `Assert.AreEqual(1, 2)`. + // Used across the scope soft-failure tests below to reduce churn when the structured-message + // format evolves; update this single constant rather than every assertion site. + private const string AreEqual1And2StructuredMessage = """ + Assertion failed. Expected values to be equal. + + expected: 1 + actual: 2 + + Assert.AreEqual(1, 2) + """; + public void Scope_NoFailures_DoesNotThrow() { Action action = () => @@ -28,15 +40,7 @@ public void Scope_SingleFailure_ThrowsOnDispose() Action action = () => scope.Dispose(); action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected values to be equal. - - expected: 1 - actual: 2 - - Assert.AreEqual(1, 2) - """); + .WithMessage(AreEqual1And2StructuredMessage); } public void Scope_MultipleFailures_CollectsAllErrors() @@ -56,15 +60,7 @@ public void Scope_MultipleFailures_CollectsAllErrors() .Which; innerException.InnerExceptions.Should().HaveCount(2); - innerException.InnerExceptions[0].Message.Should().Be( - """ - Assertion failed. Expected values to be equal. - - expected: 1 - actual: 2 - - Assert.AreEqual(1, 2) - """); + innerException.InnerExceptions[0].Message.Should().Be(AreEqual1And2StructuredMessage); innerException.InnerExceptions[1].Message.Should().Be( """ Assertion failed. Expected condition to be true. @@ -118,15 +114,7 @@ public void Scope_DoubleDispose_DoesNotThrowTwice() Action firstDispose = () => scope.Dispose(); firstDispose.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected values to be equal. - - expected: 1 - actual: 2 - - Assert.AreEqual(1, 2) - """); + .WithMessage(AreEqual1And2StructuredMessage); // Second dispose should be a no-op Action secondDispose = () => scope.Dispose(); @@ -176,15 +164,7 @@ Assertion failed. Expected value to not be null. Assert.IsNotNull(value) """); - innerException.InnerExceptions[1].Message.Should().Be( - """ - Assertion failed. Expected values to be equal. - - expected: 1 - actual: 2 - - Assert.AreEqual(1, 2) - """); + innerException.InnerExceptions[1].Message.Should().Be(AreEqual1And2StructuredMessage); } public void Scope_AssertIsInstanceOfType_IsSoftFailure() @@ -215,15 +195,7 @@ Assertion failed. Expected value to be of type Int32 (or derived). Assert.IsInstanceOfType(value) """); - innerException.InnerExceptions[1].Message.Should().Be( - """ - Assertion failed. Expected values to be equal. - - expected: 1 - actual: 2 - - Assert.AreEqual(1, 2) - """); + innerException.InnerExceptions[1].Message.Should().Be(AreEqual1And2StructuredMessage); } public void Scope_AssertIsExactInstanceOfType_IsSoftFailure() @@ -254,15 +226,7 @@ Assertion failed. Expected value to be exactly of type Object. Assert.IsExactInstanceOfType(value) """); - innerException.InnerExceptions[1].Message.Should().Be( - """ - Assertion failed. Expected values to be equal. - - expected: 1 - actual: 2 - - Assert.AreEqual(1, 2) - """); + innerException.InnerExceptions[1].Message.Should().Be(AreEqual1And2StructuredMessage); } public void Scope_AssertContainsSingle_IsSoftFailure() @@ -285,14 +249,6 @@ public void Scope_AssertContainsSingle_IsSoftFailure() innerException.InnerExceptions.Should().HaveCount(2); innerException.InnerExceptions[0].Message.Should().Be("Assert.ContainsSingle failed. Expected collection to contain exactly one element but found 3 element(s). 'collection' expression: 'items'."); - innerException.InnerExceptions[1].Message.Should().Be( - """ - Assertion failed. Expected values to be equal. - - expected: 1 - actual: 2 - - Assert.AreEqual(1, 2) - """); + innerException.InnerExceptions[1].Message.Should().Be(AreEqual1And2StructuredMessage); } } From 3b1c88fcea994b97dd627cf22f79beed130c95df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 11:04:37 +0000 Subject: [PATCH 13/13] Restore AreEqual summary xlf entries after merge conflict resolution Co-authored-by: Evangelink <11340282+Evangelink@users.noreply.github.com> --- .../Resources/xlf/FrameworkMessages.cs.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.de.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.es.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.fr.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.it.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.ja.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.ko.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.pl.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.ru.xlf | 20 +++++++++++++++++++ .../Resources/xlf/FrameworkMessages.tr.xlf | 20 +++++++++++++++++++ .../xlf/FrameworkMessages.zh-Hans.xlf | 20 +++++++++++++++++++ .../xlf/FrameworkMessages.zh-Hant.xlf | 20 +++++++++++++++++++ 13 files changed, 260 insertions(+) diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index b2828f2192..a2f777553d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -537,6 +537,26 @@ Skutečnost: {2} Očekávaná hodnota nesmí být null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Očekávaná hodnota bude null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index 3b0497e62d..cc8211906a 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -537,6 +537,26 @@ Tatsächlich: {2} Erwarteter Wert darf nicht null sein. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Erwarteter Wert ist null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index f6180a742f..5340c252ae 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -537,6 +537,26 @@ Real: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 21e859b0b8..ad4705884f 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -537,6 +537,26 @@ Réel : {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 6d2040c828..41633ba069 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -537,6 +537,26 @@ Effettivo: {2} Il valore previsto non è null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Il valore previsto dovrebbe essere null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index 392fa6ef72..b1db6ed13b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -537,6 +537,26 @@ Actual: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index caed5cd6c5..d542d08bbe 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -537,6 +537,26 @@ Actual: {2} 값이 null이 아니어야 합니다. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. 값이 null이어야 합니다. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 5da692e8b6..28f8dc6b83 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -537,6 +537,26 @@ Rzeczywiste: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index 935e661ad4..b9697d82b1 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -537,6 +537,26 @@ Real: {2} O valor esperado não deve ser nulo. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. O valor esperado deve ser nulo. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 310e5898df..0112565023 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -537,6 +537,26 @@ Actual: {2} Ожидаемое значение не должно быть null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Ожидаемое значение должно быть null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 5bb57205a5..a0a14a405c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -537,6 +537,26 @@ Gerçekte olan: {2} Expected value to not be null. + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. Expected value to be null. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 6fa3cb17f3..0b01be8bad 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -537,6 +537,26 @@ Actual: {2} 预期值不应为 null。 + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. 预期值为 null。 diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 366ab94673..6cd701117d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -537,6 +537,26 @@ Actual: {2} 預期值不可為 Null。 + + Expected values to not be equal. + Expected values to not be equal. + + + + Expected strings to be equal. + Expected strings to be equal. + + + + Expected values to be equal, but they are of different types. + Expected values to be equal, but they are of different types. + + + + Expected values to be equal. + Expected values to be equal. + + Expected value to be null. 預期值可為 Null。