From 9856e56b94103bd90adf5c0c27ca220c538ad6ed Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Wed, 11 Oct 2023 14:55:54 -0700 Subject: [PATCH] Squashed 'src/Microsoft.DotNet.XUnitAssert/src/' changes from c02d676a..46dfaa92 46dfaa92 xunit/xunit#2773: Add Assert.RaisesAny and Assert.RaisesAnyAsync non-generic for EventArgs c0485bdd Add additional NETSTANDARD excludeions for System.AppDomain usage b6cb339c xunit/xunit#2767: Verify types match when comparing FileSystemInfo values 03b07513 Use AppDomain.CurrentDomain.GetAssemblies() to find System.IO.FileSystemInfo type 482be8e0 xunit/xunit#2767: Special case FileSystemInfo objects by just comparing the FullName in Assert.Equivalent 78d70dec xunit/xunit#2767: Prevent stack overflow in Assert.Equivalent with a max traversal depth of 50 d0a234a5 Delay calling Dispose() on CollectionTracker's inner enumerator, for xunit/xunit#2762 96236a4c Add disable for CS8607 in AssertEqualityComparerAdapter because of nullability inconsistency 6193f9ee Move to explicit DateTime(Offset) handling in Assert.Equivalent (related: xunit/xunit#2758) 20e76223 xunit/xunit#2743: Assert.Equal with HashSet and custom comparer ignores comparer 247c1016 Mark BitArray as safe to multi-enumerate 8926c0fc Wrap AssertEqualityComparer collection logic in !XUNIT_FRAMEWORK for v2 xunit.execution.* 90d59772 xunit/xunit#2755: Assert.Equal regression for dictionaries with collection values 17c7b611 Add nullable annotation to Assert.NotNull(T?) (#64) 1886f126 xunit/xunit#2741: Ensure all exception factory methods are public git-subtree-dir: src/Microsoft.DotNet.XUnitAssert/src git-subtree-split: 46dfaa9248b7aa4c8c88e5cf6d4a6c84671a93f5 --- EqualityAsserts.cs | 4 +- EventAsserts.cs | 146 ++++++++++++- NullAsserts.cs | 4 + Sdk/ArgumentFormatter.cs | 1 + Sdk/AssertEqualityComparer.cs | 15 +- Sdk/AssertEqualityComparerAdapter.cs | 31 ++- Sdk/AssertHelper.cs | 196 ++++++++++++++---- Sdk/CollectionTracker.cs | 39 +++- Sdk/Exceptions/EquivalentException.cs | 32 +++ Sdk/Exceptions/IsAssignableFromException.cs | 2 +- .../IsNotAssignableFromException.cs | 2 +- Sdk/Exceptions/ThrowsAnyException.cs | 2 +- Sdk/Exceptions/ThrowsException.cs | 2 +- 13 files changed, 408 insertions(+), 68 deletions(-) diff --git a/EqualityAsserts.cs b/EqualityAsserts.cs index 2f24736d2b5..06512821705 100644 --- a/EqualityAsserts.cs +++ b/EqualityAsserts.cs @@ -163,7 +163,7 @@ public static void Equal( if (itemComparer != null) { - if (CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, out mismatchedIndex)) + if (CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex)) return; var expectedStartIdx = -1; @@ -610,7 +610,7 @@ public static void NotEqual( if (itemComparer != null) { int? mismatchedIndex; - if (!CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, out mismatchedIndex)) + if (!CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex)) return; formattedExpected = expectedTracker?.FormatStart() ?? "null"; diff --git a/EventAsserts.cs b/EventAsserts.cs index 1263101d0bf..07c927f8a82 100644 --- a/EventAsserts.cs +++ b/EventAsserts.cs @@ -21,7 +21,7 @@ namespace Xunit partial class Assert { /// - /// Verifies that a event with the exact event args is raised. + /// Verifies that an event with the exact event args is raised. /// /// The type of the event arguments to expect /// Code to attach the event handler @@ -45,6 +45,27 @@ public static RaisedEvent Raises( return raisedEvent; } + /// + /// Verifies that an event is raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static RaisedEvent RaisesAny( + Action attach, + Action detach, + Action testCode) + { + var raisedEvent = RaisesInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw RaisesAnyException.ForNoEvent(typeof(EventArgs)); + + return raisedEvent; + } + /// /// Verifies that an event with the exact or a derived event args is raised. /// @@ -67,6 +88,27 @@ public static RaisedEvent RaisesAny( return raisedEvent; } + /// + /// Verifies that an event is raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async Task> RaisesAnyAsync( + Action attach, + Action detach, + Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw RaisesAnyException.ForNoEvent(typeof(EventArgs)); + + return raisedEvent; + } + /// /// Verifies that an event with the exact or a derived event args is raised. /// @@ -90,6 +132,27 @@ public static async Task> RaisesAnyAsync( } #if XUNIT_VALUETASK + /// + /// Verifies that an event is raised. + /// + /// Code to attach the event handler + /// Code to detach the event handler + /// A delegate to the code to be tested + /// The event sender and arguments wrapped in an object + /// Thrown when the expected event was not raised. + public static async ValueTask> RaisesAnyAsync( + Action attach, + Action detach, + Func testCode) + { + var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); + + if (raisedEvent == null) + throw RaisesAnyException.ForNoEvent(typeof(EventArgs)); + + return raisedEvent; + } + /// /// Verifies that an event with the exact or a derived event args is raised. /// @@ -114,7 +177,7 @@ public static async ValueTask> RaisesAnyAsync( #endif /// - /// Verifies that a event with the exact event args (and not a derived type) is raised. + /// Verifies that an event with the exact event args (and not a derived type) is raised. /// /// The type of the event arguments to expect /// Code to attach the event handler @@ -140,7 +203,7 @@ public static async Task> RaisesAsync( #if XUNIT_VALUETASK /// - /// Verifies that a event with the exact event args (and not a derived type) is raised. + /// Verifies that an event with the exact event args (and not a derived type) is raised. /// /// The type of the event arguments to expect /// Code to attach the event handler @@ -163,8 +226,33 @@ public static async ValueTask> RaisesAsync( return raisedEvent; } +#endif +#if XUNIT_NULLABLE + static RaisedEvent? RaisesInternal( +#else + static RaisedEvent RaisesInternal( #endif + Action attach, + Action detach, + Action testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + +#if XUNIT_NULLABLE + RaisedEvent? raisedEvent = null; + void handler(object? s, EventArgs args) => raisedEvent = new RaisedEvent(s, args); +#else + RaisedEvent raisedEvent = null; + EventHandler handler = (object s, EventArgs args) => raisedEvent = new RaisedEvent(s, args); +#endif + attach(handler); + testCode(); + detach(handler); + return raisedEvent; + } #if XUNIT_NULLABLE static RaisedEvent? RaisesInternal( @@ -192,6 +280,32 @@ static RaisedEvent RaisesInternal( return raisedEvent; } +#if XUNIT_NULLABLE + static async Task?> RaisesAsyncInternal( +#else + static async Task> RaisesAsyncInternal( +#endif + Action attach, + Action detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + +#if XUNIT_NULLABLE + RaisedEvent? raisedEvent = null; + void handler(object? s, EventArgs args) => raisedEvent = new RaisedEvent(s, args); +#else + RaisedEvent raisedEvent = null; + EventHandler handler = (object s, EventArgs args) => raisedEvent = new RaisedEvent(s, args); +#endif + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + #if XUNIT_NULLABLE static async Task?> RaisesAsyncInternal( #else @@ -219,6 +333,32 @@ static async Task> RaisesAsyncInternal( } #if XUNIT_VALUETASK +#if XUNIT_NULLABLE + static async ValueTask?> RaisesAsyncInternal( +#else + static async Task> RaisesAsyncInternal( +#endif + Action attach, + Action detach, + Func testCode) + { + GuardArgumentNotNull(nameof(attach), attach); + GuardArgumentNotNull(nameof(detach), detach); + GuardArgumentNotNull(nameof(testCode), testCode); + +#if XUNIT_NULLABLE + RaisedEvent? raisedEvent = null; + void handler(object? s, EventArgs args) => raisedEvent = new RaisedEvent(s, args); +#else + RaisedEvent raisedEvent = null; + EventHandler handler = (object s, EventArgs args) => raisedEvent = new RaisedEvent(s, args); +#endif + attach(handler); + await testCode(); + detach(handler); + return raisedEvent; + } + #if XUNIT_NULLABLE static async ValueTask?> RaisesAsyncInternal( #else diff --git a/NullAsserts.cs b/NullAsserts.cs index 05a036b5059..1121468f3b3 100644 --- a/NullAsserts.cs +++ b/NullAsserts.cs @@ -39,7 +39,11 @@ public static void NotNull(object @object) /// The value to e validated /// The non-null value /// Thrown when the value is null +#if XUNIT_NULLABLE + public static T NotNull([NotNull] T? value) +#else public static T NotNull(T? value) +#endif where T : struct { if (!value.HasValue) diff --git a/Sdk/ArgumentFormatter.cs b/Sdk/ArgumentFormatter.cs index 895bee5f0d6..d5ebd812a13 100644 --- a/Sdk/ArgumentFormatter.cs +++ b/Sdk/ArgumentFormatter.cs @@ -519,6 +519,7 @@ static bool SafeToMultiEnumerate(IEnumerable? collection) => static bool SafeToMultiEnumerate(IEnumerable collection) => #endif collection is Array || + collection is BitArray || collection is IList || collection is IDictionary || GetSetElementType(collection) != null; diff --git a/Sdk/AssertEqualityComparer.cs b/Sdk/AssertEqualityComparer.cs index 43ddd142897..9d1e521a969 100644 --- a/Sdk/AssertEqualityComparer.cs +++ b/Sdk/AssertEqualityComparer.cs @@ -27,8 +27,7 @@ namespace Xunit.Sdk /// The type that is being compared. class AssertEqualityComparer : IEqualityComparer { - static readonly IEqualityComparer DefaultInnerComparer = new AssertEqualityComparerAdapter(new AssertEqualityComparer()); - static readonly TypeInfo NullableTypeInfo = typeof(Nullable<>).GetTypeInfo(); + internal static readonly IEqualityComparer DefaultInnerComparer = new AssertEqualityComparerAdapter(new AssertEqualityComparer()); readonly Lazy innerComparer; @@ -67,6 +66,18 @@ public bool Equals( if (x == null || y == null) return false; +#if !XUNIT_FRAMEWORK + // Collections? + using (var xTracker = x.AsNonStringTracker()) + using (var yTracker = y.AsNonStringTracker()) + { + int? _; + + if (xTracker != null && yTracker != null) + return CollectionTracker.AreCollectionsEqual(xTracker, yTracker, InnerComparer, InnerComparer == DefaultInnerComparer, out _); + } +#endif + // Implements IEquatable? var equatable = x as IEquatable; if (equatable != null) diff --git a/Sdk/AssertEqualityComparerAdapter.cs b/Sdk/AssertEqualityComparerAdapter.cs index bea61d8e8d0..ad6b3d8b65c 100644 --- a/Sdk/AssertEqualityComparerAdapter.cs +++ b/Sdk/AssertEqualityComparerAdapter.cs @@ -12,10 +12,10 @@ namespace Xunit.Sdk { /// - /// A class that wraps to create . + /// A class that wraps to add . /// /// The type that is being compared. - class AssertEqualityComparerAdapter : IEqualityComparer + class AssertEqualityComparerAdapter : IEqualityComparer, IEqualityComparer { readonly IEqualityComparer innerComparer; @@ -44,9 +44,28 @@ public AssertEqualityComparerAdapter(IEqualityComparer innerComparer) #endif /// - public int GetHashCode(object obj) - { - throw new NotImplementedException(); - } + public bool Equals( +#if XUNIT_NULLABLE + T? x, + T? y) => +#else + T x, + T y) => +#endif + innerComparer.Equals(x, y); + + + /// + public int GetHashCode(object obj) => + innerComparer.GetHashCode((T)obj); + + // This warning disable is here because sometimes IEqualityComparer.GetHashCode marks the obj parameter + // with [DisallowNull] and sometimes it doesn't, and we need to be able to support both scenarios when + // someone brings in the assertion library via source. +#pragma warning disable CS8607 + /// + public int GetHashCode(T obj) => + innerComparer.GetHashCode(obj); +#pragma warning restore CS8607 } } diff --git a/Sdk/AssertHelper.cs b/Sdk/AssertHelper.cs index d6bfc4408ca..cbf813f9542 100644 --- a/Sdk/AssertHelper.cs +++ b/Sdk/AssertHelper.cs @@ -3,6 +3,7 @@ #else // In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE #pragma warning disable CS8600 +#pragma warning disable CS8601 #pragma warning disable CS8603 #pragma warning disable CS8604 #pragma warning disable CS8621 @@ -44,6 +45,44 @@ internal static class AssertHelper static ConcurrentDictionary>> gettersByType = new ConcurrentDictionary>>(); #endif +#if XUNIT_NULLABLE + static readonly Lazy fileSystemInfoTypeInfo = new Lazy(() => GetTypeInfo("System.IO.FileSystemInfo")); + static readonly Lazy fileSystemInfoFullNameProperty = new Lazy(() => fileSystemInfoTypeInfo.Value?.GetDeclaredProperty("FullName")); +#else + static readonly Lazy fileSystemInfoTypeInfo = new Lazy(() => GetTypeInfo("System.IO.FileSystemInfo")); + static readonly Lazy fileSystemInfoFullNameProperty = new Lazy(() => fileSystemInfoTypeInfo.Value?.GetDeclaredProperty("FullName")); +#endif + + static readonly Lazy getAssemblies = new Lazy(() => + { +#if NETSTANDARD1_1 || NETSTANDARD1_2 || NETSTANDARD1_3 || NETSTANDARD1_4 || NETSTANDARD1_5 || NETSTANDARD1_6 + var appDomainType = Type.GetType("System.AppDomain"); + if (appDomainType != null) + { + var currentDomainProperty = appDomainType.GetRuntimeProperty("CurrentDomain"); + if (currentDomainProperty != null) + { + var getAssembliesMethod = appDomainType.GetRuntimeMethods().FirstOrDefault(m => m.Name == "GetAssemblies"); + if (getAssembliesMethod != null) + { + var currentDomain = currentDomainProperty.GetValue(null); + if (currentDomain != null) + { + var getAssembliesArgs = getAssembliesMethod.GetParameters().Length == 1 ? new object[] { false } : new object[0]; + var assemblies = getAssembliesMethod.Invoke(currentDomain, getAssembliesArgs) as Assembly[]; + if (assemblies != null) + return assemblies; + } + } + } + } + + return new Assembly[0]; +#else + return AppDomain.CurrentDomain.GetAssemblies(); +#endif + }); + #if XUNIT_NULLABLE static Dictionary> GetGettersForType(Type type) => #else @@ -77,6 +116,29 @@ static Dictionary> GetGettersForType(Type type) => .ToDictionary(g => g.name, g => g.getter); }); +#if XUNIT_NULLABLE + static TypeInfo? GetTypeInfo(string typeName) +#else + static TypeInfo GetTypeInfo(string typeName) +#endif + { + try + { + foreach (var assembly in getAssemblies.Value) + { + var type = assembly.GetType(typeName); + if (type != null) + return type.GetTypeInfo(); + } + + return null; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Fatal error: Exception occured while trying to retrieve type '{typeName}'", ex); + } + } + internal static string ShortenAndEncodeString( #if XUNIT_NULLABLE string? value, @@ -184,7 +246,7 @@ public static EquivalentException VerifyEquivalence( #endif bool strict) { - return VerifyEquivalence(expected, actual, strict, string.Empty, new HashSet(), new HashSet()); + return VerifyEquivalence(expected, actual, strict, string.Empty, new HashSet(), new HashSet(), 1); } #if XUNIT_NULLABLE @@ -199,8 +261,13 @@ static EquivalentException VerifyEquivalence( bool strict, string prefix, HashSet expectedRefs, - HashSet actualRefs) + HashSet actualRefs, + int depth) { + // Check for exceeded depth + if (depth == 50) + return EquivalentException.ForExceededDepth(50, prefix); + // Check for null equivalence if (expected == null) return @@ -229,53 +296,30 @@ static EquivalentException VerifyEquivalence( { var expectedType = expected.GetType(); var expectedTypeInfo = expectedType.GetTypeInfo(); + var actualType = actual.GetType(); + var actualTypeInfo = actualType.GetTypeInfo(); // Primitive types, enums and strings should just fall back to their Equals implementation if (expectedTypeInfo.IsPrimitive || expectedTypeInfo.IsEnum || expectedType == typeof(string)) return VerifyEquivalenceIntrinsics(expected, actual, prefix); - // IComparable value types should fall back to their CompareTo implementation - if (expectedTypeInfo.IsValueType) - { - // TODO: Should we support more than just IComparable? This feels like it was added solely - // to support DateTime (a built-in non-intrinsic value type). Should we support the full - // gamut of everything that we do in AssertEqualityComparer? - try - { - var expectedComparable = expected as IComparable; - if (expectedComparable != null) - return - expectedComparable.CompareTo(actual) == 0 - ? null - : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); - } - catch (Exception ex) - { - return EquivalentException.ForMemberValueMismatch(expected, actual, prefix, ex); - } + // DateTime and DateTimeOffset need to be compared via IComparable (because of a circular + // reference via the Date property). + if (expectedType == typeof(DateTime) || expectedType == typeof(DateTimeOffset)) + return VerifyEquivalenceDateTime(expected, actual, prefix); - try - { - var actualComparable = actual as IComparable; - if (actualComparable != null) - return - actualComparable.CompareTo(expected) == 0 - ? null - : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); - } - catch (Exception ex) - { - return EquivalentException.ForMemberValueMismatch(expected, actual, prefix, ex); - } - } + // FileSystemInfo has a recursion problem when getting the root directory + if (fileSystemInfoTypeInfo.Value != null) + if (fileSystemInfoTypeInfo.Value.IsAssignableFrom(expectedTypeInfo) && fileSystemInfoTypeInfo.Value.IsAssignableFrom(actualTypeInfo)) + return VerifyEquivalenceFileSystemInfo(expected, actual, strict, prefix, expectedRefs, actualRefs, depth); // Enumerables? Check equivalence of individual members var enumerableExpected = expected as IEnumerable; var enumerableActual = actual as IEnumerable; if (enumerableExpected != null && enumerableActual != null) - return VerifyEquivalenceEnumerable(enumerableExpected, enumerableActual, strict, prefix, expectedRefs, actualRefs); + return VerifyEquivalenceEnumerable(enumerableExpected, enumerableActual, strict, prefix, expectedRefs, actualRefs, depth); - return VerifyEquivalenceReference(expected, actual, strict, prefix, expectedRefs, actualRefs); + return VerifyEquivalenceReference(expected, actual, strict, prefix, expectedRefs, actualRefs, depth); } finally { @@ -284,6 +328,46 @@ static EquivalentException VerifyEquivalence( } } +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceDateTime( +#else + static EquivalentException VerifyEquivalenceDateTime( +#endif + object expected, + object actual, + string prefix) + { + try + { + var expectedComparable = expected as IComparable; + if (expectedComparable != null) + return + expectedComparable.CompareTo(actual) == 0 + ? null + : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + } + catch (Exception ex) + { + return EquivalentException.ForMemberValueMismatch(expected, actual, prefix, ex); + } + + try + { + var actualComparable = actual as IComparable; + if (actualComparable != null) + return + actualComparable.CompareTo(expected) == 0 + ? null + : EquivalentException.ForMemberValueMismatch(expected, actual, prefix); + } + catch (Exception ex) + { + return EquivalentException.ForMemberValueMismatch(expected, actual, prefix, ex); + } + + throw new InvalidOperationException($"VerifyEquivalenceDateTime was given non-DateTime(Offset) objects; typeof(expected) = {ArgumentFormatter.FormatTypeName(expected.GetType())}, typeof(actual) = {ArgumentFormatter.FormatTypeName(actual.GetType())}"); + } + #if XUNIT_NULLABLE static EquivalentException? VerifyEquivalenceEnumerable( #else @@ -294,7 +378,8 @@ static EquivalentException VerifyEquivalenceEnumerable( bool strict, string prefix, HashSet expectedRefs, - HashSet actualRefs) + HashSet actualRefs, + int depth) { #if XUNIT_NULLABLE var expectedValues = expected.Cast().ToList(); @@ -310,7 +395,7 @@ static EquivalentException VerifyEquivalenceEnumerable( { var actualIdx = 0; for (; actualIdx < actualValues.Count; ++actualIdx) - if (VerifyEquivalence(expectedValue, actualValues[actualIdx], strict, "", expectedRefs, actualRefs) == null) + if (VerifyEquivalence(expectedValue, actualValues[actualIdx], strict, "", expectedRefs, actualRefs, depth) == null) break; if (actualIdx == actualValues.Count) @@ -325,6 +410,34 @@ static EquivalentException VerifyEquivalenceEnumerable( return null; } +#if XUNIT_NULLABLE + static EquivalentException? VerifyEquivalenceFileSystemInfo( +#else + static EquivalentException VerifyEquivalenceFileSystemInfo( +#endif + object expected, + object actual, + bool strict, + string prefix, + HashSet expectedRefs, + HashSet actualRefs, + int depth) + { + if (fileSystemInfoFullNameProperty.Value == null) + throw new InvalidOperationException("Could not find 'FullName' property on type 'System.IO.FileSystemInfo'"); + + var expectedType = expected.GetType(); + var actualType = actual.GetType(); + + if (expectedType != actualType) + return EquivalentException.ForMismatchedTypes(expectedType, actualType, prefix); + + var fullName = fileSystemInfoFullNameProperty.Value.GetValue(expected); + var expectedAnonymous = new { FullName = fullName }; + + return VerifyEquivalenceReference(expectedAnonymous, actual, strict, prefix, expectedRefs, actualRefs, depth); + } + #if XUNIT_NULLABLE static EquivalentException? VerifyEquivalenceIntrinsics( #else @@ -355,7 +468,8 @@ static EquivalentException VerifyEquivalenceReference( bool strict, string prefix, HashSet expectedRefs, - HashSet actualRefs) + HashSet actualRefs, + int depth) { var prefixDot = prefix == string.Empty ? string.Empty : prefix + "."; @@ -380,7 +494,7 @@ static EquivalentException VerifyEquivalenceReference( var expectedMemberValue = kvp.Value(expected); var actualMemberValue = actualGetter(actual); - var ex = VerifyEquivalence(expectedMemberValue, actualMemberValue, strict, prefixDot + kvp.Key, expectedRefs, actualRefs); + var ex = VerifyEquivalence(expectedMemberValue, actualMemberValue, strict, prefixDot + kvp.Key, expectedRefs, actualRefs, depth + 1); if (ex != null) return ex; } diff --git a/Sdk/CollectionTracker.cs b/Sdk/CollectionTracker.cs index 3682dab641e..b1c346debd7 100644 --- a/Sdk/CollectionTracker.cs +++ b/Sdk/CollectionTracker.cs @@ -61,6 +61,8 @@ protected CollectionTracker(IEnumerable innerEnumerable) /// First value to compare /// Second value to comare /// The comparer used for individual item comparisons + /// Pass true if the is the default item + /// comparer from ; pass false, otherwise. /// The output mismatched item index when the collections are not equal /// Returns true if the collections are equal; false, otherwise. public static bool AreCollectionsEqual( @@ -72,13 +74,14 @@ public static bool AreCollectionsEqual( CollectionTracker y, #endif IEqualityComparer itemComparer, + bool isDefaultItemComparer, out int? mismatchedIndex) { mismatchedIndex = null; return CheckIfDictionariesAreEqual(x, y, itemComparer) ?? - CheckIfSetsAreEqual(x, y) ?? + CheckIfSetsAreEqual(x, y, isDefaultItemComparer ? null : itemComparer) ?? CheckIfArraysAreEqual(x, y, itemComparer, out mismatchedIndex) ?? CheckIfEnumerablesAreEqual(x, y, itemComparer, out mismatchedIndex); } @@ -239,10 +242,12 @@ static bool CheckIfEnumerablesAreEqual( static bool? CheckIfSetsAreEqual( #if XUNIT_NULLABLE CollectionTracker? x, - CollectionTracker? y) + CollectionTracker? y, + IEqualityComparer? itemComparer) #else CollectionTracker x, - CollectionTracker y) + CollectionTracker y, + IEqualityComparer itemComparer) #endif { if (x == null || y == null) @@ -259,18 +264,29 @@ static bool CheckIfEnumerablesAreEqual( var genericCompareMethod = openGenericCompareTypedSetsMethod.MakeGenericMethod(elementTypeX); #if XUNIT_NULLABLE - return (bool)genericCompareMethod.Invoke(null, new[] { x, y })!; + return (bool)genericCompareMethod.Invoke(null, new object?[] { x.InnerEnumerable, y.InnerEnumerable, itemComparer })!; #else - return (bool)genericCompareMethod.Invoke(null, new[] { x, y }); + return (bool)genericCompareMethod.Invoke(null, new object[] { x.InnerEnumerable, y.InnerEnumerable, itemComparer }); #endif } static bool CompareTypedSets( - IEnumerable enumX, - IEnumerable enumY) + ISet setX, + ISet setY, +#if XUNIT_NULLABLE + IEqualityComparer? itemComparer) +#else + IEqualityComparer itemComparer) +#endif { - var setX = new HashSet(enumX.Cast()); - var setY = new HashSet(enumY.Cast()); + if (setX.Count != setY.Count) + return false; + + if (itemComparer != null) + { + setX = new HashSet(setX, itemComparer); + setY = new HashSet(setY, itemComparer); + } return setX.SetEquals(setY); } @@ -349,7 +365,7 @@ internal CollectionTracker( /// public override void Dispose() => - enumerator?.Dispose(); + enumerator?.DisposeInternal(); /// /// Formats the collection when you have a mismatched index. The formatted result will be the section of the @@ -707,6 +723,9 @@ public Enumerator(IEnumerator innerEnumerator) public List StartItems { get; } = new List(); public void Dispose() + { } + + public void DisposeInternal() { innerEnumerator.Dispose(); } diff --git a/Sdk/Exceptions/EquivalentException.cs b/Sdk/Exceptions/EquivalentException.cs index dd73747e253..18b91373f78 100644 --- a/Sdk/Exceptions/EquivalentException.cs +++ b/Sdk/Exceptions/EquivalentException.cs @@ -44,6 +44,17 @@ static string FormatMemberNameList( public static EquivalentException ForCircularReference(string memberName) => new EquivalentException($"Assert.Equivalent() Failure: Circular reference found in '{memberName}'"); + /// + /// Creates a new instance of which shows a message that indicates + /// that the maximum comparison depth was exceeded. + /// + /// The depth reached + /// The member access which caused the failure + public static EquivalentException ForExceededDepth( + int depth, + string memberName) => + new EquivalentException($"Assert.Equivalent() Failure: Exceeded the maximum depth {depth} with '{memberName}'; check for infinite recursion or circular references"); + /// /// Creates a new instance of which shows a message that indicates /// that the list of available members does not match. @@ -144,5 +155,26 @@ public static EquivalentException ForExtraCollectionValue( "Expected: " + ArgumentFormatter.Format(expected) + Environment.NewLine + "Actual: " + ArgumentFormatter.Format(actualLeftovers) + " left over from " + ArgumentFormatter.Format(actual) ); + + /// + /// Creates a new instance of which shows a message that indicates + /// that does not match . This is typically + /// only used in special case comparison where it would be known that general comparison would fail + /// for other reasons, like two objects derived from with + /// different concrete types. + /// + /// The expected type + /// The actual type + /// The name of the member that was being inspected (may be an empty + /// string for a top-level comparison) + public static EquivalentException ForMismatchedTypes( + Type expectedType, + Type actualType, + string memberName) => + new EquivalentException( + "Assert.Equivalent() Failure: Types did not match" + (memberName == string.Empty ? string.Empty : $" in member '{memberName}'") + Environment.NewLine + + "Expected type: " + ArgumentFormatter.FormatTypeName(expectedType, fullTypeName: true) + Environment.NewLine + + "Actual type: " + ArgumentFormatter.FormatTypeName(actualType, fullTypeName: true) + ); } } diff --git a/Sdk/Exceptions/IsAssignableFromException.cs b/Sdk/Exceptions/IsAssignableFromException.cs index 2466d011ab0..a5286cae350 100644 --- a/Sdk/Exceptions/IsAssignableFromException.cs +++ b/Sdk/Exceptions/IsAssignableFromException.cs @@ -29,7 +29,7 @@ partial class IsAssignableFromException : XunitException /// /// The expected type /// The actual object value - internal static IsAssignableFromException ForIncompatibleType( + public static IsAssignableFromException ForIncompatibleType( Type expected, #if XUNIT_NULLABLE object? actual) => diff --git a/Sdk/Exceptions/IsNotAssignableFromException.cs b/Sdk/Exceptions/IsNotAssignableFromException.cs index a692d4369db..488dee77752 100644 --- a/Sdk/Exceptions/IsNotAssignableFromException.cs +++ b/Sdk/Exceptions/IsNotAssignableFromException.cs @@ -26,7 +26,7 @@ partial class IsNotAssignableFromException : XunitException /// /// The expected type /// The actual object value - internal static IsNotAssignableFromException ForCompatibleType( + public static IsNotAssignableFromException ForCompatibleType( Type expected, object actual) => new IsNotAssignableFromException( diff --git a/Sdk/Exceptions/ThrowsAnyException.cs b/Sdk/Exceptions/ThrowsAnyException.cs index 5defeb3888c..89ea54f7d06 100644 --- a/Sdk/Exceptions/ThrowsAnyException.cs +++ b/Sdk/Exceptions/ThrowsAnyException.cs @@ -35,7 +35,7 @@ partial class ThrowsAnyException : XunitException /// /// The expected exception type /// The actual exception - internal static ThrowsAnyException ForIncorrectExceptionType( + public static ThrowsAnyException ForIncorrectExceptionType( Type expected, Exception actual) => new ThrowsAnyException( diff --git a/Sdk/Exceptions/ThrowsException.cs b/Sdk/Exceptions/ThrowsException.cs index 2a5a1b00615..7fdd9c0337f 100644 --- a/Sdk/Exceptions/ThrowsException.cs +++ b/Sdk/Exceptions/ThrowsException.cs @@ -35,7 +35,7 @@ partial class ThrowsException : XunitException /// /// The expected exception type /// The actual exception - internal static ThrowsException ForIncorrectExceptionType( + public static ThrowsException ForIncorrectExceptionType( Type expected, Exception actual) => new ThrowsException(