Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
234 changes: 234 additions & 0 deletions src/TestFramework/TestFramework/Assertions/Assert.AreAllDistinct.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
// 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;

/// <summary>
/// A collection of helper classes to test various conditions within
/// unit tests. If the condition being tested is not met, an exception
/// is thrown.
/// </summary>
#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 sealed partial class Assert
{
#region AreAllDistinct

/// <summary>
/// Tests whether all items in the specified collection are distinct (no two
/// elements are equal) and throws an exception if any two elements in the
/// collection are equal.
/// </summary>
/// <typeparam name="T">The type of the collection items.</typeparam>
/// <param name="collection">
/// The collection in which to search for duplicate elements.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="collection"/> contains
/// at least one duplicate element. The message is shown in test results.
/// </param>
/// <param name="collectionExpression">
/// The syntactic expression of collection as given by the compiler via caller argument expression.
/// Users shouldn't pass a value for this parameter.
/// </param>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="collection"/> is null or contains at least one duplicate element.
/// </exception>
public static void AreAllDistinct<T>([NotNull] IEnumerable<T>? collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "")
{
CheckParameterNotNull(collection, "Assert.AreAllDistinct", "collection");
AreAllDistinctImpl(collection, EqualityComparer<T>.Default, comparerTypeName: null, message, collectionExpression);
}

/// <summary>
/// Tests whether all items in the specified collection are distinct (no two
/// elements are equal) using the supplied <paramref name="comparer"/> and throws
/// an exception if any two elements in the collection are equal.
/// </summary>
/// <typeparam name="T">The type of the collection items.</typeparam>
/// <param name="collection">
/// The collection in which to search for duplicate elements.
/// </param>
/// <param name="comparer">
/// The equality comparer to use when comparing elements.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="collection"/> contains
/// at least one duplicate element. The message is shown in test results.
/// </param>
/// <param name="collectionExpression">
/// The syntactic expression of collection as given by the compiler via caller argument expression.
/// Users shouldn't pass a value for this parameter.
/// </param>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="collection"/> is null or contains at least one duplicate element.
/// </exception>
public static void AreAllDistinct<T>([NotNull] IEnumerable<T>? collection, [NotNull] IEqualityComparer<T>? comparer, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "")
{
CheckParameterNotNull(collection, "Assert.AreAllDistinct", "collection");
CheckParameterNotNull(comparer, "Assert.AreAllDistinct", "comparer");
AreAllDistinctImpl(collection, comparer, comparerTypeName: comparer.GetType().ToString(), message, collectionExpression);
}

/// <summary>
/// Tests whether all items in the specified collection are distinct (no two
/// elements are equal) and throws an exception if any two elements in the
/// collection are equal.
/// </summary>
/// <param name="collection">
/// The collection in which to search for duplicate elements.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="collection"/> contains
/// at least one duplicate element. The message is shown in test results.
/// </param>
/// <param name="collectionExpression">
/// The syntactic expression of collection as given by the compiler via caller argument expression.
/// Users shouldn't pass a value for this parameter.
/// </param>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="collection"/> is null or contains at least one duplicate element.
/// </exception>
public static void AreAllDistinct([NotNull] IEnumerable? collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "")
{
CheckParameterNotNull(collection, "Assert.AreAllDistinct", "collection");
AreAllDistinctImpl(collection.Cast<object?>(), EqualityComparer<object?>.Default, comparerTypeName: null, message, collectionExpression);
}

/// <summary>
/// Tests whether all items in the specified collection are distinct (no two
/// elements are equal) using the supplied <paramref name="comparer"/> and throws
/// an exception if any two elements in the collection are equal.
/// </summary>
/// <param name="collection">
/// The collection in which to search for duplicate elements.
/// </param>
/// <param name="comparer">
/// The equality comparer to use when comparing elements.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="collection"/> contains
/// at least one duplicate element. The message is shown in test results.
/// </param>
/// <param name="collectionExpression">
/// The syntactic expression of collection as given by the compiler via caller argument expression.
/// Users shouldn't pass a value for this parameter.
/// </param>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="collection"/> is null or contains at least one duplicate element.
/// </exception>
public static void AreAllDistinct([NotNull] IEnumerable? collection, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "")
{
CheckParameterNotNull(collection, "Assert.AreAllDistinct", "collection");
CheckParameterNotNull(comparer, "Assert.AreAllDistinct", "comparer");
AreAllDistinctImpl(collection.Cast<object?>(), new NonGenericEqualityComparerAdapter(comparer), comparerTypeName: comparer.GetType().ToString(), message, collectionExpression);
}

#pragma warning disable CS8714 // The type cannot be used as type parameter in the generic type or method. Nullability of type argument doesn't match 'notnull' constraint.
private static void AreAllDistinctImpl<T>(IEnumerable<T> collection, IEqualityComparer<T> comparer, string? comparerTypeName, string? message, string collectionExpression)
{
List<T> snapshot = collection is List<T> list ? list : [.. collection];

#pragma warning disable IDE0028 // Collection initialization can be simplified - target-typed `new` cannot pass the comparer in the same syntactic form expected.
var seen = new HashSet<T>(comparer);
#pragma warning restore IDE0028

bool seenNull = false;
List<T>? duplicates = null;
HashSet<T>? duplicatesSeen = null;
bool nullDuplicateRecorded = false;

foreach (T item in snapshot)
{
if (item is null)
{
if (!seenNull)
{
seenNull = true;
continue;
}

if (!nullDuplicateRecorded)
{
duplicates ??= [];
duplicates.Add(default!);
nullDuplicateRecorded = true;
}

continue;
}

if (!seen.Add(item))
{
#pragma warning disable IDE0028 // Collection initialization can be simplified - target-typed `new` cannot pass the comparer in the same syntactic form expected.
duplicatesSeen ??= new(comparer);
#pragma warning restore IDE0028
if (duplicatesSeen.Add(item))
{
duplicates ??= [];
duplicates.Add(item);
}
}
}

if (duplicates is not null)
{
ReportAssertAreAllDistinctFailed(snapshot, duplicates, comparerTypeName, message, collectionExpression);
}
}
#pragma warning restore CS8714

[DoesNotReturn]
private static void ReportAssertAreAllDistinctFailed<T>(IEnumerable<T> collection, List<T> duplicates, string? comparerTypeName, string? message, string collectionExpression)
{
string collectionText = AssertionValueRenderer.RenderValue(collection);
string duplicatesText = AssertionValueRenderer.RenderValue(duplicates);

EvidenceBlock evidence = EvidenceBlock.Create()
.AddLine("duplicates:", duplicatesText)
.AddLine("collection:", collectionText);

if (comparerTypeName is not null)
{
evidence.AddLine("comparer:", comparerTypeName);
}

StructuredAssertionMessage structured = new(FrameworkMessages.AreAllDistinctFailedSummary);
structured.WithUserMessage(message);
structured.WithEvidence(evidence);
structured.WithExpectedAndActual(expectedText: null, actualText: collectionText);
structured.WithCallSiteExpression(BuildCallSiteWithComparerForCollection("Assert.AreAllDistinct", collectionExpression, comparerTypeName is not null));

ReportAssertFailed(structured);
}

private static string? BuildCallSiteWithComparerForCollection(string assertionMethodName, string collectionExpression, bool hasComparer)
{
string? callSite = FormatCallSiteExpression(assertionMethodName, collectionExpression, "<collection>");
if (callSite is null || !hasComparer)
{
return callSite;
}

// FormatCallSiteExpression has no overload accepting a third argument expression; insert
// the <comparer> placeholder so the rendered call-site reflects the overload that was actually invoked.
Debug.Assert(callSite.Length > 0 && callSite[callSite.Length - 1] == ')', "FormatCallSiteExpression contract: rendered call-site must end with ')'.");
return string.Concat(callSite.Remove(callSite.Length - 1), ", <comparer>)");
}

#endregion // AreAllDistinct

// TODO: Deduplicate with the same adapter in Assert.CollectionEquivalence.cs (introduced by PR #8234)
// once both PRs have landed.
private sealed class NonGenericEqualityComparerAdapter : IEqualityComparer<object?>
{
private readonly IEqualityComparer _comparer;

public NonGenericEqualityComparerAdapter(IEqualityComparer comparer)
=> _comparer = comparer;

bool IEqualityComparer<object?>.Equals(object? x, object? y) => _comparer.Equals(x, y);

int IEqualityComparer<object?>.GetHashCode(object? obj) => obj is null ? 0 : _comparer.GetHashCode(obj);
}
}
105 changes: 105 additions & 0 deletions src/TestFramework/TestFramework/Assertions/Assert.AreAllNotNull.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// 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;

/// <summary>
/// A collection of helper classes to test various conditions within
/// unit tests. If the condition being tested is not met, an exception
/// is thrown.
/// </summary>
#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 sealed partial class Assert
{
#region AreAllNotNull

/// <summary>
/// Tests whether all items in the specified collection are non-null and throws
/// an exception if any element is null.
/// </summary>
/// <param name="collection">
/// The collection in which to search for null elements.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="collection"/> contains
/// a null element. The message is shown in test results.
/// </param>
/// <param name="collectionExpression">
/// The syntactic expression of collection as given by the compiler via caller argument expression.
/// Users shouldn't pass a value for this parameter.
/// </param>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="collection"/> is null or contains at least one null element.
/// </exception>
public static void AreAllNotNull([NotNull] IEnumerable? collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "")
{
CheckParameterNotNull(collection, "Assert.AreAllNotNull", "collection");
AreAllNotNullImpl(collection.Cast<object?>(), message, collectionExpression);
}

/// <summary>
/// Tests whether all items in the specified collection are non-null and throws
/// an exception if any element is null.
/// </summary>
/// <typeparam name="T">The type of the collection items.</typeparam>
/// <param name="collection">
/// The collection in which to search for null elements.
/// </param>
/// <param name="message">
/// The message to include in the exception when <paramref name="collection"/> contains
/// a null element. The message is shown in test results.
/// </param>
/// <param name="collectionExpression">
/// The syntactic expression of collection as given by the compiler via caller argument expression.
/// Users shouldn't pass a value for this parameter.
/// </param>
/// <exception cref="AssertFailedException">
/// Thrown if <paramref name="collection"/> is null or contains at least one null element.
/// </exception>
public static void AreAllNotNull<T>([NotNull] IEnumerable<T>? collection, string? message = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "")
{
CheckParameterNotNull(collection, "Assert.AreAllNotNull", "collection");
AreAllNotNullImpl(collection, message, collectionExpression);
}

private static void AreAllNotNullImpl<T>(IEnumerable<T> collection, string? message, string collectionExpression)
{
List<T> snapshot = collection is List<T> list ? list : [.. collection];
List<int>? nullIndices = null;
for (int i = 0; i < snapshot.Count; i++)
{
if (snapshot[i] is null)
{
nullIndices ??= [];
nullIndices.Add(i);
}
}

if (nullIndices is not null)
{
ReportAssertAreAllNotNullFailed(snapshot, nullIndices, message, collectionExpression);
}
}

[DoesNotReturn]
private static void ReportAssertAreAllNotNullFailed<T>(IEnumerable<T> collection, List<int> nullIndices, string? message, string collectionExpression)
{
string collectionText = AssertionValueRenderer.RenderValue(collection);
string nullIndicesText = AssertionValueRenderer.RenderValue(nullIndices);

EvidenceBlock evidence = EvidenceBlock.Create()
.AddLine("null indices:", nullIndicesText)
.AddLine("collection:", collectionText);

StructuredAssertionMessage structured = new(FrameworkMessages.AreAllNotNullFailedSummary);
structured.WithUserMessage(message);
structured.WithEvidence(evidence);
structured.WithExpectedAndActual(expectedText: null, actualText: collectionText);
structured.WithCallSiteExpression(FormatCallSiteExpression("Assert.AreAllNotNull", collectionExpression, "<collection>"));

ReportAssertFailed(structured);
}

#endregion // AreAllNotNull
}
Loading
Loading