Skip to content
41 changes: 30 additions & 11 deletions src/TestFramework/TestFramework/Assertions/Assert.IsNull.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
{
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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(FrameworkMessages.IsNullFailedSummary);
structured.WithUserMessage(message);
structured.WithEvidence(evidence);
structured.WithExpectedAndActual(null, actualValue);
structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNull", valueExpression, nameof(value)));

ReportAssertFailed(structured);
}

/// <inheritdoc cref="IsNull(object?, string, string)" />
#pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578
Expand Down Expand Up @@ -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(FrameworkMessages.IsNotNullFailedSummary);
Comment thread
Evangelink marked this conversation as resolved.
structured.WithUserMessage(message);
structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsNotNull", valueExpression, "value"));

Comment thread
Evangelink marked this conversation as resolved.
Comment thread
Evangelink marked this conversation as resolved.
ReportAssertFailed(structured);
}
}
48 changes: 37 additions & 11 deletions src/TestFramework/TestFramework/Assertions/Assert.IsTrue.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
{
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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(FrameworkMessages.IsTrueFailedSummary);
structured.WithUserMessage(message);
structured.WithEvidence(evidence);
structured.WithExpectedAndActual(null, actualValue);
structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsTrue", conditionExpression, nameof(condition)));

ReportAssertFailed(structured);
}

/// <inheritdoc cref="IsFalse(bool?, string, string)" />
#pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578
Expand Down Expand Up @@ -188,14 +202,26 @@ public static void IsFalse([DoesNotReturnIf(true)] bool? condition, string? mess
{
if (IsFalseFailing(condition))
{
ReportAssertIsFalseFailed(BuildUserMessageForConditionExpression(message, conditionExpression));
ReportAssertIsFalseFailed(condition, message, conditionExpression);
}
}

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);

Comment thread
Evangelink marked this conversation as resolved.
StructuredAssertionMessage structured = new(FrameworkMessages.IsFalseFailedSummary);
structured.WithUserMessage(message);
structured.WithEvidence(evidence);
structured.WithExpectedAndActual(null, actualValue);
structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.IsFalse", conditionExpression, nameof(condition)));

ReportAssertFailed(structured);
}
}
41 changes: 38 additions & 3 deletions src/TestFramework/TestFramework/Assertions/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,44 @@ internal static void ThrowAssertFailed(StructuredAssertionMessage structuredMess
throw CreateAssertFailedException(structuredMessage);
}

/// <summary>
/// Formats a call-site expression for display at the bottom of a structured assertion message.
/// When the expression is empty, the call-site is omitted. When the expression contains newlines,
/// it is replaced with a <c>&lt;paramName&gt;</c> placeholder.
/// </summary>
internal static string? FormatCallSiteExpression(string assertionMethodName, string expression, string paramName)
{
if (string.IsNullOrWhiteSpace(expression))
{
return null;
}

// If expression contains newlines (multiline constant), replace with placeholder per RFC
string arg = expression.Contains('\n') || expression.Contains('\r')
? $"<{paramName}>"
: expression;

return $"{assertionMethodName}({arg})";
}

/// <summary>
/// Formats a call-site expression for display at the bottom of a structured assertion message,
/// using two captured expressions. When an expression contains newlines, it is replaced with a
/// <c>&lt;paramName&gt;</c> placeholder.
/// </summary>
internal static string? FormatCallSiteExpression(string assertionMethodName, string expression1, string paramName1, string expression2, string paramName2)
{
if (string.IsNullOrWhiteSpace(expression1) || string.IsNullOrWhiteSpace(expression2))
{
return null;
}
Comment thread
Evangelink marked this conversation as resolved.
Outdated

string arg1 = expression1.Contains('\n') || expression1.Contains('\r') ? $"<{paramName1}>" : expression1;
string arg2 = expression2.Contains('\n') || expression2.Contains('\r') ? $"<{paramName2}>" : expression2;

return $"{assertionMethodName}({arg1}, {arg2})";
}

private static string FormatAssertionFailed(string assertionName, string? message)
{
string failedMessage = string.Format(CultureInfo.CurrentCulture, FrameworkMessages.AssertionFailed, assertionName);
Expand Down Expand Up @@ -240,9 +278,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");

Expand Down
12 changes: 12 additions & 0 deletions src/TestFramework/TestFramework/Resources/FrameworkMessages.resx
Original file line number Diff line number Diff line change
Expand Up @@ -408,4 +408,16 @@ Actual: {2}</value>
<data name="STATestMethodNonWindowsNotSupported" xml:space="preserve">
<value>[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.</value>
</data>
<data name="IsTrueFailedSummary" xml:space="preserve">
<value>Expected condition to be true.</value>
</data>
<data name="IsFalseFailedSummary" xml:space="preserve">
<value>Expected condition to be false.</value>
</data>
<data name="IsNullFailedSummary" xml:space="preserve">
<value>Expected value to be null.</value>
</data>
<data name="IsNotNullFailedSummary" xml:space="preserve">
<value>Expected value to not be null.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ Skutečnost: {2}</target>
<target state="translated">Neplatná adresa URL lístku GitHubu</target>
<note />
</trans-unit>
<trans-unit id="IsFalseFailedSummary">
<source>Expected condition to be false.</source>
<target state="new">Expected condition to be false.</target>
<note />
</trans-unit>
<trans-unit id="IsInRangeFail">
<source>Value '{0}' is not within the expected range [{1}..{2}]. {3}</source>
<target state="translated">Hodnota {0} není v očekávaném rozsahu [{1}..{2}]. {3}</target>
Expand Down Expand Up @@ -317,6 +322,21 @@ Skutečnost: {2}</target>
<target state="translated">Řetězec „{0}“ odpovídá vzoru „{1}“. {2}</target>
<note />
</trans-unit>
<trans-unit id="IsNotNullFailedSummary">
<source>Expected value to not be null.</source>
<target state="new">Expected value to not be null.</target>
<note />
</trans-unit>
<trans-unit id="IsNullFailedSummary">
<source>Expected value to be null.</source>
<target state="new">Expected value to be null.</target>
<note />
</trans-unit>
<trans-unit id="IsTrueFailedSummary">
<source>Expected condition to be true.</source>
<target state="new">Expected condition to be true.</target>
<note />
</trans-unit>
<trans-unit id="PrivateAccessorMemberNotFound">
<source>
The member specified ({0}) could not be found. You might need to regenerate your private accessor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ Tatsächlich: {2}</target>
<target state="translated">Ungültige GitHub-Ticket-URL.</target>
<note />
</trans-unit>
<trans-unit id="IsFalseFailedSummary">
<source>Expected condition to be false.</source>
<target state="new">Expected condition to be false.</target>
<note />
</trans-unit>
<trans-unit id="IsInRangeFail">
<source>Value '{0}' is not within the expected range [{1}..{2}]. {3}</source>
<target state="translated">Der Wert „{0}“ liegt nicht im erwarteten Bereich [{1}..{2}]. {3}</target>
Expand Down Expand Up @@ -317,6 +322,21 @@ Tatsächlich: {2}</target>
<target state="translated">Die Zeichenfolge „{0}“ stimmt mit dem Muster „{1}“ überein. {2}</target>
<note />
</trans-unit>
<trans-unit id="IsNotNullFailedSummary">
<source>Expected value to not be null.</source>
<target state="new">Expected value to not be null.</target>
<note />
</trans-unit>
<trans-unit id="IsNullFailedSummary">
<source>Expected value to be null.</source>
<target state="new">Expected value to be null.</target>
<note />
</trans-unit>
<trans-unit id="IsTrueFailedSummary">
<source>Expected condition to be true.</source>
<target state="new">Expected condition to be true.</target>
<note />
</trans-unit>
<trans-unit id="PrivateAccessorMemberNotFound">
<source>
The member specified ({0}) could not be found. You might need to regenerate your private accessor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,11 @@ Real: {2}</target>
<target state="translated">Dirección URL de vale de GitHub no válida</target>
<note />
</trans-unit>
<trans-unit id="IsFalseFailedSummary">
<source>Expected condition to be false.</source>
<target state="new">Expected condition to be false.</target>
<note />
</trans-unit>
<trans-unit id="IsInRangeFail">
<source>Value '{0}' is not within the expected range [{1}..{2}]. {3}</source>
<target state="translated">El valor "{0}" no está dentro del rango esperado [{1}..{2}]. {3}</target>
Expand Down Expand Up @@ -317,6 +322,21 @@ Real: {2}</target>
<target state="translated">La cadena "{0}" coincide con el patrón "{1}". {2}</target>
<note />
</trans-unit>
<trans-unit id="IsNotNullFailedSummary">
<source>Expected value to not be null.</source>
<target state="new">Expected value to not be null.</target>
<note />
</trans-unit>
<trans-unit id="IsNullFailedSummary">
<source>Expected value to be null.</source>
<target state="new">Expected value to be null.</target>
<note />
</trans-unit>
<trans-unit id="IsTrueFailedSummary">
<source>Expected condition to be true.</source>
<target state="new">Expected condition to be true.</target>
<note />
</trans-unit>
<trans-unit id="PrivateAccessorMemberNotFound">
<source>
The member specified ({0}) could not be found. You might need to regenerate your private accessor,
Expand Down
Loading
Loading