From a737337b925f3f103a78a32edbb1eb4d95ec4250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Thu, 14 May 2026 21:27:08 +0200 Subject: [PATCH 1/8] Add Assert.IsSubsetOf and Assert.IsNotSubsetOf with structured assertion messages Mirrors the existing CollectionAssert.IsSubsetOf / CollectionAssert.IsNotSubsetOf APIs but uses the RFC 012 structured assertion message format. Multiplicity-aware diffing surfaces every excess element (with null support). When a non-default IEqualityComparer is supplied, its short type name is added to the evidence block and a placeholder is appended to the call-site, per RFC 012. --- .../Assertions/Assert.IsSubsetOf.cs | 468 ++++++++++++++++++ .../PublicAPI/PublicAPI.Unshipped.txt | 8 + .../Resources/FrameworkMessages.resx | 6 + .../Resources/xlf/FrameworkMessages.cs.xlf | 10 + .../Resources/xlf/FrameworkMessages.de.xlf | 10 + .../Resources/xlf/FrameworkMessages.es.xlf | 10 + .../Resources/xlf/FrameworkMessages.fr.xlf | 10 + .../Resources/xlf/FrameworkMessages.it.xlf | 10 + .../Resources/xlf/FrameworkMessages.ja.xlf | 10 + .../Resources/xlf/FrameworkMessages.ko.xlf | 10 + .../Resources/xlf/FrameworkMessages.pl.xlf | 10 + .../Resources/xlf/FrameworkMessages.pt-BR.xlf | 10 + .../Resources/xlf/FrameworkMessages.ru.xlf | 10 + .../Resources/xlf/FrameworkMessages.tr.xlf | 10 + .../xlf/FrameworkMessages.zh-Hans.xlf | 10 + .../xlf/FrameworkMessages.zh-Hant.xlf | 10 + .../Assertions/AssertTests.IsSubsetOf.cs | 351 +++++++++++++ 17 files changed, 963 insertions(+) create mode 100644 src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs new file mode 100644 index 0000000000..de9f857036 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs @@ -0,0 +1,468 @@ +// 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; + +/// +/// A collection of helper classes to test various conditions within +/// unit tests. If the condition being tested is not met, an exception +/// is thrown. +/// +#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 IsSubsetOf + + /// + /// Tests whether one collection is a subset of another collection and throws an + /// exception if any element in the subset is not also in the superset. + /// + /// The type of the collection items. + /// + /// The collection expected to be a subset of . + /// + /// + /// The collection expected to be a superset of . + /// + /// + /// The message to include in the exception when an element in + /// is not found in . The message is shown in test results. + /// + /// + /// The syntactic expression of subset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of superset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if contains at least one element not contained in . + /// + public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") + { + CheckParameterNotNull(subset, "Assert.IsSubsetOf", "subset"); + CheckParameterNotNull(superset, "Assert.IsSubsetOf", "superset"); + IsSubsetOfImpl(subset, superset, EqualityComparer.Default, comparerName: null, message, subsetExpression, supersetExpression); + } + + /// + /// Tests whether one collection is a subset of another collection and throws an + /// exception if any element in the subset is not also in the superset. + /// + /// The type of the collection items. + /// + /// The collection expected to be a subset of . + /// + /// + /// The collection expected to be a superset of . + /// + /// + /// The equality comparer to use when comparing elements. + /// + /// + /// The message to include in the exception when an element in + /// is not found in . The message is shown in test results. + /// + /// + /// The syntactic expression of subset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of superset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if contains at least one element not contained in . + /// + public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") + { + CheckParameterNotNull(subset, "Assert.IsSubsetOf", "subset"); + CheckParameterNotNull(superset, "Assert.IsSubsetOf", "superset"); + CheckParameterNotNull(comparer, "Assert.IsSubsetOf", "comparer"); + IsSubsetOfImpl(subset, superset, comparer, comparer.GetType().Name, message, subsetExpression, supersetExpression); + } + + /// + /// Tests whether one collection is a subset of another collection and throws an + /// exception if any element in the subset is not also in the superset. + /// + /// + /// The collection expected to be a subset of . + /// + /// + /// The collection expected to be a superset of . + /// + /// + /// The message to include in the exception when an element in + /// is not found in . The message is shown in test results. + /// + /// + /// The syntactic expression of subset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of superset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if contains at least one element not contained in . + /// + public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") + { + CheckParameterNotNull(subset, "Assert.IsSubsetOf", "subset"); + CheckParameterNotNull(superset, "Assert.IsSubsetOf", "superset"); + + IsSubsetOfImpl(subset.Cast(), superset.Cast(), EqualityComparer.Default, comparerName: null, message, subsetExpression, supersetExpression); + } + + /// + /// Tests whether one collection is a subset of another collection and throws an + /// exception if any element in the subset is not also in the superset. + /// + /// + /// The collection expected to be a subset of . + /// + /// + /// The collection expected to be a superset of . + /// + /// + /// The equality comparer to use when comparing elements. + /// + /// + /// The message to include in the exception when an element in + /// is not found in . The message is shown in test results. + /// + /// + /// The syntactic expression of subset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of superset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if contains at least one element not contained in . + /// + public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") + { + CheckParameterNotNull(subset, "Assert.IsSubsetOf", "subset"); + CheckParameterNotNull(superset, "Assert.IsSubsetOf", "superset"); + CheckParameterNotNull(comparer, "Assert.IsSubsetOf", "comparer"); + + IsSubsetOfImpl(subset.Cast(), superset.Cast(), new NonGenericEqualityComparerAdapter(comparer), comparer.GetType().Name, message, subsetExpression, supersetExpression); + } + + #endregion // IsSubsetOf + + #region IsNotSubsetOf + + /// + /// Tests whether one collection is not a subset of another collection and throws + /// an exception if all elements in the subset are also in the superset. + /// + /// The type of the collection items. + /// + /// The collection expected not to be a subset of . + /// + /// + /// The collection expected not to be a superset of . + /// + /// + /// The message to include in the exception when every element in + /// is also found in . The message is shown in test results. + /// + /// + /// The syntactic expression of subset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of superset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if all elements of are contained in . + /// + public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") + { + CheckParameterNotNull(subset, "Assert.IsNotSubsetOf", "subset"); + CheckParameterNotNull(superset, "Assert.IsNotSubsetOf", "superset"); + IsNotSubsetOfImpl(subset, superset, EqualityComparer.Default, comparerName: null, message, subsetExpression, supersetExpression); + } + + /// + /// Tests whether one collection is not a subset of another collection and throws + /// an exception if all elements in the subset are also in the superset. + /// + /// The type of the collection items. + /// + /// The collection expected not to be a subset of . + /// + /// + /// The collection expected not to be a superset of . + /// + /// + /// The equality comparer to use when comparing elements. + /// + /// + /// The message to include in the exception when every element in + /// is also found in . The message is shown in test results. + /// + /// + /// The syntactic expression of subset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of superset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if all elements of are contained in . + /// + public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") + { + CheckParameterNotNull(subset, "Assert.IsNotSubsetOf", "subset"); + CheckParameterNotNull(superset, "Assert.IsNotSubsetOf", "superset"); + CheckParameterNotNull(comparer, "Assert.IsNotSubsetOf", "comparer"); + IsNotSubsetOfImpl(subset, superset, comparer, comparer.GetType().Name, message, subsetExpression, supersetExpression); + } + + /// + /// Tests whether one collection is not a subset of another collection and throws + /// an exception if all elements in the subset are also in the superset. + /// + /// + /// The collection expected not to be a subset of . + /// + /// + /// The collection expected not to be a superset of . + /// + /// + /// The message to include in the exception when every element in + /// is also found in . The message is shown in test results. + /// + /// + /// The syntactic expression of subset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of superset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if all elements of are contained in . + /// + public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") + { + CheckParameterNotNull(subset, "Assert.IsNotSubsetOf", "subset"); + CheckParameterNotNull(superset, "Assert.IsNotSubsetOf", "superset"); + + IsNotSubsetOfImpl(subset.Cast(), superset.Cast(), EqualityComparer.Default, comparerName: null, message, subsetExpression, supersetExpression); + } + + /// + /// Tests whether one collection is not a subset of another collection and throws + /// an exception if all elements in the subset are also in the superset. + /// + /// + /// The collection expected not to be a subset of . + /// + /// + /// The collection expected not to be a superset of . + /// + /// + /// The equality comparer to use when comparing elements. + /// + /// + /// The message to include in the exception when every element in + /// is also found in . The message is shown in test results. + /// + /// + /// The syntactic expression of subset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of superset as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if all elements of are contained in . + /// + public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") + { + CheckParameterNotNull(subset, "Assert.IsNotSubsetOf", "subset"); + CheckParameterNotNull(superset, "Assert.IsNotSubsetOf", "superset"); + CheckParameterNotNull(comparer, "Assert.IsNotSubsetOf", "comparer"); + + IsNotSubsetOfImpl(subset.Cast(), superset.Cast(), new NonGenericEqualityComparerAdapter(comparer), comparer.GetType().Name, message, subsetExpression, supersetExpression); + } + + #endregion // IsNotSubsetOf + + private static void IsSubsetOfImpl(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer, string? comparerName, string? message, string subsetExpression, string supersetExpression) + { + // Snapshot once so we don't enumerate twice (counting + rendering on failure) + // and so lazy/single-pass enumerables behave deterministically. + List subsetList = subset is List sl ? sl : [.. subset]; + List supersetList = superset is List spl ? spl : [.. superset]; + + if (TryFindMissingElements(subsetList, supersetList, comparer, out List? missing)) + { + ReportAssertIsSubsetOfFailed(subsetList, supersetList, missing, comparerName, message, subsetExpression, supersetExpression); + } + } + + private static void IsNotSubsetOfImpl(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer, string? comparerName, string? message, string subsetExpression, string supersetExpression) + { + List subsetList = subset is List sl ? sl : [.. subset]; + List supersetList = superset is List spl ? spl : [.. superset]; + + if (!TryFindMissingElements(subsetList, supersetList, comparer, out _)) + { + ReportAssertIsNotSubsetOfFailed(subsetList, supersetList, comparerName, message, subsetExpression, supersetExpression); + } + } + + /// + /// Determines whether contains any element not present + /// (with sufficient multiplicity) in . + /// + /// + /// if at least one element is missing — in which case + /// holds the excess elements (in their first-seen order) — and when every + /// element of is matched in . + /// + private static bool TryFindMissingElements(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer, [NotNullWhen(true)] out List? missing) + { +#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. + Dictionary subsetCounts = CountElements(subset, comparer, out int subsetNulls); + Dictionary supersetCounts = CountElements(superset, comparer, out int supersetNulls); +#pragma warning restore CS8714 + + missing = null; + + if (subsetNulls > supersetNulls) + { + missing = []; + for (int i = 0; i < subsetNulls - supersetNulls; i++) + { + missing.Add(default); + } + } + + foreach (KeyValuePair entry in subsetCounts) + { + supersetCounts.TryGetValue(entry.Key, out int supersetCount); + if (entry.Value > supersetCount) + { + missing ??= []; + int excess = entry.Value - supersetCount; + for (int i = 0; i < excess; i++) + { + missing.Add(entry.Key); + } + } + } + + return missing is not null; + } + +#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 Dictionary CountElements(IEnumerable collection, IEqualityComparer comparer, out int nullCount) + { +#pragma warning disable IDE0028 // Collection initialization can be simplified - target-typed new with constructor argument is preferred over collection expression here + Dictionary counts = new(comparer); +#pragma warning restore IDE0028 + nullCount = 0; + foreach (T? element in collection) + { + if (element is null) + { + nullCount++; + continue; + } + + counts.TryGetValue(element, out int count); + counts[element] = count + 1; + } + + return counts; + } +#pragma warning restore CS8714 + + [DoesNotReturn] + private static void ReportAssertIsSubsetOfFailed(IEnumerable subset, IEnumerable superset, List missing, string? comparerName, string? message, string subsetExpression, string supersetExpression) + { + string subsetText = AssertionValueRenderer.RenderValue(subset); + string supersetText = AssertionValueRenderer.RenderValue(superset); + string missingText = AssertionValueRenderer.RenderValue(missing); + + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("missing:", missingText) + .AddLine("subset:", subsetText) + .AddLine("superset:", supersetText); + + if (comparerName is not null) + { + evidence.AddLine("comparer:", comparerName); + } + + StructuredAssertionMessage structured = new(FrameworkMessages.IsSubsetOfFailedSummary); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(supersetText, subsetText); + structured.WithCallSiteExpression(BuildCallSiteWithComparer("Assert.IsSubsetOf", subsetExpression, supersetExpression, comparerName is not null)); + + ReportAssertFailed(structured); + } + + [DoesNotReturn] + private static void ReportAssertIsNotSubsetOfFailed(IEnumerable subset, IEnumerable superset, string? comparerName, string? message, string subsetExpression, string supersetExpression) + { + string subsetText = AssertionValueRenderer.RenderValue(subset); + string supersetText = AssertionValueRenderer.RenderValue(superset); + + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("subset:", subsetText) + .AddLine("superset:", supersetText); + + if (comparerName is not null) + { + evidence.AddLine("comparer:", comparerName); + } + + StructuredAssertionMessage structured = new(FrameworkMessages.IsNotSubsetOfFailedSummary); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(expectedText: null, actualText: subsetText); + structured.WithCallSiteExpression(BuildCallSiteWithComparer("Assert.IsNotSubsetOf", subsetExpression, supersetExpression, comparerName is not null)); + + ReportAssertFailed(structured); + } + + private static string? BuildCallSiteWithComparer(string assertionMethodName, string subsetExpression, string supersetExpression, bool hasComparer) + { + string? callSite = FormatCallSiteExpression(assertionMethodName, subsetExpression, supersetExpression, "", ""); + if (callSite is null || !hasComparer) + { + return callSite; + } + + // FormatCallSiteExpression has no overload accepting a third argument expression; insert + // the placeholder so the rendered call-site reflects the overload that was actually invoked. + return string.Concat(callSite.Substring(0, callSite.Length - 1), ", )"); + } + + private sealed class NonGenericEqualityComparerAdapter : IEqualityComparer + { + private readonly IEqualityComparer _comparer; + + public NonGenericEqualityComparerAdapter(IEqualityComparer comparer) + => _comparer = comparer; + + public new bool Equals(object? x, object? y) => _comparer.Equals(x, y); + + public int GetHashCode(object? obj) => obj is null ? 0 : _comparer.GetHashCode(obj); + } +} diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index cc143fac2e..bb48daf596 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -2,3 +2,11 @@ [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? +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotSubsetOf(System.Collections.IEnumerable? subset, System.Collections.IEnumerable? superset, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotSubsetOf(System.Collections.IEnumerable? subset, System.Collections.IEnumerable? superset, System.Collections.IEqualityComparer? comparer, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotSubsetOf(System.Collections.Generic.IEnumerable? subset, System.Collections.Generic.IEnumerable? superset, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotSubsetOf(System.Collections.Generic.IEnumerable? subset, System.Collections.Generic.IEnumerable? superset, System.Collections.Generic.IEqualityComparer? comparer, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsSubsetOf(System.Collections.IEnumerable? subset, System.Collections.IEnumerable? superset, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsSubsetOf(System.Collections.IEnumerable? subset, System.Collections.IEnumerable? superset, System.Collections.IEqualityComparer? comparer, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsSubsetOf(System.Collections.Generic.IEnumerable? subset, System.Collections.Generic.IEnumerable? superset, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsSubsetOf(System.Collections.Generic.IEnumerable? subset, System.Collections.Generic.IEnumerable? superset, System.Collections.Generic.IEqualityComparer? comparer, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 691f3edab7..0669eccf29 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -420,4 +420,10 @@ Actual: {2} Expected value to not be null. + + Expected collection to be a subset of the specified superset. + + + Expected collection to not be a subset of the specified superset. + diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index 4ad5deac23..eeaa5f395f 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -327,11 +327,21 @@ Skutečnost: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index f61ee46c58..e1db49343c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -327,11 +327,21 @@ Tatsächlich: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 94a5797fc0..a9ffe9d54d 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -327,11 +327,21 @@ Real: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 00d78302f3..bb89b3a730 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -327,11 +327,21 @@ Réel : {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 80b0b1b715..3b864ae6a6 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -327,11 +327,21 @@ Effettivo: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index ee5344f86b..c501d5081c 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -327,11 +327,21 @@ Actual: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index e1d64ff123..8ba9b7cb49 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -327,11 +327,21 @@ Actual: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index b09f5eb893..6e03188099 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -327,11 +327,21 @@ Rzeczywiste: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index 3fdabfe37d..3ffc9ec097 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -327,11 +327,21 @@ Real: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 8262985ee7..30d04f198b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -327,11 +327,21 @@ Actual: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 8420e144a3..17518be3cb 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -327,11 +327,21 @@ Gerçekte olan: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 0b7558f104..fbd2c2d53b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -327,11 +327,21 @@ Actual: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index cc658d8057..92b37b7727 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -327,11 +327,21 @@ Actual: {2} Expected value to not be null. + + Expected collection to not be a subset of the specified superset. + Expected collection to not be a subset of the specified superset. + + Expected value to be null. Expected value to be null. + + Expected collection to be a subset of the specified superset. + Expected collection to be a subset of the specified superset. + + Expected condition to be true. Expected condition to be true. diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs new file mode 100644 index 0000000000..5aa156e9d9 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs @@ -0,0 +1,351 @@ +// 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.Collections; + +using AwesomeAssertions; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public partial class AssertTests : TestContainer +{ + public void IsSubsetOf_Generic_AllPresent_ShouldPass() + => Assert.IsSubsetOf(new[] { 1, 2 }, new[] { 1, 2, 3 }); + + public void IsSubsetOf_Generic_EmptySubset_ShouldPass() + => Assert.IsSubsetOf(Array.Empty(), new[] { 1, 2, 3 }); + + public void IsSubsetOf_Generic_DuplicatesWithinSupersetMultiplicity_ShouldPass() + => Assert.IsSubsetOf(new[] { 1, 1, 2 }, new[] { 1, 1, 2, 3 }); + + public void IsSubsetOf_Generic_DuplicatesExceedSupersetMultiplicity_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new[] { 1, 1, 1 }, new[] { 1, 1 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + + missing: [1] + subset: [1, 1, 1] + superset: [1, 1] + + Assert.IsSubsetOf(new[] { 1, 1, 1 }, new[] { 1, 1 }) + """); + } + + public void IsSubsetOf_Generic_MissingElement_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + + missing: [3] + subset: [1, 2, 3] + superset: [1, 2] + + Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }) + """); + } + + public void IsSubsetOf_Generic_NullInSubsetButNotInSuperset_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new string?[] { "a", null }, new string?[] { "a" }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + + missing: [null] + subset: ["a", null] + superset: ["a"] + + Assert.IsSubsetOf(new string?[] { "a", null }, new string?[] { "a" }) + """); + } + + public void IsSubsetOf_Generic_StringMessage_MissingElement_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }, "User-provided message"); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + User-provided message + + missing: [3] + subset: [1, 2, 3] + superset: [1, 2] + + Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }) + """); + } + + public void IsSubsetOf_Generic_WithComparer_AllPresent_ShouldPass() + => Assert.IsSubsetOf(new[] { "A" }, new[] { "a", "b" }, StringComparer.OrdinalIgnoreCase); + + public void IsSubsetOf_Generic_WithComparer_MissingElement_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, StringComparer.OrdinalIgnoreCase); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + + missing: ["C"] + subset: ["A", "C"] + superset: ["a", "b"] + comparer: OrdinalIgnoreCaseComparer + + Assert.IsSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, ) + """); + } + + public void IsSubsetOf_Generic_NullSubset_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(null, new[] { 1, 2 }); + action.Should().Throw() + .WithMessage("Assert.IsSubsetOf failed. The parameter 'subset' is invalid. The value cannot be null."); + } + + public void IsSubsetOf_Generic_NullSuperset_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new[] { 1, 2 }, null); + action.Should().Throw() + .WithMessage("Assert.IsSubsetOf failed. The parameter 'superset' is invalid. The value cannot be null."); + } + + public void IsSubsetOf_Generic_NullComparer_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new[] { 1 }, new[] { 1 }, (IEqualityComparer?)null); + action.Should().Throw() + .WithMessage("Assert.IsSubsetOf failed. The parameter 'comparer' is invalid. The value cannot be null."); + } + + public void IsSubsetOf_NonGeneric_AllPresent_ShouldPass() + => Assert.IsSubsetOf(new ArrayList { 1, 2 }, new ArrayList { 1, 2, 3 }); + + public void IsSubsetOf_NonGeneric_MissingElement_ShouldFail() + { + IEnumerable subset = new ArrayList { 1, 2, 3 }; + IEnumerable superset = new ArrayList { 1, 2 }; + Action action = () => Assert.IsSubsetOf(subset, superset); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + + missing: [3] + subset: [1, 2, 3] + superset: [1, 2] + + Assert.IsSubsetOf(subset, superset) + """); + } + + public void IsSubsetOf_NonGeneric_WithComparer_MissingElement_ShouldFail() + { + IEnumerable subset = new ArrayList { "A", "C" }; + IEnumerable superset = new ArrayList { "a", "b" }; + Action action = () => Assert.IsSubsetOf(subset, superset, StringComparer.OrdinalIgnoreCase); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + + missing: ["C"] + subset: ["A", "C"] + superset: ["a", "b"] + comparer: OrdinalIgnoreCaseComparer + + Assert.IsSubsetOf(subset, superset, ) + """); + } + + public void IsNotSubsetOf_Generic_MissingElement_ShouldPass() + => Assert.IsNotSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }); + + public void IsNotSubsetOf_Generic_AllPresent_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(new[] { 1, 2 }, new[] { 1, 2, 3 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not be a subset of the specified superset. + + subset: [1, 2] + superset: [1, 2, 3] + + Assert.IsNotSubsetOf(new[] { 1, 2 }, new[] { 1, 2, 3 }) + """); + } + + public void IsNotSubsetOf_Generic_StringMessage_AllPresent_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(new[] { 1 }, new[] { 1, 2 }, "User-provided message"); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not be a subset of the specified superset. + User-provided message + + subset: [1] + superset: [1, 2] + + Assert.IsNotSubsetOf(new[] { 1 }, new[] { 1, 2 }) + """); + } + + public void IsNotSubsetOf_Generic_WithComparer_MissingElement_ShouldPass() + => Assert.IsNotSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, StringComparer.OrdinalIgnoreCase); + + public void IsNotSubsetOf_Generic_WithComparer_AllPresent_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(new[] { "A" }, new[] { "a", "b" }, StringComparer.OrdinalIgnoreCase); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not be a subset of the specified superset. + + subset: ["A"] + superset: ["a", "b"] + comparer: OrdinalIgnoreCaseComparer + + Assert.IsNotSubsetOf(new[] { "A" }, new[] { "a", "b" }, ) + """); + } + + public void IsNotSubsetOf_Generic_NullSubset_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(null, new[] { 1, 2 }); + action.Should().Throw() + .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'subset' is invalid. The value cannot be null."); + } + + public void IsNotSubsetOf_Generic_NullSuperset_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(new[] { 1, 2 }, null); + action.Should().Throw() + .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'superset' is invalid. The value cannot be null."); + } + + public void IsNotSubsetOf_Generic_NullComparer_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(new[] { 1 }, new[] { 1 }, (IEqualityComparer?)null); + action.Should().Throw() + .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'comparer' is invalid. The value cannot be null."); + } + + public void IsSubsetOf_NonGeneric_NullSubset_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(null, new ArrayList { 1, 2 }); + action.Should().Throw() + .WithMessage("Assert.IsSubsetOf failed. The parameter 'subset' is invalid. The value cannot be null."); + } + + public void IsSubsetOf_NonGeneric_NullSuperset_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new ArrayList { 1 }, null); + action.Should().Throw() + .WithMessage("Assert.IsSubsetOf failed. The parameter 'superset' is invalid. The value cannot be null."); + } + + public void IsSubsetOf_NonGeneric_NullComparer_ShouldFail() + { + Action action = () => Assert.IsSubsetOf(new ArrayList { 1 }, new ArrayList { 1 }, (IEqualityComparer?)null); + action.Should().Throw() + .WithMessage("Assert.IsSubsetOf failed. The parameter 'comparer' is invalid. The value cannot be null."); + } + + public void IsNotSubsetOf_NonGeneric_NullSubset_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(null, new ArrayList { 1, 2 }); + action.Should().Throw() + .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'subset' is invalid. The value cannot be null."); + } + + public void IsNotSubsetOf_NonGeneric_NullSuperset_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(new ArrayList { 1 }, null); + action.Should().Throw() + .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'superset' is invalid. The value cannot be null."); + } + + public void IsNotSubsetOf_NonGeneric_NullComparer_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(new ArrayList { 1 }, new ArrayList { 1 }, (IEqualityComparer?)null); + action.Should().Throw() + .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'comparer' is invalid. The value cannot be null."); + } + + public void IsNotSubsetOf_Generic_BothEmpty_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(Array.Empty(), Array.Empty()); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not be a subset of the specified superset. + + subset: [] + superset: [] + + Assert.IsNotSubsetOf(Array.Empty(), Array.Empty()) + """); + } + + public void IsNotSubsetOf_Generic_EmptySubset_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(Array.Empty(), new[] { 1 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not be a subset of the specified superset. + + subset: [] + superset: [1] + + Assert.IsNotSubsetOf(Array.Empty(), new[] { 1 }) + """); + } + + public void IsNotSubsetOf_NonGeneric_WithComparer_AllPresent_ShouldFail() + { + IEnumerable subset = new ArrayList { "A" }; + IEnumerable superset = new ArrayList { "a", "b" }; + Action action = () => Assert.IsNotSubsetOf(subset, superset, StringComparer.OrdinalIgnoreCase); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not be a subset of the specified superset. + + subset: ["A"] + superset: ["a", "b"] + comparer: OrdinalIgnoreCaseComparer + + Assert.IsNotSubsetOf(subset, superset, ) + """); + } + + public void IsSubsetOf_Generic_NullInSupersetButNotInSubset_ShouldPass() + => Assert.IsSubsetOf(new string?[] { "a" }, new string?[] { "a", null }); + + public void IsSubsetOf_AssertFailedException_PopulatesExpectedAndActual() + { + Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }); + AssertFailedException ex = action.Should().Throw().Which; + ex.ExpectedText.Should().Be("[1, 2]"); + ex.ActualText.Should().Be("[1, 2, 3]"); + } + + public void IsNotSubsetOf_AssertFailedException_PopulatesActualOnly() + { + Action action = () => Assert.IsNotSubsetOf(new[] { 1 }, new[] { 1, 2 }); + AssertFailedException ex = action.Should().Throw().Which; + ex.ExpectedText.Should().BeNull(); + ex.ActualText.Should().Be("[1]"); + } +} From 02ad596f4cd8998c2b967f2e1d7bf66a9e687905 Mon Sep 17 00:00:00 2001 From: Evangelink <11340282+Evangelink@users.noreply.github.com> Date: Fri, 15 May 2026 11:39:46 +0200 Subject: [PATCH 2/8] Address PR review feedback - Add 3-expression overload of FormatCallSiteExpression and use it in BuildCallSiteWithComparer (no more brittle string surgery on the trailing parenthesis). - Rewrite TryFindMissingElements to walk the subset in source order, so 'missing' is in true first-seen order with multiplicity (no longer relies on Dictionary enumeration order). - Add an inline comment on NonGenericEqualityComparerAdapter.Equals explaining why 'new' is required. - Use a custom CaseInsensitiveStringComparer in tests so the comparer name is stable across .NET Framework and .NET (StringComparer.OrdinalIgnoreCase reports OrdinalComparer on netfx). - Add IsSubsetOf_NonGeneric_WithComparer_AllPresent_ShouldPass and IsNotSubsetOf_NonGeneric_WithComparer_MissingElement_ShouldPass happy-path tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/Assert.IsSubsetOf.cs | 59 ++++++++++--------- .../TestFramework/Assertions/Assert.cs | 23 ++++++++ .../Assertions/AssertTests.IsSubsetOf.cs | 37 ++++++++---- 3 files changed, 81 insertions(+), 38 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs index de9f857036..c5851fc4f4 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs @@ -330,38 +330,46 @@ private static void IsNotSubsetOfImpl(IEnumerable subset, IEnumerable /// /// /// if at least one element is missing — in which case - /// holds the excess elements (in their first-seen order) — and when every - /// element of is matched in . + /// holds the excess elements (in their first-seen order in ) — and + /// when every element of is matched in + /// . /// private static bool TryFindMissingElements(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer, [NotNullWhen(true)] out List? missing) { #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. - Dictionary subsetCounts = CountElements(subset, comparer, out int subsetNulls); Dictionary supersetCounts = CountElements(superset, comparer, out int supersetNulls); #pragma warning restore CS8714 missing = null; - if (subsetNulls > supersetNulls) + // Walk the subset in source order so excess elements appear in first-seen positional order + // (with multiplicity preserved). For each element, decrement its remaining quota in the superset; + // when the quota reaches zero, additional occurrences are reported as missing. + foreach (T? element in subset) { - missing = []; - for (int i = 0; i < subsetNulls - supersetNulls; i++) + if (element is null) { - missing.Add(default); + if (supersetNulls > 0) + { + supersetNulls--; + } + else + { + missing ??= []; + missing.Add(default); + } + + continue; } - } - foreach (KeyValuePair entry in subsetCounts) - { - supersetCounts.TryGetValue(entry.Key, out int supersetCount); - if (entry.Value > supersetCount) + if (supersetCounts.TryGetValue(element, out int remaining) && remaining > 0) + { + supersetCounts[element] = remaining - 1; + } + else { missing ??= []; - int excess = entry.Value - supersetCount; - for (int i = 0; i < excess; i++) - { - missing.Add(entry.Key); - } + missing.Add(element); } } @@ -442,17 +450,9 @@ private static void ReportAssertIsNotSubsetOfFailed(IEnumerable subset, I } private static string? BuildCallSiteWithComparer(string assertionMethodName, string subsetExpression, string supersetExpression, bool hasComparer) - { - string? callSite = FormatCallSiteExpression(assertionMethodName, subsetExpression, supersetExpression, "", ""); - if (callSite is null || !hasComparer) - { - return callSite; - } - - // FormatCallSiteExpression has no overload accepting a third argument expression; insert - // the placeholder so the rendered call-site reflects the overload that was actually invoked. - return string.Concat(callSite.Substring(0, callSite.Length - 1), ", )"); - } + => hasComparer + ? FormatCallSiteExpression(assertionMethodName, subsetExpression, supersetExpression, expression3: string.Empty, "", "", "") + : FormatCallSiteExpression(assertionMethodName, subsetExpression, supersetExpression, "", ""); private sealed class NonGenericEqualityComparerAdapter : IEqualityComparer { @@ -461,6 +461,9 @@ private sealed class NonGenericEqualityComparerAdapter : IEqualityComparer _comparer = comparer; + // The 'new' modifier suppresses CS0108: this instance method intentionally hides the + // static 'object.Equals(object?, object?)' (only sharing its name/signature) to satisfy + // the IEqualityComparer.Equals contract. There is nothing to override. public new bool Equals(object? x, object? y) => _comparer.Equals(x, y); public int GetHashCode(object? obj) => obj is null ? 0 : _comparer.GetHashCode(obj); diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index 099aa683f4..c4ca3465ef 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.cs @@ -213,6 +213,29 @@ internal static void ThrowAssertFailed(StructuredAssertionMessage structuredMess return $"{assertionMethodName}({arg1}, {arg2})"; } + /// + /// Formats a call-site expression for display at the bottom of a structured assertion message, + /// using three captured expressions. Multiline (or empty/whitespace) expressions are replaced with the + /// supplied placeholders. Only when all three expressions are empty/whitespace is the entire call-site + /// line suppressed. + /// + internal static string? FormatCallSiteExpression(string assertionMethodName, string expression1, string expression2, string expression3, string placeholder1 = "", string placeholder2 = "", string placeholder3 = "") + { + bool empty1 = string.IsNullOrWhiteSpace(expression1); + bool empty2 = string.IsNullOrWhiteSpace(expression2); + bool empty3 = string.IsNullOrWhiteSpace(expression3); + if (empty1 && empty2 && empty3) + { + return null; + } + + string arg1 = empty1 || IsMultiline(expression1) ? NormalizeCallSitePlaceholder(placeholder1) : expression1; + string arg2 = empty2 || IsMultiline(expression2) ? NormalizeCallSitePlaceholder(placeholder2) : expression2; + string arg3 = empty3 || IsMultiline(expression3) ? NormalizeCallSitePlaceholder(placeholder3) : expression3; + + return $"{assertionMethodName}({arg1}, {arg2}, {arg3})"; + } + // string.Contains(char) is not available on netstandard2.0 / net462, so use IndexOf to check for newline characters. private static bool IsMultiline(string expression) => expression.IndexOf('\n') >= 0 || expression.IndexOf('\r') >= 0; diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs index 5aa156e9d9..f5b1ba3f10 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs @@ -86,11 +86,11 @@ Assertion failed. Expected collection to be a subset of the specified superset. } public void IsSubsetOf_Generic_WithComparer_AllPresent_ShouldPass() - => Assert.IsSubsetOf(new[] { "A" }, new[] { "a", "b" }, StringComparer.OrdinalIgnoreCase); + => Assert.IsSubsetOf(new[] { "A" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); public void IsSubsetOf_Generic_WithComparer_MissingElement_ShouldFail() { - Action action = () => Assert.IsSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, StringComparer.OrdinalIgnoreCase); + Action action = () => Assert.IsSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); action.Should().Throw() .WithMessage( """ @@ -99,7 +99,7 @@ Assertion failed. Expected collection to be a subset of the specified superset. missing: ["C"] subset: ["A", "C"] superset: ["a", "b"] - comparer: OrdinalIgnoreCaseComparer + comparer: CaseInsensitiveStringComparer Assert.IsSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, ) """); @@ -129,6 +129,12 @@ public void IsSubsetOf_Generic_NullComparer_ShouldFail() public void IsSubsetOf_NonGeneric_AllPresent_ShouldPass() => Assert.IsSubsetOf(new ArrayList { 1, 2 }, new ArrayList { 1, 2, 3 }); + public void IsSubsetOf_NonGeneric_WithComparer_AllPresent_ShouldPass() + => Assert.IsSubsetOf(new ArrayList { "A", "b" }, new ArrayList { "a", "B", "c" }, new CaseInsensitiveStringComparer()); + + public void IsNotSubsetOf_NonGeneric_WithComparer_MissingElement_ShouldPass() + => Assert.IsNotSubsetOf(new ArrayList { "A", "C" }, new ArrayList { "a", "b" }, new CaseInsensitiveStringComparer()); + public void IsSubsetOf_NonGeneric_MissingElement_ShouldFail() { IEnumerable subset = new ArrayList { 1, 2, 3 }; @@ -151,7 +157,7 @@ public void IsSubsetOf_NonGeneric_WithComparer_MissingElement_ShouldFail() { IEnumerable subset = new ArrayList { "A", "C" }; IEnumerable superset = new ArrayList { "a", "b" }; - Action action = () => Assert.IsSubsetOf(subset, superset, StringComparer.OrdinalIgnoreCase); + Action action = () => Assert.IsSubsetOf(subset, superset, new CaseInsensitiveStringComparer()); action.Should().Throw() .WithMessage( """ @@ -160,7 +166,7 @@ Assertion failed. Expected collection to be a subset of the specified superset. missing: ["C"] subset: ["A", "C"] superset: ["a", "b"] - comparer: OrdinalIgnoreCaseComparer + comparer: CaseInsensitiveStringComparer Assert.IsSubsetOf(subset, superset, ) """); @@ -201,11 +207,11 @@ Assertion failed. Expected collection to not be a subset of the specified supers } public void IsNotSubsetOf_Generic_WithComparer_MissingElement_ShouldPass() - => Assert.IsNotSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, StringComparer.OrdinalIgnoreCase); + => Assert.IsNotSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); public void IsNotSubsetOf_Generic_WithComparer_AllPresent_ShouldFail() { - Action action = () => Assert.IsNotSubsetOf(new[] { "A" }, new[] { "a", "b" }, StringComparer.OrdinalIgnoreCase); + Action action = () => Assert.IsNotSubsetOf(new[] { "A" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); action.Should().Throw() .WithMessage( """ @@ -213,7 +219,7 @@ Assertion failed. Expected collection to not be a subset of the specified supers subset: ["A"] superset: ["a", "b"] - comparer: OrdinalIgnoreCaseComparer + comparer: CaseInsensitiveStringComparer Assert.IsNotSubsetOf(new[] { "A" }, new[] { "a", "b" }, ) """); @@ -316,7 +322,7 @@ public void IsNotSubsetOf_NonGeneric_WithComparer_AllPresent_ShouldFail() { IEnumerable subset = new ArrayList { "A" }; IEnumerable superset = new ArrayList { "a", "b" }; - Action action = () => Assert.IsNotSubsetOf(subset, superset, StringComparer.OrdinalIgnoreCase); + Action action = () => Assert.IsNotSubsetOf(subset, superset, new CaseInsensitiveStringComparer()); action.Should().Throw() .WithMessage( """ @@ -324,7 +330,7 @@ Assertion failed. Expected collection to not be a subset of the specified supers subset: ["A"] superset: ["a", "b"] - comparer: OrdinalIgnoreCaseComparer + comparer: CaseInsensitiveStringComparer Assert.IsNotSubsetOf(subset, superset, ) """); @@ -348,4 +354,15 @@ public void IsNotSubsetOf_AssertFailedException_PopulatesActualOnly() ex.ExpectedText.Should().BeNull(); ex.ActualText.Should().Be("[1]"); } + + private sealed class CaseInsensitiveStringComparer : IEqualityComparer, IEqualityComparer + { + public bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + + public int GetHashCode(string obj) => StringComparer.OrdinalIgnoreCase.GetHashCode(obj); + + bool IEqualityComparer.Equals(object? x, object? y) => StringComparer.OrdinalIgnoreCase.Equals(x as string, y as string); + + public int GetHashCode(object obj) => obj is string s ? StringComparer.OrdinalIgnoreCase.GetHashCode(s) : 0; + } } From bdaecd6d43c73d4c45b2af8b1364628f259c9fc4 Mon Sep 17 00:00:00 2001 From: Evangelink <11340282+Evangelink@users.noreply.github.com> Date: Fri, 15 May 2026 11:57:18 +0200 Subject: [PATCH 3/8] Address expert reviewer iteration 1 feedback - Clarify multiplicity semantics in XML docs of all 8 IsSubsetOf/IsNotSubsetOf overloads. - Add HasAnyMissingElement fast path used by IsNotSubsetOfImpl that short-circuits on the first uncovered element and avoids allocating the missing-elements list. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/Assert.IsSubsetOf.cs | 138 +++++++++++++----- 1 file changed, 105 insertions(+), 33 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs index c5851fc4f4..6a28539efe 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs @@ -15,9 +15,12 @@ public sealed partial class Assert #region IsSubsetOf /// - /// Tests whether one collection is a subset of another collection and throws an - /// exception if any element in the subset is not also in the superset. + /// Tests whether one collection is a subset of another collection (with multiplicity) + /// and throws an exception if any element in the subset occurs more times than in the superset. /// + /// + /// Element multiplicity is significant: [1, 1] is not a subset of [1]. + /// /// The type of the collection items. /// /// The collection expected to be a subset of . @@ -27,7 +30,7 @@ public sealed partial class Assert /// /// /// The message to include in the exception when an element in - /// is not found in . The message is shown in test results. + /// is not found (with sufficient multiplicity) in . The message is shown in test results. /// /// /// The syntactic expression of subset as given by the compiler via caller argument expression. @@ -38,7 +41,8 @@ public sealed partial class Assert /// Users shouldn't pass a value for this parameter. /// /// - /// Thrown if contains at least one element not contained in . + /// Thrown if contains at least one element that occurs more times + /// than in . /// public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") { @@ -48,9 +52,12 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEn } /// - /// Tests whether one collection is a subset of another collection and throws an - /// exception if any element in the subset is not also in the superset. + /// Tests whether one collection is a subset of another collection (with multiplicity) + /// and throws an exception if any element in the subset occurs more times than in the superset. /// + /// + /// Element multiplicity is significant: [1, 1] is not a subset of [1]. + /// /// The type of the collection items. /// /// The collection expected to be a subset of . @@ -63,7 +70,7 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEn /// /// /// The message to include in the exception when an element in - /// is not found in . The message is shown in test results. + /// is not found (with sufficient multiplicity) in . The message is shown in test results. /// /// /// The syntactic expression of subset as given by the compiler via caller argument expression. @@ -74,7 +81,8 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEn /// Users shouldn't pass a value for this parameter. /// /// - /// Thrown if contains at least one element not contained in . + /// Thrown if contains at least one element that occurs more times + /// than in . /// public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") { @@ -85,9 +93,12 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEn } /// - /// Tests whether one collection is a subset of another collection and throws an - /// exception if any element in the subset is not also in the superset. + /// Tests whether one collection is a subset of another collection (with multiplicity) + /// and throws an exception if any element in the subset occurs more times than in the superset. /// + /// + /// Element multiplicity is significant: [1, 1] is not a subset of [1]. + /// /// /// The collection expected to be a subset of . /// @@ -96,7 +107,7 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEn /// /// /// The message to include in the exception when an element in - /// is not found in . The message is shown in test results. + /// is not found (with sufficient multiplicity) in . The message is shown in test results. /// /// /// The syntactic expression of subset as given by the compiler via caller argument expression. @@ -107,7 +118,8 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEn /// Users shouldn't pass a value for this parameter. /// /// - /// Thrown if contains at least one element not contained in . + /// Thrown if contains at least one element that occurs more times + /// than in . /// public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") { @@ -118,9 +130,12 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerab } /// - /// Tests whether one collection is a subset of another collection and throws an - /// exception if any element in the subset is not also in the superset. + /// Tests whether one collection is a subset of another collection (with multiplicity) + /// and throws an exception if any element in the subset occurs more times than in the superset. /// + /// + /// Element multiplicity is significant: [1, 1] is not a subset of [1]. + /// /// /// The collection expected to be a subset of . /// @@ -132,7 +147,7 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerab /// /// /// The message to include in the exception when an element in - /// is not found in . The message is shown in test results. + /// is not found (with sufficient multiplicity) in . The message is shown in test results. /// /// /// The syntactic expression of subset as given by the compiler via caller argument expression. @@ -143,7 +158,8 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerab /// Users shouldn't pass a value for this parameter. /// /// - /// Thrown if contains at least one element not contained in . + /// Thrown if contains at least one element that occurs more times + /// than in . /// public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") { @@ -159,9 +175,13 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerab #region IsNotSubsetOf /// - /// Tests whether one collection is not a subset of another collection and throws - /// an exception if all elements in the subset are also in the superset. + /// Tests whether one collection is not a subset of another collection (with multiplicity) + /// and throws an exception if every element in the subset occurs at least as many times in the superset. /// + /// + /// Element multiplicity is significant: [1, 1] is considered not to be a subset of [1] + /// (so this assertion would pass for those inputs). + /// /// The type of the collection items. /// /// The collection expected not to be a subset of . @@ -171,7 +191,7 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerab /// /// /// The message to include in the exception when every element in - /// is also found in . The message is shown in test results. + /// is also found (with sufficient multiplicity) in . The message is shown in test results. /// /// /// The syntactic expression of subset as given by the compiler via caller argument expression. @@ -182,7 +202,7 @@ public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerab /// Users shouldn't pass a value for this parameter. /// /// - /// Thrown if all elements of are contained in . + /// Thrown if every element of occurs at least as many times in . /// public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") { @@ -192,9 +212,13 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] } /// - /// Tests whether one collection is not a subset of another collection and throws - /// an exception if all elements in the subset are also in the superset. + /// Tests whether one collection is not a subset of another collection (with multiplicity) + /// and throws an exception if every element in the subset occurs at least as many times in the superset. /// + /// + /// Element multiplicity is significant: [1, 1] is considered not to be a subset of [1] + /// (so this assertion would pass for those inputs). + /// /// The type of the collection items. /// /// The collection expected not to be a subset of . @@ -207,7 +231,7 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] /// /// /// The message to include in the exception when every element in - /// is also found in . The message is shown in test results. + /// is also found (with sufficient multiplicity) in . The message is shown in test results. /// /// /// The syntactic expression of subset as given by the compiler via caller argument expression. @@ -218,7 +242,7 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] /// Users shouldn't pass a value for this parameter. /// /// - /// Thrown if all elements of are contained in . + /// Thrown if every element of occurs at least as many times in . /// public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") { @@ -229,9 +253,13 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] } /// - /// Tests whether one collection is not a subset of another collection and throws - /// an exception if all elements in the subset are also in the superset. + /// Tests whether one collection is not a subset of another collection (with multiplicity) + /// and throws an exception if every element in the subset occurs at least as many times in the superset. /// + /// + /// Element multiplicity is significant: [1, 1] is considered not to be a subset of [1] + /// (so this assertion would pass for those inputs). + /// /// /// The collection expected not to be a subset of . /// @@ -240,7 +268,7 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] /// /// /// The message to include in the exception when every element in - /// is also found in . The message is shown in test results. + /// is also found (with sufficient multiplicity) in . The message is shown in test results. /// /// /// The syntactic expression of subset as given by the compiler via caller argument expression. @@ -251,7 +279,7 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] /// Users shouldn't pass a value for this parameter. /// /// - /// Thrown if all elements of are contained in . + /// Thrown if every element of occurs at least as many times in . /// public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") { @@ -262,9 +290,13 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnume } /// - /// Tests whether one collection is not a subset of another collection and throws - /// an exception if all elements in the subset are also in the superset. + /// Tests whether one collection is not a subset of another collection (with multiplicity) + /// and throws an exception if every element in the subset occurs at least as many times in the superset. /// + /// + /// Element multiplicity is significant: [1, 1] is considered not to be a subset of [1] + /// (so this assertion would pass for those inputs). + /// /// /// The collection expected not to be a subset of . /// @@ -276,7 +308,7 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnume /// /// /// The message to include in the exception when every element in - /// is also found in . The message is shown in test results. + /// is also found (with sufficient multiplicity) in . The message is shown in test results. /// /// /// The syntactic expression of subset as given by the compiler via caller argument expression. @@ -287,7 +319,7 @@ public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnume /// Users shouldn't pass a value for this parameter. /// /// - /// Thrown if all elements of are contained in . + /// Thrown if every element of occurs at least as many times in . /// public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") { @@ -318,7 +350,7 @@ private static void IsNotSubsetOfImpl(IEnumerable subset, IEnumerable List subsetList = subset is List sl ? sl : [.. subset]; List supersetList = superset is List spl ? spl : [.. superset]; - if (!TryFindMissingElements(subsetList, supersetList, comparer, out _)) + if (!HasAnyMissingElement(subsetList, supersetList, comparer)) { ReportAssertIsNotSubsetOfFailed(subsetList, supersetList, comparerName, message, subsetExpression, supersetExpression); } @@ -376,6 +408,46 @@ private static bool TryFindMissingElements(IEnumerable subset, IEnumerabl return missing is not null; } + /// + /// Fast-path equivalent of + /// for the IsNotSubsetOf path: short-circuits as soon as a single uncovered element is found + /// and avoids allocating a missing-elements list when only the boolean answer is needed. + /// + private static bool HasAnyMissingElement(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer) + { +#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. + Dictionary supersetCounts = CountElements(superset, comparer, out int supersetNulls); +#pragma warning restore CS8714 + + foreach (T? element in subset) + { + if (element is null) + { + if (supersetNulls > 0) + { + supersetNulls--; + } + else + { + return true; + } + + continue; + } + + if (supersetCounts.TryGetValue(element, out int remaining) && remaining > 0) + { + supersetCounts[element] = remaining - 1; + } + else + { + return true; + } + } + + return false; + } + #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 Dictionary CountElements(IEnumerable collection, IEqualityComparer comparer, out int nullCount) { From a997dfd6f6396cd6aa23b0ae01f7db1508a7eb3f Mon Sep 17 00:00:00 2001 From: Evangelink <11340282+Evangelink@users.noreply.github.com> Date: Fri, 15 May 2026 12:18:14 +0200 Subject: [PATCH 4/8] Address expert reviewer iteration 2 feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add IsNotSubsetOf multiplicity-contract tests (DuplicatesExceedSupersetMultiplicity_ShouldPass, DuplicatesWithinSupersetMultiplicity_ShouldFail) — directly guard the HasAnyMissingElement quota arithmetic. - Add multi-element 'missing:' rendering tests (MultipleMissing_PreservesFirstSeenOrderAndMultiplicity, ExcessInMiddleOfMatchingRun_ReportsOnlyExcess) to lock in the documented first-seen positional order and multiplicity preservation. - Tighten the HasAnyMissingElement summary comment to accurately describe what is actually short-circuited (the subset walk, not the O(|superset|) count-dictionary build). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/Assert.IsSubsetOf.cs | 7 +-- .../Assertions/AssertTests.IsSubsetOf.cs | 51 +++++++++++++++++++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs index 6a28539efe..195914d7d1 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs @@ -409,9 +409,10 @@ private static bool TryFindMissingElements(IEnumerable subset, IEnumerabl } /// - /// Fast-path equivalent of - /// for the IsNotSubsetOf path: short-circuits as soon as a single uncovered element is found - /// and avoids allocating a missing-elements list when only the boolean answer is needed. + /// Equivalent to + /// for the IsNotSubsetOf path, but skips the of excess elements + /// and exits the walk on the first uncovered element. The + /// O(|superset|) count-dictionary build is still performed up-front. /// private static bool HasAnyMissingElement(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer) { diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs index f5b1ba3f10..fca69b12c9 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs @@ -36,6 +36,57 @@ Assertion failed. Expected collection to be a subset of the specified superset. """); } + public void IsSubsetOf_Generic_MultipleMissing_PreservesFirstSeenOrderAndMultiplicity() + { + // Walk: 3 ✓, 1 ✓, 1 → missing(1), 4 → missing(4), 1 → missing(1) => [1, 4, 1] + Action action = () => Assert.IsSubsetOf(new[] { 3, 1, 1, 4, 1 }, new[] { 1, 2, 3 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + + missing: [1, 4, 1] + subset: [3, 1, 1, 4, 1] + superset: [1, 2, 3] + + Assert.IsSubsetOf(new[] { 3, 1, 1, 4, 1 }, new[] { 1, 2, 3 }) + """); + } + + public void IsSubsetOf_Generic_ExcessInMiddleOfMatchingRun_ReportsOnlyExcess() + { + Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 1, 3 }, new[] { 1, 2, 3 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to be a subset of the specified superset. + + missing: [1] + subset: [1, 2, 1, 3] + superset: [1, 2, 3] + + Assert.IsSubsetOf(new[] { 1, 2, 1, 3 }, new[] { 1, 2, 3 }) + """); + } + + public void IsNotSubsetOf_Generic_DuplicatesExceedSupersetMultiplicity_ShouldPass() + => Assert.IsNotSubsetOf(new[] { 1, 1 }, new[] { 1 }); + + public void IsNotSubsetOf_Generic_DuplicatesWithinSupersetMultiplicity_ShouldFail() + { + Action action = () => Assert.IsNotSubsetOf(new[] { 1, 1 }, new[] { 1, 1, 2 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not be a subset of the specified superset. + + subset: [1, 1] + superset: [1, 1, 2] + + Assert.IsNotSubsetOf(new[] { 1, 1 }, new[] { 1, 1, 2 }) + """); + } + public void IsSubsetOf_Generic_MissingElement_ShouldFail() { Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }); From 9c1ced50dfad9d42b9f60785536c0c9dd6e4941d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 15 May 2026 16:56:19 +0200 Subject: [PATCH 5/8] Rename Assert.IsSubsetOf/IsNotSubsetOf to ContainsAll/DoesNotContainAll Renames the recently-added Assert.IsSubsetOf and Assert.IsNotSubsetOf APIs to Assert.ContainsAll and Assert.DoesNotContainAll. Parameter rename: subset -> expected, superset -> collection. This matches the existing Assert.Contains(expected, collection) convention. Failure-message evidence labels updated accordingly: subset: -> expected:, superset: -> collection:. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/Assert.ContainsAll.cs | 552 ++++++++++++++++++ .../Assertions/Assert.IsSubsetOf.cs | 544 ----------------- .../PublicAPI/PublicAPI.Unshipped.txt | 16 +- .../Resources/FrameworkMessages.resx | 8 +- .../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 +- .../Assertions/AssertTests.ContainsAll.cs | 419 +++++++++++++ .../Assertions/AssertTests.IsSubsetOf.cs | 419 ------------- 19 files changed, 1113 insertions(+), 1105 deletions(-) create mode 100644 src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs delete mode 100644 src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs create mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs delete mode 100644 test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs b/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs new file mode 100644 index 0000000000..676aa6cd51 --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs @@ -0,0 +1,552 @@ +// 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; + +/// +/// A collection of helper classes to test various conditions within +/// unit tests. If the condition being tested is not met, an exception +/// is thrown. +/// +#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 ContainsAll + + /// + /// Tests whether contains every element of + /// (with multiplicity) and throws an exception if any element in occurs + /// more times than in . + /// + /// + /// Element multiplicity is significant: [1] does not contain all of [1, 1]. + /// + /// The type of the collection items. + /// + /// The collection of items expected to all be present in . + /// + /// + /// The collection expected to contain every item of . + /// + /// + /// The message to include in the exception when an element in + /// is not found (with sufficient multiplicity) in . The message is shown in test results. + /// + /// + /// The syntactic expression of expected as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if contains at least one element that occurs more times + /// than in . + /// + public static void ContainsAll([NotNull] IEnumerable? expected, [NotNull] IEnumerable? collection, string? message = "", [CallerArgumentExpression(nameof(expected))] string expectedExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + CheckParameterNotNull(expected, "Assert.ContainsAll", "expected"); + CheckParameterNotNull(collection, "Assert.ContainsAll", "collection"); + ContainsAllImpl(expected, collection, EqualityComparer.Default, comparerName: null, message, expectedExpression, collectionExpression); + } + + /// + /// Tests whether contains every element of + /// (with multiplicity) and throws an exception if any element in occurs + /// more times than in . + /// + /// + /// Element multiplicity is significant: [1] does not contain all of [1, 1]. + /// + /// The type of the collection items. + /// + /// The collection of items expected to all be present in . + /// + /// + /// The collection expected to contain every item of . + /// + /// + /// The equality comparer to use when comparing elements. + /// + /// + /// The message to include in the exception when an element in + /// is not found (with sufficient multiplicity) in . The message is shown in test results. + /// + /// + /// The syntactic expression of expected as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if contains at least one element that occurs more times + /// than in . + /// + public static void ContainsAll([NotNull] IEnumerable? expected, [NotNull] IEnumerable? collection, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(expected))] string expectedExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + CheckParameterNotNull(expected, "Assert.ContainsAll", "expected"); + CheckParameterNotNull(collection, "Assert.ContainsAll", "collection"); + CheckParameterNotNull(comparer, "Assert.ContainsAll", "comparer"); + ContainsAllImpl(expected, collection, comparer, comparer.GetType().Name, message, expectedExpression, collectionExpression); + } + + /// + /// Tests whether contains every element of + /// (with multiplicity) and throws an exception if any element in occurs + /// more times than in . + /// + /// + /// Element multiplicity is significant: [1] does not contain all of [1, 1]. + /// + /// + /// The collection of items expected to all be present in . + /// + /// + /// The collection expected to contain every item of . + /// + /// + /// The message to include in the exception when an element in + /// is not found (with sufficient multiplicity) in . The message is shown in test results. + /// + /// + /// The syntactic expression of expected as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if contains at least one element that occurs more times + /// than in . + /// + public static void ContainsAll([NotNull] IEnumerable? expected, [NotNull] IEnumerable? collection, string? message = "", [CallerArgumentExpression(nameof(expected))] string expectedExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + CheckParameterNotNull(expected, "Assert.ContainsAll", "expected"); + CheckParameterNotNull(collection, "Assert.ContainsAll", "collection"); + + ContainsAllImpl(expected.Cast(), collection.Cast(), EqualityComparer.Default, comparerName: null, message, expectedExpression, collectionExpression); + } + + /// + /// Tests whether contains every element of + /// (with multiplicity) and throws an exception if any element in occurs + /// more times than in . + /// + /// + /// Element multiplicity is significant: [1] does not contain all of [1, 1]. + /// + /// + /// The collection of items expected to all be present in . + /// + /// + /// The collection expected to contain every item of . + /// + /// + /// The equality comparer to use when comparing elements. + /// + /// + /// The message to include in the exception when an element in + /// is not found (with sufficient multiplicity) in . The message is shown in test results. + /// + /// + /// The syntactic expression of expected as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if contains at least one element that occurs more times + /// than in . + /// + public static void ContainsAll([NotNull] IEnumerable? expected, [NotNull] IEnumerable? collection, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(expected))] string expectedExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + CheckParameterNotNull(expected, "Assert.ContainsAll", "expected"); + CheckParameterNotNull(collection, "Assert.ContainsAll", "collection"); + CheckParameterNotNull(comparer, "Assert.ContainsAll", "comparer"); + + ContainsAllImpl(expected.Cast(), collection.Cast(), new NonGenericEqualityComparerAdapter(comparer), comparer.GetType().Name, message, expectedExpression, collectionExpression); + } + + #endregion // ContainsAll + + #region DoesNotContainAll + + /// + /// Tests whether does not contain every element of + /// (with multiplicity) and throws an exception if every element in occurs at + /// least as many times in . + /// + /// + /// Element multiplicity is significant: [1] is considered not to contain all of [1, 1] + /// (so this assertion would pass for those inputs). + /// + /// The type of the collection items. + /// + /// The collection of items expected not to all be present in . + /// + /// + /// The collection expected not to contain every item of . + /// + /// + /// The message to include in the exception when every element in + /// is also found (with sufficient multiplicity) in . The message is shown in test results. + /// + /// + /// The syntactic expression of expected as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if every element of occurs at least as many times in . + /// + public static void DoesNotContainAll([NotNull] IEnumerable? expected, [NotNull] IEnumerable? collection, string? message = "", [CallerArgumentExpression(nameof(expected))] string expectedExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + CheckParameterNotNull(expected, "Assert.DoesNotContainAll", "expected"); + CheckParameterNotNull(collection, "Assert.DoesNotContainAll", "collection"); + DoesNotContainAllImpl(expected, collection, EqualityComparer.Default, comparerName: null, message, expectedExpression, collectionExpression); + } + + /// + /// Tests whether does not contain every element of + /// (with multiplicity) and throws an exception if every element in occurs at + /// least as many times in . + /// + /// + /// Element multiplicity is significant: [1] is considered not to contain all of [1, 1] + /// (so this assertion would pass for those inputs). + /// + /// The type of the collection items. + /// + /// The collection of items expected not to all be present in . + /// + /// + /// The collection expected not to contain every item of . + /// + /// + /// The equality comparer to use when comparing elements. + /// + /// + /// The message to include in the exception when every element in + /// is also found (with sufficient multiplicity) in . The message is shown in test results. + /// + /// + /// The syntactic expression of expected as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if every element of occurs at least as many times in . + /// + public static void DoesNotContainAll([NotNull] IEnumerable? expected, [NotNull] IEnumerable? collection, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(expected))] string expectedExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + CheckParameterNotNull(expected, "Assert.DoesNotContainAll", "expected"); + CheckParameterNotNull(collection, "Assert.DoesNotContainAll", "collection"); + CheckParameterNotNull(comparer, "Assert.DoesNotContainAll", "comparer"); + DoesNotContainAllImpl(expected, collection, comparer, comparer.GetType().Name, message, expectedExpression, collectionExpression); + } + + /// + /// Tests whether does not contain every element of + /// (with multiplicity) and throws an exception if every element in occurs at + /// least as many times in . + /// + /// + /// Element multiplicity is significant: [1] is considered not to contain all of [1, 1] + /// (so this assertion would pass for those inputs). + /// + /// + /// The collection of items expected not to all be present in . + /// + /// + /// The collection expected not to contain every item of . + /// + /// + /// The message to include in the exception when every element in + /// is also found (with sufficient multiplicity) in . The message is shown in test results. + /// + /// + /// The syntactic expression of expected as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if every element of occurs at least as many times in . + /// + public static void DoesNotContainAll([NotNull] IEnumerable? expected, [NotNull] IEnumerable? collection, string? message = "", [CallerArgumentExpression(nameof(expected))] string expectedExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + CheckParameterNotNull(expected, "Assert.DoesNotContainAll", "expected"); + CheckParameterNotNull(collection, "Assert.DoesNotContainAll", "collection"); + + DoesNotContainAllImpl(expected.Cast(), collection.Cast(), EqualityComparer.Default, comparerName: null, message, expectedExpression, collectionExpression); + } + + /// + /// Tests whether does not contain every element of + /// (with multiplicity) and throws an exception if every element in occurs at + /// least as many times in . + /// + /// + /// Element multiplicity is significant: [1] is considered not to contain all of [1, 1] + /// (so this assertion would pass for those inputs). + /// + /// + /// The collection of items expected not to all be present in . + /// + /// + /// The collection expected not to contain every item of . + /// + /// + /// The equality comparer to use when comparing elements. + /// + /// + /// The message to include in the exception when every element in + /// is also found (with sufficient multiplicity) in . The message is shown in test results. + /// + /// + /// The syntactic expression of expected as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// The syntactic expression of collection as given by the compiler via caller argument expression. + /// Users shouldn't pass a value for this parameter. + /// + /// + /// Thrown if every element of occurs at least as many times in . + /// + public static void DoesNotContainAll([NotNull] IEnumerable? expected, [NotNull] IEnumerable? collection, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(expected))] string expectedExpression = "", [CallerArgumentExpression(nameof(collection))] string collectionExpression = "") + { + CheckParameterNotNull(expected, "Assert.DoesNotContainAll", "expected"); + CheckParameterNotNull(collection, "Assert.DoesNotContainAll", "collection"); + CheckParameterNotNull(comparer, "Assert.DoesNotContainAll", "comparer"); + + DoesNotContainAllImpl(expected.Cast(), collection.Cast(), new NonGenericEqualityComparerAdapter(comparer), comparer.GetType().Name, message, expectedExpression, collectionExpression); + } + + #endregion // DoesNotContainAll + + private static void ContainsAllImpl(IEnumerable expected, IEnumerable collection, IEqualityComparer comparer, string? comparerName, string? message, string expectedExpression, string collectionExpression) + { + // Snapshot once so we don't enumerate twice (counting + rendering on failure) + // and so lazy/single-pass enumerables behave deterministically. + List expectedList = expected is List el ? el : [.. expected]; + List collectionList = collection is List cl ? cl : [.. collection]; + + if (TryFindMissingElements(expectedList, collectionList, comparer, out List? missing)) + { + ReportAssertContainsAllFailed(expectedList, collectionList, missing, comparerName, message, expectedExpression, collectionExpression); + } + } + + private static void DoesNotContainAllImpl(IEnumerable expected, IEnumerable collection, IEqualityComparer comparer, string? comparerName, string? message, string expectedExpression, string collectionExpression) + { + List expectedList = expected is List el ? el : [.. expected]; + List collectionList = collection is List cl ? cl : [.. collection]; + + if (!HasAnyMissingElement(expectedList, collectionList, comparer)) + { + ReportAssertDoesNotContainAllFailed(expectedList, collectionList, comparerName, message, expectedExpression, collectionExpression); + } + } + + /// + /// Determines whether contains any element not present + /// (with sufficient multiplicity) in . + /// + /// + /// if at least one element is missing — in which case + /// holds the excess elements (in their first-seen order in ) — and + /// when every element of is matched in + /// . + /// + private static bool TryFindMissingElements(IEnumerable expected, IEnumerable collection, IEqualityComparer comparer, [NotNullWhen(true)] out List? missing) + { +#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. + Dictionary collectionCounts = CountElements(collection, comparer, out int collectionNulls); +#pragma warning restore CS8714 + + missing = null; + + // Walk the expected items in source order so excess elements appear in first-seen positional order + // (with multiplicity preserved). For each element, decrement its remaining quota in the collection; + // when the quota reaches zero, additional occurrences are reported as missing. + foreach (T? element in expected) + { + if (element is null) + { + if (collectionNulls > 0) + { + collectionNulls--; + } + else + { + missing ??= []; + missing.Add(default); + } + + continue; + } + + if (collectionCounts.TryGetValue(element, out int remaining) && remaining > 0) + { + collectionCounts[element] = remaining - 1; + } + else + { + missing ??= []; + missing.Add(element); + } + } + + return missing is not null; + } + + /// + /// Equivalent to + /// for the DoesNotContainAll path, but skips the of excess elements + /// and exits the walk on the first uncovered element. The + /// O(|collection|) count-dictionary build is still performed up-front. + /// + private static bool HasAnyMissingElement(IEnumerable expected, IEnumerable collection, IEqualityComparer comparer) + { +#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. + Dictionary collectionCounts = CountElements(collection, comparer, out int collectionNulls); +#pragma warning restore CS8714 + + foreach (T? element in expected) + { + if (element is null) + { + if (collectionNulls > 0) + { + collectionNulls--; + } + else + { + return true; + } + + continue; + } + + if (collectionCounts.TryGetValue(element, out int remaining) && remaining > 0) + { + collectionCounts[element] = remaining - 1; + } + else + { + return true; + } + } + + return false; + } + +#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 Dictionary CountElements(IEnumerable collection, IEqualityComparer comparer, out int nullCount) + { +#pragma warning disable IDE0028 // Collection initialization can be simplified - target-typed new with constructor argument is preferred over collection expression here + Dictionary counts = new(comparer); +#pragma warning restore IDE0028 + nullCount = 0; + foreach (T? element in collection) + { + if (element is null) + { + nullCount++; + continue; + } + + counts.TryGetValue(element, out int count); + counts[element] = count + 1; + } + + return counts; + } +#pragma warning restore CS8714 + + [DoesNotReturn] + private static void ReportAssertContainsAllFailed(IEnumerable expected, IEnumerable collection, List missing, string? comparerName, string? message, string expectedExpression, string collectionExpression) + { + string expectedText = AssertionValueRenderer.RenderValue(expected); + string collectionText = AssertionValueRenderer.RenderValue(collection); + string missingText = AssertionValueRenderer.RenderValue(missing); + + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("missing:", missingText) + .AddLine("expected:", expectedText) + .AddLine("collection:", collectionText); + + if (comparerName is not null) + { + evidence.AddLine("comparer:", comparerName); + } + + StructuredAssertionMessage structured = new(FrameworkMessages.ContainsAllFailedSummary); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(expectedText, collectionText); + structured.WithCallSiteExpression(BuildCallSiteWithComparer("Assert.ContainsAll", expectedExpression, collectionExpression, comparerName is not null)); + + ReportAssertFailed(structured); + } + + [DoesNotReturn] + private static void ReportAssertDoesNotContainAllFailed(IEnumerable expected, IEnumerable collection, string? comparerName, string? message, string expectedExpression, string collectionExpression) + { + string expectedText = AssertionValueRenderer.RenderValue(expected); + string collectionText = AssertionValueRenderer.RenderValue(collection); + + EvidenceBlock evidence = EvidenceBlock.Create() + .AddLine("expected:", expectedText) + .AddLine("collection:", collectionText); + + if (comparerName is not null) + { + evidence.AddLine("comparer:", comparerName); + } + + StructuredAssertionMessage structured = new(FrameworkMessages.DoesNotContainAllFailedSummary); + structured.WithUserMessage(message); + structured.WithEvidence(evidence); + structured.WithExpectedAndActual(expectedText: null, actualText: collectionText); + structured.WithCallSiteExpression(BuildCallSiteWithComparer("Assert.DoesNotContainAll", expectedExpression, collectionExpression, comparerName is not null)); + + ReportAssertFailed(structured); + } + + private static string? BuildCallSiteWithComparer(string assertionMethodName, string expectedExpression, string collectionExpression, bool hasComparer) + => hasComparer + ? FormatCallSiteExpression(assertionMethodName, expectedExpression, collectionExpression, expression3: string.Empty, "", "", "") + : FormatCallSiteExpression(assertionMethodName, expectedExpression, collectionExpression, "", ""); + + private sealed class NonGenericEqualityComparerAdapter : IEqualityComparer + { + private readonly IEqualityComparer _comparer; + + public NonGenericEqualityComparerAdapter(IEqualityComparer comparer) + => _comparer = comparer; + + // The 'new' modifier suppresses CS0108: this instance method intentionally hides the + // static 'object.Equals(object?, object?)' (only sharing its name/signature) to satisfy + // the IEqualityComparer.Equals contract. There is nothing to override. + public new bool Equals(object? x, object? y) => _comparer.Equals(x, y); + + public int GetHashCode(object? obj) => obj is null ? 0 : _comparer.GetHashCode(obj); + } +} diff --git a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs b/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs deleted file mode 100644 index 195914d7d1..0000000000 --- a/src/TestFramework/TestFramework/Assertions/Assert.IsSubsetOf.cs +++ /dev/null @@ -1,544 +0,0 @@ -// 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; - -/// -/// A collection of helper classes to test various conditions within -/// unit tests. If the condition being tested is not met, an exception -/// is thrown. -/// -#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 IsSubsetOf - - /// - /// Tests whether one collection is a subset of another collection (with multiplicity) - /// and throws an exception if any element in the subset occurs more times than in the superset. - /// - /// - /// Element multiplicity is significant: [1, 1] is not a subset of [1]. - /// - /// The type of the collection items. - /// - /// The collection expected to be a subset of . - /// - /// - /// The collection expected to be a superset of . - /// - /// - /// The message to include in the exception when an element in - /// is not found (with sufficient multiplicity) in . The message is shown in test results. - /// - /// - /// The syntactic expression of subset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// The syntactic expression of superset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// Thrown if contains at least one element that occurs more times - /// than in . - /// - public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") - { - CheckParameterNotNull(subset, "Assert.IsSubsetOf", "subset"); - CheckParameterNotNull(superset, "Assert.IsSubsetOf", "superset"); - IsSubsetOfImpl(subset, superset, EqualityComparer.Default, comparerName: null, message, subsetExpression, supersetExpression); - } - - /// - /// Tests whether one collection is a subset of another collection (with multiplicity) - /// and throws an exception if any element in the subset occurs more times than in the superset. - /// - /// - /// Element multiplicity is significant: [1, 1] is not a subset of [1]. - /// - /// The type of the collection items. - /// - /// The collection expected to be a subset of . - /// - /// - /// The collection expected to be a superset of . - /// - /// - /// The equality comparer to use when comparing elements. - /// - /// - /// The message to include in the exception when an element in - /// is not found (with sufficient multiplicity) in . The message is shown in test results. - /// - /// - /// The syntactic expression of subset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// The syntactic expression of superset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// Thrown if contains at least one element that occurs more times - /// than in . - /// - public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") - { - CheckParameterNotNull(subset, "Assert.IsSubsetOf", "subset"); - CheckParameterNotNull(superset, "Assert.IsSubsetOf", "superset"); - CheckParameterNotNull(comparer, "Assert.IsSubsetOf", "comparer"); - IsSubsetOfImpl(subset, superset, comparer, comparer.GetType().Name, message, subsetExpression, supersetExpression); - } - - /// - /// Tests whether one collection is a subset of another collection (with multiplicity) - /// and throws an exception if any element in the subset occurs more times than in the superset. - /// - /// - /// Element multiplicity is significant: [1, 1] is not a subset of [1]. - /// - /// - /// The collection expected to be a subset of . - /// - /// - /// The collection expected to be a superset of . - /// - /// - /// The message to include in the exception when an element in - /// is not found (with sufficient multiplicity) in . The message is shown in test results. - /// - /// - /// The syntactic expression of subset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// The syntactic expression of superset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// Thrown if contains at least one element that occurs more times - /// than in . - /// - public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") - { - CheckParameterNotNull(subset, "Assert.IsSubsetOf", "subset"); - CheckParameterNotNull(superset, "Assert.IsSubsetOf", "superset"); - - IsSubsetOfImpl(subset.Cast(), superset.Cast(), EqualityComparer.Default, comparerName: null, message, subsetExpression, supersetExpression); - } - - /// - /// Tests whether one collection is a subset of another collection (with multiplicity) - /// and throws an exception if any element in the subset occurs more times than in the superset. - /// - /// - /// Element multiplicity is significant: [1, 1] is not a subset of [1]. - /// - /// - /// The collection expected to be a subset of . - /// - /// - /// The collection expected to be a superset of . - /// - /// - /// The equality comparer to use when comparing elements. - /// - /// - /// The message to include in the exception when an element in - /// is not found (with sufficient multiplicity) in . The message is shown in test results. - /// - /// - /// The syntactic expression of subset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// The syntactic expression of superset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// Thrown if contains at least one element that occurs more times - /// than in . - /// - public static void IsSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") - { - CheckParameterNotNull(subset, "Assert.IsSubsetOf", "subset"); - CheckParameterNotNull(superset, "Assert.IsSubsetOf", "superset"); - CheckParameterNotNull(comparer, "Assert.IsSubsetOf", "comparer"); - - IsSubsetOfImpl(subset.Cast(), superset.Cast(), new NonGenericEqualityComparerAdapter(comparer), comparer.GetType().Name, message, subsetExpression, supersetExpression); - } - - #endregion // IsSubsetOf - - #region IsNotSubsetOf - - /// - /// Tests whether one collection is not a subset of another collection (with multiplicity) - /// and throws an exception if every element in the subset occurs at least as many times in the superset. - /// - /// - /// Element multiplicity is significant: [1, 1] is considered not to be a subset of [1] - /// (so this assertion would pass for those inputs). - /// - /// The type of the collection items. - /// - /// The collection expected not to be a subset of . - /// - /// - /// The collection expected not to be a superset of . - /// - /// - /// The message to include in the exception when every element in - /// is also found (with sufficient multiplicity) in . The message is shown in test results. - /// - /// - /// The syntactic expression of subset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// The syntactic expression of superset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// Thrown if every element of occurs at least as many times in . - /// - public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") - { - CheckParameterNotNull(subset, "Assert.IsNotSubsetOf", "subset"); - CheckParameterNotNull(superset, "Assert.IsNotSubsetOf", "superset"); - IsNotSubsetOfImpl(subset, superset, EqualityComparer.Default, comparerName: null, message, subsetExpression, supersetExpression); - } - - /// - /// Tests whether one collection is not a subset of another collection (with multiplicity) - /// and throws an exception if every element in the subset occurs at least as many times in the superset. - /// - /// - /// Element multiplicity is significant: [1, 1] is considered not to be a subset of [1] - /// (so this assertion would pass for those inputs). - /// - /// The type of the collection items. - /// - /// The collection expected not to be a subset of . - /// - /// - /// The collection expected not to be a superset of . - /// - /// - /// The equality comparer to use when comparing elements. - /// - /// - /// The message to include in the exception when every element in - /// is also found (with sufficient multiplicity) in . The message is shown in test results. - /// - /// - /// The syntactic expression of subset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// The syntactic expression of superset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// Thrown if every element of occurs at least as many times in . - /// - public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") - { - CheckParameterNotNull(subset, "Assert.IsNotSubsetOf", "subset"); - CheckParameterNotNull(superset, "Assert.IsNotSubsetOf", "superset"); - CheckParameterNotNull(comparer, "Assert.IsNotSubsetOf", "comparer"); - IsNotSubsetOfImpl(subset, superset, comparer, comparer.GetType().Name, message, subsetExpression, supersetExpression); - } - - /// - /// Tests whether one collection is not a subset of another collection (with multiplicity) - /// and throws an exception if every element in the subset occurs at least as many times in the superset. - /// - /// - /// Element multiplicity is significant: [1, 1] is considered not to be a subset of [1] - /// (so this assertion would pass for those inputs). - /// - /// - /// The collection expected not to be a subset of . - /// - /// - /// The collection expected not to be a superset of . - /// - /// - /// The message to include in the exception when every element in - /// is also found (with sufficient multiplicity) in . The message is shown in test results. - /// - /// - /// The syntactic expression of subset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// The syntactic expression of superset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// Thrown if every element of occurs at least as many times in . - /// - public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") - { - CheckParameterNotNull(subset, "Assert.IsNotSubsetOf", "subset"); - CheckParameterNotNull(superset, "Assert.IsNotSubsetOf", "superset"); - - IsNotSubsetOfImpl(subset.Cast(), superset.Cast(), EqualityComparer.Default, comparerName: null, message, subsetExpression, supersetExpression); - } - - /// - /// Tests whether one collection is not a subset of another collection (with multiplicity) - /// and throws an exception if every element in the subset occurs at least as many times in the superset. - /// - /// - /// Element multiplicity is significant: [1, 1] is considered not to be a subset of [1] - /// (so this assertion would pass for those inputs). - /// - /// - /// The collection expected not to be a subset of . - /// - /// - /// The collection expected not to be a superset of . - /// - /// - /// The equality comparer to use when comparing elements. - /// - /// - /// The message to include in the exception when every element in - /// is also found (with sufficient multiplicity) in . The message is shown in test results. - /// - /// - /// The syntactic expression of subset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// The syntactic expression of superset as given by the compiler via caller argument expression. - /// Users shouldn't pass a value for this parameter. - /// - /// - /// Thrown if every element of occurs at least as many times in . - /// - public static void IsNotSubsetOf([NotNull] IEnumerable? subset, [NotNull] IEnumerable? superset, [NotNull] IEqualityComparer? comparer, string? message = "", [CallerArgumentExpression(nameof(subset))] string subsetExpression = "", [CallerArgumentExpression(nameof(superset))] string supersetExpression = "") - { - CheckParameterNotNull(subset, "Assert.IsNotSubsetOf", "subset"); - CheckParameterNotNull(superset, "Assert.IsNotSubsetOf", "superset"); - CheckParameterNotNull(comparer, "Assert.IsNotSubsetOf", "comparer"); - - IsNotSubsetOfImpl(subset.Cast(), superset.Cast(), new NonGenericEqualityComparerAdapter(comparer), comparer.GetType().Name, message, subsetExpression, supersetExpression); - } - - #endregion // IsNotSubsetOf - - private static void IsSubsetOfImpl(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer, string? comparerName, string? message, string subsetExpression, string supersetExpression) - { - // Snapshot once so we don't enumerate twice (counting + rendering on failure) - // and so lazy/single-pass enumerables behave deterministically. - List subsetList = subset is List sl ? sl : [.. subset]; - List supersetList = superset is List spl ? spl : [.. superset]; - - if (TryFindMissingElements(subsetList, supersetList, comparer, out List? missing)) - { - ReportAssertIsSubsetOfFailed(subsetList, supersetList, missing, comparerName, message, subsetExpression, supersetExpression); - } - } - - private static void IsNotSubsetOfImpl(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer, string? comparerName, string? message, string subsetExpression, string supersetExpression) - { - List subsetList = subset is List sl ? sl : [.. subset]; - List supersetList = superset is List spl ? spl : [.. superset]; - - if (!HasAnyMissingElement(subsetList, supersetList, comparer)) - { - ReportAssertIsNotSubsetOfFailed(subsetList, supersetList, comparerName, message, subsetExpression, supersetExpression); - } - } - - /// - /// Determines whether contains any element not present - /// (with sufficient multiplicity) in . - /// - /// - /// if at least one element is missing — in which case - /// holds the excess elements (in their first-seen order in ) — and - /// when every element of is matched in - /// . - /// - private static bool TryFindMissingElements(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer, [NotNullWhen(true)] out List? missing) - { -#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. - Dictionary supersetCounts = CountElements(superset, comparer, out int supersetNulls); -#pragma warning restore CS8714 - - missing = null; - - // Walk the subset in source order so excess elements appear in first-seen positional order - // (with multiplicity preserved). For each element, decrement its remaining quota in the superset; - // when the quota reaches zero, additional occurrences are reported as missing. - foreach (T? element in subset) - { - if (element is null) - { - if (supersetNulls > 0) - { - supersetNulls--; - } - else - { - missing ??= []; - missing.Add(default); - } - - continue; - } - - if (supersetCounts.TryGetValue(element, out int remaining) && remaining > 0) - { - supersetCounts[element] = remaining - 1; - } - else - { - missing ??= []; - missing.Add(element); - } - } - - return missing is not null; - } - - /// - /// Equivalent to - /// for the IsNotSubsetOf path, but skips the of excess elements - /// and exits the walk on the first uncovered element. The - /// O(|superset|) count-dictionary build is still performed up-front. - /// - private static bool HasAnyMissingElement(IEnumerable subset, IEnumerable superset, IEqualityComparer comparer) - { -#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. - Dictionary supersetCounts = CountElements(superset, comparer, out int supersetNulls); -#pragma warning restore CS8714 - - foreach (T? element in subset) - { - if (element is null) - { - if (supersetNulls > 0) - { - supersetNulls--; - } - else - { - return true; - } - - continue; - } - - if (supersetCounts.TryGetValue(element, out int remaining) && remaining > 0) - { - supersetCounts[element] = remaining - 1; - } - else - { - return true; - } - } - - return false; - } - -#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 Dictionary CountElements(IEnumerable collection, IEqualityComparer comparer, out int nullCount) - { -#pragma warning disable IDE0028 // Collection initialization can be simplified - target-typed new with constructor argument is preferred over collection expression here - Dictionary counts = new(comparer); -#pragma warning restore IDE0028 - nullCount = 0; - foreach (T? element in collection) - { - if (element is null) - { - nullCount++; - continue; - } - - counts.TryGetValue(element, out int count); - counts[element] = count + 1; - } - - return counts; - } -#pragma warning restore CS8714 - - [DoesNotReturn] - private static void ReportAssertIsSubsetOfFailed(IEnumerable subset, IEnumerable superset, List missing, string? comparerName, string? message, string subsetExpression, string supersetExpression) - { - string subsetText = AssertionValueRenderer.RenderValue(subset); - string supersetText = AssertionValueRenderer.RenderValue(superset); - string missingText = AssertionValueRenderer.RenderValue(missing); - - EvidenceBlock evidence = EvidenceBlock.Create() - .AddLine("missing:", missingText) - .AddLine("subset:", subsetText) - .AddLine("superset:", supersetText); - - if (comparerName is not null) - { - evidence.AddLine("comparer:", comparerName); - } - - StructuredAssertionMessage structured = new(FrameworkMessages.IsSubsetOfFailedSummary); - structured.WithUserMessage(message); - structured.WithEvidence(evidence); - structured.WithExpectedAndActual(supersetText, subsetText); - structured.WithCallSiteExpression(BuildCallSiteWithComparer("Assert.IsSubsetOf", subsetExpression, supersetExpression, comparerName is not null)); - - ReportAssertFailed(structured); - } - - [DoesNotReturn] - private static void ReportAssertIsNotSubsetOfFailed(IEnumerable subset, IEnumerable superset, string? comparerName, string? message, string subsetExpression, string supersetExpression) - { - string subsetText = AssertionValueRenderer.RenderValue(subset); - string supersetText = AssertionValueRenderer.RenderValue(superset); - - EvidenceBlock evidence = EvidenceBlock.Create() - .AddLine("subset:", subsetText) - .AddLine("superset:", supersetText); - - if (comparerName is not null) - { - evidence.AddLine("comparer:", comparerName); - } - - StructuredAssertionMessage structured = new(FrameworkMessages.IsNotSubsetOfFailedSummary); - structured.WithUserMessage(message); - structured.WithEvidence(evidence); - structured.WithExpectedAndActual(expectedText: null, actualText: subsetText); - structured.WithCallSiteExpression(BuildCallSiteWithComparer("Assert.IsNotSubsetOf", subsetExpression, supersetExpression, comparerName is not null)); - - ReportAssertFailed(structured); - } - - private static string? BuildCallSiteWithComparer(string assertionMethodName, string subsetExpression, string supersetExpression, bool hasComparer) - => hasComparer - ? FormatCallSiteExpression(assertionMethodName, subsetExpression, supersetExpression, expression3: string.Empty, "", "", "") - : FormatCallSiteExpression(assertionMethodName, subsetExpression, supersetExpression, "", ""); - - private sealed class NonGenericEqualityComparerAdapter : IEqualityComparer - { - private readonly IEqualityComparer _comparer; - - public NonGenericEqualityComparerAdapter(IEqualityComparer comparer) - => _comparer = comparer; - - // The 'new' modifier suppresses CS0108: this instance method intentionally hides the - // static 'object.Equals(object?, object?)' (only sharing its name/signature) to satisfy - // the IEqualityComparer.Equals contract. There is nothing to override. - public new bool Equals(object? x, object? y) => _comparer.Equals(x, y); - - public int GetHashCode(object? obj) => obj is null ? 0 : _comparer.GetHashCode(obj); - } -} diff --git a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index bb48daf596..b21cb18ef7 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -2,11 +2,11 @@ [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? -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotSubsetOf(System.Collections.IEnumerable? subset, System.Collections.IEnumerable? superset, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotSubsetOf(System.Collections.IEnumerable? subset, System.Collections.IEnumerable? superset, System.Collections.IEqualityComparer? comparer, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotSubsetOf(System.Collections.Generic.IEnumerable? subset, System.Collections.Generic.IEnumerable? superset, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNotSubsetOf(System.Collections.Generic.IEnumerable? subset, System.Collections.Generic.IEnumerable? superset, System.Collections.Generic.IEqualityComparer? comparer, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsSubsetOf(System.Collections.IEnumerable? subset, System.Collections.IEnumerable? superset, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsSubsetOf(System.Collections.IEnumerable? subset, System.Collections.IEnumerable? superset, System.Collections.IEqualityComparer? comparer, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsSubsetOf(System.Collections.Generic.IEnumerable? subset, System.Collections.Generic.IEnumerable? superset, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void -static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsSubsetOf(System.Collections.Generic.IEnumerable? subset, System.Collections.Generic.IEnumerable? superset, System.Collections.Generic.IEqualityComparer? comparer, string? message = "", string! subsetExpression = "", string! supersetExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ContainsAll(System.Collections.IEnumerable? expected, System.Collections.IEnumerable? collection, string? message = "", string! expectedExpression = "", string! collectionExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ContainsAll(System.Collections.IEnumerable? expected, System.Collections.IEnumerable? collection, System.Collections.IEqualityComparer? comparer, string? message = "", string! expectedExpression = "", string! collectionExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ContainsAll(System.Collections.Generic.IEnumerable? expected, System.Collections.Generic.IEnumerable? collection, string? message = "", string! expectedExpression = "", string! collectionExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.ContainsAll(System.Collections.Generic.IEnumerable? expected, System.Collections.Generic.IEnumerable? collection, System.Collections.Generic.IEqualityComparer? comparer, string? message = "", string! expectedExpression = "", string! collectionExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.DoesNotContainAll(System.Collections.IEnumerable? expected, System.Collections.IEnumerable? collection, string? message = "", string! expectedExpression = "", string! collectionExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.DoesNotContainAll(System.Collections.IEnumerable? expected, System.Collections.IEnumerable? collection, System.Collections.IEqualityComparer? comparer, string? message = "", string! expectedExpression = "", string! collectionExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.DoesNotContainAll(System.Collections.Generic.IEnumerable? expected, System.Collections.Generic.IEnumerable? collection, string? message = "", string! expectedExpression = "", string! collectionExpression = "") -> void +static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.DoesNotContainAll(System.Collections.Generic.IEnumerable? expected, System.Collections.Generic.IEnumerable? collection, System.Collections.Generic.IEqualityComparer? comparer, string? message = "", string! expectedExpression = "", string! collectionExpression = "") -> void diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 0669eccf29..f183caa8d6 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -420,10 +420,10 @@ Actual: {2} Expected value to not be null. - - Expected collection to be a subset of the specified superset. + + Expected collection to contain all specified items. - - Expected collection to not be a subset of the specified superset. + + Expected collection to not contain all specified items. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index eeaa5f395f..ca225cc958 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. Řetězec '{0}' neobsahuje řetězec '{1}'. {2}. @@ -183,6 +188,11 @@ Očekávala se přesně jedna položka odpovídající predikátu, ale našlo se tolik položek: {1}. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. Řetězec {0} obsahuje řetězec {1}. {2}. @@ -327,21 +337,11 @@ Skutečnost: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index e1db49343c..6946489228 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. Die Zeichenfolge "{0}" enthält nicht die Zeichenfolge "{1}". {2}. @@ -183,6 +188,11 @@ Es wurde erwartet, dass genau ein Element mit dem Prädikat übereinstimmt, es wurden jedoch {1} Element(e) gefunden. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. Die Zeichenfolge „{0}“ enthält die Zeichenfolge „{1}“. {2}. @@ -327,21 +337,11 @@ Tatsächlich: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index a9ffe9d54d..4c95ac81ee 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. La cadena '{0}' no contiene la cadena '{1}'. {2}. @@ -183,6 +188,11 @@ Se esperaba exactamente un elemento para que coincida con el predicado, pero se encontraron {1} elementos. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. La cadena '{0}' contiene la cadena '{1}'. {2}. @@ -327,21 +337,11 @@ Real: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index bb89b3a730..096f92732b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. La chaîne '{0}' ne contient pas la chaîne '{1}'. {2}. @@ -183,6 +188,11 @@ Un seul élément était attendu pour correspondre au prédicat, mais {1} élément(s) trouvé(s). {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. La chaîne « {0} » contient la chaîne « {1} ». {2}. @@ -327,21 +337,11 @@ Réel : {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 3b864ae6a6..0cc559b199 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. La stringa '{0}' non contiene la stringa '{1}'. {2}. @@ -183,6 +188,11 @@ Un unico elemento dovrebbe corrispondere al predicato, ma ne sono stati trovati {1}. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. La stringa '{0}' contiene la stringa '{1}'. {2}. @@ -327,21 +337,11 @@ Effettivo: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index c501d5081c..39d9348ed0 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -158,6 +158,11 @@ {0}。{1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. 文字列 '{0}' は文字列 '{1}' を含んでいません。{2}。 @@ -183,6 +188,11 @@ 述語と一致する項目が 1 つだけ必要ですが、{1} 項目が見つかりました。{0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. 文字列 '{0}' は文字列 '{1}' を含んでいます。{2}。 @@ -327,21 +337,11 @@ Actual: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index 8ba9b7cb49..f3aba025d9 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. '{0}' 문자열이 '{1}' 문자열을 포함하지 않습니다. {2} @@ -183,6 +188,11 @@ 조건자에 일치하는 항목이 하나만 필요하지만 {1}개 항목이 발견되었습니다. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. '{0}' 문자열에 '{1}' 문자열이 포함되어 있습니다. {2}. @@ -327,21 +337,11 @@ Actual: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 6e03188099..0cd74e5106 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. Ciąg „{0}” nie zawiera ciągu „{1}”. {2}. @@ -183,6 +188,11 @@ Oczekiwano dokładnie jednego elementu zgodnego z predykatem, ale znaleziono {1} elementów. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. Ciąg „{0}” zawiera ciąg „{1}”. {2}. @@ -327,21 +337,11 @@ Rzeczywiste: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index 3ffc9ec097..559f85805e 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. A cadeia de caracteres '{0}' não contém a cadeia de caracteres '{1}'. {2}. @@ -183,6 +188,11 @@ Esperava-se exatamente um item para corresponder ao predicado, mas encontrou {1} itens. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. A cadeia de caracteres "{0}" contém a cadeia de caracteres "{1}". {2}. @@ -327,21 +337,11 @@ Real: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index 30d04f198b..6c8792f88a 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. Строка "{0}" не содержит строку "{1}". {2}. @@ -183,6 +188,11 @@ Ожидался ровно один элемент, соответствующий предикату, но найдено элементов: {1}. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. Строка "{0}" не содержит строку "{1}". {2}. @@ -327,21 +337,11 @@ Actual: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index 17518be3cb..5f2ce42921 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -158,6 +158,11 @@ {0}. {1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. '{0}' dizesi, '{1}' dizesini içermiyor. {2}. @@ -183,6 +188,11 @@ Özellikle bir öğenin koşulla eşleşmesi beklenirken {1} öğe bulundu. {0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. '{0}' dizesi, '{1}' dizesini içermiyor. {2}. @@ -327,21 +337,11 @@ Gerçekte olan: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index fbd2c2d53b..1014543e81 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -158,6 +158,11 @@ {0}。{1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. 字符串“{0}”不包含字符串“{1}”。{2}。 @@ -183,6 +188,11 @@ 应恰好只有一项与谓词匹配,但找到 {1} 项。{0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. 字符串“{0}”确实包含字符串“{1}”。{2} @@ -327,21 +337,11 @@ Actual: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index 92b37b7727..5a187a11b2 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -158,6 +158,11 @@ {0}。{1} + + Expected collection to contain all specified items. + Expected collection to contain all specified items. + + String '{0}' does not contain string '{1}'. {2}. 字串 '{0}' 未包含字串 '{1}'。{2}。 @@ -183,6 +188,11 @@ 預期只有一個項目符合述詞,但找到 {1} 個項目。{0} + + Expected collection to not contain all specified items. + Expected collection to not contain all specified items. + + String '{0}' does contain string '{1}'. {2}. 字串 '{0}' 有包含字串 '{1}'。{2}。 @@ -327,21 +337,11 @@ Actual: {2} Expected value to not be null. - - Expected collection to not be a subset of the specified superset. - Expected collection to not be a subset of the specified superset. - - Expected value to be null. Expected value to be null. - - Expected collection to be a subset of the specified superset. - Expected collection to be a subset of the specified superset. - - Expected condition to be true. Expected condition to be true. diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs new file mode 100644 index 0000000000..d824ed8393 --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs @@ -0,0 +1,419 @@ +// 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.Collections; + +using AwesomeAssertions; + +using TestFramework.ForTestingMSTest; + +namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; + +public partial class AssertTests : TestContainer +{ + public void ContainsAll_Generic_AllPresent_ShouldPass() + => Assert.ContainsAll(new[] { 1, 2 }, new[] { 1, 2, 3 }); + + public void ContainsAll_Generic_EmptyExpected_ShouldPass() + => Assert.ContainsAll(Array.Empty(), new[] { 1, 2, 3 }); + + public void ContainsAll_Generic_DuplicatesWithinCollectionMultiplicity_ShouldPass() + => Assert.ContainsAll(new[] { 1, 1, 2 }, new[] { 1, 1, 2, 3 }); + + public void ContainsAll_Generic_DuplicatesExceedCollectionMultiplicity_ShouldFail() + { + Action action = () => Assert.ContainsAll(new[] { 1, 1, 1 }, new[] { 1, 1 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: [1] + expected: [1, 1, 1] + collection: [1, 1] + + Assert.ContainsAll(new[] { 1, 1, 1 }, new[] { 1, 1 }) + """); + } + + public void ContainsAll_Generic_MultipleMissing_PreservesFirstSeenOrderAndMultiplicity() + { + // Walk: 3 ✓, 1 ✓, 1 → missing(1), 4 → missing(4), 1 → missing(1) => [1, 4, 1] + Action action = () => Assert.ContainsAll(new[] { 3, 1, 1, 4, 1 }, new[] { 1, 2, 3 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: [1, 4, 1] + expected: [3, 1, 1, 4, 1] + collection: [1, 2, 3] + + Assert.ContainsAll(new[] { 3, 1, 1, 4, 1 }, new[] { 1, 2, 3 }) + """); + } + + public void ContainsAll_Generic_ExcessInMiddleOfMatchingRun_ReportsOnlyExcess() + { + Action action = () => Assert.ContainsAll(new[] { 1, 2, 1, 3 }, new[] { 1, 2, 3 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: [1] + expected: [1, 2, 1, 3] + collection: [1, 2, 3] + + Assert.ContainsAll(new[] { 1, 2, 1, 3 }, new[] { 1, 2, 3 }) + """); + } + + public void DoesNotContainAll_Generic_DuplicatesExceedCollectionMultiplicity_ShouldPass() + => Assert.DoesNotContainAll(new[] { 1, 1 }, new[] { 1 }); + + public void DoesNotContainAll_Generic_DuplicatesWithinCollectionMultiplicity_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(new[] { 1, 1 }, new[] { 1, 1, 2 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not contain all specified items. + + expected: [1, 1] + collection: [1, 1, 2] + + Assert.DoesNotContainAll(new[] { 1, 1 }, new[] { 1, 1, 2 }) + """); + } + + public void ContainsAll_Generic_MissingElement_ShouldFail() + { + Action action = () => Assert.ContainsAll(new[] { 1, 2, 3 }, new[] { 1, 2 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: [3] + expected: [1, 2, 3] + collection: [1, 2] + + Assert.ContainsAll(new[] { 1, 2, 3 }, new[] { 1, 2 }) + """); + } + + public void ContainsAll_Generic_NullInExpectedButNotInCollection_ShouldFail() + { + Action action = () => Assert.ContainsAll(new string?[] { "a", null }, new string?[] { "a" }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: [null] + expected: ["a", null] + collection: ["a"] + + Assert.ContainsAll(new string?[] { "a", null }, new string?[] { "a" }) + """); + } + + public void ContainsAll_Generic_StringMessage_MissingElement_ShouldFail() + { + Action action = () => Assert.ContainsAll(new[] { 1, 2, 3 }, new[] { 1, 2 }, "User-provided message"); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + User-provided message + + missing: [3] + expected: [1, 2, 3] + collection: [1, 2] + + Assert.ContainsAll(new[] { 1, 2, 3 }, new[] { 1, 2 }) + """); + } + + public void ContainsAll_Generic_WithComparer_AllPresent_ShouldPass() + => Assert.ContainsAll(new[] { "A" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); + + public void ContainsAll_Generic_WithComparer_MissingElement_ShouldFail() + { + Action action = () => Assert.ContainsAll(new[] { "A", "C" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: ["C"] + expected: ["A", "C"] + collection: ["a", "b"] + comparer: CaseInsensitiveStringComparer + + Assert.ContainsAll(new[] { "A", "C" }, new[] { "a", "b" }, ) + """); + } + + public void ContainsAll_Generic_NullExpected_ShouldFail() + { + Action action = () => Assert.ContainsAll(null, new[] { 1, 2 }); + action.Should().Throw() + .WithMessage("Assert.ContainsAll failed. The parameter 'expected' is invalid. The value cannot be null."); + } + + public void ContainsAll_Generic_NullCollection_ShouldFail() + { + Action action = () => Assert.ContainsAll(new[] { 1, 2 }, null); + action.Should().Throw() + .WithMessage("Assert.ContainsAll failed. The parameter 'collection' is invalid. The value cannot be null."); + } + + public void ContainsAll_Generic_NullComparer_ShouldFail() + { + Action action = () => Assert.ContainsAll(new[] { 1 }, new[] { 1 }, (IEqualityComparer?)null); + action.Should().Throw() + .WithMessage("Assert.ContainsAll failed. The parameter 'comparer' is invalid. The value cannot be null."); + } + + public void ContainsAll_NonGeneric_AllPresent_ShouldPass() + => Assert.ContainsAll(new ArrayList { 1, 2 }, new ArrayList { 1, 2, 3 }); + + public void ContainsAll_NonGeneric_WithComparer_AllPresent_ShouldPass() + => Assert.ContainsAll(new ArrayList { "A", "b" }, new ArrayList { "a", "B", "c" }, new CaseInsensitiveStringComparer()); + + public void DoesNotContainAll_NonGeneric_WithComparer_MissingElement_ShouldPass() + => Assert.DoesNotContainAll(new ArrayList { "A", "C" }, new ArrayList { "a", "b" }, new CaseInsensitiveStringComparer()); + + public void ContainsAll_NonGeneric_MissingElement_ShouldFail() + { + IEnumerable expected = new ArrayList { 1, 2, 3 }; + IEnumerable collection = new ArrayList { 1, 2 }; + Action action = () => Assert.ContainsAll(expected, collection); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: [3] + expected: [1, 2, 3] + collection: [1, 2] + + Assert.ContainsAll(expected, collection) + """); + } + + public void ContainsAll_NonGeneric_WithComparer_MissingElement_ShouldFail() + { + IEnumerable expected = new ArrayList { "A", "C" }; + IEnumerable collection = new ArrayList { "a", "b" }; + Action action = () => Assert.ContainsAll(expected, collection, new CaseInsensitiveStringComparer()); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: ["C"] + expected: ["A", "C"] + collection: ["a", "b"] + comparer: CaseInsensitiveStringComparer + + Assert.ContainsAll(expected, collection, ) + """); + } + + public void DoesNotContainAll_Generic_MissingElement_ShouldPass() + => Assert.DoesNotContainAll(new[] { 1, 2, 3 }, new[] { 1, 2 }); + + public void DoesNotContainAll_Generic_AllPresent_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(new[] { 1, 2 }, new[] { 1, 2, 3 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not contain all specified items. + + expected: [1, 2] + collection: [1, 2, 3] + + Assert.DoesNotContainAll(new[] { 1, 2 }, new[] { 1, 2, 3 }) + """); + } + + public void DoesNotContainAll_Generic_StringMessage_AllPresent_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(new[] { 1 }, new[] { 1, 2 }, "User-provided message"); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not contain all specified items. + User-provided message + + expected: [1] + collection: [1, 2] + + Assert.DoesNotContainAll(new[] { 1 }, new[] { 1, 2 }) + """); + } + + public void DoesNotContainAll_Generic_WithComparer_MissingElement_ShouldPass() + => Assert.DoesNotContainAll(new[] { "A", "C" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); + + public void DoesNotContainAll_Generic_WithComparer_AllPresent_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(new[] { "A" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not contain all specified items. + + expected: ["A"] + collection: ["a", "b"] + comparer: CaseInsensitiveStringComparer + + Assert.DoesNotContainAll(new[] { "A" }, new[] { "a", "b" }, ) + """); + } + + public void DoesNotContainAll_Generic_NullExpected_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(null, new[] { 1, 2 }); + action.Should().Throw() + .WithMessage("Assert.DoesNotContainAll failed. The parameter 'expected' is invalid. The value cannot be null."); + } + + public void DoesNotContainAll_Generic_NullCollection_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(new[] { 1, 2 }, null); + action.Should().Throw() + .WithMessage("Assert.DoesNotContainAll failed. The parameter 'collection' is invalid. The value cannot be null."); + } + + public void DoesNotContainAll_Generic_NullComparer_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(new[] { 1 }, new[] { 1 }, (IEqualityComparer?)null); + action.Should().Throw() + .WithMessage("Assert.DoesNotContainAll failed. The parameter 'comparer' is invalid. The value cannot be null."); + } + + public void ContainsAll_NonGeneric_NullExpected_ShouldFail() + { + Action action = () => Assert.ContainsAll(null, new ArrayList { 1, 2 }); + action.Should().Throw() + .WithMessage("Assert.ContainsAll failed. The parameter 'expected' is invalid. The value cannot be null."); + } + + public void ContainsAll_NonGeneric_NullCollection_ShouldFail() + { + Action action = () => Assert.ContainsAll(new ArrayList { 1 }, null); + action.Should().Throw() + .WithMessage("Assert.ContainsAll failed. The parameter 'collection' is invalid. The value cannot be null."); + } + + public void ContainsAll_NonGeneric_NullComparer_ShouldFail() + { + Action action = () => Assert.ContainsAll(new ArrayList { 1 }, new ArrayList { 1 }, (IEqualityComparer?)null); + action.Should().Throw() + .WithMessage("Assert.ContainsAll failed. The parameter 'comparer' is invalid. The value cannot be null."); + } + + public void DoesNotContainAll_NonGeneric_NullExpected_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(null, new ArrayList { 1, 2 }); + action.Should().Throw() + .WithMessage("Assert.DoesNotContainAll failed. The parameter 'expected' is invalid. The value cannot be null."); + } + + public void DoesNotContainAll_NonGeneric_NullCollection_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(new ArrayList { 1 }, null); + action.Should().Throw() + .WithMessage("Assert.DoesNotContainAll failed. The parameter 'collection' is invalid. The value cannot be null."); + } + + public void DoesNotContainAll_NonGeneric_NullComparer_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(new ArrayList { 1 }, new ArrayList { 1 }, (IEqualityComparer?)null); + action.Should().Throw() + .WithMessage("Assert.DoesNotContainAll failed. The parameter 'comparer' is invalid. The value cannot be null."); + } + + public void DoesNotContainAll_Generic_BothEmpty_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(Array.Empty(), Array.Empty()); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not contain all specified items. + + expected: [] + collection: [] + + Assert.DoesNotContainAll(Array.Empty(), Array.Empty()) + """); + } + + public void DoesNotContainAll_Generic_EmptyExpected_ShouldFail() + { + Action action = () => Assert.DoesNotContainAll(Array.Empty(), new[] { 1 }); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not contain all specified items. + + expected: [] + collection: [1] + + Assert.DoesNotContainAll(Array.Empty(), new[] { 1 }) + """); + } + + public void DoesNotContainAll_NonGeneric_WithComparer_AllPresent_ShouldFail() + { + IEnumerable expected = new ArrayList { "A" }; + IEnumerable collection = new ArrayList { "a", "b" }; + Action action = () => Assert.DoesNotContainAll(expected, collection, new CaseInsensitiveStringComparer()); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not contain all specified items. + + expected: ["A"] + collection: ["a", "b"] + comparer: CaseInsensitiveStringComparer + + Assert.DoesNotContainAll(expected, collection, ) + """); + } + + public void ContainsAll_Generic_NullInCollectionButNotInExpected_ShouldPass() + => Assert.ContainsAll(new string?[] { "a" }, new string?[] { "a", null }); + + public void ContainsAll_AssertFailedException_PopulatesExpectedAndActual() + { + Action action = () => Assert.ContainsAll(new[] { 1, 2, 3 }, new[] { 1, 2 }); + AssertFailedException ex = action.Should().Throw().Which; + ex.ExpectedText.Should().Be("[1, 2, 3]"); + ex.ActualText.Should().Be("[1, 2]"); + } + + public void DoesNotContainAll_AssertFailedException_PopulatesActualOnly() + { + Action action = () => Assert.DoesNotContainAll(new[] { 1 }, new[] { 1, 2 }); + AssertFailedException ex = action.Should().Throw().Which; + ex.ExpectedText.Should().BeNull(); + ex.ActualText.Should().Be("[1, 2]"); + } + + private sealed class CaseInsensitiveStringComparer : IEqualityComparer, IEqualityComparer + { + public bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + + public int GetHashCode(string obj) => StringComparer.OrdinalIgnoreCase.GetHashCode(obj); + + bool IEqualityComparer.Equals(object? x, object? y) => StringComparer.OrdinalIgnoreCase.Equals(x as string, y as string); + + public int GetHashCode(object obj) => obj is string s ? StringComparer.OrdinalIgnoreCase.GetHashCode(s) : 0; + } +} diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs deleted file mode 100644 index fca69b12c9..0000000000 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.IsSubsetOf.cs +++ /dev/null @@ -1,419 +0,0 @@ -// 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.Collections; - -using AwesomeAssertions; - -using TestFramework.ForTestingMSTest; - -namespace Microsoft.VisualStudio.TestPlatform.TestFramework.UnitTests; - -public partial class AssertTests : TestContainer -{ - public void IsSubsetOf_Generic_AllPresent_ShouldPass() - => Assert.IsSubsetOf(new[] { 1, 2 }, new[] { 1, 2, 3 }); - - public void IsSubsetOf_Generic_EmptySubset_ShouldPass() - => Assert.IsSubsetOf(Array.Empty(), new[] { 1, 2, 3 }); - - public void IsSubsetOf_Generic_DuplicatesWithinSupersetMultiplicity_ShouldPass() - => Assert.IsSubsetOf(new[] { 1, 1, 2 }, new[] { 1, 1, 2, 3 }); - - public void IsSubsetOf_Generic_DuplicatesExceedSupersetMultiplicity_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new[] { 1, 1, 1 }, new[] { 1, 1 }); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - - missing: [1] - subset: [1, 1, 1] - superset: [1, 1] - - Assert.IsSubsetOf(new[] { 1, 1, 1 }, new[] { 1, 1 }) - """); - } - - public void IsSubsetOf_Generic_MultipleMissing_PreservesFirstSeenOrderAndMultiplicity() - { - // Walk: 3 ✓, 1 ✓, 1 → missing(1), 4 → missing(4), 1 → missing(1) => [1, 4, 1] - Action action = () => Assert.IsSubsetOf(new[] { 3, 1, 1, 4, 1 }, new[] { 1, 2, 3 }); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - - missing: [1, 4, 1] - subset: [3, 1, 1, 4, 1] - superset: [1, 2, 3] - - Assert.IsSubsetOf(new[] { 3, 1, 1, 4, 1 }, new[] { 1, 2, 3 }) - """); - } - - public void IsSubsetOf_Generic_ExcessInMiddleOfMatchingRun_ReportsOnlyExcess() - { - Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 1, 3 }, new[] { 1, 2, 3 }); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - - missing: [1] - subset: [1, 2, 1, 3] - superset: [1, 2, 3] - - Assert.IsSubsetOf(new[] { 1, 2, 1, 3 }, new[] { 1, 2, 3 }) - """); - } - - public void IsNotSubsetOf_Generic_DuplicatesExceedSupersetMultiplicity_ShouldPass() - => Assert.IsNotSubsetOf(new[] { 1, 1 }, new[] { 1 }); - - public void IsNotSubsetOf_Generic_DuplicatesWithinSupersetMultiplicity_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(new[] { 1, 1 }, new[] { 1, 1, 2 }); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to not be a subset of the specified superset. - - subset: [1, 1] - superset: [1, 1, 2] - - Assert.IsNotSubsetOf(new[] { 1, 1 }, new[] { 1, 1, 2 }) - """); - } - - public void IsSubsetOf_Generic_MissingElement_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - - missing: [3] - subset: [1, 2, 3] - superset: [1, 2] - - Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }) - """); - } - - public void IsSubsetOf_Generic_NullInSubsetButNotInSuperset_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new string?[] { "a", null }, new string?[] { "a" }); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - - missing: [null] - subset: ["a", null] - superset: ["a"] - - Assert.IsSubsetOf(new string?[] { "a", null }, new string?[] { "a" }) - """); - } - - public void IsSubsetOf_Generic_StringMessage_MissingElement_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }, "User-provided message"); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - User-provided message - - missing: [3] - subset: [1, 2, 3] - superset: [1, 2] - - Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }) - """); - } - - public void IsSubsetOf_Generic_WithComparer_AllPresent_ShouldPass() - => Assert.IsSubsetOf(new[] { "A" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); - - public void IsSubsetOf_Generic_WithComparer_MissingElement_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - - missing: ["C"] - subset: ["A", "C"] - superset: ["a", "b"] - comparer: CaseInsensitiveStringComparer - - Assert.IsSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, ) - """); - } - - public void IsSubsetOf_Generic_NullSubset_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(null, new[] { 1, 2 }); - action.Should().Throw() - .WithMessage("Assert.IsSubsetOf failed. The parameter 'subset' is invalid. The value cannot be null."); - } - - public void IsSubsetOf_Generic_NullSuperset_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new[] { 1, 2 }, null); - action.Should().Throw() - .WithMessage("Assert.IsSubsetOf failed. The parameter 'superset' is invalid. The value cannot be null."); - } - - public void IsSubsetOf_Generic_NullComparer_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new[] { 1 }, new[] { 1 }, (IEqualityComparer?)null); - action.Should().Throw() - .WithMessage("Assert.IsSubsetOf failed. The parameter 'comparer' is invalid. The value cannot be null."); - } - - public void IsSubsetOf_NonGeneric_AllPresent_ShouldPass() - => Assert.IsSubsetOf(new ArrayList { 1, 2 }, new ArrayList { 1, 2, 3 }); - - public void IsSubsetOf_NonGeneric_WithComparer_AllPresent_ShouldPass() - => Assert.IsSubsetOf(new ArrayList { "A", "b" }, new ArrayList { "a", "B", "c" }, new CaseInsensitiveStringComparer()); - - public void IsNotSubsetOf_NonGeneric_WithComparer_MissingElement_ShouldPass() - => Assert.IsNotSubsetOf(new ArrayList { "A", "C" }, new ArrayList { "a", "b" }, new CaseInsensitiveStringComparer()); - - public void IsSubsetOf_NonGeneric_MissingElement_ShouldFail() - { - IEnumerable subset = new ArrayList { 1, 2, 3 }; - IEnumerable superset = new ArrayList { 1, 2 }; - Action action = () => Assert.IsSubsetOf(subset, superset); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - - missing: [3] - subset: [1, 2, 3] - superset: [1, 2] - - Assert.IsSubsetOf(subset, superset) - """); - } - - public void IsSubsetOf_NonGeneric_WithComparer_MissingElement_ShouldFail() - { - IEnumerable subset = new ArrayList { "A", "C" }; - IEnumerable superset = new ArrayList { "a", "b" }; - Action action = () => Assert.IsSubsetOf(subset, superset, new CaseInsensitiveStringComparer()); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to be a subset of the specified superset. - - missing: ["C"] - subset: ["A", "C"] - superset: ["a", "b"] - comparer: CaseInsensitiveStringComparer - - Assert.IsSubsetOf(subset, superset, ) - """); - } - - public void IsNotSubsetOf_Generic_MissingElement_ShouldPass() - => Assert.IsNotSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }); - - public void IsNotSubsetOf_Generic_AllPresent_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(new[] { 1, 2 }, new[] { 1, 2, 3 }); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to not be a subset of the specified superset. - - subset: [1, 2] - superset: [1, 2, 3] - - Assert.IsNotSubsetOf(new[] { 1, 2 }, new[] { 1, 2, 3 }) - """); - } - - public void IsNotSubsetOf_Generic_StringMessage_AllPresent_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(new[] { 1 }, new[] { 1, 2 }, "User-provided message"); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to not be a subset of the specified superset. - User-provided message - - subset: [1] - superset: [1, 2] - - Assert.IsNotSubsetOf(new[] { 1 }, new[] { 1, 2 }) - """); - } - - public void IsNotSubsetOf_Generic_WithComparer_MissingElement_ShouldPass() - => Assert.IsNotSubsetOf(new[] { "A", "C" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); - - public void IsNotSubsetOf_Generic_WithComparer_AllPresent_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(new[] { "A" }, new[] { "a", "b" }, new CaseInsensitiveStringComparer()); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to not be a subset of the specified superset. - - subset: ["A"] - superset: ["a", "b"] - comparer: CaseInsensitiveStringComparer - - Assert.IsNotSubsetOf(new[] { "A" }, new[] { "a", "b" }, ) - """); - } - - public void IsNotSubsetOf_Generic_NullSubset_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(null, new[] { 1, 2 }); - action.Should().Throw() - .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'subset' is invalid. The value cannot be null."); - } - - public void IsNotSubsetOf_Generic_NullSuperset_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(new[] { 1, 2 }, null); - action.Should().Throw() - .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'superset' is invalid. The value cannot be null."); - } - - public void IsNotSubsetOf_Generic_NullComparer_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(new[] { 1 }, new[] { 1 }, (IEqualityComparer?)null); - action.Should().Throw() - .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'comparer' is invalid. The value cannot be null."); - } - - public void IsSubsetOf_NonGeneric_NullSubset_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(null, new ArrayList { 1, 2 }); - action.Should().Throw() - .WithMessage("Assert.IsSubsetOf failed. The parameter 'subset' is invalid. The value cannot be null."); - } - - public void IsSubsetOf_NonGeneric_NullSuperset_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new ArrayList { 1 }, null); - action.Should().Throw() - .WithMessage("Assert.IsSubsetOf failed. The parameter 'superset' is invalid. The value cannot be null."); - } - - public void IsSubsetOf_NonGeneric_NullComparer_ShouldFail() - { - Action action = () => Assert.IsSubsetOf(new ArrayList { 1 }, new ArrayList { 1 }, (IEqualityComparer?)null); - action.Should().Throw() - .WithMessage("Assert.IsSubsetOf failed. The parameter 'comparer' is invalid. The value cannot be null."); - } - - public void IsNotSubsetOf_NonGeneric_NullSubset_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(null, new ArrayList { 1, 2 }); - action.Should().Throw() - .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'subset' is invalid. The value cannot be null."); - } - - public void IsNotSubsetOf_NonGeneric_NullSuperset_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(new ArrayList { 1 }, null); - action.Should().Throw() - .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'superset' is invalid. The value cannot be null."); - } - - public void IsNotSubsetOf_NonGeneric_NullComparer_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(new ArrayList { 1 }, new ArrayList { 1 }, (IEqualityComparer?)null); - action.Should().Throw() - .WithMessage("Assert.IsNotSubsetOf failed. The parameter 'comparer' is invalid. The value cannot be null."); - } - - public void IsNotSubsetOf_Generic_BothEmpty_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(Array.Empty(), Array.Empty()); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to not be a subset of the specified superset. - - subset: [] - superset: [] - - Assert.IsNotSubsetOf(Array.Empty(), Array.Empty()) - """); - } - - public void IsNotSubsetOf_Generic_EmptySubset_ShouldFail() - { - Action action = () => Assert.IsNotSubsetOf(Array.Empty(), new[] { 1 }); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to not be a subset of the specified superset. - - subset: [] - superset: [1] - - Assert.IsNotSubsetOf(Array.Empty(), new[] { 1 }) - """); - } - - public void IsNotSubsetOf_NonGeneric_WithComparer_AllPresent_ShouldFail() - { - IEnumerable subset = new ArrayList { "A" }; - IEnumerable superset = new ArrayList { "a", "b" }; - Action action = () => Assert.IsNotSubsetOf(subset, superset, new CaseInsensitiveStringComparer()); - action.Should().Throw() - .WithMessage( - """ - Assertion failed. Expected collection to not be a subset of the specified superset. - - subset: ["A"] - superset: ["a", "b"] - comparer: CaseInsensitiveStringComparer - - Assert.IsNotSubsetOf(subset, superset, ) - """); - } - - public void IsSubsetOf_Generic_NullInSupersetButNotInSubset_ShouldPass() - => Assert.IsSubsetOf(new string?[] { "a" }, new string?[] { "a", null }); - - public void IsSubsetOf_AssertFailedException_PopulatesExpectedAndActual() - { - Action action = () => Assert.IsSubsetOf(new[] { 1, 2, 3 }, new[] { 1, 2 }); - AssertFailedException ex = action.Should().Throw().Which; - ex.ExpectedText.Should().Be("[1, 2]"); - ex.ActualText.Should().Be("[1, 2, 3]"); - } - - public void IsNotSubsetOf_AssertFailedException_PopulatesActualOnly() - { - Action action = () => Assert.IsNotSubsetOf(new[] { 1 }, new[] { 1, 2 }); - AssertFailedException ex = action.Should().Throw().Which; - ex.ExpectedText.Should().BeNull(); - ex.ActualText.Should().Be("[1]"); - } - - private sealed class CaseInsensitiveStringComparer : IEqualityComparer, IEqualityComparer - { - public bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); - - public int GetHashCode(string obj) => StringComparer.OrdinalIgnoreCase.GetHashCode(obj); - - bool IEqualityComparer.Equals(object? x, object? y) => StringComparer.OrdinalIgnoreCase.Equals(x as string, y as string); - - public int GetHashCode(object obj) => obj is string s ? StringComparer.OrdinalIgnoreCase.GetHashCode(s) : 0; - } -} From 1f381d2c8b97a9d7d872b3a37e370c80c73b3edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Fri, 15 May 2026 20:40:41 +0200 Subject: [PATCH 6/8] Fix ContainsAll test comparer semantics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/AssertTests.ContainsAll.cs | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs index d824ed8393..9e9ad31b20 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections; +using System.Runtime.CompilerServices; using AwesomeAssertions; @@ -406,14 +407,46 @@ public void DoesNotContainAll_AssertFailedException_PopulatesActualOnly() ex.ActualText.Should().Be("[1, 2]"); } + public void ContainsAll_CaseInsensitiveStringComparer_NonGenericEquals_WithNonStringValues_ShouldReturnFalse() + { + IEqualityComparer comparer = new CaseInsensitiveStringComparer(); + object left = new(); + object right = new(); + + comparer.Equals(null, null).Should().BeTrue(); + comparer.Equals(null, "a").Should().BeFalse(); + comparer.Equals(left, right).Should().BeFalse(); + comparer.Equals("A", "a").Should().BeTrue(); + } + + public void ContainsAll_CaseInsensitiveStringComparer_NonGenericGetHashCode_WithNonStringValue_ShouldUseRuntimeHashCode() + { + IEqualityComparer comparer = new CaseInsensitiveStringComparer(); + object value = new(); + + comparer.GetHashCode(value).Should().Be(RuntimeHelpers.GetHashCode(value)); + } + private sealed class CaseInsensitiveStringComparer : IEqualityComparer, IEqualityComparer { public bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); public int GetHashCode(string obj) => StringComparer.OrdinalIgnoreCase.GetHashCode(obj); - bool IEqualityComparer.Equals(object? x, object? y) => StringComparer.OrdinalIgnoreCase.Equals(x as string, y as string); - - public int GetHashCode(object obj) => obj is string s ? StringComparer.OrdinalIgnoreCase.GetHashCode(s) : 0; + bool IEqualityComparer.Equals(object? x, object? y) + => (x, y) switch + { + (null, null) => true, + (string left, string right) => Equals(left, right), + (null, _) or (_, null) => false, + _ => false, + }; + + int IEqualityComparer.GetHashCode(object obj) + => obj is string value + ? GetHashCode(value) + : obj is null + ? 0 + : RuntimeHelpers.GetHashCode(obj); } } From d007f5638a8ee4af7607fa815c404a07f7c84e51 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 17:34:27 +0200 Subject: [PATCH 7/8] Render clear call site for ContainsAll comparer overload Pass as expression3 in BuildCallSiteWithComparer so the structured assertion call site is always rendered when the comparer overload is used, even if callers do not propagate [CallerArgumentExpression]. Previously, when all three expression slots were empty, the call site was suppressed entirely, losing the useful signal that the comparer overload was invoked. Added two unit tests covering the no-CallerArgumentExpression case for both ContainsAll and DoesNotContainAll. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/Assert.ContainsAll.cs | 7 ++- .../Assertions/AssertTests.ContainsAll.cs | 48 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs b/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs index 676aa6cd51..a0c4bd760c 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs @@ -532,7 +532,12 @@ private static void ReportAssertDoesNotContainAllFailed(IEnumerable expec private static string? BuildCallSiteWithComparer(string assertionMethodName, string expectedExpression, string collectionExpression, bool hasComparer) => hasComparer - ? FormatCallSiteExpression(assertionMethodName, expectedExpression, collectionExpression, expression3: string.Empty, "", "", "") + // No [CallerArgumentExpression] is captured for the comparer parameter, so the third + // expression slot is always unavailable. Pass the "" placeholder directly as + // the third expression to ensure the call site is rendered (e.g. as + // "Assert.ContainsAll(, , )") even when callers do not + // support [CallerArgumentExpression] and the other expressions are also empty. + ? FormatCallSiteExpression(assertionMethodName, expectedExpression, collectionExpression, expression3: "", "", "", "") : FormatCallSiteExpression(assertionMethodName, expectedExpression, collectionExpression, "", ""); private sealed class NonGenericEqualityComparerAdapter : IEqualityComparer diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs index 9e9ad31b20..39ef9e9dee 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs @@ -391,6 +391,54 @@ Assertion failed. Expected collection to not contain all specified items. public void ContainsAll_Generic_NullInCollectionButNotInExpected_ShouldPass() => Assert.ContainsAll(new string?[] { "a" }, new string?[] { "a", null }); + public void ContainsAll_Generic_WithComparer_NoCallerArgumentExpression_ShouldRenderPlaceholderCallSite() + { + // Simulate a caller (e.g. another language or a wrapper) that does not propagate + // [CallerArgumentExpression] values. The call site must still render with placeholders, + // including a "" placeholder, so the failure shows which overload was invoked. + Action action = () => Assert.ContainsAll( + new[] { "A", "C" }, + new[] { "a", "b" }, + new CaseInsensitiveStringComparer(), + message: null, + expectedExpression: string.Empty, + collectionExpression: string.Empty); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to contain all specified items. + + missing: ["C"] + expected: ["A", "C"] + collection: ["a", "b"] + comparer: CaseInsensitiveStringComparer + + Assert.ContainsAll(, , ) + """); + } + + public void DoesNotContainAll_Generic_WithComparer_NoCallerArgumentExpression_ShouldRenderPlaceholderCallSite() + { + Action action = () => Assert.DoesNotContainAll( + new[] { "A" }, + new[] { "a", "b" }, + new CaseInsensitiveStringComparer(), + message: null, + expectedExpression: string.Empty, + collectionExpression: string.Empty); + action.Should().Throw() + .WithMessage( + """ + Assertion failed. Expected collection to not contain all specified items. + + expected: ["A"] + collection: ["a", "b"] + comparer: CaseInsensitiveStringComparer + + Assert.DoesNotContainAll(, , ) + """); + } + public void ContainsAll_AssertFailedException_PopulatesExpectedAndActual() { Action action = () => Assert.ContainsAll(new[] { 1, 2, 3 }, new[] { 1, 2 }); From 5c6b4879c01eae2318f94a45b0350f645ca09d2e Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 09:47:28 +0200 Subject: [PATCH 8/8] Fix comparer helper collisions in ContainsAll PR Reuse the existing non-generic comparer adapter and shared case-insensitive test comparer after main introduced equivalent helpers, which resolves the PR's duplicate type CI failures.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Assertions/Assert.ContainsAll.cs | 15 ------------ .../Assertions/AssertTests.AreAll.cs | 19 ++++++++++++--- .../Assertions/AssertTests.ContainsAll.cs | 23 ------------------- 3 files changed, 16 insertions(+), 41 deletions(-) diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs b/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs index a0c4bd760c..106568b46f 100644 --- a/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs +++ b/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs @@ -539,19 +539,4 @@ private static void ReportAssertDoesNotContainAllFailed(IEnumerable expec // support [CallerArgumentExpression] and the other expressions are also empty. ? FormatCallSiteExpression(assertionMethodName, expectedExpression, collectionExpression, expression3: "", "", "", "") : FormatCallSiteExpression(assertionMethodName, expectedExpression, collectionExpression, "", ""); - - private sealed class NonGenericEqualityComparerAdapter : IEqualityComparer - { - private readonly IEqualityComparer _comparer; - - public NonGenericEqualityComparerAdapter(IEqualityComparer comparer) - => _comparer = comparer; - - // The 'new' modifier suppresses CS0108: this instance method intentionally hides the - // static 'object.Equals(object?, object?)' (only sharing its name/signature) to satisfy - // the IEqualityComparer.Equals contract. There is nothing to override. - public new bool Equals(object? x, object? y) => _comparer.Equals(x, y); - - public int GetHashCode(object? obj) => obj is null ? 0 : _comparer.GetHashCode(obj); - } } diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreAll.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreAll.cs index 6ae04d2944..33373d1f65 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreAll.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.AreAll.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections; +using System.Runtime.CompilerServices; using AwesomeAssertions; @@ -381,9 +382,21 @@ private sealed class CaseInsensitiveStringComparer : IEqualityComparer, public int GetHashCode(string? obj) => obj is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj); - bool IEqualityComparer.Equals(object? x, object? y) => Equals(x as string, y as string); - - int IEqualityComparer.GetHashCode(object obj) => obj is string value ? GetHashCode(value) : 0; + bool IEqualityComparer.Equals(object? x, object? y) + => (x, y) switch + { + (null, null) => true, + (string left, string right) => Equals(left, right), + (null, _) or (_, null) => false, + _ => false, + }; + + int IEqualityComparer.GetHashCode(object obj) + => obj is string value + ? GetHashCode(value) + : obj is null + ? 0 + : RuntimeHelpers.GetHashCode(obj); } private sealed class NullEqualsEmptyStringComparer : IEqualityComparer diff --git a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs index 39ef9e9dee..568ea0b7ef 100644 --- a/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs @@ -474,27 +474,4 @@ public void ContainsAll_CaseInsensitiveStringComparer_NonGenericGetHashCode_With comparer.GetHashCode(value).Should().Be(RuntimeHelpers.GetHashCode(value)); } - - private sealed class CaseInsensitiveStringComparer : IEqualityComparer, IEqualityComparer - { - public bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); - - public int GetHashCode(string obj) => StringComparer.OrdinalIgnoreCase.GetHashCode(obj); - - bool IEqualityComparer.Equals(object? x, object? y) - => (x, y) switch - { - (null, null) => true, - (string left, string right) => Equals(left, right), - (null, _) or (_, null) => false, - _ => false, - }; - - int IEqualityComparer.GetHashCode(object obj) - => obj is string value - ? GetHashCode(value) - : obj is null - ? 0 - : RuntimeHelpers.GetHashCode(obj); - } }