diff --git a/.editorconfig b/.editorconfig index 985f244830c..7b22689bcf9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -227,4 +227,5 @@ dotnet_diagnostic.CA1200.severity = none # Avoid using cref tags with a prefix dotnet_diagnostic.CA1707.severity = none # Remove the underscores from type name dotnet_diagnostic.CA1720.severity = none # Identifier contains type name dotnet_diagnostic.CA1810.severity = none # Do not use static constructors +dotnet_diagnostic.CA1859.severity = none # Use concrete types when possible for improved performance dotnet_diagnostic.CA2007.severity = none # Consider calling ConfigureAwait on the awaited task diff --git a/AsyncCollectionAsserts.cs b/AsyncCollectionAsserts.cs new file mode 100644 index 00000000000..a82ff214cc2 --- /dev/null +++ b/AsyncCollectionAsserts.cs @@ -0,0 +1,466 @@ +#if NETCOREAPP3_0_OR_GREATER + +#if XUNIT_NULLABLE +#nullable enable +#endif + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit.Internal; +using Xunit.Sdk; + +namespace Xunit +{ +#if XUNIT_VISIBILITY_INTERNAL + internal +#else + public +#endif + partial class Assert + { + /// + /// Verifies that all items in the collection pass when executed against + /// action. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static void All( + IAsyncEnumerable collection, + Action action) => + All(AssertHelper.ToEnumerable(collection), action); + + /// + /// Verifies that all items in the collection pass when executed against + /// action. The item index is provided to the action, in addition to the item. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static void All( + IAsyncEnumerable collection, + Action action) => + All(AssertHelper.ToEnumerable(collection), action); + + /// + /// Verifies that all items in the collection pass when executed against + /// action. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static Task AllAsync( + IAsyncEnumerable collection, + Func action) => + AllAsync(AssertHelper.ToEnumerable(collection), action); + + /// + /// Verifies that all items in the collection pass when executed against + /// action. The item index is provided to the action, in addition to the item. + /// + /// The type of the object to be verified + /// The collection + /// The action to test each item against + /// Thrown when the collection contains at least one non-matching element + public static Task AllAsync( + IAsyncEnumerable collection, + Func action) => + AllAsync(AssertHelper.ToEnumerable(collection), action); + + /// + /// Verifies that a collection contains exactly a given number of elements, which meet + /// the criteria provided by the element inspectors. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The element inspectors, which inspect each element in turn. The + /// total number of element inspectors must exactly match the number of elements in the collection. + public static void Collection( + IAsyncEnumerable collection, + params Action[] elementInspectors) => + Collection(AssertHelper.ToEnumerable(collection), elementInspectors); + + /// + /// Verifies that a collection contains exactly a given number of elements, which meet + /// the criteria provided by the element inspectors. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The element inspectors, which inspect each element in turn. The + /// total number of element inspectors must exactly match the number of elements in the collection. + public static Task CollectionAsync( + IAsyncEnumerable collection, + params Func[] elementInspectors) => + CollectionAsync(AssertHelper.ToEnumerable(collection), elementInspectors); + + /// + /// Verifies that a collection contains a given object. + /// + /// The type of the object to be verified + /// The object expected to be in the collection + /// The collection to be inspected + /// Thrown when the object is not present in the collection + public static void Contains( + T expected, + IAsyncEnumerable collection) => + Contains(expected, AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that a collection contains a given object, using an equality comparer. + /// + /// The type of the object to be verified + /// The object expected to be in the collection + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when the object is not present in the collection + public static void Contains( + T expected, + IAsyncEnumerable collection, + IEqualityComparer comparer) => + Contains(expected, AssertHelper.ToEnumerable(collection), comparer); + + /// + /// Verifies that a collection contains a given object. + /// + /// The type of the object to be verified + /// The collection to be inspected + /// The filter used to find the item you're ensuring the collection contains + /// Thrown when the object is not present in the collection + public static void Contains( + IAsyncEnumerable collection, + Predicate filter) => + Contains(AssertHelper.ToEnumerable(collection), filter); + + /// + /// Verifies that a collection contains each object only once. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// Thrown when an object is present inside the collection more than once + public static void Distinct(IAsyncEnumerable collection) => + Distinct(AssertHelper.ToEnumerable(collection), EqualityComparer.Default); + + /// + /// Verifies that a collection contains each object only once. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when an object is present inside the collection more than once + public static void Distinct( + IAsyncEnumerable collection, + IEqualityComparer comparer) => + Distinct(AssertHelper.ToEnumerable(collection), comparer); + + /// + /// Verifies that a collection does not contain a given object. + /// + /// The type of the object to be compared + /// The object that is expected not to be in the collection + /// The collection to be inspected + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + T expected, + IAsyncEnumerable collection) => + DoesNotContain(expected, AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that a collection does not contain a given object, using an equality comparer. + /// + /// The type of the object to be compared + /// The object that is expected not to be in the collection + /// The collection to be inspected + /// The comparer used to equate objects in the collection with the expected object + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + T expected, + IAsyncEnumerable collection, + IEqualityComparer comparer) => + DoesNotContain(expected, AssertHelper.ToEnumerable(collection), comparer); + + /// + /// Verifies that a collection does not contain a given object. + /// + /// The type of the object to be compared + /// The collection to be inspected + /// The filter used to find the item you're ensuring the collection does not contain + /// Thrown when the object is present inside the collection + public static void DoesNotContain( + IAsyncEnumerable collection, + Predicate filter) => + DoesNotContain(AssertHelper.ToEnumerable(collection), filter); + + /// + /// Verifies that a collection is empty. + /// + /// The collection to be inspected + /// Thrown when the collection is null + /// Thrown when the collection is not empty + public static void Empty(IAsyncEnumerable collection) => + Empty(AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that two sequences are equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual) => +#else + IEnumerable expected, + IAsyncEnumerable actual) => +#endif + Equal(expected, AssertHelper.ToEnumerable(actual), GetEqualityComparer()); + + /// + /// Verifies that two sequences are equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual) => +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual) => +#endif + Equal(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), GetEqualityComparer()); + + /// + /// Verifies that two sequences are equivalent, using a custom equatable comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The comparer used to compare the two objects + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual, +#else + IEnumerable expected, + IAsyncEnumerable actual, +#endif + IEqualityComparer comparer) => + Equal(expected, AssertHelper.ToEnumerable(actual), GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two sequences are equivalent, using a custom equatable comparer. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The comparer used to compare the two objects + /// Thrown when the objects are not equal + public static void Equal( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual, +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual, +#endif + IEqualityComparer comparer) => + Equal(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two collections are equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void Equal( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual, +#else + IEnumerable expected, + IAsyncEnumerable actual, +#endif + Func comparer) => + Equal(expected, AssertHelper.ToEnumerable(actual), AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that two collections are equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void Equal( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual, +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual, +#endif + Func comparer) => + Equal(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that a collection is not empty. + /// + /// The collection to be inspected + /// Thrown when a null collection is passed + /// Thrown when the collection is empty + public static void NotEmpty(IAsyncEnumerable collection) => + NotEmpty(AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that two sequences are not equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual) => +#else + IEnumerable expected, + IAsyncEnumerable actual) => +#endif + NotEqual(expected, AssertHelper.ToEnumerable(actual), GetEqualityComparer()); + + /// + /// Verifies that two sequences are not equivalent, using a default comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual) => +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual) => +#endif + NotEqual(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), GetEqualityComparer()); + + /// + /// Verifies that two sequences are not equivalent, using a custom equality comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// The comparer used to compare the two objects + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual, +#else + IEnumerable expected, + IAsyncEnumerable actual, +#endif + IEqualityComparer comparer) => + NotEqual(expected, AssertHelper.ToEnumerable(actual), GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two sequences are not equivalent, using a custom equality comparer. + /// + /// The type of the objects to be compared + /// The expected object + /// The actual object + /// The comparer used to compare the two objects + /// Thrown when the objects are equal + public static void NotEqual( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual, +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual, +#endif + IEqualityComparer comparer) => + NotEqual(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), GetEqualityComparer>(new AssertEqualityComparerAdapter(comparer))); + + /// + /// Verifies that two collections are not equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void NotEqual( +#if XUNIT_NULLABLE + IEnumerable? expected, + IAsyncEnumerable? actual, +#else + IEnumerable expected, + IAsyncEnumerable actual, +#endif + Func comparer) => + NotEqual(expected, AssertHelper.ToEnumerable(actual), AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that two collections are not equal, using a comparer function against + /// items in the two collections. + /// + /// The type of the objects to be compared + /// The expected value + /// The value to be compared against + /// The function to compare two items for equality + public static void NotEqual( +#if XUNIT_NULLABLE + IAsyncEnumerable? expected, + IAsyncEnumerable? actual, +#else + IAsyncEnumerable expected, + IAsyncEnumerable actual, +#endif + Func comparer) => + NotEqual(AssertHelper.ToEnumerable(expected), AssertHelper.ToEnumerable(actual), AssertEqualityComparer.FromComparer(comparer)); + + /// + /// Verifies that the given collection contains only a single + /// element of the given type. + /// + /// The collection type. + /// The collection. + /// The single item in the collection. + /// Thrown when the collection does not contain + /// exactly one element. + public static T Single(IAsyncEnumerable collection) => + Single(AssertHelper.ToEnumerable(collection)); + + /// + /// Verifies that the given collection contains only a single + /// element of the given type which matches the given predicate. The + /// collection may or may not contain other values which do not + /// match the given predicate. + /// + /// The collection type. + /// The collection. + /// The item matching predicate. + /// The single item in the filtered collection. + /// Thrown when the filtered collection does + /// not contain exactly one element. + public static T Single( + IAsyncEnumerable collection, + Predicate predicate) => + Single(AssertHelper.ToEnumerable(collection), predicate); + } +} + +#endif diff --git a/CollectionAsserts.cs b/CollectionAsserts.cs index 96678fcfaec..d176724fb5f 100644 --- a/CollectionAsserts.cs +++ b/CollectionAsserts.cs @@ -226,13 +226,25 @@ partial class Assert { GuardArgumentNotNull(nameof(collection), collection); - // We special case HashSet because it has a custom Contains implementation that is based on the comparer - // passed into their constructors, which we don't have access to. - var hashSet = collection as HashSet; - if (hashSet != null) - Contains(expected, hashSet); - else - Contains(expected, collection, GetEqualityComparer()); + // We special case sets because they are constructed with their comparers, which we don't have access to. + // We want to let them do their normal logic when appropriate, and not try to use our default comparer. + var set = collection as ISet; + if (set != null) + { + Contains(expected, set); + return; + } +#if NET5_0_OR_GREATER + var readOnlySet = collection as IReadOnlySet; + if (readOnlySet != null) + { + Contains(expected, readOnlySet); + return; + } +#endif + + // Fall back to the assumption that this is a linear container and use our default comparer + Contains(expected, collection, GetEqualityComparer()); } /// @@ -326,13 +338,25 @@ partial class Assert { GuardArgumentNotNull(nameof(collection), collection); - // We special case HashSet because it has a custom Contains implementation that is based on the comparer - // passed into their constructors, which we don't have access to. - var hashSet = collection as HashSet; - if (hashSet != null) - DoesNotContain(expected, hashSet); - else - DoesNotContain(expected, collection, GetEqualityComparer()); + // We special case sets because they are constructed with their comparers, which we don't have access to. + // We want to let them do their normal logic when appropriate, and not try to use our default comparer. + var set = collection as ISet; + if (set != null) + { + DoesNotContain(expected, set); + return; + } +#if NET5_0_OR_GREATER + var readOnlySet = collection as IReadOnlySet; + if (readOnlySet != null) + { + DoesNotContain(expected, readOnlySet); + return; + } +#endif + + // Fall back to the assumption that this is a linear container and use our default comparer + DoesNotContain(expected, collection, GetEqualityComparer()); } /// diff --git a/Comparers.cs b/Comparers.cs index 147b3cf7e7f..f4761545b9f 100644 --- a/Comparers.cs +++ b/Comparers.cs @@ -19,10 +19,6 @@ namespace Xunit #endif partial class Assert { - static IComparer GetComparer() - where T : IComparable => - new AssertComparer(); - #if XUNIT_NULLABLE static IEqualityComparer GetEqualityComparer(IEqualityComparer? innerComparer = null) => new AssertEqualityComparer(innerComparer); @@ -30,5 +26,9 @@ static IComparer GetComparer() static IEqualityComparer GetEqualityComparer(IEqualityComparer innerComparer = null) => new AssertEqualityComparer(innerComparer); #endif + + static IComparer GetRangeComparer() + where T : IComparable => + new AssertRangeComparer(); } } diff --git a/EqualityAsserts.cs b/EqualityAsserts.cs index a3198d511aa..cd676e85e28 100644 --- a/EqualityAsserts.cs +++ b/EqualityAsserts.cs @@ -12,6 +12,7 @@ using System.Globalization; using System.Linq; using System.Reflection; +using Xunit.Internal; using Xunit.Sdk; #if XUNIT_NULLABLE @@ -176,7 +177,15 @@ partial class Assert { try { - if (CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex)) + bool result; + + // Call AssertEqualityComparer.Equals because it checks for IEquatable<> before using CollectionTracker + if (aec != null) + result = aec.Equals(expected, expectedTracker, actual, actualTracker, out mismatchedIndex); + else + result = CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex); + + if (result) return; } catch (Exception ex) @@ -251,8 +260,8 @@ partial class Assert if (expectedType != actualType) { - var expectedTypeName = expectedType == null ? "" : ArgumentFormatter.FormatTypeName(expectedType) + " "; - var actualTypeName = actualType == null ? "" : ArgumentFormatter.FormatTypeName(actualType) + " "; + var expectedTypeName = expectedType == null ? "" : (AssertHelper.IsCompilerGenerated(expectedType) ? " " : ArgumentFormatter.FormatTypeName(expectedType) + " "); + var actualTypeName = actualType == null ? "" : (AssertHelper.IsCompilerGenerated(actualType) ? " " : ArgumentFormatter.FormatTypeName(actualType) + " "); var typeNameIndent = Math.Max(expectedTypeName.Length, actualTypeName.Length); @@ -651,7 +660,15 @@ partial class Assert { try { - if (!CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex)) + bool result; + + // Call AssertEqualityComparer.Equals because it checks for IEquatable<> before using CollectionTracker + if (aec != null) + result = aec.Equals(expected, expectedTracker, actual, actualTracker, out mismatchedIndex); + else + result = CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex); + + if (!result) return; // For NotEqual that doesn't throw, pointers are irrelevant, because @@ -719,8 +736,8 @@ partial class Assert if (expectedType != actualType) { - var expectedTypeName = expectedType == null ? "" : ArgumentFormatter.FormatTypeName(expectedType) + " "; - var actualTypeName = actualType == null ? "" : ArgumentFormatter.FormatTypeName(actualType) + " "; + var expectedTypeName = expectedType == null ? "" : (AssertHelper.IsCompilerGenerated(expectedType) ? " " : ArgumentFormatter.FormatTypeName(expectedType) + " "); + var actualTypeName = actualType == null ? "" : (AssertHelper.IsCompilerGenerated(actualType) ? " " : ArgumentFormatter.FormatTypeName(actualType) + " "); var typeNameIndent = Math.Max(expectedTypeName.Length, actualTypeName.Length); diff --git a/README.md b/README.md index ab204441bdc..721110887ee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # About This Project -This project contains the xUnit.net assertion library source code, intended to be used as a Git submodule. Code here is built with a target-framework of `netstandard1.1`, and must support both `net452` and `netcoreapp1.0`. The code must be buildable by a minimum of C# 6.0. These constraints are supported by the [suggested contribution workflow](#suggested-contribution-workflow), which makes it trivial to know when you've used unavailable features. +This project contains the xUnit.net assertion library source code, intended to be used as a Git submodule. + +Code here is built with several target frameworks: `netstandard1.1` and `net6.0` for xUnit.net v2; and `netstandard2.0` and `net6.0` for xUnit.net v3. At a minimum the code needs to be able to support `net452` and later for .NET Framework, `netcoreapp1.0` and later for .NET Core, and `net5.0` and later for .NET. The minimum (and default) C# version is 6.0, unless specific features require targeting later compilers. Additionally, we compile with the full Roslyn analyzer set enabled when building for v3, so you will frequently see conditional code and/or rules being disabled as appropriate. These constraints are supported by the [suggested contribution workflow](#suggested-contribution-workflow), which aims to make it easy to know when you've used unavailable features. > _**Note:** If your PR requires a newer target framework or a newer C# language to build, please start a discussion in the related issue(s) before starting any work. PRs that arbitrarily use newer target frameworks and/or newer C# language features will need to be fixed; you may be asked to fix them, or we may fix them for you, or we may decline the PR (at our discretion)._ diff --git a/RangeAsserts.cs b/RangeAsserts.cs index 7686978dc2b..bddfdd87f56 100644 --- a/RangeAsserts.cs +++ b/RangeAsserts.cs @@ -31,7 +31,7 @@ partial class Assert T low, T high) where T : IComparable => - InRange(actual, low, high, GetComparer()); + InRange(actual, low, high, GetRangeComparer()); /// /// Verifies that a value is within a given range, using a comparer. @@ -70,7 +70,7 @@ partial class Assert T low, T high) where T : IComparable => - NotInRange(actual, low, high, GetComparer()); + NotInRange(actual, low, high, GetRangeComparer()); /// /// Verifies that a value is not within a given range, using a comparer. diff --git a/Sdk/ArgumentFormatter.cs b/Sdk/ArgumentFormatter.cs index 69928bb93ec..9ad3b22faac 100644 --- a/Sdk/ArgumentFormatter.cs +++ b/Sdk/ArgumentFormatter.cs @@ -123,8 +123,12 @@ static ArgumentFormatter() /// The string value to be escaped public static string EscapeString(string s) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(s); +#else if (s == null) throw new ArgumentNullException(nameof(s)); +#endif var builder = new StringBuilder(s.Length); for (var i = 0; i < s.Length; i++) @@ -322,7 +326,11 @@ static string FormatCharValue(char value) string.Format(CultureInfo.CurrentCulture, "{0:G17}", value); static string FormatEnumValue(object value) => +#if NETCOREAPP2_0_OR_GREATER + value.ToString()?.Replace(", ", " | ", StringComparison.Ordinal) ?? "null"; +#else value.ToString()?.Replace(", ", " | ") ?? "null"; +#endif static string FormatEnumerableValue( IEnumerable enumerable, @@ -365,7 +373,11 @@ static string FormatCharValue(char value) static string FormatStringValue(string value) { +#if NETCOREAPP2_0_OR_GREATER + value = EscapeString(value).Replace(@"""", @"\""", StringComparison.Ordinal); // escape double quotes +#else value = EscapeString(value).Replace(@"""", @"\"""); // escape double quotes +#endif if (value.Length > MAX_STRING_LENGTH) { @@ -452,7 +464,11 @@ static string FormatStringValue(string value) if (result == null) return typeInfo.Name; +#if NETCOREAPP2_1_OR_GREATER + var tickIdx = result.IndexOf('`', StringComparison.Ordinal); +#else var tickIdx = result.IndexOf('`'); +#endif if (tickIdx > 0) result = result.Substring(0, tickIdx); @@ -508,7 +524,11 @@ static bool IsAnonymousType(this TypeInfo typeInfo) if (typeInfo.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) == null) return false; +#if NETCOREAPP2_1_OR_GREATER + return typeInfo.Name.Contains("AnonymousType", StringComparison.Ordinal); +#else return typeInfo.Name.Contains("AnonymousType"); +#endif } static bool IsSZArrayType(this TypeInfo typeInfo) @@ -565,8 +585,15 @@ static bool IsSZArrayType(this TypeInfo typeInfo) return value != null; } - static Exception UnwrapException(Exception ex) +#if XUNIT_NULLABLE + internal static Exception? UnwrapException(Exception? ex) +#else + internal static Exception UnwrapException(Exception ex) +#endif { + if (ex == null) + return null; + while (true) { var tiex = ex as TargetInvocationException; diff --git a/Sdk/AssertEqualityComparer.cs b/Sdk/AssertEqualityComparer.cs index f1a3ee5ac57..ad627fe4982 100644 --- a/Sdk/AssertEqualityComparer.cs +++ b/Sdk/AssertEqualityComparer.cs @@ -15,6 +15,8 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Reflection; #if XUNIT_NULLABLE @@ -23,6 +25,84 @@ namespace Xunit.Sdk { + static class AssertEqualityComparer + { + static readonly ConcurrentDictionary cachedDefaultComparers = new ConcurrentDictionary(); + static readonly ConcurrentDictionary cachedDefaultInnerComparers = new ConcurrentDictionary(); +#if XUNIT_NULLABLE + static readonly object?[] singleNullObject = new object?[] { null }; +#else + static readonly object[] singleNullObject = new object[] { null }; +#endif + + /// + /// Gets the default comparer to be used for the provided when a custom one + /// has not been provided. Creates an instance of wrapped + /// by . + /// + /// The type to be compared + internal static IEqualityComparer GetDefaultComparer(Type type) => + cachedDefaultComparers.GetOrAdd(type, itemType => + { + var comparerType = typeof(AssertEqualityComparer<>).MakeGenericType(itemType); + var comparer = Activator.CreateInstance(comparerType, singleNullObject); + if (comparer == null) + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Could not create instance of AssertEqualityComparer<{0}>", itemType.FullName ?? itemType.Name)); + + var wrapperType = typeof(AssertEqualityComparerAdapter<>).MakeGenericType(itemType); + var result = Activator.CreateInstance(wrapperType, new object[] { comparer }) as IEqualityComparer; + if (result == null) + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Could not create instance of AssertEqualityComparerAdapter<{0}>", itemType.FullName ?? itemType.Name)); + + return result; + }); + + /// + /// Gets the default comparer to be used as an inner comparer for the provided + /// when a custom one has not been provided. For non-collections, this defaults to an -based + /// comparer; for collections, this creates an inner comparer based on the item type in the collection. + /// + /// The type to create an inner comparer for + internal static IEqualityComparer GetDefaultInnerComparer(Type type) => + cachedDefaultInnerComparers.GetOrAdd(type, t => + { + var innerType = typeof(object); + + // string is enumerable, but we don't treat it like a collection + if (t != typeof(string)) + { + var enumerableOfT = + t.GetTypeInfo() + .ImplementedInterfaces + .Select(i => i.GetTypeInfo()) + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + + if (enumerableOfT != null) + innerType = enumerableOfT.GenericTypeArguments[0]; + } + + return GetDefaultComparer(innerType); + }); + + /// + /// This exception is thrown when an operation failure has occured during equality comparison operations. + /// This generally indicates that a necessary pre-condition was not met for comparison operations to succeed. + /// + public sealed class OperationalFailureException : Exception + { + OperationalFailureException(string message) : + base(message) + { } + + /// + /// Gets an exception that indicates that GetHashCode was called on + /// which usually indicates that an item comparison function was used to try to compare two hash sets. + /// + public static OperationalFailureException ForIllegalGetHashCode() => + new OperationalFailureException("During comparison of two collections, GetHashCode was called, but only a comparison function was provided. This typically indicates trying to compare two sets with an item comparison function, which is not supported. For more information, see https://xunit.net/docs/hash-sets-vs-linear-containers"); + } + } + /// /// Default implementation of used by the xUnit.net equality assertions /// (except for collections, which are handled directly by the appropriate assertion methods). @@ -30,7 +110,7 @@ namespace Xunit.Sdk /// The type that is being compared. sealed class AssertEqualityComparer : IEqualityComparer { - internal static readonly IEqualityComparer DefaultInnerComparer = new AssertEqualityComparerAdapter(new AssertEqualityComparer()); + internal static readonly IEqualityComparer DefaultInnerComparer = AssertEqualityComparer.GetDefaultInnerComparer(typeof(T)); static readonly ConcurrentDictionary cacheOfIComparableOfT = new ConcurrentDictionary(); static readonly ConcurrentDictionary cacheOfIEquatableOfT = new ConcurrentDictionary(); @@ -64,23 +144,46 @@ public AssertEqualityComparer(IEqualityComparer innerComparer = null) T y) #endif { - // Null? - if (x == null && y == null) - return true; - if (x == null || y == null) - return false; + int? _; -#if !XUNIT_FRAMEWORK - // Collections? +#if XUNIT_FRAMEWORK + return Equals(x, y, out _); +#else using (var xTracker = x.AsNonStringTracker()) using (var yTracker = y.AsNonStringTracker()) - { - int? _; + return Equals(x, xTracker, y, yTracker, out _); +#endif + } - if (xTracker != null && yTracker != null) - return CollectionTracker.AreCollectionsEqual(xTracker, yTracker, InnerComparer, InnerComparer == DefaultInnerComparer, out _); - } + internal bool Equals( +#if XUNIT_NULLABLE + [AllowNull] T x, +#if !XUNIT_FRAMEWORK + CollectionTracker? xTracker, +#endif + [AllowNull] T y, +#if !XUNIT_FRAMEWORK + CollectionTracker? yTracker, +#endif +#else + T x, +#if !XUNIT_FRAMEWORK + CollectionTracker xTracker, +#endif + T y, +#if !XUNIT_FRAMEWORK + CollectionTracker yTracker, #endif +#endif + out int? mismatchedIndex) + { + mismatchedIndex = null; + + // Null? + if (x == null && y == null) + return true; + if (x == null || y == null) + return false; // Implements IEquatable? var equatable = x as IEquatable; @@ -109,6 +212,12 @@ public AssertEqualityComparer(IEqualityComparer innerComparer = null) } } +#if !XUNIT_FRAMEWORK + // Special case collections (before IStructuralEquatable because arrays implement that in a way we don't want to call) + if (xTracker != null && yTracker != null) + return CollectionTracker.AreCollectionsEqual(xTracker, yTracker, InnerComparer, InnerComparer == DefaultInnerComparer, out mismatchedIndex); +#endif + // Implements IStructuralEquatable? var structuralEquatable = x as IStructuralEquatable; if (structuralEquatable != null && structuralEquatable.Equals(y, new TypeErasedEqualityComparer(innerComparer.Value))) @@ -179,9 +288,35 @@ public AssertEqualityComparer(IEqualityComparer innerComparer = null) yType.IsConstructedGenericType && yType.GetGenericTypeDefinition() == typeKeyValuePair) { - return - innerComparer.Value.Equals(xType.GetRuntimeProperty("Key")?.GetValue(x), yType.GetRuntimeProperty("Key")?.GetValue(y)) && - innerComparer.Value.Equals(xType.GetRuntimeProperty("Value")?.GetValue(x), yType.GetRuntimeProperty("Value")?.GetValue(y)); + var xKey = xType.GetRuntimeProperty("Key")?.GetValue(x); + var yKey = yType.GetRuntimeProperty("Key")?.GetValue(y); + + if (xKey == null) + { + if (yKey != null) + return false; + } + else + { + var xKeyType = xKey.GetType(); + var yKeyType = yKey?.GetType(); + + var keyComparer = AssertEqualityComparer.GetDefaultComparer(xKeyType == yKeyType ? xKeyType : typeof(object)); + if (!keyComparer.Equals(xKey, yKey)) + return false; + } + + var xValue = xType.GetRuntimeProperty("Value")?.GetValue(x); + var yValue = yType.GetRuntimeProperty("Value")?.GetValue(y); + + if (xValue == null) + return yValue == null; + + var xValueType = xValue.GetType(); + var yValueType = yValue?.GetType(); + + var valueComparer = AssertEqualityComparer.GetDefaultComparer(xValueType == yValueType ? xValueType : typeof(object)); + return valueComparer.Equals(xValue, yValue); } // Last case, rely on object.Equals @@ -209,8 +344,12 @@ sealed class FuncEqualityComparer : IEqualityComparer public FuncEqualityComparer(Func comparer) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(comparer); +#else if (comparer == null) throw new ArgumentNullException(nameof(comparer)); +#endif this.comparer = comparer; } @@ -234,11 +373,13 @@ public FuncEqualityComparer(Func comparer) } #if XUNIT_NULLABLE - public int GetHashCode(T? obj) => + public int GetHashCode(T? obj) #else - public int GetHashCode(T obj) => + public int GetHashCode(T obj) #endif - GuardArgumentNotNull(nameof(obj), obj).GetHashCode(); + { + throw AssertEqualityComparer.OperationalFailureException.ForIllegalGetHashCode(); + } } sealed class TypeErasedEqualityComparer : IEqualityComparer diff --git a/Sdk/AssertEqualityComparerAdapter.cs b/Sdk/AssertEqualityComparerAdapter.cs index a5665f646fe..1bdbc56ff00 100644 --- a/Sdk/AssertEqualityComparerAdapter.cs +++ b/Sdk/AssertEqualityComparerAdapter.cs @@ -25,8 +25,12 @@ sealed class AssertEqualityComparerAdapter : IEqualityComparer, IEqualityComp /// The comparer that is being adapted. public AssertEqualityComparerAdapter(IEqualityComparer innerComparer) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(innerComparer); +#else if (innerComparer == null) throw new ArgumentNullException(nameof(innerComparer)); +#endif this.innerComparer = innerComparer; } diff --git a/Sdk/AssertHelper.cs b/Sdk/AssertHelper.cs index 5f388d38a16..c27e74173aa 100644 --- a/Sdk/AssertHelper.cs +++ b/Sdk/AssertHelper.cs @@ -23,6 +23,10 @@ using System.Diagnostics.CodeAnalysis; #endif +#if NETCOREAPP3_0_OR_GREATER +using System.Threading.Tasks; +#endif + namespace Xunit.Internal { internal static class AssertHelper @@ -140,6 +144,9 @@ static TypeInfo GetTypeInfo(string typeName) } } + internal static bool IsCompilerGenerated(Type type) => + type.GetTypeInfo().CustomAttributes.Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); + internal static string ShortenAndEncodeString( #if XUNIT_NULLABLE string? value, @@ -223,6 +230,33 @@ internal static string ShortenAndEncodeStringEnd(string value) return ShortenAndEncodeString(value, (value?.Length - 1) ?? 0, out pointerIndent); } +#if NETCOREAPP3_0_OR_GREATER + +#if XUNIT_NULLABLE + [return: NotNullIfNotNull(nameof(data))] + internal static IEnumerable? ToEnumerable(IAsyncEnumerable? data) => +#else + internal static IEnumerable ToEnumerable(IAsyncEnumerable data) => +#endif + data == null ? null : ToEnumerableImpl(data); + + static IEnumerable ToEnumerableImpl(IAsyncEnumerable data) + { + var enumerator = data.GetAsyncEnumerator(); + + try + { + while (WaitForValueTask(enumerator.MoveNextAsync())) + yield return enumerator.Current; + } + finally + { + WaitForValueTask(enumerator.DisposeAsync()); + } + } + +#endif + static bool TryConvert( object value, Type targetType, @@ -511,5 +545,29 @@ internal static string ShortenAndEncodeStringEnd(string value) return null; } + +#if NETCOREAPP3_0_OR_GREATER + + static void WaitForValueTask(ValueTask valueTask) + { + var valueTaskAwaiter = valueTask.GetAwaiter(); + if (valueTaskAwaiter.IsCompleted) + return; + + // Let the task complete on a thread pool thread while we block the main thread + Task.Run(valueTask.AsTask).GetAwaiter().GetResult(); + } + + static T WaitForValueTask(ValueTask valueTask) + { + var valueTaskAwaiter = valueTask.GetAwaiter(); + if (valueTaskAwaiter.IsCompleted) + return valueTaskAwaiter.GetResult(); + + // Let the task complete on a thread pool thread while we block the main thread + return Task.Run(valueTask.AsTask).GetAwaiter().GetResult(); + } + +#endif } } diff --git a/Sdk/AssertComparer.cs b/Sdk/AssertRangeComparer.cs similarity index 95% rename from Sdk/AssertComparer.cs rename to Sdk/AssertRangeComparer.cs index 4ded0911e58..aad8bc836d9 100644 --- a/Sdk/AssertComparer.cs +++ b/Sdk/AssertRangeComparer.cs @@ -18,7 +18,7 @@ namespace Xunit.Sdk /// Default implementation of used by the xUnit.net range assertions. /// /// The type that is being compared. - sealed class AssertComparer : IComparer + sealed class AssertRangeComparer : IComparer where T : IComparable { /// diff --git a/Sdk/CollectionTracker.cs b/Sdk/CollectionTracker.cs index 4384ff44ed2..1c8cd475eca 100644 --- a/Sdk/CollectionTracker.cs +++ b/Sdk/CollectionTracker.cs @@ -40,8 +40,12 @@ abstract class CollectionTracker : IDisposable /// protected CollectionTracker(IEnumerable innerEnumerable) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(innerEnumerable); +#else if (innerEnumerable == null) throw new ArgumentNullException(nameof(innerEnumerable)); +#endif InnerEnumerable = innerEnumerable; } @@ -86,7 +90,7 @@ protected CollectionTracker(IEnumerable innerEnumerable) mismatchedIndex = null; return - CheckIfDictionariesAreEqual(x, y, itemComparer) ?? + CheckIfDictionariesAreEqual(x, y) ?? CheckIfSetsAreEqual(x, y, isDefaultItemComparer ? null : itemComparer) ?? CheckIfArraysAreEqual(x, y, itemComparer, isDefaultItemComparer, out mismatchedIndex) ?? CheckIfEnumerablesAreEqual(x, y, itemComparer, isDefaultItemComparer, out mismatchedIndex); @@ -150,12 +154,11 @@ protected CollectionTracker(IEnumerable innerEnumerable) static bool? CheckIfDictionariesAreEqual( #if XUNIT_NULLABLE CollectionTracker? x, - CollectionTracker? y, + CollectionTracker? y) #else CollectionTracker x, - CollectionTracker y, + CollectionTracker y) #endif - IEqualityComparer itemComparer) { if (x == null || y == null) return null; @@ -171,6 +174,9 @@ protected CollectionTracker(IEnumerable innerEnumerable) var dictionaryYKeys = new HashSet(dictionaryY.Keys.Cast()); + // We don't pass along the itemComparer from AreCollectionsEqual because we aren't directly + // comparing the KeyValuePair<> objects. Instead we rely on Contains() on the dictionary to + // match up keys, and then create type-appropriate comparers for the values. foreach (var key in dictionaryX.Keys.Cast()) { if (!dictionaryYKeys.Contains(key)) @@ -179,8 +185,22 @@ protected CollectionTracker(IEnumerable innerEnumerable) var valueX = dictionaryX[key]; var valueY = dictionaryY[key]; - if (!itemComparer.Equals(valueX, valueY)) + if (valueX == null) + { + if (valueY != null) + return false; + } + else if (valueY == null) return false; + else + { + var valueXType = valueX.GetType(); + var valueYType = valueY.GetType(); + + var comparer = AssertEqualityComparer.GetDefaultComparer(valueXType == valueYType ? valueXType : typeof(object)); + if (!comparer.Equals(valueX, valueY)) + return false; + } dictionaryYKeys.Remove(key); } @@ -416,8 +436,12 @@ sealed class CollectionTracker : CollectionTracker, IEnumerable IEnumerable castCollection) : base(collection) { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(castCollection); +#else if (castCollection == null) throw new ArgumentNullException(nameof(castCollection)); +#endif this.collection = castCollection; } diff --git a/Sdk/Exceptions/AllException.cs b/Sdk/Exceptions/AllException.cs index 2bd1faa91d5..ae707a8fbfc 100644 --- a/Sdk/Exceptions/AllException.cs +++ b/Sdk/Exceptions/AllException.cs @@ -58,10 +58,18 @@ partial class AllException : XunitException CultureInfo.CurrentCulture, "{0}Item: {1}{2}{3}Error: {4}", string.Format(CultureInfo.CurrentCulture, "[{0}]:", error.Item1).PadRight(maxItemIndexLength), +#if NETCOREAPP2_0_OR_GREATER + error.Item2.Replace(Environment.NewLine, wrapSpaces, StringComparison.Ordinal), +#else error.Item2.Replace(Environment.NewLine, wrapSpaces), +#endif Environment.NewLine, indexSpaces, +#if NETCOREAPP2_0_OR_GREATER + error.Item3.Message.Replace(Environment.NewLine, wrapSpaces, StringComparison.Ordinal) +#else error.Item3.Message.Replace(Environment.NewLine, wrapSpaces) +#endif ) ) ) diff --git a/Sdk/Exceptions/CollectionException.cs b/Sdk/Exceptions/CollectionException.cs index 01903912dba..eb55146260a 100644 --- a/Sdk/Exceptions/CollectionException.cs +++ b/Sdk/Exceptions/CollectionException.cs @@ -1,10 +1,14 @@ #if XUNIT_NULLABLE #nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 #endif using System; using System.Globalization; using System.Linq; +using Xunit.Internal; namespace Xunit.Sdk { @@ -18,16 +22,27 @@ namespace Xunit.Sdk #endif partial class CollectionException : XunitException { + static readonly char[] crlfSeparators = new[] { '\r', '\n' }; + CollectionException(string message) : base(message) { } static string FormatInnerException(Exception innerException) { + var text = innerException.Message; + var filteredStack = ExceptionUtility.TransformStackTrace(ExceptionUtility.FilterStackTrace(innerException.StackTrace), " "); + if (!string.IsNullOrWhiteSpace(filteredStack)) + { + if (text.Length != 0) + text += Environment.NewLine; + + text += "Stack Trace:" + Environment.NewLine + filteredStack; + } + var lines = - innerException - .Message - .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + text + .Split(crlfSeparators, StringSplitOptions.RemoveEmptyEntries) .Select((value, idx) => idx > 0 ? " " + value : value); return string.Join(Environment.NewLine, lines); diff --git a/Sdk/Exceptions/EqualException.cs b/Sdk/Exceptions/EqualException.cs index d47eb305ac0..8a94e09c370 100644 --- a/Sdk/Exceptions/EqualException.cs +++ b/Sdk/Exceptions/EqualException.cs @@ -102,6 +102,10 @@ partial class EqualException : XunitException { Assert.GuardArgumentNotNull(nameof(actual), actual); + error = ArgumentFormatter.UnwrapException(error); + if (error is AssertEqualityComparer.OperationalFailureException) + return new EqualException("Assert.Equal() Failure: " + error.Message); + var message = error == null ? string.Format(CultureInfo.CurrentCulture, "Assert.Equal() Failure: {0} differ", collectionDisplay ?? "Collections") @@ -226,9 +230,17 @@ partial class EqualException : XunitException "{0}{1}Expected: {2}{3}Actual: {4}", message, Environment.NewLine, +#if NETCOREAPP2_0_OR_GREATER + expectedText.Replace(Environment.NewLine, newLineAndIndent, StringComparison.Ordinal), +#else expectedText.Replace(Environment.NewLine, newLineAndIndent), +#endif Environment.NewLine, +#if NETCOREAPP2_0_OR_GREATER + actualText.Replace(Environment.NewLine, newLineAndIndent, StringComparison.Ordinal) +#else actualText.Replace(Environment.NewLine, newLineAndIndent) +#endif ), error ); diff --git a/Sdk/Exceptions/ExceptionUtility.cs b/Sdk/Exceptions/ExceptionUtility.cs new file mode 100644 index 00000000000..d0ed65a00a3 --- /dev/null +++ b/Sdk/Exceptions/ExceptionUtility.cs @@ -0,0 +1,99 @@ +#if XUNIT_NULLABLE +#nullable enable +#else +// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8603 +#endif + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Xunit.Internal +{ + // Adapted from ExceptionUtility (xunit.v3.common) and StackFrameTransformer (xunit.v3.runner.common) + internal static class ExceptionUtility + { + static readonly Regex transformRegex; + + static ExceptionUtility() + { + transformRegex = new Regex(@"^\s*at (?.*) in (?.*):(line )?(?\d+)$"); + } + + static bool FilterStackFrame(string stackFrame) + { + Assert.GuardArgumentNotNull(nameof(stackFrame), stackFrame); + +#if DEBUG + return false; +#else + return stackFrame.StartsWith("at Xunit.", StringComparison.Ordinal); +#endif + } + +#if XUNIT_NULLABLE + public static string? FilterStackTrace(string? stack) +#else + public static string FilterStackTrace(string stack) +#endif + { + if (stack == null) + return null; + + var results = new List(); + + foreach (var line in stack.Split(new[] { Environment.NewLine }, StringSplitOptions.None)) + { + var trimmedLine = line.TrimStart(); + if (!FilterStackFrame(trimmedLine)) + results.Add(line); + } + + return string.Join(Environment.NewLine, results.ToArray()); + } + +#if XUNIT_NULLABLE + public static string? TransformStackFrame( + string? stackFrame, +#else + public static string TransformStackFrame( + string stackFrame, +#endif + string indent = "") + { + if (stackFrame == null) + return null; + + var match = transformRegex.Match(stackFrame); + if (match == Match.Empty) + return stackFrame; + + var file = match.Groups["file"].Value; + return string.Format(CultureInfo.InvariantCulture, "{0}{1}({2},0): at {3}", indent, file, match.Groups["line"].Value, match.Groups["method"].Value); + } + +#if XUNIT_NULLABLE + public static string? TransformStackTrace( + string? stack, +#else + public static string TransformStackTrace( + string stack, +#endif + string indent = "") + { + if (stack == null) + return null; + + return string.Join( + Environment.NewLine, + stack + .Split(new[] { Environment.NewLine }, StringSplitOptions.None) + .Select(frame => TransformStackFrame(frame, indent)) + .ToArray() + ); + } + } +} diff --git a/Sdk/Exceptions/NotEqualException.cs b/Sdk/Exceptions/NotEqualException.cs index 04030cd8393..142c53a18e7 100644 --- a/Sdk/Exceptions/NotEqualException.cs +++ b/Sdk/Exceptions/NotEqualException.cs @@ -79,6 +79,10 @@ partial class NotEqualException : XunitException Assert.GuardArgumentNotNull(nameof(expected), expected); Assert.GuardArgumentNotNull(nameof(actual), actual); + error = ArgumentFormatter.UnwrapException(error); + if (error is AssertEqualityComparer.OperationalFailureException) + return new NotEqualException("Assert.NotEqual() Failure: " + error.Message); + var message = error == null ? string.Format(CultureInfo.CurrentCulture, "Assert.NotEqual() Failure: {0} are equal", collectionDisplay ?? "Collections") diff --git a/SetAsserts.cs b/SetAsserts.cs index 663a59e8d95..f7bd5a3de1b 100644 --- a/SetAsserts.cs +++ b/SetAsserts.cs @@ -77,6 +77,18 @@ partial class Assert HashSet set) => Contains(expected, (ISet)set); + /// + /// Verifies that the sorted hashset contains the given object. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void Contains( + T expected, + SortedSet set) => + Contains(expected, (ISet)set); + #if XUNIT_IMMUTABLE_COLLECTIONS /// /// Verifies that the immutable hashset contains the given object. @@ -89,6 +101,18 @@ partial class Assert T expected, ImmutableHashSet set) => Contains(expected, (ISet)set); + + /// + /// Verifies that the immutable sorted hashset contains the given object. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void Contains( + T expected, + ImmutableSortedSet set) => + Contains(expected, (ISet)set); #endif /// @@ -145,6 +169,18 @@ partial class Assert HashSet set) => DoesNotContain(expected, (ISet)set); + /// + /// Verifies that the sorted hashset does not contain the given item. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void DoesNotContain( + T expected, + SortedSet set) => + DoesNotContain(expected, (ISet)set); + #if XUNIT_IMMUTABLE_COLLECTIONS /// /// Verifies that the immutable hashset does not contain the given item. @@ -157,6 +193,18 @@ partial class Assert T expected, ImmutableHashSet set) => DoesNotContain(expected, (ISet)set); + + /// + /// Verifies that the immutable sorted hashset does not contain the given item. + /// + /// The type of the object to be verified + /// The object expected to be in the set + /// The set to be inspected + /// Thrown when the object is not present in the set + public static void DoesNotContain( + T expected, + ImmutableSortedSet set) => + DoesNotContain(expected, (ISet)set); #endif /// diff --git a/SpanAsserts.cs b/SpanAsserts.cs index c299d186d9c..d9b73284fec 100644 --- a/SpanAsserts.cs +++ b/SpanAsserts.cs @@ -5,7 +5,6 @@ #endif using System; -using System.Globalization; using Xunit.Sdk; namespace Xunit @@ -24,108 +23,6 @@ partial class Assert // Also note that these classes will convert nulls into empty arrays automatically, since there // is no way to represent a null readonly struct. - /// - /// Verifies that a span contains a given sub-span, using the default comparison type. - /// - /// The sub-span expected to be in the span - /// The span to be inspected - /// Thrown when the sub-span is not present inside the span - public static void Contains( - Span expectedSubSpan, - Span actualSpan) => - Contains((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span contains a given sub-span, using the default comparison type. - /// - /// The sub-span expected to be in the span - /// The span to be inspected - /// Thrown when the sub-span is not present inside the span - public static void Contains( - Span expectedSubSpan, - ReadOnlySpan actualSpan) => - Contains((ReadOnlySpan)expectedSubSpan, actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span contains a given sub-span, using the default comparison type. - /// - /// The sub-span expected to be in the span - /// The span to be inspected - /// Thrown when the sub-span is not present inside the span - public static void Contains( - ReadOnlySpan expectedSubSpan, - Span actualSpan) => - Contains(expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span contains a given sub-span, using the default comparison type. - /// - /// The sub-span expected to be in the span - /// The span to be inspected - /// Thrown when the sub-span is not present inside the span - public static void Contains( - ReadOnlySpan expectedSubSpan, - ReadOnlySpan actualSpan) => - Contains(expectedSubSpan, actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span contains a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be in the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-span is not present inside the span - public static void Contains( - Span expectedSubSpan, - Span actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - Contains((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, comparisonType); - - /// - /// Verifies that a span contains a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be in the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-span is not present inside the span - public static void Contains( - Span expectedSubSpan, - ReadOnlySpan actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - Contains((ReadOnlySpan)expectedSubSpan, actualSpan, comparisonType); - - /// - /// Verifies that a span contains a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be in the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-span is not present inside the span - public static void Contains( - ReadOnlySpan expectedSubSpan, - Span actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - Contains(expectedSubSpan, (ReadOnlySpan)actualSpan, comparisonType); - - /// - /// Verifies that a span contains a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be in the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-span is not present inside the span - public static void Contains( - ReadOnlySpan expectedSubSpan, - ReadOnlySpan actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) - { - if (actualSpan.IndexOf(expectedSubSpan, comparisonType) < 0) - throw ContainsException.ForSubStringNotFound( - expectedSubSpan.ToString(), - actualSpan.ToString() - ); - } - /// /// Verifies that a span contains a given sub-span /// @@ -180,106 +77,6 @@ partial class Assert ); } - /// - /// Verifies that a span does not contain a given sub-span, using the default comparison type. - /// - /// The sub-span expected not to be in the span - /// The span to be inspected - /// Thrown when the sub-span is present inside the span - public static void DoesNotContain( - Span expectedSubSpan, - Span actualSpan) => - DoesNotContain((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span does not contain a given sub-span, using the default comparison type. - /// - /// The sub-span expected not to be in the span - /// The span to be inspected - /// Thrown when the sub-span is present inside the span - public static void DoesNotContain( - Span expectedSubSpan, - ReadOnlySpan actualSpan) => - DoesNotContain((ReadOnlySpan)expectedSubSpan, actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span does not contain a given sub-span, using the default comparison type. - /// - /// The sub-span expected not to be in the span - /// The span to be inspected - /// Thrown when the sub-span is present inside the span - public static void DoesNotContain( - ReadOnlySpan expectedSubSpan, - Span actualSpan) => - DoesNotContain(expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span does not contain a given sub-span, using the default comparison type. - /// - /// The sub-span expected not to be in the span - /// The span to be inspected - /// Thrown when the sub-span is present inside the span - public static void DoesNotContain( - ReadOnlySpan expectedSubSpan, - ReadOnlySpan actualSpan) => - DoesNotContain((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span does not contain a given sub-span, using the given comparison type. - /// - /// The sub-span expected not to be in the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-span is present inside the span - public static void DoesNotContain( - Span expectedSubSpan, - Span actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - DoesNotContain((ReadOnlySpan)expectedSubSpan, (ReadOnlySpan)actualSpan, comparisonType); - - /// - /// Verifies that a span does not contain a given sub-span, using the given comparison type. - /// - /// The sub-span expected not to be in the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-span is present inside the span - public static void DoesNotContain( - Span expectedSubSpan, - ReadOnlySpan actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - DoesNotContain((ReadOnlySpan)expectedSubSpan, actualSpan, comparisonType); - - /// - /// Verifies that a span does not contain a given sub-span, using the given comparison type. - /// - /// The sub-span expected not to be in the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-span is present inside the span - public static void DoesNotContain( - ReadOnlySpan expectedSubSpan, - Span actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - DoesNotContain(expectedSubSpan, (ReadOnlySpan)actualSpan, comparisonType); - - /// - /// Verifies that a span does not contain a given sub-span, using the given comparison type. - /// - /// The sub-span expected not to be in the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the sub-span is present inside the span - public static void DoesNotContain( - ReadOnlySpan expectedSubSpan, - ReadOnlySpan actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) - { - var idx = actualSpan.IndexOf(expectedSubSpan, comparisonType); - if (idx > -1) - throw DoesNotContainException.ForSubStringFound(expectedSubSpan.ToString(), idx, actualSpan.ToString()); - } - /// /// Verifies that a span does not contain a given sub-span /// @@ -343,280 +140,6 @@ partial class Assert } } - /// - /// Verifies that a span ends with a given sub-span, using the default StringComparison.CurrentCulture comparison type. - /// - /// The sub-span expected to be at the end of the span - /// The span to be inspected - /// Thrown when the span does not end with the expected subspan - public static void EndsWith( - Span expectedEndSpan, - Span actualSpan) => - EndsWith((ReadOnlySpan)expectedEndSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span ends with a given sub-span, using the default StringComparison.CurrentCulture comparison type. - /// - /// The sub-span expected to be at the end of the span - /// The span to be inspected - /// Thrown when the span does not end with the expected subspan - public static void EndsWith( - Span expectedEndSpan, - ReadOnlySpan actualSpan) => - EndsWith((ReadOnlySpan)expectedEndSpan, actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span ends with a given sub-span, using the default StringComparison.CurrentCulture comparison type. - /// - /// The sub-span expected to be at the end of the span - /// The span to be inspected - /// Thrown when the span does not end with the expected subspan - public static void EndsWith( - ReadOnlySpan expectedEndSpan, - Span actualSpan) => - EndsWith(expectedEndSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span ends with a given sub-span, using the default StringComparison.CurrentCulture comparison type. - /// - /// The sub-span expected to be at the end of the span - /// The span to be inspected - /// Thrown when the span does not end with the expected subspan - public static void EndsWith( - ReadOnlySpan expectedEndSpan, - ReadOnlySpan actualSpan) => - EndsWith(expectedEndSpan, actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span ends with a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be at the end of the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the span does not end with the expected subspan - public static void EndsWith( - Span expectedEndSpan, - Span actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - EndsWith((ReadOnlySpan)expectedEndSpan, (ReadOnlySpan)actualSpan, comparisonType); - - /// - /// Verifies that a span ends with a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be at the end of the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the span does not end with the expected subspan - public static void EndsWith( - Span expectedEndSpan, - ReadOnlySpan actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - EndsWith((ReadOnlySpan)expectedEndSpan, actualSpan, comparisonType); - - /// - /// Verifies that a span ends with a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be at the end of the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the span does not end with the expected subspan - public static void EndsWith( - ReadOnlySpan expectedEndSpan, - Span actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - EndsWith(expectedEndSpan, (ReadOnlySpan)actualSpan, comparisonType); - - /// - /// Verifies that a span ends with a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be at the end of the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the span does not end with the expected subspan - public static void EndsWith( - ReadOnlySpan expectedEndSpan, - ReadOnlySpan actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) - { - if (!actualSpan.EndsWith(expectedEndSpan, comparisonType)) - throw EndsWithException.ForStringNotFound(expectedEndSpan.ToString(), actualSpan.ToString()); - } - - /// - /// Verifies that two spans are equivalent. - /// - /// The expected span value. - /// The actual span value. - /// Thrown when the spans are not equivalent. - public static void Equal( - Span expectedSpan, - Span actualSpan) => - Equal((ReadOnlySpan)expectedSpan, (ReadOnlySpan)actualSpan, false, false, false, false); - - /// - /// Verifies that two spans are equivalent. - /// - /// The expected span value. - /// The actual span value. - /// Thrown when the spans are not equivalent. - public static void Equal( - Span expectedSpan, - ReadOnlySpan actualSpan) => - Equal((ReadOnlySpan)expectedSpan, actualSpan, false, false, false, false); - - /// - /// Verifies that two spans are equivalent. - /// - /// The expected span value. - /// The actual span value. - /// Thrown when the spans are not equivalent. - public static void Equal( - ReadOnlySpan expectedSpan, - Span actualSpan) => - Equal(expectedSpan, (ReadOnlySpan)actualSpan, false, false, false, false); - - /// - /// Verifies that two spans are equivalent. - /// - /// The expected span value. - /// The actual span value. - /// Thrown when the spans are not equivalent. - public static void Equal( - ReadOnlySpan expectedSpan, - ReadOnlySpan actualSpan) => - Equal(expectedSpan, actualSpan, false, false, false, false); - - /// - /// Verifies that two spans are equivalent. - /// - /// The expected span value. - /// The actual span value. - /// If set to true, ignores cases differences. The invariant culture is used. - /// If set to true, treats \r\n, \r, and \n as equivalent. - /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. - /// If set to true, ignores all white space differences during comparison. - /// Thrown when the spans are not equivalent. - public static void Equal( - Span expectedSpan, - Span actualSpan, - bool ignoreCase = false, - bool ignoreLineEndingDifferences = false, - bool ignoreWhiteSpaceDifferences = false, - bool ignoreAllWhiteSpace = false) => - Equal((ReadOnlySpan)expectedSpan, (ReadOnlySpan)actualSpan, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); - - /// - /// Verifies that two spans are equivalent. - /// - /// The expected span value. - /// The actual span value. - /// If set to true, ignores cases differences. The invariant culture is used. - /// If set to true, treats \r\n, \r, and \n as equivalent. - /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. - /// If set to true, ignores all white space differences during comparison. - /// Thrown when the spans are not equivalent. - public static void Equal( - Span expectedSpan, - ReadOnlySpan actualSpan, - bool ignoreCase = false, - bool ignoreLineEndingDifferences = false, - bool ignoreWhiteSpaceDifferences = false, - bool ignoreAllWhiteSpace = false) => - Equal((ReadOnlySpan)expectedSpan, actualSpan, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); - - /// - /// Verifies that two spans are equivalent. - /// - /// The expected span value. - /// The actual span value. - /// If set to true, ignores cases differences. The invariant culture is used. - /// If set to true, treats \r\n, \r, and \n as equivalent. - /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. - /// If set to true, removes all whitespaces and tabs before comparing. - /// Thrown when the spans are not equivalent. - public static void Equal( - ReadOnlySpan expectedSpan, - Span actualSpan, - bool ignoreCase = false, - bool ignoreLineEndingDifferences = false, - bool ignoreWhiteSpaceDifferences = false, - bool ignoreAllWhiteSpace = false) => - Equal(expectedSpan, (ReadOnlySpan)actualSpan, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); - - /// - /// Verifies that two spans are equivalent. - /// - /// The expected span value. - /// The actual span value. - /// If set to true, ignores cases differences. The invariant culture is used. - /// If set to true, treats \r\n, \r, and \n as equivalent. - /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. - /// If set to true, ignores all white space differences during comparison. - /// Thrown when the spans are not equivalent. - public static void Equal( - ReadOnlySpan expectedSpan, - ReadOnlySpan actualSpan, - bool ignoreCase = false, - bool ignoreLineEndingDifferences = false, - bool ignoreWhiteSpaceDifferences = false, - bool ignoreAllWhiteSpace = false) - { - // Walk the string, keeping separate indices since we can skip variable amounts of - // data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences. - var expectedIndex = 0; - var actualIndex = 0; - var expectedLength = expectedSpan.Length; - var actualLength = actualSpan.Length; - - // Block used to fix edge case of Equal("", " ") when ignoreAllWhiteSpace enabled. - if (ignoreAllWhiteSpace) - { - if (expectedLength == 0 && SkipWhitespace(actualSpan, 0) == actualLength) - return; - if (actualLength == 0 && SkipWhitespace(expectedSpan, 0) == expectedLength) - return; - } - - while (expectedIndex < expectedLength && actualIndex < actualLength) - { - var expectedChar = expectedSpan[expectedIndex]; - var actualChar = actualSpan[actualIndex]; - - if (ignoreLineEndingDifferences && IsLineEnding(expectedChar) && IsLineEnding(actualChar)) - { - expectedIndex = SkipLineEnding(expectedSpan, expectedIndex); - actualIndex = SkipLineEnding(actualSpan, actualIndex); - } - else if (ignoreAllWhiteSpace && (IsWhiteSpace(expectedChar) || IsWhiteSpace(actualChar))) - { - expectedIndex = SkipWhitespace(expectedSpan, expectedIndex); - actualIndex = SkipWhitespace(actualSpan, actualIndex); - } - else if (ignoreWhiteSpaceDifferences && IsWhiteSpace(expectedChar) && IsWhiteSpace(actualChar)) - { - expectedIndex = SkipWhitespace(expectedSpan, expectedIndex); - actualIndex = SkipWhitespace(actualSpan, actualIndex); - } - else - { - if (ignoreCase) - { - expectedChar = char.ToUpperInvariant(expectedChar); - actualChar = char.ToUpperInvariant(actualChar); - } - - if (expectedChar != actualChar) - break; - - expectedIndex++; - actualIndex++; - } - } - - if (expectedIndex < expectedLength || actualIndex < actualLength) - throw EqualException.ForMismatchedStrings(expectedSpan.ToString(), actualSpan.ToString(), expectedIndex, actualIndex); - } - /// /// Verifies that two spans are equivalent. /// @@ -667,155 +190,6 @@ partial class Assert if (!expectedSpan.SequenceEqual(actualSpan)) Equal(expectedSpan.ToArray(), actualSpan.ToArray()); } - - /// - /// Verifies that a span starts with a given sub-span, using the default StringComparison.CurrentCulture comparison type. - /// - /// The sub-span expected to be at the start of the span - /// The span to be inspected - /// Thrown when the span does not start with the expected subspan - public static void StartsWith( - Span expectedStartSpan, - Span actualSpan) => - StartsWith((ReadOnlySpan)expectedStartSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span starts with a given sub-span, using the default StringComparison.CurrentCulture comparison type. - /// - /// The sub-span expected to be at the start of the span - /// The span to be inspected - /// Thrown when the span does not start with the expected subspan - public static void StartsWith( - Span expectedStartSpan, - ReadOnlySpan actualSpan) => - StartsWith((ReadOnlySpan)expectedStartSpan, actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span starts with a given sub-span, using the default StringComparison.CurrentCulture comparison type. - /// - /// The sub-span expected to be at the start of the span - /// The span to be inspected - /// Thrown when the span does not start with the expected subspan - public static void StartsWith( - ReadOnlySpan expectedStartSpan, - Span actualSpan) => - StartsWith(expectedStartSpan, (ReadOnlySpan)actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span starts with a given sub-span, using the default StringComparison.CurrentCulture comparison type. - /// - /// The sub-span expected to be at the start of the span - /// The span to be inspected - /// Thrown when the span does not start with the expected subspan - public static void StartsWith( - ReadOnlySpan expectedStartSpan, - ReadOnlySpan actualSpan) => - StartsWith(expectedStartSpan, actualSpan, StringComparison.CurrentCulture); - - /// - /// Verifies that a span starts with a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be at the start of the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the span does not start with the expected subspan - public static void StartsWith( - Span expectedStartSpan, - Span actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - StartsWith((ReadOnlySpan)expectedStartSpan, (ReadOnlySpan)actualSpan, comparisonType); - - /// - /// Verifies that a span starts with a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be at the start of the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the span does not start with the expected subspan - public static void StartsWith( - Span expectedStartSpan, - ReadOnlySpan actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - StartsWith((ReadOnlySpan)expectedStartSpan, actualSpan, comparisonType); - - /// - /// Verifies that a span starts with a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be at the start of the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the span does not start with the expected subspan - public static void StartsWith( - ReadOnlySpan expectedStartSpan, - Span actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) => - StartsWith(expectedStartSpan, (ReadOnlySpan)actualSpan, comparisonType); - - /// - /// Verifies that a span starts with a given sub-span, using the given comparison type. - /// - /// The sub-span expected to be at the start of the span - /// The span to be inspected - /// The type of string comparison to perform - /// Thrown when the span does not start with the expected subspan - public static void StartsWith( - ReadOnlySpan expectedStartSpan, - ReadOnlySpan actualSpan, - StringComparison comparisonType = StringComparison.CurrentCulture) - { - if (!actualSpan.StartsWith(expectedStartSpan, comparisonType)) - throw StartsWithException.ForStringNotFound(expectedStartSpan.ToString(), actualSpan.ToString()); - } - - // ReadOnlySpan helper methods - - static bool IsLineEnding(char c) => - c == '\r' || c == '\n'; - - static bool IsWhiteSpace(char c) - { - const char mongolianVowelSeparator = '\u180E'; - const char zeroWidthSpace = '\u200B'; - const char zeroWidthNoBreakSpace = '\uFEFF'; - const char tabulation = '\u0009'; - - var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); - - return - unicodeCategory == UnicodeCategory.SpaceSeparator || - c == mongolianVowelSeparator || - c == zeroWidthSpace || - c == zeroWidthNoBreakSpace || - c == tabulation; - } - - static int SkipLineEnding( - ReadOnlySpan value, - int index) - { - if (value[index] == '\r') - ++index; - - if (index < value.Length && value[index] == '\n') - ++index; - - return index; - } - - static int SkipWhitespace( - ReadOnlySpan value, - int index) - { - while (index < value.Length) - { - if (IsWhiteSpace(value[index])) - index++; - else - return index; - } - - return index; - } } } diff --git a/StringAsserts.cs b/StringAsserts.cs index 1ad78c81549..14f7f5ee6ce 100644 --- a/StringAsserts.cs +++ b/StringAsserts.cs @@ -6,6 +6,7 @@ #endif using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using Xunit.Internal; using Xunit.Sdk; @@ -56,6 +57,112 @@ partial class Assert throw ContainsException.ForSubStringNotFound(expectedSubstring, actualString); } +#if XUNIT_SPAN + + /// + /// Verifies that a string contains a given string, using the given comparison type. + /// + /// The string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string is not present inside the string + public static void Contains( + Span expectedSubstring, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string contains a given string, using the given comparison type. + /// + /// The string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string is not present inside the string + public static void Contains( + Span expectedSubstring, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains((ReadOnlySpan)expectedSubstring, actualString, comparisonType); + + /// + /// Verifies that a string contains a given string, using the given comparison type. + /// + /// The string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string is not present inside the string + public static void Contains( + ReadOnlySpan expectedSubstring, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + Contains(expectedSubstring, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string contains a given string, using the given comparison type. + /// + /// The string expected to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string is not present inside the string + public static void Contains( + ReadOnlySpan expectedSubstring, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (actualString.IndexOf(expectedSubstring, comparisonType) < 0) + throw ContainsException.ForSubStringNotFound( + expectedSubstring.ToString(), + actualString.ToString() + ); + } + + /// + /// Verifies that a string contains a given string, using the current culture. + /// + /// The string expected to be in the string + /// The string to be inspected + /// Thrown when the string is not present inside the string + public static void Contains( + Span expectedSubstring, + Span actualString) => + Contains((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given string, using the current culture. + /// + /// The string expected to be in the string + /// The string to be inspected + /// Thrown when the string is not present inside the string + public static void Contains( + Span expectedSubstring, + ReadOnlySpan actualString) => + Contains((ReadOnlySpan)expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given string, using the current culture. + /// + /// The string expected to be in the string + /// The string to be inspected + /// Thrown when the string is not present inside the string + public static void Contains( + ReadOnlySpan expectedSubstring, + Span actualString) => + Contains(expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string contains a given string, using the current culture. + /// + /// The string expected to be in the string + /// The string to be inspected + /// Thrown when the string is not present inside the string + public static void Contains( + ReadOnlySpan expectedSubstring, + ReadOnlySpan actualString) => + Contains(expectedSubstring, actualString, StringComparison.CurrentCulture); + +#endif + /// /// Verifies that a string does not contain a given sub-string, using the current culture. /// @@ -97,6 +204,110 @@ partial class Assert } } +#if XUNIT_SPAN + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Span expectedSubstring, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Span expectedSubstring, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain((ReadOnlySpan)expectedSubstring, actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlySpan expectedSubstring, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + DoesNotContain(expectedSubstring, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string does not contain a given sub-string, using the given comparison type. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlySpan expectedSubstring, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + var idx = actualString.IndexOf(expectedSubstring, comparisonType); + if (idx > -1) + throw DoesNotContainException.ForSubStringFound(expectedSubstring.ToString(), idx, actualString.ToString()); + } + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Span expectedSubstring, + Span actualString) => + DoesNotContain((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + Span expectedSubstring, + ReadOnlySpan actualString) => + DoesNotContain((ReadOnlySpan)expectedSubstring, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlySpan expectedSubstring, + Span actualString) => + DoesNotContain(expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string does not contain a given sub-string, using the current culture. + /// + /// The sub-string expected not to be in the string + /// The string to be inspected + /// Thrown when the sub-string is present inside the string + public static void DoesNotContain( + ReadOnlySpan expectedSubstring, + ReadOnlySpan actualString) => + DoesNotContain((ReadOnlySpan)expectedSubstring, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + +#endif + /// /// Verifies that a string does not match a regular expression. /// @@ -208,83 +419,109 @@ public static void Empty(string value) throw EndsWithException.ForStringNotFound(expectedEndString, actualString); } +#if XUNIT_SPAN + /// - /// Verifies that a string starts with a given string, using the current culture. + /// Verifies that a string ends with a given sub-string, using the current culture. /// - /// The string expected to be at the start of the string + /// The sub-string expected to be at the end of the string /// The string to be inspected - /// Thrown when the string does not start with the expected string - public static void StartsWith( -#if XUNIT_NULLABLE - string? expectedStartString, - string? actualString) => -#else - string expectedStartString, - string actualString) => -#endif - StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); + /// Thrown when the string does not end with the expected substring + public static void EndsWith( + Span expectedEndString, + Span actualString) => + EndsWith((ReadOnlySpan)expectedEndString, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); /// - /// Verifies that a string starts with a given string, using the given comparison type. + /// Verifies that a string ends with a given sub-string, using the current culture. /// - /// The string expected to be at the start of the string + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected substring + public static void EndsWith( + Span expectedEndString, + ReadOnlySpan actualString) => + EndsWith((ReadOnlySpan)expectedEndString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected substring + public static void EndsWith( + ReadOnlySpan expectedEndString, + Span actualString) => + EndsWith(expectedEndString, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// Thrown when the string does not end with the expected substring + public static void EndsWith( + ReadOnlySpan expectedEndString, + ReadOnlySpan actualString) => + EndsWith(expectedEndString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string /// The string to be inspected /// The type of string comparison to perform - /// Thrown when the string does not start with the expected string - public static void StartsWith( -#if XUNIT_NULLABLE - string? expectedStartString, - string? actualString, -#else - string expectedStartString, - string actualString, -#endif - StringComparison comparisonType) - { - if (expectedStartString == null || actualString == null || !actualString.StartsWith(expectedStartString, comparisonType)) - throw StartsWithException.ForStringNotFound(expectedStartString, actualString); - } + /// Thrown when the string does not end with the expected substring + public static void EndsWith( + Span expectedEndString, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlySpan)expectedEndString, (ReadOnlySpan)actualString, comparisonType); /// - /// Verifies that a string matches a regular expression. + /// Verifies that a string ends with a given sub-string, using the given comparison type. /// - /// The regex pattern expected to match + /// The sub-string expected to be at the end of the string /// The string to be inspected - /// Thrown when the string does not match the regex pattern - public static void Matches( - string expectedRegexPattern, -#if XUNIT_NULLABLE - string? actualString) -#else - string actualString) -#endif - { - GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern); + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected substring + public static void EndsWith( + Span expectedEndString, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith((ReadOnlySpan)expectedEndString, actualString, comparisonType); - if (actualString == null || !Regex.IsMatch(actualString, expectedRegexPattern)) - throw MatchesException.ForMatchNotFound(expectedRegexPattern, actualString); - } + /// + /// Verifies that a string ends with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the end of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected substring + public static void EndsWith( + ReadOnlySpan expectedEndString, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + EndsWith(expectedEndString, (ReadOnlySpan)actualString, comparisonType); /// - /// Verifies that a string matches a regular expression. + /// Verifies that a string ends with a given sub-string, using the given comparison type. /// - /// The regex expected to match + /// The sub-string expected to be at the end of the string /// The string to be inspected - /// Thrown when the string does not match the regex - public static void Matches( - Regex expectedRegex, -#if XUNIT_NULLABLE - string? actualString) -#else - string actualString) -#endif + /// The type of string comparison to perform + /// Thrown when the string does not end with the expected substring + public static void EndsWith( + ReadOnlySpan expectedEndString, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) { - GuardArgumentNotNull(nameof(expectedRegex), expectedRegex); - - if (actualString == null || !expectedRegex.IsMatch(actualString)) - throw MatchesException.ForMatchNotFound(expectedRegex.ToString(), actualString); + if (!actualString.EndsWith(expectedEndString, comparisonType)) + throw EndsWithException.ForStringNotFound(expectedEndString.ToString(), actualString.ToString()); } +#endif + /// /// Verifies that two strings are equivalent. /// @@ -308,11 +545,42 @@ public static void Empty(string value) /// The actual string value. /// If set to true, ignores cases differences. The invariant culture is used. /// If set to true, treats \r\n, \r, and \n as equivalent. - /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. - /// If set to true, ignores all white space differences during comparison. + /// If set to true, treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to true, treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + public static void Equal( -#if XUNIT_NULLABLE +#if XUNIT_SPAN + ReadOnlySpan expected, + ReadOnlySpan actual, +#elif XUNIT_NULLABLE string? expected, string? actual, #else @@ -324,98 +592,457 @@ public static void Empty(string value) bool ignoreWhiteSpaceDifferences = false, bool ignoreAllWhiteSpace = false) { -#if XUNIT_SPAN +#if !XUNIT_SPAN if (expected == null && actual == null) return; if (expected == null || actual == null) throw EqualException.ForMismatchedStrings(expected, actual, -1, -1); +#endif - Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); -#else - // Start out assuming the one of the values is null - int expectedIndex = -1; - int actualIndex = -1; - int expectedLength = 0; - int actualLength = 0; + // Walk the string, keeping separate indices since we can skip variable amounts of + // data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences. + var expectedIndex = 0; + var actualIndex = 0; + var expectedLength = expected.Length; + var actualLength = actual.Length; - if (expected == null) + // Block used to fix edge case of Equal("", " ") when ignoreAllWhiteSpace enabled. + if (ignoreAllWhiteSpace) { - if (actual == null) + if (expectedLength == 0 && SkipWhitespace(actual, 0) == actualLength) + return; + if (actualLength == 0 && SkipWhitespace(expected, 0) == expectedLength) return; } - else if (actual != null) + + while (expectedIndex < expectedLength && actualIndex < actualLength) { - // Walk the string, keeping separate indices since we can skip variable amounts of - // data based on ignoreLineEndingDifferences and ignoreWhiteSpaceDifferences. - expectedIndex = 0; - actualIndex = 0; - expectedLength = expected.Length; - actualLength = actual.Length; - - // Block used to fix edge case of Equal("", " ") when ignoreAllWhiteSpace enabled. - if (ignoreAllWhiteSpace) + var expectedChar = expected[expectedIndex]; + var actualChar = actual[actualIndex]; + + if (ignoreLineEndingDifferences && charsLineEndings.Contains(expectedChar) && charsLineEndings.Contains(actualChar)) { - if (expectedLength == 0 && SkipWhitespace(actual, 0) == actualLength) - return; - if (actualLength == 0 && SkipWhitespace(expected, 0) == expectedLength) - return; + expectedIndex = SkipLineEnding(expected, expectedIndex); + actualIndex = SkipLineEnding(actual, actualIndex); } - - while (expectedIndex < expectedLength && actualIndex < actualLength) + else if (ignoreAllWhiteSpace && (charsWhitespace.Contains(expectedChar) || charsWhitespace.Contains(actualChar))) { - char expectedChar = expected[expectedIndex]; - char actualChar = actual[actualIndex]; - - if (ignoreLineEndingDifferences && IsLineEnding(expectedChar) && IsLineEnding(actualChar)) - { - expectedIndex = SkipLineEnding(expected, expectedIndex); - actualIndex = SkipLineEnding(actual, actualIndex); - } - else if (ignoreAllWhiteSpace && (IsWhiteSpace(expectedChar) || IsWhiteSpace(actualChar))) - { - expectedIndex = SkipWhitespace(expected, expectedIndex); - actualIndex = SkipWhitespace(actual, actualIndex); - } - else if (ignoreWhiteSpaceDifferences && IsWhiteSpace(expectedChar) && IsWhiteSpace(actualChar)) + expectedIndex = SkipWhitespace(expected, expectedIndex); + actualIndex = SkipWhitespace(actual, actualIndex); + } + else if (ignoreWhiteSpaceDifferences && charsWhitespace.Contains(expectedChar) && charsWhitespace.Contains(actualChar)) + { + expectedIndex = SkipWhitespace(expected, expectedIndex); + actualIndex = SkipWhitespace(actual, actualIndex); + } + else + { + if (ignoreCase) { - expectedIndex = SkipWhitespace(expected, expectedIndex); - actualIndex = SkipWhitespace(actual, actualIndex); + expectedChar = char.ToUpperInvariant(expectedChar); + actualChar = char.ToUpperInvariant(actualChar); } - else - { - if (ignoreCase) - { - expectedChar = Char.ToUpperInvariant(expectedChar); - actualChar = Char.ToUpperInvariant(actualChar); - } - if (expectedChar != actualChar) - break; + if (expectedChar != actualChar) + break; - expectedIndex++; - actualIndex++; - } + expectedIndex++; + actualIndex++; } } if (expectedIndex < expectedLength || actualIndex < actualLength) - throw EqualException.ForMismatchedStrings(expected, actual, expectedIndex, actualIndex); + throw EqualException.ForMismatchedStrings(expected.ToString(), actual.ToString(), expectedIndex, actualIndex); + } + +#if XUNIT_SPAN + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + Span expected, + Span actual) => + Equal((ReadOnlySpan)expected, (ReadOnlySpan)actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + Span expected, + ReadOnlySpan actual) => + Equal((ReadOnlySpan)expected, actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + ReadOnlySpan expected, + Span actual) => + Equal(expected, (ReadOnlySpan)actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// Thrown when the strings are not equivalent. + public static void Equal( + ReadOnlySpan expected, + ReadOnlySpan actual) => + Equal(expected, actual, false, false, false, false); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to true, ignores all white space differences during comparison. + /// Thrown when the strings are not equivalent. + public static void Equal( + Span expected, + Span actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal((ReadOnlySpan)expected, (ReadOnlySpan)actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to true, ignores all white space differences during comparison. + /// Thrown when the strings are not equivalent. + public static void Equal( + Span expected, + ReadOnlySpan actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal((ReadOnlySpan)expected, actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats spaces and tabs (in any non-zero quantity) as equivalent. + /// If set to true, removes all whitespaces and tabs before comparing. + /// Thrown when the strings are not equivalent. + public static void Equal( + ReadOnlySpan expected, + Span actual, + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) => + Equal(expected, (ReadOnlySpan)actual, ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); + + /// + /// Verifies that two strings are equivalent. + /// + /// The expected string value. + /// The actual string value. + /// If set to true, ignores cases differences. The invariant culture is used. + /// If set to true, treats \r\n, \r, and \n as equivalent. + /// If set to true, treats horizontal white-space (i.e. spaces, tabs, and others; see remarks) in any non-zero quantity as equivalent. + /// If set to true, treats horizontal white-space (i.e. spaces, tabs, and others; see remarks), including zero quantities, as equivalent. + /// Thrown when the strings are not equivalent. + /// + /// The and flags consider + /// the following characters to be white-space: + /// Tab (\t), + /// Space (\u0020), + /// No-Break Space (\u00A0), + /// Ogham Space Mark (\u1680), + /// Mongolian Vowel Separator (\u180E), + /// En Quad (\u2000), + /// Em Quad (\u2001), + /// En Space (\u2002), + /// Em Space (\u2003), + /// Three-Per-Em Space (\u2004), + /// Four-Per-Em Space (\u2004), + /// Six-Per-Em Space (\u2006), + /// Figure Space (\u2007), + /// Punctuation Space (\u2008), + /// Thin Space (\u2009), + /// Hair Space (\u200A), + /// Zero Width Space (\u200B), + /// Narrow No-Break Space (\u202F), + /// Medium Mathematical Space (\u205F), + /// Ideographic Space (\u3000), + /// and Zero Width No-Break Space (\uFEFF). + /// In particular, it does not include carriage return (\r) or line feed (\n), which are covered by + /// . + /// + + public static void Equal( +#if XUNIT_NULLABLE + string? expected, + string? actual, +#else + string expected, + string actual, #endif + bool ignoreCase = false, + bool ignoreLineEndingDifferences = false, + bool ignoreWhiteSpaceDifferences = false, + bool ignoreAllWhiteSpace = false) + { + // This overload is inside #if XUNIT_SPAN because the string version is dynamically converted + // to a span version, so this string version is a backup that then delegates to the span version. + + if (expected == null && actual == null) + return; + if (expected == null || actual == null) + throw EqualException.ForMismatchedStrings(expected, actual, -1, -1); + + Equal(expected.AsSpan(), actual.AsSpan(), ignoreCase, ignoreLineEndingDifferences, ignoreWhiteSpaceDifferences, ignoreAllWhiteSpace); } -#if !XUNIT_SPAN - static bool IsLineEnding(char c) => - c == '\r' || c == '\n'; +#endif + + /// + /// Verifies that a string matches a regular expression. + /// + /// The regex pattern expected to match + /// The string to be inspected + /// Thrown when the string does not match the regex pattern + public static void Matches( + string expectedRegexPattern, +#if XUNIT_NULLABLE + string? actualString) +#else + string actualString) +#endif + { + GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern); - static bool IsWhiteSpace(char c) => - c == ' ' || c == '\t'; + if (actualString == null || !Regex.IsMatch(actualString, expectedRegexPattern)) + throw MatchesException.ForMatchNotFound(expectedRegexPattern, actualString); + } + + /// + /// Verifies that a string matches a regular expression. + /// + /// The regex expected to match + /// The string to be inspected + /// Thrown when the string does not match the regex + public static void Matches( + Regex expectedRegex, +#if XUNIT_NULLABLE + string? actualString) +#else + string actualString) +#endif + { + GuardArgumentNotNull(nameof(expectedRegex), expectedRegex); + + if (actualString == null || !expectedRegex.IsMatch(actualString)) + throw MatchesException.ForMatchNotFound(expectedRegex.ToString(), actualString); + } + + /// + /// Verifies that a string starts with a given string, using the current culture. + /// + /// The string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected string + public static void StartsWith( +#if XUNIT_NULLABLE + string? expectedStartString, + string? actualString) => +#else + string expectedStartString, + string actualString) => +#endif + StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given string, using the given comparison type. + /// + /// The string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected string + public static void StartsWith( +#if XUNIT_NULLABLE + string? expectedStartString, + string? actualString, +#else + string expectedStartString, + string actualString, +#endif + StringComparison comparisonType) + { + if (expectedStartString == null || actualString == null || !actualString.StartsWith(expectedStartString, comparisonType)) + throw StartsWithException.ForStringNotFound(expectedStartString, actualString); + } + +#if XUNIT_SPAN + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected substring + public static void StartsWith( + Span expectedStartString, + Span actualString) => + StartsWith((ReadOnlySpan)expectedStartString, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected substring + public static void StartsWith( + Span expectedStartString, + ReadOnlySpan actualString) => + StartsWith((ReadOnlySpan)expectedStartString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected substring + public static void StartsWith( + ReadOnlySpan expectedStartString, + Span actualString) => + StartsWith(expectedStartString, (ReadOnlySpan)actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the current culture. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// Thrown when the string does not start with the expected substring + public static void StartsWith( + ReadOnlySpan expectedStartString, + ReadOnlySpan actualString) => + StartsWith(expectedStartString, actualString, StringComparison.CurrentCulture); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected substring + public static void StartsWith( + Span expectedStartString, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlySpan)expectedStartString, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected substring + public static void StartsWith( + Span expectedStartString, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith((ReadOnlySpan)expectedStartString, actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected substring + public static void StartsWith( + ReadOnlySpan expectedStartString, + Span actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) => + StartsWith(expectedStartString, (ReadOnlySpan)actualString, comparisonType); + + /// + /// Verifies that a string starts with a given sub-string, using the given comparison type. + /// + /// The sub-string expected to be at the start of the string + /// The string to be inspected + /// The type of string comparison to perform + /// Thrown when the string does not start with the expected substring + public static void StartsWith( + ReadOnlySpan expectedStartString, + ReadOnlySpan actualString, + StringComparison comparisonType = StringComparison.CurrentCulture) + { + if (!actualString.StartsWith(expectedStartString, comparisonType)) + throw StartsWithException.ForStringNotFound(expectedStartString.ToString(), actualString.ToString()); + } + +#endif + + static readonly HashSet charsLineEndings = new HashSet() + { + '\r', // Carriage Return + '\n', // Line feed + }; + static readonly HashSet charsWhitespace = new HashSet() + { + '\t', // Tab + ' ', // Space + '\u00A0', // No-Break Space + '\u1680', // Ogham Space Mark + '\u180E', // Mongolian Vowel Separator + '\u2000', // En Quad + '\u2001', // Em Quad + '\u2002', // En Space + '\u2003', // Em Space + '\u2004', // Three-Per-Em Space + '\u2005', // Four-Per-Em Space + '\u2006', // Six-Per-Em Space + '\u2007', // Figure Space + '\u2008', // Punctuation Space + '\u2009', // Thin Space + '\u200A', // Hair Space + '\u200B', // Zero Width Space + '\u202F', // Narrow No-Break Space + '\u205F', // Medium Mathematical Space + '\u3000', // Ideographic Space + '\uFEFF', // Zero Width No-Break Space + }; static int SkipLineEnding( +#if XUNIT_SPAN + ReadOnlySpan value, +#else string value, +#endif int index) { if (value[index] == '\r') ++index; + if (index < value.Length && value[index] == '\n') ++index; @@ -423,25 +1050,22 @@ public static void Empty(string value) } static int SkipWhitespace( +#if XUNIT_SPAN + ReadOnlySpan value, +#else string value, +#endif int index) { while (index < value.Length) { - switch (value[index]) - { - case ' ': - case '\t': - index++; - break; - - default: - return index; - } + if (charsWhitespace.Contains(value[index])) + index++; + else + return index; } return index; } -#endif } }