diff --git a/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs b/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs new file mode 100644 index 0000000000..106568b46f --- /dev/null +++ b/src/TestFramework/TestFramework/Assertions/Assert.ContainsAll.cs @@ -0,0 +1,542 @@ +// 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 + // 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, "", ""); +} diff --git a/src/TestFramework/TestFramework/Assertions/Assert.cs b/src/TestFramework/TestFramework/Assertions/Assert.cs index b3c20de7f0..ed1550c0bd 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/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt index b33be34749..a10010875c 100644 --- a/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/TestFramework/TestFramework/PublicAPI/PublicAPI.Unshipped.txt @@ -14,3 +14,11 @@ static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEquivalent(T? e static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEquivalent(T? expected, T? actual, string? message = "", string! expectedExpression = "", string! actualExpression = "") -> void static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreNotEquivalent(T? notExpected, T? actual, bool strict, string? message = "", string! notExpectedExpression = "", string! actualExpression = "") -> void static Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreNotEquivalent(T? notExpected, T? actual, string? message = "", string! notExpectedExpression = "", string! actualExpression = "") -> 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 \ No newline at end of file diff --git a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx index 30692162b0..f83e8f7320 100644 --- a/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx +++ b/src/TestFramework/TestFramework/Resources/FrameworkMessages.resx @@ -494,6 +494,12 @@ Actual: {2} Expected string to not match the specified regular expression. + + Expected collection to contain all specified items. + + + Expected collection to not contain all specified items. + Expected collection to contain exactly one element. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf index f8916f09cf..d54e5c0328 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.cs.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf index 97b028d506..ca7993ca71 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.de.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf index 9098b0fd10..cb24bcb3b1 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.es.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf index 825e132acb..0173e96d42 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.fr.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf index 93dfbf878b..d302a8c4fb 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.it.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf index adcc0d0fa6..d4105787cf 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ja.xlf @@ -298,6 +298,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}。 @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}。 diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf index 726e6be85b..25220dd512 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ko.xlf @@ -298,6 +298,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} @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf index 35f61e31ed..d84a4c64b4 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pl.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf index ca7b4e1d27..9416b439f6 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.pt-BR.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf index a971123ced..ebe12cbd84 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.ru.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf index bc731fe9a4..c97e68be85 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.tr.xlf @@ -298,6 +298,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}. @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}. diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf index 0c8f340763..43c378a0c1 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hans.xlf @@ -298,6 +298,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}。 @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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} diff --git a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf index d8d8fcf1e0..245072218b 100644 --- a/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf +++ b/src/TestFramework/TestFramework/Resources/xlf/FrameworkMessages.zh-Hant.xlf @@ -298,6 +298,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}。 @@ -348,6 +353,11 @@ Expected string to contain the specified substring. + + 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}。 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 new file mode 100644 index 0000000000..568ea0b7ef --- /dev/null +++ b/test/UnitTests/TestFramework.UnitTests/Assertions/AssertTests.ContainsAll.cs @@ -0,0 +1,477 @@ +// 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 System.Runtime.CompilerServices; + +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_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 }); + 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]"); + } + + 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)); + } +}