Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce interpolated string handler overloads for assertions #4476

Merged
merged 46 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
35f2d07
Add interpolated string handler overloads for Assert.IsTrue/IsFalse
Youssef1313 Dec 27, 2024
13d599f
Add interpolated string handler overloads for Assert.IsNull/IsNotNull
Youssef1313 Dec 27, 2024
7cbafc1
Refactor
Youssef1313 Dec 27, 2024
94084b1
Assert.AreEqual/AreNotEqual [WIP]
Youssef1313 Dec 27, 2024
4c21c0b
Refactor
Youssef1313 Dec 30, 2024
5f92199
AreEqual progress [WIP]
Youssef1313 Dec 30, 2024
9eb9615
Progress [WIP]
Youssef1313 Dec 30, 2024
d020353
Better progress for AreEqual
Youssef1313 Dec 30, 2024
8dffd67
Suppress FPs
Youssef1313 Dec 30, 2024
ebd0bc5
PublicAPI
Youssef1313 Dec 30, 2024
7c903d2
PublicAPI
Youssef1313 Dec 30, 2024
90a460a
Fix conditionals
Youssef1313 Dec 30, 2024
bbce272
PublicAPI
Youssef1313 Dec 30, 2024
aaa98bb
Progress for IsInstanceOfType/IsNotInstanceOfType
Youssef1313 Dec 30, 2024
8e437e3
AreSame/AreNotSame progress
Youssef1313 Dec 30, 2024
3ed20e8
Fix formatting
Youssef1313 Dec 30, 2024
41392d3
Better coverage for IsTrue/IsFalse
Youssef1313 Dec 30, 2024
60cd5ad
Better coverage
Youssef1313 Dec 30, 2024
56a258d
Refactor Assert.Throws in preparation for interpolated string handler…
Youssef1313 Dec 30, 2024
15731f0
Progress for Assert.Throws/ThrowsExactly
Youssef1313 Dec 30, 2024
715bb30
Fix missing assignment to _state
Youssef1313 Dec 30, 2024
409afc1
Test coverage for AreSame/AreNotSame
Youssef1313 Dec 30, 2024
9f7780e
Reference Roslyn issue link
Youssef1313 Dec 30, 2024
271a7ef
More AreSame tests
Youssef1313 Dec 30, 2024
4b9c798
More prgoress
Youssef1313 Dec 30, 2024
cecdbc5
Missing changes for IsInstanceOf
Youssef1313 Dec 31, 2024
f6753aa
Little more coverage for IsInstanceOfType
Youssef1313 Dec 31, 2024
5fe77b1
Fix test
Youssef1313 Dec 31, 2024
32d15fc
Fix test
Youssef1313 Dec 31, 2024
12a011c
Avoid extra ToString
Youssef1313 Dec 31, 2024
38f6c91
Revert temporarily, will apply globally once it's more understood
Youssef1313 Dec 31, 2024
7795c53
Remove redundant readonly
Youssef1313 Dec 31, 2024
cb56d77
More AppendFormatted overloads, modulo tests
Youssef1313 Jan 1, 2025
bffbd63
Use AppendFormat
Youssef1313 Jan 1, 2025
15c42ed
Fix build
Youssef1313 Jan 1, 2025
8ec485d
Address TODO
Youssef1313 Jan 1, 2025
abcf014
Fix build
Youssef1313 Jan 1, 2025
6723745
Fix build
Youssef1313 Jan 2, 2025
99d3e8b
More progress
Youssef1313 Jan 2, 2025
df8264d
AppendFormatted
Youssef1313 Jan 2, 2025
56fa24c
Remove unused using
Youssef1313 Jan 2, 2025
cf1c868
More test coverage
Youssef1313 Jan 2, 2025
f51cb70
Renames
Youssef1313 Jan 2, 2025
b8738a3
Renames
Youssef1313 Jan 2, 2025
344196c
Small fix
Youssef1313 Jan 2, 2025
1c137de
Add AppendFormatted string overload
Youssef1313 Jan 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
715 changes: 589 additions & 126 deletions src/TestFramework/TestFramework/Assertions/Assert.AreEqual.cs

Large diffs are not rendered by default.

168 changes: 156 additions & 12 deletions src/TestFramework/TestFramework/Assertions/Assert.AreSame.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// 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;

namespace Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
Expand All @@ -10,6 +12,124 @@ namespace Microsoft.VisualStudio.TestTools.UnitTesting;
/// </summary>
public sealed partial class Assert
{
[InterpolatedStringHandler]
[EditorBrowsable(EditorBrowsableState.Never)]
public readonly struct AssertAreSameInterpolatedStringHandler<TArgument>
{
private readonly StringBuilder? _builder;
private readonly TArgument? _expected;
private readonly TArgument? _actual;

public AssertAreSameInterpolatedStringHandler(int literalLength, int formattedCount, TArgument? expected, TArgument? actual, out bool shouldAppend)
{
_expected = expected;
_actual = actual;
shouldAppend = IsAreSameFailing(expected, actual);
if (shouldAppend)
{
_builder = new StringBuilder(literalLength + formattedCount);
}
}

internal void ComputeAssertion()
{
if (_builder is not null)
{
ThrowAssertAreSameFailed(_expected, _actual, _builder.ToString());
}
}

public void AppendLiteral(string value) => _builder!.Append(value);

public void AppendFormatted<T>(T value) => AppendFormatted(value, format: null);
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved

#if NETCOREAPP3_1_OR_GREATER
public void AppendFormatted(ReadOnlySpan<char> value) => _builder!.Append(value);
Youssef1313 marked this conversation as resolved.
Show resolved Hide resolved

#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null) => AppendFormatted(value.ToString(), alignment, format);
#pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
#endif

// NOTE: All the overloads involving format and/or alignment are not super efficient.
// This code path is only for when an assert is failing, so that's not the common scenario
// and should be okay if not very optimized.
// A more efficient implementation that can be used for .NET 6 and later is to delegate the work to
// the BCL's StringBuilder.AppendInterpolatedStringHandler
public void AppendFormatted<T>(T value, string? format) => _builder!.AppendFormat(null, $"{{0:{format}}}", value);

public void AppendFormatted<T>(T value, int alignment) => _builder!.AppendFormat(null, $"{{0,{alignment}}}", value);

public void AppendFormatted<T>(T value, int alignment, string? format) => _builder!.AppendFormat(null, $"{{0,{alignment}:{format}}}", value);

public void AppendFormatted(string? value) => _builder!.Append(value);

#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
public void AppendFormatted(string? value, int alignment = 0, string? format = null) => _builder!.AppendFormat(null, $"{{0,{alignment}:{format}}}", value);

public void AppendFormatted(object? value, int alignment = 0, string? format = null) => _builder!.AppendFormat(null, $"{{0,{alignment}:{format}}}", value);
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
#pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
}

[InterpolatedStringHandler]
[EditorBrowsable(EditorBrowsableState.Never)]
public readonly struct AssertAreNotSameInterpolatedStringHandler<TArgument>
{
private readonly StringBuilder? _builder;

public AssertAreNotSameInterpolatedStringHandler(int literalLength, int formattedCount, TArgument? notExpected, TArgument? actual, out bool shouldAppend)
{
shouldAppend = IsAreNotSameFailing(notExpected, actual);
if (shouldAppend)
{
_builder = new StringBuilder(literalLength + formattedCount);
}
}

internal void ComputeAssertion()
{
if (_builder is not null)
{
ThrowAssertAreNotSameFailed(_builder.ToString());
}
}

public void AppendLiteral(string value) => _builder!.Append(value);

public void AppendFormatted<T>(T value) => AppendFormatted(value, format: null);

#if NETCOREAPP3_1_OR_GREATER
public void AppendFormatted(ReadOnlySpan<char> value) => _builder!.Append(value);

#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null) => AppendFormatted(value.ToString(), alignment, format);
#pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
#endif

// NOTE: All the overloads involving format and/or alignment are not super efficient.
// This code path is only for when an assert is failing, so that's not the common scenario
// and should be okay if not very optimized.
// A more efficient implementation that can be used for .NET 6 and later is to delegate the work to
// the BCL's StringBuilder.AppendInterpolatedStringHandler
public void AppendFormatted<T>(T value, string? format) => _builder!.AppendFormat(null, $"{{0:{format}}}", value);

public void AppendFormatted<T>(T value, int alignment) => _builder!.AppendFormat(null, $"{{0,{alignment}}}", value);

public void AppendFormatted<T>(T value, int alignment, string? format) => _builder!.AppendFormat(null, $"{{0,{alignment}:{format}}}", value);

public void AppendFormatted(string? value) => _builder!.Append(value);

#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
#pragma warning disable RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
public void AppendFormatted(string? value, int alignment = 0, string? format = null) => _builder!.AppendFormat(null, $"{{0,{alignment}:{format}}}", value);

public void AppendFormatted(object? value, int alignment = 0, string? format = null) => _builder!.AppendFormat(null, $"{{0,{alignment}:{format}}}", value);
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
#pragma warning restore RS0027 // API with optional parameter(s) should have the most parameters amongst its public overloads
}

/// <summary>
/// Tests whether the specified objects both refer to the same object and
/// throws an exception if the two inputs do not refer to the same object.
Expand Down Expand Up @@ -55,6 +175,12 @@ public static void AreSame<T>(T? expected, T? actual)
public static void AreSame<T>(T? expected, T? actual, string? message)
=> AreSame(expected, actual, message, null);

/// <inheritdoc cref="AreSame{T}(T, T, string?)" />
#pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578
public static void AreSame<T>(T? expected, T? actual, [InterpolatedStringHandlerArgument(nameof(expected), nameof(actual))] ref AssertAreSameInterpolatedStringHandler<T> message)
#pragma warning restore IDE0060 // Remove unused parameter
=> message.ComputeAssertion();

/// <summary>
/// Tests whether the specified objects both refer to the same object and
/// throws an exception if the two inputs do not refer to the same object.
Expand Down Expand Up @@ -82,23 +208,28 @@ public static void AreSame<T>(T? expected, T? actual, string? message)
/// </exception>
public static void AreSame<T>(T? expected, T? actual, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string? message, params object?[]? parameters)
{
if (ReferenceEquals(expected, actual))
if (!IsAreSameFailing(expected, actual))
{
return;
}

string userMessage = BuildUserMessage(message, parameters);
string finalMessage = userMessage;
ThrowAssertAreSameFailed(expected, actual, userMessage);
}

private static bool IsAreSameFailing<T>(T? expected, T? actual)
=> !ReferenceEquals(expected, actual);

if (expected is ValueType)
[DoesNotReturn]
private static void ThrowAssertAreSameFailed<T>(T? expected, T? actual, string userMessage)
{
string finalMessage = userMessage;
if (expected is ValueType && actual is ValueType)
{
if (actual is ValueType)
{
finalMessage = string.Format(
CultureInfo.CurrentCulture,
FrameworkMessages.AreSameGivenValues,
userMessage);
}
finalMessage = string.Format(
CultureInfo.CurrentCulture,
FrameworkMessages.AreSameGivenValues,
userMessage);
}

ThrowAssertFailed("Assert.AreSame", finalMessage);
Expand Down Expand Up @@ -151,6 +282,12 @@ public static void AreNotSame<T>(T? notExpected, T? actual)
public static void AreNotSame<T>(T? notExpected, T? actual, string? message)
=> AreNotSame(notExpected, actual, message, null);

/// <inheritdoc cref="AreNotSame{T}(T, T, string?)" />
#pragma warning disable IDE0060 // Remove unused parameter - https://github.com/dotnet/roslyn/issues/76578
public static void AreNotSame<T>(T? notExpected, T? actual, [InterpolatedStringHandlerArgument(nameof(notExpected), nameof(actual))] ref AssertAreNotSameInterpolatedStringHandler<T> message)
#pragma warning restore IDE0060 // Remove unused parameter
=> message.ComputeAssertion();

/// <summary>
/// Tests whether the specified objects refer to different objects and
/// throws an exception if the two inputs refer to the same object.
Expand Down Expand Up @@ -179,9 +316,16 @@ public static void AreNotSame<T>(T? notExpected, T? actual, string? message)
/// </exception>
public static void AreNotSame<T>(T? notExpected, T? actual, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string? message, params object?[]? parameters)
{
if (ReferenceEquals(notExpected, actual))
if (IsAreNotSameFailing(notExpected, actual))
{
ThrowAssertFailed("Assert.AreNotSame", BuildUserMessage(message, parameters));
ThrowAssertAreNotSameFailed(BuildUserMessage(message, parameters));
}
}

private static bool IsAreNotSameFailing<T>(T? notExpected, T? actual)
=> ReferenceEquals(notExpected, actual);

[DoesNotReturn]
private static void ThrowAssertAreNotSameFailed(string userMessage)
=> ThrowAssertFailed("Assert.AreNotSame", userMessage);
}
Loading
Loading