diff --git a/CollectionAsserts.cs b/CollectionAsserts.cs index dfbf6a5ae19..96678fcfaec 100644 --- a/CollectionAsserts.cs +++ b/CollectionAsserts.cs @@ -11,11 +11,8 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using Xunit.Sdk; - -#if XUNIT_VALUETASK using System.Threading.Tasks; -#endif +using Xunit.Sdk; namespace Xunit { @@ -80,7 +77,6 @@ partial class Assert throw AllException.ForFailures(idx, errors); } -#if XUNIT_VALUETASK /// /// Verifies that all items in the collection pass when executed against /// action. @@ -89,9 +85,9 @@ partial class Assert /// The collection /// The action to test each item against /// Thrown when the collection contains at least one non-matching element - public static async ValueTask AllAsync( + public static async Task AllAsync( IEnumerable collection, - Func action) + Func action) { GuardArgumentNotNull(nameof(collection), collection); GuardArgumentNotNull(nameof(action), action); @@ -107,9 +103,9 @@ partial class Assert /// The collection /// The action to test each item against /// Thrown when the collection contains at least one non-matching element - public static async ValueTask AllAsync( + public static async Task AllAsync( IEnumerable collection, - Func action) + Func action) { GuardArgumentNotNull(nameof(collection), collection); GuardArgumentNotNull(nameof(action), action); @@ -134,7 +130,6 @@ partial class Assert if (errors.Count > 0) throw AllException.ForFailures(idx, errors.ToArray()); } -#endif /// /// Verifies that a collection contains exactly a given number of elements, which meet @@ -177,7 +172,6 @@ partial class Assert } } -#if XUNIT_VALUETASK /// /// Verifies that a collection contains exactly a given number of elements, which meet /// the criteria provided by the element inspectors. @@ -186,9 +180,9 @@ partial class Assert /// 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 async ValueTask CollectionAsync( + public static async Task CollectionAsync( IEnumerable collection, - params Func[] elementInspectors) + params Func[] elementInspectors) { GuardArgumentNotNull(nameof(collection), collection); GuardArgumentNotNull(nameof(elementInspectors), elementInspectors); @@ -218,7 +212,6 @@ partial class Assert throw CollectionException.ForMismatchedItemCount(elementInspectors.Length, tracker.IterationCount, tracker.FormatStart()); } } -#endif /// /// Verifies that a collection contains a given object. @@ -430,9 +423,11 @@ public static void Empty(IEnumerable collection) GuardArgumentNotNull(nameof(collection), collection); using (var tracker = collection.AsTracker()) - using (var enumerator = tracker.GetEnumerator()) + { + var enumerator = tracker.GetEnumerator(); if (enumerator.MoveNext()) throw EmptyException.ForNonEmptyCollection(tracker.FormatStart()); + } } /// diff --git a/EqualityAsserts.cs b/EqualityAsserts.cs index 4a5329cb345..a3198d511aa 100644 --- a/EqualityAsserts.cs +++ b/EqualityAsserts.cs @@ -123,6 +123,7 @@ partial class Assert var expectedTracker = expected.AsNonStringTracker(); var actualTracker = actual.AsNonStringTracker(); + var exception = default(Exception); try { @@ -133,8 +134,17 @@ partial class Assert if (!haveCollections) { - if (!comparer.Equals(expected, actual)) - throw EqualException.ForMismatchedValues(expected, actual); + try + { + if (comparer.Equals(expected, actual)) + return; + } + catch (Exception ex) + { + exception = ex; + } + + throw EqualException.ForMismatchedValuesWithError(expected, actual, exception); } else { @@ -164,8 +174,15 @@ partial class Assert if (itemComparer != null) { - if (CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex)) - return; + try + { + if (CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex)) + return; + } + catch (Exception ex) + { + exception = ex; + } var expectedStartIdx = -1; var expectedEndIdx = -1; @@ -197,8 +214,15 @@ partial class Assert } else { - if (comparer.Equals(expected, actual)) - return; + try + { + if (comparer.Equals(expected, actual)) + return; + } + catch (Exception ex) + { + exception = ex; + } formattedExpected = ArgumentFormatter.Format(expected); formattedActual = ArgumentFormatter.Format(actual); @@ -241,7 +265,7 @@ partial class Assert actualPointer += typeNameIndent; } - throw EqualException.ForMismatchedCollections(mismatchedIndex, formattedExpected, expectedPointer, expectedItemType, formattedActual, actualPointer, actualItemType, collectionDisplay); + throw EqualException.ForMismatchedCollectionsWithError(mismatchedIndex, formattedExpected, expectedPointer, expectedItemType, formattedActual, actualPointer, actualItemType, exception, collectionDisplay); } } finally @@ -268,9 +292,9 @@ partial class Assert if (!object.Equals(expectedRounded, actualRounded)) throw EqualException.ForMismatchedValues( - $"{expectedRounded:G17} (rounded from {expected:G17})", - $"{actualRounded:G17} (rounded from {actual:G17})", - $"Values are not within {precision} decimal place{(precision == 1 ? "" : "s")}" + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are not within {0} decimal place{1}", precision, precision == 1 ? "" : "s") ); } @@ -294,9 +318,9 @@ partial class Assert if (!object.Equals(expectedRounded, actualRounded)) throw EqualException.ForMismatchedValues( - $"{expectedRounded:G17} (rounded from {expected:G17})", - $"{actualRounded:G17} (rounded from {actual:G17})", - $"Values are not within {precision} decimal place{(precision == 1 ? "" : "s")}" + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are not within {0} decimal place{1}", precision, precision == 1 ? "" : "s") ); } @@ -319,7 +343,7 @@ partial class Assert throw EqualException.ForMismatchedValues( expected.ToString("G17", CultureInfo.CurrentCulture), actual.ToString("G17", CultureInfo.CurrentCulture), - $"Values are not within tolerance {tolerance:G17}" + string.Format(CultureInfo.CurrentCulture, "Values are not within tolerance {0:G17}", tolerance) ); } @@ -340,9 +364,9 @@ partial class Assert if (!object.Equals(expectedRounded, actualRounded)) throw EqualException.ForMismatchedValues( - $"{expectedRounded:G9} (rounded from {expected:G9})", - $"{actualRounded:G9} (rounded from {actual:G9})", - $"Values are not within {precision} decimal place{(precision == 1 ? "" : "s")}" + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are not within {0} decimal place{1}", precision, precision == 1 ? "" : "s") ); } @@ -366,9 +390,9 @@ partial class Assert if (!object.Equals(expectedRounded, actualRounded)) throw EqualException.ForMismatchedValues( - $"{expectedRounded:G9} (rounded from {expected:G9})", - $"{actualRounded:G9} (rounded from {actual:G9})", - $"Values are not within {precision} decimal place{(precision == 1 ? "" : "s")}" + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are not within {0} decimal place{1}", precision, precision == 1 ? "" : "s") ); } @@ -391,7 +415,7 @@ partial class Assert throw EqualException.ForMismatchedValues( expected.ToString("G9", CultureInfo.CurrentCulture), actual.ToString("G9", CultureInfo.CurrentCulture), - $"Values are not within tolerance {tolerance:G9}" + string.Format(CultureInfo.CurrentCulture, "Values are not within tolerance {0:G9}", tolerance) ); } @@ -411,7 +435,10 @@ partial class Assert var actualRounded = Math.Round(actual, precision); if (expectedRounded != actualRounded) - throw EqualException.ForMismatchedValues($"{expectedRounded} (rounded from {expected})", $"{actualRounded} (rounded from {actual})"); + throw EqualException.ForMismatchedValues( + string.Format(CultureInfo.CurrentCulture, "{0} (rounded from {1})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0} (rounded from {1})", actualRounded, actual) + ); } /// @@ -442,7 +469,7 @@ partial class Assert { var actualValue = ArgumentFormatter.Format(actual) + - (precision == TimeSpan.Zero ? "" : $" (difference {difference} is larger than {precision})"); + (precision == TimeSpan.Zero ? "" : string.Format(CultureInfo.CurrentCulture, " (difference {0} is larger than {1})", difference, precision)); throw EqualException.ForMismatchedValues(expected, actualValue); } @@ -476,7 +503,7 @@ partial class Assert { var actualValue = ArgumentFormatter.Format(actual) + - (precision == TimeSpan.Zero ? "" : $" (difference {difference} is larger than {precision})"); + (precision == TimeSpan.Zero ? "" : string.Format(CultureInfo.CurrentCulture, " (difference {0} is larger than {1})", difference, precision)); throw EqualException.ForMismatchedValues(expected, actualValue); } @@ -565,6 +592,7 @@ partial class Assert var expectedTracker = expected.AsNonStringTracker(); var actualTracker = actual.AsNonStringTracker(); + var exception = default(Exception); try { @@ -575,26 +603,35 @@ partial class Assert if (!haveCollections) { - if (comparer.Equals(expected, actual)) + try { - var formattedExpected = ArgumentFormatter.Format(expected); - var formattedActual = ArgumentFormatter.Format(actual); - - var expectedIsString = expected is string; - var actualIsString = actual is string; - var isStrings = - (expectedIsString && actual == null) || - (actualIsString && expected == null) || - (expectedIsString && actualIsString); - - if (isStrings) - throw NotEqualException.ForEqualCollections(formattedExpected, formattedActual, "Strings"); - else - throw NotEqualException.ForEqualValues(formattedExpected, formattedActual); + if (!comparer.Equals(expected, actual)) + return; + } + catch (Exception ex) + { + exception = ex; } + + var formattedExpected = ArgumentFormatter.Format(expected); + var formattedActual = ArgumentFormatter.Format(actual); + + var expectedIsString = expected is string; + var actualIsString = actual is string; + var isStrings = + (expectedIsString && actual == null) || + (actualIsString && expected == null) || + (expectedIsString && actualIsString); + + if (isStrings) + throw NotEqualException.ForEqualCollectionsWithError(null, formattedExpected, null, formattedActual, null, exception, "Strings"); + else + throw NotEqualException.ForEqualValuesWithError(formattedExpected, formattedActual, exception); } else { + int? mismatchedIndex = null; + // If we have "known" comparers, we can ignore them and instead do our own thing, since we know // we want to be able to consume the tracker, and that's not type compatible. var itemComparer = default(IEqualityComparer); @@ -607,20 +644,53 @@ partial class Assert string formattedExpected; string formattedActual; + int? expectedPointer = null; + int? actualPointer = null; if (itemComparer != null) { - int? mismatchedIndex; - if (!CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex)) - return; + try + { + if (!CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer.DefaultInnerComparer, out mismatchedIndex)) + return; + + // For NotEqual that doesn't throw, pointers are irrelevant, because + // the values are considered to be equal + formattedExpected = expectedTracker?.FormatStart() ?? "null"; + formattedActual = actualTracker?.FormatStart() ?? "null"; + } + catch (Exception ex) + { + exception = ex; + + // When an exception was thrown, we want to provide a pointer so the user knows + // which item was being inspected when the exception was thrown + var expectedStartIdx = -1; + var expectedEndIdx = -1; + expectedTracker?.GetMismatchExtents(mismatchedIndex, out expectedStartIdx, out expectedEndIdx); + + var actualStartIdx = -1; + var actualEndIdx = -1; + actualTracker?.GetMismatchExtents(mismatchedIndex, out actualStartIdx, out actualEndIdx); - formattedExpected = expectedTracker?.FormatStart() ?? "null"; - formattedActual = actualTracker?.FormatStart() ?? "null"; + expectedPointer = null; + formattedExpected = expectedTracker?.FormatIndexedMismatch(expectedStartIdx, expectedEndIdx, mismatchedIndex, out expectedPointer) ?? ArgumentFormatter.Format(expected); + + actualPointer = null; + formattedActual = actualTracker?.FormatIndexedMismatch(actualStartIdx, actualEndIdx, mismatchedIndex, out actualPointer) ?? ArgumentFormatter.Format(actual); + } } else { - if (!comparer.Equals(expected, actual)) - return; + try + { + if (!comparer.Equals(expected, actual)) + return; + } + catch (Exception ex) + { + exception = ex; + } formattedExpected = ArgumentFormatter.Format(expected); formattedActual = ArgumentFormatter.Format(actual); @@ -656,9 +726,14 @@ partial class Assert formattedExpected = expectedTypeName.PadRight(typeNameIndent) + formattedExpected; formattedActual = actualTypeName.PadRight(typeNameIndent) + formattedActual; + + if (expectedPointer != null) + expectedPointer += typeNameIndent; + if (actualPointer != null) + actualPointer += typeNameIndent; } - throw NotEqualException.ForEqualCollections(formattedExpected, formattedActual, collectionDisplay); + throw NotEqualException.ForEqualCollectionsWithError(mismatchedIndex, formattedExpected, expectedPointer, formattedActual, actualPointer, exception, collectionDisplay); } } finally @@ -685,9 +760,9 @@ partial class Assert if (object.Equals(expectedRounded, actualRounded)) throw NotEqualException.ForEqualValues( - $"{expectedRounded:G17} (rounded from {expected:G17})", - $"{actualRounded:G17} (rounded from {actual:G17})", - $"Values are within {precision} decimal places" + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are within {0} decimal places", precision) ); } @@ -711,9 +786,9 @@ partial class Assert if (object.Equals(expectedRounded, actualRounded)) throw NotEqualException.ForEqualValues( - $"{expectedRounded:G17} (rounded from {expected:G17})", - $"{actualRounded:G17} (rounded from {actual:G17})", - $"Values are within {precision} decimal places" + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G17} (rounded from {1:G17})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are within {0} decimal places", precision) ); } @@ -736,7 +811,7 @@ partial class Assert throw NotEqualException.ForEqualValues( expected.ToString("G17", CultureInfo.CurrentCulture), actual.ToString("G17", CultureInfo.CurrentCulture), - $"Values are within tolerance {tolerance:G17}" + string.Format(CultureInfo.CurrentCulture, "Values are within tolerance {0:G17}", tolerance) ); } @@ -757,9 +832,9 @@ partial class Assert if (object.Equals(expectedRounded, actualRounded)) throw NotEqualException.ForEqualValues( - $"{expectedRounded:G9} (rounded from {expected:G9})", - $"{actualRounded:G9} (rounded from {actual:G9})", - $"Values are within {precision} decimal places" + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are within {0} decimal places", precision) ); } @@ -783,9 +858,9 @@ partial class Assert if (object.Equals(expectedRounded, actualRounded)) throw NotEqualException.ForEqualValues( - $"{expectedRounded:G9} (rounded from {expected:G9})", - $"{actualRounded:G9} (rounded from {actual:G9})", - $"Values are within {precision} decimal places" + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0:G9} (rounded from {1:G9})", actualRounded, actual), + string.Format(CultureInfo.CurrentCulture, "Values are within {0} decimal places", precision) ); } @@ -808,7 +883,7 @@ partial class Assert throw NotEqualException.ForEqualValues( expected.ToString("G9", CultureInfo.CurrentCulture), actual.ToString("G9", CultureInfo.CurrentCulture), - $"Values are within tolerance {tolerance:G9}" + string.Format(CultureInfo.CurrentCulture, "Values are within tolerance {0:G9}", tolerance) ); } @@ -829,8 +904,8 @@ partial class Assert if (expectedRounded == actualRounded) throw NotEqualException.ForEqualValues( - $"{expectedRounded} (rounded from {expected})", - $"{actualRounded} (rounded from {actual})" + string.Format(CultureInfo.CurrentCulture, "{0} (rounded from {1})", expectedRounded, expected), + string.Format(CultureInfo.CurrentCulture, "{0} (rounded from {1})", actualRounded, actual) ); } diff --git a/EventAsserts.cs b/EventAsserts.cs index 07c927f8a82..331b797319b 100644 --- a/EventAsserts.cs +++ b/EventAsserts.cs @@ -131,51 +131,6 @@ partial class Assert return raisedEvent; } -#if XUNIT_VALUETASK - /// - /// Verifies that an event is raised. - /// - /// Code to attach the event handler - /// Code to detach the event handler - /// A delegate to the code to be tested - /// The event sender and arguments wrapped in an object - /// Thrown when the expected event was not raised. - public static async ValueTask> RaisesAnyAsync( - Action attach, - Action detach, - Func testCode) - { - var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); - - if (raisedEvent == null) - throw RaisesAnyException.ForNoEvent(typeof(EventArgs)); - - return raisedEvent; - } - - /// - /// Verifies that an event with the exact or a derived event args is raised. - /// - /// The type of the event arguments to expect - /// Code to attach the event handler - /// Code to detach the event handler - /// A delegate to the code to be tested - /// The event sender and arguments wrapped in an object - /// Thrown when the expected event was not raised. - public static async ValueTask> RaisesAnyAsync( - Action> attach, - Action> detach, - Func testCode) - { - var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); - - if (raisedEvent == null) - throw RaisesAnyException.ForNoEvent(typeof(T)); - - return raisedEvent; - } -#endif - /// /// Verifies that an event with the exact event args (and not a derived type) is raised. /// @@ -201,33 +156,6 @@ partial class Assert return raisedEvent; } -#if XUNIT_VALUETASK - /// - /// Verifies that an event with the exact event args (and not a derived type) is raised. - /// - /// The type of the event arguments to expect - /// Code to attach the event handler - /// Code to detach the event handler - /// A delegate to the code to be tested - /// The event sender and arguments wrapped in an object - /// Thrown when the expected event was not raised. - public static async ValueTask> RaisesAsync( - Action> attach, - Action> detach, - Func testCode) - { - var raisedEvent = await RaisesAsyncInternal(attach, detach, testCode); - - if (raisedEvent == null) - throw RaisesException.ForNoEvent(typeof(T)); - - if (raisedEvent.Arguments != null && !raisedEvent.Arguments.GetType().Equals(typeof(T))) - throw RaisesException.ForIncorrectType(typeof(T), raisedEvent.Arguments.GetType()); - - return raisedEvent; - } -#endif - #if XUNIT_NULLABLE static RaisedEvent? RaisesInternal( #else @@ -332,60 +260,6 @@ partial class Assert return raisedEvent; } -#if XUNIT_VALUETASK -#if XUNIT_NULLABLE - static async ValueTask?> RaisesAsyncInternal( -#else - static async Task> RaisesAsyncInternal( -#endif - Action attach, - Action detach, - Func testCode) - { - GuardArgumentNotNull(nameof(attach), attach); - GuardArgumentNotNull(nameof(detach), detach); - GuardArgumentNotNull(nameof(testCode), testCode); - -#if XUNIT_NULLABLE - RaisedEvent? raisedEvent = null; - void handler(object? s, EventArgs args) => raisedEvent = new RaisedEvent(s, args); -#else - RaisedEvent raisedEvent = null; - EventHandler handler = (object s, EventArgs args) => raisedEvent = new RaisedEvent(s, args); -#endif - attach(handler); - await testCode(); - detach(handler); - return raisedEvent; - } - -#if XUNIT_NULLABLE - static async ValueTask?> RaisesAsyncInternal( -#else - static async Task> RaisesAsyncInternal( -#endif - Action> attach, - Action> detach, - Func testCode) - { - GuardArgumentNotNull(nameof(attach), attach); - GuardArgumentNotNull(nameof(detach), detach); - GuardArgumentNotNull(nameof(testCode), testCode); - -#if XUNIT_NULLABLE - RaisedEvent? raisedEvent = null; - void handler(object? s, T args) => raisedEvent = new RaisedEvent(s, args); -#else - RaisedEvent raisedEvent = null; - EventHandler handler = (object s, T args) => raisedEvent = new RaisedEvent(s, args); -#endif - attach(handler); - await testCode(); - detach(handler); - return raisedEvent; - } -#endif - /// /// Represents a raised event after the fact. /// diff --git a/ExceptionAsserts.cs b/ExceptionAsserts.cs index 34dfe9316bd..7c0f99a23da 100644 --- a/ExceptionAsserts.cs +++ b/ExceptionAsserts.cs @@ -76,18 +76,6 @@ partial class Assert throw new NotImplementedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); } -#if XUNIT_VALUETASK - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] - public static Exception Throws( - Type exceptionType, - Func testCode) - { - throw new NotImplementedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); - } -#endif - /// /// Verifies that the exact exception is thrown (and not a derived exception type). /// @@ -126,17 +114,6 @@ public static T Throws(Func testCode) throw new NotImplementedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); } -#if XUNIT_VALUETASK - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] - public static T Throws(Func testCode) - where T : Exception - { - throw new NotImplementedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); - } -#endif - /// /// Verifies that the exact exception is thrown (and not a derived exception type), where the exception /// derives from and has the given parameter name. @@ -201,23 +178,6 @@ public static T Throws(Func testCode) throw new NotImplementedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); } -#if XUNIT_VALUETASK - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("You must call Assert.ThrowsAsync (and await the result) when testing async code.", true)] - public static T Throws( -#if XUNIT_NULLABLE - string? paramName, -#else - string paramName, -#endif - Func testCode) - where T : ArgumentException - { - throw new NotImplementedException("You must call Assert.ThrowsAsync (and await the result) when testing async code."); - } -#endif - static Exception ThrowsAny( Type exceptionType, #if XUNIT_NULLABLE @@ -271,17 +231,6 @@ public static T ThrowsAny(Func testCode) throw new NotImplementedException("You must call Assert.ThrowsAnyAsync (and await the result) when testing async code."); } -#if XUNIT_VALUETASK - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("You must call Assert.ThrowsAnyAsync (and await the result) when testing async code.", true)] - public static T ThrowsAny(Func testCode) - where T : Exception - { - throw new NotImplementedException("You must call Assert.ThrowsAnyAsync (and await the result) when testing async code."); - } -#endif - /// /// Verifies that the exact exception or a derived exception type is thrown. /// @@ -292,18 +241,6 @@ public static async Task ThrowsAnyAsync(Func testCode) where T : Exception => (T)ThrowsAny(typeof(T), await RecordExceptionAsync(testCode)); -#if XUNIT_VALUETASK - /// - /// Verifies that the exact exception or a derived exception type is thrown. - /// - /// The type of the exception expected to be thrown - /// A delegate to the task to be tested - /// The exception that was thrown, when successful - public static async ValueTask ThrowsAnyAsync(Func testCode) - where T : Exception => - (T)ThrowsAny(typeof(T), await RecordExceptionAsync(testCode)); -#endif - /// /// Verifies that the exact exception is thrown (and not a derived exception type). /// @@ -315,19 +252,6 @@ public static async ValueTask ThrowsAnyAsync(Func testCode) Func testCode) => Throws(exceptionType, await RecordExceptionAsync(testCode)); -#if XUNIT_VALUETASK - /// - /// Verifies that the exact exception is thrown (and not a derived exception type). - /// - /// The type of the exception expected to be thrown - /// A delegate to the task to be tested - /// The exception that was thrown, when successful - public static async ValueTask ThrowsAsync( - Type exceptionType, - Func testCode) => - Throws(exceptionType, await RecordExceptionAsync(testCode)); -#endif - /// /// Verifies that the exact exception is thrown (and not a derived exception type). /// @@ -340,18 +264,6 @@ public static async Task ThrowsAsync(Func testCode) (T)Throws(typeof(T), await RecordExceptionAsync(testCode)); #pragma warning restore xUnit2015 -#if XUNIT_VALUETASK - /// - /// Verifies that the exact exception is thrown (and not a derived exception type). - /// - /// The type of the exception expected to be thrown - /// A delegate to the task to be tested - /// The exception that was thrown, when successful - public static async ValueTask ThrowsAsync(Func testCode) - where T : Exception => - (T)Throws(typeof(T), await RecordExceptionAsync(testCode)); -#endif - /// /// Verifies that the exact exception is thrown (and not a derived exception type), where the exception /// derives from and has the given parameter name. @@ -375,31 +287,5 @@ public static async ValueTask ThrowsAsync(Func testCode) return ex; } - -#if XUNIT_VALUETASK - /// - /// Verifies that the exact exception is thrown (and not a derived exception type), where the exception - /// derives from and has the given parameter name. - /// - /// The parameter name that is expected to be in the exception - /// A delegate to the task to be tested - /// The exception that was thrown, when successful - public static async ValueTask ThrowsAsync( -#if XUNIT_NULLABLE - string? paramName, -#else - string paramName, -#endif - Func testCode) - where T : ArgumentException - { - var ex = await ThrowsAsync(testCode); - - if (paramName != ex.ParamName) - throw ThrowsException.ForIncorrectParameterName(typeof(T), paramName, ex.ParamName); - - return ex; - } -#endif } } diff --git a/Guards.cs b/Guards.cs index 909bc88fd06..61e1fbf3efd 100644 --- a/Guards.cs +++ b/Guards.cs @@ -18,6 +18,9 @@ namespace Xunit partial class Assert { /// +#if XUNIT_NULLABLE + [return: NotNull] +#endif internal static T GuardArgumentNotNull( string argName, #if XUNIT_NULLABLE diff --git a/PropertyAsserts.cs b/PropertyAsserts.cs index 5c5e19de05d..0d3ac6c7de6 100644 --- a/PropertyAsserts.cs +++ b/PropertyAsserts.cs @@ -60,22 +60,9 @@ partial class Assert string propertyName, Func testCode) { - throw new NotImplementedException(); + throw new NotImplementedException("You must call Assert.PropertyChangedAsync (and await the result) when testing async code."); } -#if XUNIT_VALUETASK - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("You must call Assert.PropertyChangedAsync (and await the result) when testing async code.", true)] - public static void PropertyChanged( - INotifyPropertyChanged @object, - string propertyName, - Func testCode) - { - throw new NotImplementedException(); - } -#endif - /// /// Verifies that the provided object raised /// as a result of executing the given test code. @@ -111,43 +98,5 @@ partial class Assert @object.PropertyChanged -= handler; } } - -#if XUNIT_VALUETASK - /// - /// Verifies that the provided object raised - /// as a result of executing the given test code. - /// - /// The object which should raise the notification - /// The property name for which the notification should be raised - /// The test code which should cause the notification to be raised - /// Thrown when the notification is not raised - public static async ValueTask PropertyChangedAsync( - INotifyPropertyChanged @object, - string propertyName, - Func testCode) - { - GuardArgumentNotNull(nameof(@object), @object); - GuardArgumentNotNull(nameof(propertyName), propertyName); - GuardArgumentNotNull(nameof(testCode), testCode); - - var propertyChangeHappened = false; - - PropertyChangedEventHandler handler = (sender, args) => - propertyChangeHappened = propertyChangeHappened || string.IsNullOrEmpty(args.PropertyName) || propertyName.Equals(args.PropertyName, StringComparison.OrdinalIgnoreCase); - - @object.PropertyChanged += handler; - - try - { - await testCode(); - if (!propertyChangeHappened) - throw PropertyChangedException.ForUnsetProperty(propertyName); - } - finally - { - @object.PropertyChanged -= handler; - } - } -#endif } } diff --git a/README.md b/README.md index 2c42603bed5..ab204441bdc 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,6 @@ The Skip family of assertions (like `Assert.Skip`) require xUnit.net v3. Define There are optimized versions of `Assert.Equal` for arrays which use `Span`- and/or `Memory`-based comparison options. If you are using a target framework that supports `Span` and `Memory`, you should define `XUNIT_SPAN` to enable these new assertions. -### `XUNIT_VALUETASK` (min: C# 6.0, xUnit.net v2) - -Any asynchronous assertion API (like `Assert.ThrowsAsync`) is available with versions that consume `Task` or `Task`. If you are using a target framework and compiler that support `ValueTask`, you should define `XUNIT_VALUETASK` to enable additional versions of those assertions that will consume `ValueTask` and/or `ValueTask`. - ### `XUNIT_VISIBILITY_INTERNAL` By default, the `Assert` class has `public` visibility. This is appropriate for the default usage (as a shipped library). If your consumption of `Assert` via source is intended to be local to a single library, you should define `XUNIT_VISIBILITY_INTERNAL` to move the visibility of the `Assert` class to `internal`. diff --git a/Record.cs b/Record.cs index 5c9f86c98cc..7030f1c3205 100644 --- a/Record.cs +++ b/Record.cs @@ -7,6 +7,7 @@ using System; using System.ComponentModel; +using System.Globalization; using System.Threading.Tasks; namespace Xunit @@ -72,12 +73,8 @@ protected static Exception RecordException(Action testCode) return ex; } -#if XUNIT_VALUETASK - if (result is Task || result is ValueTask) -#else if (result is Task) -#endif - throw new InvalidOperationException($"You must call Assert.{asyncMethodName} when testing async code"); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "You must call Assert.{0} when testing async code", asyncMethodName)); return null; } @@ -87,27 +84,9 @@ protected static Exception RecordException(Action testCode) [Obsolete("You must call Assert.RecordExceptionAsync (and await the result) when testing async code.", true)] protected static Exception RecordException(Func testCode) { - throw new NotImplementedException(); - } - -#if XUNIT_VALUETASK - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("You must call Assert.RecordExceptionAsync (and await the result) when testing async code.", true)] - protected static Exception RecordException(Func testCode) - { - throw new NotImplementedException(); + throw new NotImplementedException("You must call Assert.RecordExceptionAsync (and await the result) when testing async code."); } - /// - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("You must call Assert.RecordExceptionAsync (and await the result) when testing async code.", true)] - protected static Exception RecordException(Func> testCode) - { - throw new NotImplementedException(); - } -#endif - /// /// Records any exception which is thrown by the given task. /// @@ -131,56 +110,5 @@ protected static async Task RecordExceptionAsync(Func testCode) return ex; } } - -#if XUNIT_VALUETASK - /// - /// Records any exception which is thrown by the given task. - /// - /// The task which may thrown an exception. - /// Returns the exception that was thrown by the code; null, otherwise. -#if XUNIT_NULLABLE - protected static async ValueTask RecordExceptionAsync(Func testCode) -#else - protected static async ValueTask RecordExceptionAsync(Func testCode) -#endif - { - GuardArgumentNotNull(nameof(testCode), testCode); - - try - { - await testCode(); - return null; - } - catch (Exception ex) - { - return ex; - } - } - - /// - /// Records any exception which is thrown by the given task. - /// - /// The task which may thrown an exception. - /// The type of the ValueTask return value. - /// Returns the exception that was thrown by the code; null, otherwise. -#if XUNIT_NULLABLE - protected static async ValueTask RecordExceptionAsync(Func> testCode) -#else - protected static async ValueTask RecordExceptionAsync(Func> testCode) -#endif - { - GuardArgumentNotNull(nameof(testCode), testCode); - - try - { - await testCode(); - return null; - } - catch (Exception ex) - { - return ex; - } - } -#endif } } diff --git a/Sdk/ArgumentFormatter.cs b/Sdk/ArgumentFormatter.cs index 9f33cc7b995..69928bb93ec 100644 --- a/Sdk/ArgumentFormatter.cs +++ b/Sdk/ArgumentFormatter.cs @@ -138,7 +138,7 @@ public static string EscapeString(string s) if (TryGetEscapeSequence(ch, out escapeSequence)) builder.Append(escapeSequence); else if (ch < 32) // C0 control char - builder.AppendFormat(CultureInfo.InvariantCulture, @"\x{0}", (+ch).ToString("x2", CultureInfo.InvariantCulture)); + builder.AppendFormat(CultureInfo.CurrentCulture, @"\x{0}", (+ch).ToString("x2", CultureInfo.CurrentCulture)); else if (char.IsSurrogatePair(s, i)) // should handle the case of ch being the last one { // For valid surrogates, append like normal @@ -148,7 +148,7 @@ public static string EscapeString(string s) // Check for stray surrogates/other invalid chars else if (char.IsSurrogate(ch) || ch == '\uFFFE' || ch == '\uFFFF') { - builder.AppendFormat(CultureInfo.InvariantCulture, @"\x{0}", (+ch).ToString("x4", CultureInfo.InvariantCulture)); + builder.AppendFormat(CultureInfo.CurrentCulture, @"\x{0}", (+ch).ToString("x4", CultureInfo.CurrentCulture)); } else builder.Append(ch); // Append the char like normal @@ -174,7 +174,7 @@ public static string EscapeString(string s) var valueAsType = value as Type; if (valueAsType != null) - return $"typeof({FormatTypeName(valueAsType, fullTypeName: true)})"; + return string.Format(CultureInfo.CurrentCulture, "typeof({0})", FormatTypeName(valueAsType, fullTypeName: true)); try { @@ -220,8 +220,12 @@ public static string EscapeString(string s) if (task != null) { var typeParameters = typeInfo.GenericTypeArguments; - var typeName = typeParameters.Length == 0 ? "Task" : $"Task<{string.Join(",", typeParameters.Select(t => FormatTypeName(t)))}>"; - return $"{typeName} {{ Status = {task.Status} }}"; + var typeName = + typeParameters.Length == 0 + ? "Task" + : string.Format(CultureInfo.CurrentCulture, "Task<{0}>", string.Join(",", typeParameters.Select(t => FormatTypeName(t)))); + + return string.Format(CultureInfo.CurrentCulture, "{0} {{ Status = {1} }}", typeName, task.Status); } // TODO: ValueTask? @@ -245,7 +249,7 @@ public static string EscapeString(string s) { // Sometimes an exception is thrown when formatting an argument, such as in ToString. // In these cases, we don't want xunit to crash, as tests may have passed despite this. - return $"{ex.GetType().Name} was thrown formatting an object of type \"{value.GetType()}\""; + return string.Format(CultureInfo.CurrentCulture, "{0} was thrown formatting an object of type \"{1}\"", ex.GetType().Name, value.GetType()); } } @@ -261,13 +265,13 @@ static string FormatCharValue(char value) string escapeSequence; #endif if (TryGetEscapeSequence(value, out escapeSequence)) - return $"'{escapeSequence}'"; + return string.Format(CultureInfo.CurrentCulture, "'{0}'", escapeSequence); if (char.IsLetterOrDigit(value) || char.IsPunctuation(value) || char.IsSymbol(value) || value == ' ') - return $"'{value}'"; + return string.Format(CultureInfo.CurrentCulture, "'{0}'", value); // Fallback to hex - return $"0x{(int)value:x4}"; + return string.Format(CultureInfo.CurrentCulture, "0x{0:x4}", (int)value); } static string FormatComplexValue( @@ -276,10 +280,10 @@ static string FormatCharValue(char value) Type type, bool isAnonymousType) { - var typeName = isAnonymousType ? "" : $"{type.Name} "; + var typeName = isAnonymousType ? "" : type.Name + " "; if (depth == MAX_DEPTH) - return $"{typeName}{{ {Ellipsis} }}"; + return string.Format(CultureInfo.CurrentCulture, "{0}{{ {1} }}", typeName, Ellipsis); var fields = type @@ -301,21 +305,21 @@ static string FormatCharValue(char value) .ToList(); if (parameters.Count == 0) - return $"{typeName}{{ }}"; + return string.Format(CultureInfo.CurrentCulture, "{0}{{ }}", typeName); - var formattedParameters = string.Join(", ", parameters.Take(MAX_OBJECT_ITEM_COUNT).Select(p => $"{p.name} = {p.value}")); + var formattedParameters = string.Join(", ", parameters.Take(MAX_OBJECT_ITEM_COUNT).Select(p => string.Format(CultureInfo.CurrentCulture, "{0} = {1}", p.name, p.value))); if (parameters.Count > MAX_OBJECT_ITEM_COUNT) formattedParameters += ", " + Ellipsis; - return $"{typeName}{{ {formattedParameters} }}"; + return string.Format(CultureInfo.CurrentCulture, "{0}{{ {1} }}", typeName, formattedParameters); } static string FormatDateTimeValue(object value) => - $"{value:o}"; + string.Format(CultureInfo.CurrentCulture, "{0:o}", value); static string FormatDoubleValue(object value) => - $"{value:G17}"; + string.Format(CultureInfo.CurrentCulture, "{0:G17}", value); static string FormatEnumValue(object value) => value.ToString()?.Replace(", ", " | ") ?? "null"; @@ -357,7 +361,7 @@ static string FormatCharValue(char value) } static string FormatFloatValue(object value) => - $"{value:G9}"; + string.Format(CultureInfo.CurrentCulture, "{0:G9}", value); static string FormatStringValue(string value) { @@ -366,10 +370,10 @@ static string FormatStringValue(string value) if (value.Length > MAX_STRING_LENGTH) { var displayed = value.Substring(0, MAX_STRING_LENGTH); - return $"\"{displayed}\"" + Ellipsis; + return string.Format(CultureInfo.CurrentCulture, "\"{0}\"{1}", displayed, Ellipsis); } - return $"\"{value}\""; + return string.Format(CultureInfo.CurrentCulture, "\"{0}\"", value); } static string FormatTupleValue( @@ -425,7 +429,7 @@ static string FormatStringValue(string value) if (rank == 1) arraySuffix += "[*]"; else - arraySuffix += $"[{new string(',', rank - 1)}]"; + arraySuffix += string.Format(CultureInfo.CurrentCulture, "[{0}]", new string(',', rank - 1)); } #if XUNIT_NULLABLE @@ -453,13 +457,13 @@ static string FormatStringValue(string value) result = result.Substring(0, tickIdx); if (typeInfo.IsGenericTypeDefinition) - result = $"{result}<{new string(',', typeInfo.GenericTypeParameters.Length - 1)}>"; + result = string.Format(CultureInfo.CurrentCulture, "{0}<{1}>", result, new string(',', typeInfo.GenericTypeParameters.Length - 1)); else if (typeInfo.IsGenericType) { if (typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) result = FormatTypeName(typeInfo.GenericTypeArguments[0]) + "?"; else - result = $"{result}<{string.Join(", ", typeInfo.GenericTypeArguments.Select(t => FormatTypeName(t)))}>"; + result = string.Format(CultureInfo.CurrentCulture, "{0}<{1}>", result, string.Join(", ", typeInfo.GenericTypeArguments.Select(t => FormatTypeName(t)))); } return result + arraySuffix; @@ -474,7 +478,7 @@ static string FormatStringValue(string value) var k = typeInfo.GetDeclaredProperty("Key")?.GetValue(value, null); var v = typeInfo.GetDeclaredProperty("Value")?.GetValue(value, null); - return $"[{Format(k)}] = {Format(v)}"; + return string.Format(CultureInfo.CurrentCulture, "[{0}] = {1}", Format(k), Format(v)); } return Convert.ToString(value, CultureInfo.CurrentCulture) ?? "null"; @@ -587,7 +591,7 @@ static Exception UnwrapException(Exception ex) } catch (Exception ex) { - return $"(throws {UnwrapException(ex)?.GetType().Name})"; + return string.Format(CultureInfo.CurrentCulture, "(throws {0})", UnwrapException(ex)?.GetType().Name); } } } diff --git a/Sdk/AssertEqualityComparer.cs b/Sdk/AssertEqualityComparer.cs index d9a65cd5a62..f1a3ee5ac57 100644 --- a/Sdk/AssertEqualityComparer.cs +++ b/Sdk/AssertEqualityComparer.cs @@ -3,6 +3,8 @@ #else // In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE #pragma warning disable CS8601 +#pragma warning disable CS8602 +#pragma warning disable CS8604 #pragma warning disable CS8605 #pragma warning disable CS8618 #pragma warning disable CS8625 @@ -11,6 +13,7 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Reflection; @@ -29,7 +32,10 @@ sealed class AssertEqualityComparer : IEqualityComparer { internal static readonly IEqualityComparer DefaultInnerComparer = new AssertEqualityComparerAdapter(new AssertEqualityComparer()); + static readonly ConcurrentDictionary cacheOfIComparableOfT = new ConcurrentDictionary(); + static readonly ConcurrentDictionary cacheOfIEquatableOfT = new ConcurrentDictionary(); readonly Lazy innerComparer; + static readonly Type typeKeyValuePair = typeof(KeyValuePair<,>); /// /// Initializes a new instance of the class. @@ -58,8 +64,6 @@ public AssertEqualityComparer(IEqualityComparer innerComparer = null) T y) #endif { - var typeInfo = typeof(T).GetTypeInfo(); - // Null? if (x == null && y == null) return true; @@ -83,19 +87,26 @@ public AssertEqualityComparer(IEqualityComparer innerComparer = null) if (equatable != null) return equatable.Equals(y); + var xType = x.GetType(); + var xTypeInfo = xType.GetTypeInfo(); + var yType = y.GetType(); + // Implements IEquatable? - var iequatableY = typeof(IEquatable<>).MakeGenericType(y.GetType()).GetTypeInfo(); - if (iequatableY.IsAssignableFrom(x.GetType().GetTypeInfo())) + if (xType != yType) { - var equalsMethod = iequatableY.GetDeclaredMethod(nameof(IEquatable.Equals)); - if (equalsMethod == null) - return false; + var iequatableY = cacheOfIEquatableOfT.GetOrAdd(yType, (t) => typeof(IEquatable<>).MakeGenericType(t).GetTypeInfo()); + if (iequatableY.IsAssignableFrom(xTypeInfo)) + { + var equalsMethod = iequatableY.GetDeclaredMethod(nameof(IEquatable.Equals)); + if (equalsMethod == null) + return false; #if XUNIT_NULLABLE - return equalsMethod.Invoke(x, new object[] { y }) is true; + return equalsMethod.Invoke(x, new object[] { y }) is true; #else - return (bool)equalsMethod.Invoke(x, new object[] { y }); + return (bool)equalsMethod.Invoke(x, new object[] { y }); #endif + } } // Implements IStructuralEquatable? @@ -120,26 +131,29 @@ public AssertEqualityComparer(IEqualityComparer innerComparer = null) } // Implements IComparable? - var icomparableY = typeof(IComparable<>).MakeGenericType(y.GetType()).GetTypeInfo(); - if (icomparableY.IsAssignableFrom(x.GetType().GetTypeInfo())) + if (xType != yType) { - var compareToMethod = icomparableY.GetDeclaredMethod(nameof(IComparable.CompareTo)); - if (compareToMethod == null) - return false; - - try + var icomparableY = cacheOfIComparableOfT.GetOrAdd(yType, (t) => typeof(IComparable<>).MakeGenericType(t).GetTypeInfo()); + if (icomparableY.IsAssignableFrom(xTypeInfo)) { + var compareToMethod = icomparableY.GetDeclaredMethod(nameof(IComparable.CompareTo)); + if (compareToMethod == null) + return false; + + try + { #if XUNIT_NULLABLE - return compareToMethod.Invoke(x, new object[] { y }) is 0; + return compareToMethod.Invoke(x, new object[] { y }) is 0; #else - return (int)compareToMethod.Invoke(x, new object[] { y }) == 0; + return (int)compareToMethod.Invoke(x, new object[] { y }) == 0; #endif - } - catch - { - // Some implementations of IComparable.CompareTo throw exceptions in - // certain situations, such as if x can't compare against y. - // If this happens, just swallow up the exception and continue comparing. + } + catch + { + // Some implementations of IComparable.CompareTo throw exceptions in + // certain situations, such as if x can't compare against y. + // If this happens, just swallow up the exception and continue comparing. + } } } @@ -159,6 +173,17 @@ public AssertEqualityComparer(IEqualityComparer innerComparer = null) } } + // Special case KeyValuePair + if (xType.IsConstructedGenericType && + xType.GetGenericTypeDefinition() == typeKeyValuePair && + 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)); + } + // Last case, rely on object.Equals return object.Equals(x, y); } @@ -171,10 +196,8 @@ public AssertEqualityComparer(IEqualityComparer innerComparer = null) new FuncEqualityComparer(comparer); /// - public int GetHashCode(T obj) - { - throw new NotImplementedException(); - } + public int GetHashCode(T obj) => + innerComparer.Value.GetHashCode(GuardArgumentNotNull(nameof(obj), obj)); #if XUNIT_NULLABLE sealed class FuncEqualityComparer : IEqualityComparer @@ -211,13 +234,11 @@ 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 - { - throw new NotImplementedException(); - } + GuardArgumentNotNull(nameof(obj), obj).GetHashCode(); } sealed class TypeErasedEqualityComparer : IEqualityComparer @@ -277,10 +298,26 @@ public TypeErasedEqualityComparer(IEqualityComparer innerComparer) U y) => new AssertEqualityComparer(innerComparer: innerComparer).Equals(x, y); - public int GetHashCode(object obj) - { - throw new NotImplementedException(); - } + public int GetHashCode(object obj) => + GuardArgumentNotNull(nameof(obj), obj).GetHashCode(); + } + + /// +#if XUNIT_NULLABLE + [return: NotNull] +#endif + internal static TArg GuardArgumentNotNull( + string argName, +#if XUNIT_NULLABLE + [NotNull] TArg? argValue) +#else + TArg argValue) +#endif + { + if (argValue == null) + throw new ArgumentNullException(argName.TrimStart('@')); + + return argValue; } } } diff --git a/Sdk/AssertHelper.cs b/Sdk/AssertHelper.cs index 1f86a00042a..5f388d38a16 100644 --- a/Sdk/AssertHelper.cs +++ b/Sdk/AssertHelper.cs @@ -136,7 +136,7 @@ static TypeInfo GetTypeInfo(string typeName) } catch (Exception ex) { - throw new InvalidOperationException($"Fatal error: Exception occured while trying to retrieve type '{typeName}'", ex); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Fatal error: Exception occurred while trying to retrieve type '{0}'", typeName), ex); } } @@ -285,10 +285,10 @@ internal static string ShortenAndEncodeStringEnd(string value) // Prevent circular references if (expectedRefs.Contains(expected)) - return EquivalentException.ForCircularReference($"{nameof(expected)}.{prefix}"); + return EquivalentException.ForCircularReference(string.Format(CultureInfo.CurrentCulture, "{0}.{1}", nameof(expected), prefix)); if (actualRefs.Contains(actual)) - return EquivalentException.ForCircularReference($"{nameof(actual)}.{prefix}"); + return EquivalentException.ForCircularReference(string.Format(CultureInfo.CurrentCulture, "{0}.{1}", nameof(actual), prefix)); expectedRefs.Add(expected); actualRefs.Add(actual); @@ -366,7 +366,14 @@ internal static string ShortenAndEncodeStringEnd(string value) return EquivalentException.ForMemberValueMismatch(expected, actual, prefix, ex); } - throw new InvalidOperationException($"VerifyEquivalenceDateTime was given non-DateTime(Offset) objects; typeof(expected) = {ArgumentFormatter.FormatTypeName(expected.GetType())}, typeof(actual) = {ArgumentFormatter.FormatTypeName(actual.GetType())}"); + throw new InvalidOperationException( + string.Format( + CultureInfo.CurrentCulture, + "VerifyEquivalenceDateTime was given non-DateTime(Offset) objects; typeof(expected) = {0}, typeof(actual) = {1}", + ArgumentFormatter.FormatTypeName(expected.GetType()), + ArgumentFormatter.FormatTypeName(actual.GetType()) + ) + ); } #if XUNIT_NULLABLE diff --git a/Sdk/CollectionTracker.cs b/Sdk/CollectionTracker.cs index d4fa08025d8..4384ff44ed2 100644 --- a/Sdk/CollectionTracker.cs +++ b/Sdk/CollectionTracker.cs @@ -2,7 +2,7 @@ #nullable enable #else // In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE -#pragma warning disable CS8600 +#pragma warning disable CS8601 #pragma warning disable CS8603 #pragma warning disable CS8604 #pragma warning disable CS8605 @@ -16,6 +16,10 @@ using System.Reflection; using System.Text; +#if XUNIT_NULLABLE +using System.Diagnostics.CodeAnalysis; +#endif + namespace Xunit.Sdk { /// @@ -300,6 +304,37 @@ protected CollectionTracker(IEnumerable innerEnumerable) /// public abstract void Dispose(); + /// + /// Formats the collection when you have a mismatched index. The formatted result will be the section of the + /// collection surrounded by the mismatched item. + /// + /// The index of the mismatched item + /// How many spaces into the output value the pointed-to item begins at + /// The optional printing depth (1 indicates a top-level value) + /// The formatted collection + public abstract string FormatIndexedMismatch( + int? mismatchedIndex, + out int? pointerIndent, + int depth = 1); + + /// + /// Formats the collection when you have a mismatched index. The formatted result will be the section of the + /// collection from to . These indices are usually + /// obtained by calling . + /// + /// The start index of the collection to print + /// The end index of the collection to print + /// The mismatched item index + /// How many spaces into the output value the pointed-to item begins at + /// The optional printing depth (1 indicates a top-level value) + /// The formatted collection + public abstract string FormatIndexedMismatch( + int startIndex, + int endIndex, + int? mismatchedIndex, + out int? pointerIndent, + int depth = 1); + /// /// Formats the beginning part of the collection. /// @@ -307,12 +342,40 @@ protected CollectionTracker(IEnumerable innerEnumerable) /// The formatted collection public abstract string FormatStart(int depth = 1); + /// + /// Gets the extents to print when you find a mismatched index, in the form of + /// a and . If the mismatched + /// index is null, the extents will start at index 0. + /// + /// The mismatched item index + /// The start index that should be used for printing + /// The end index that should be used for printing + public abstract void GetMismatchExtents( + int? mismatchedIndex, + out int startIndex, + out int endIndex); + /// /// Gets a safe version of that prevents double enumeration and does all /// the necessary tracking required for collection formatting. Should should be the same value /// returned by , except non-generic. /// - protected abstract IEnumerator GetSafeEnumerator(); + protected internal abstract IEnumerator GetSafeEnumerator(); + + /// + /// Gets the full name of the type of the element at the given index, if known. + /// Since this uses the item cache produced by enumeration, it may return null + /// when we haven't enumerated enough to see the given element, or if we enumerated + /// so much that the item has left the cache, or if the item at the given index + /// is null. It will also return null when the + /// is null. + /// + /// The item index +#if XUNIT_NULLABLE + public abstract string? TypeAt(int? index); +#else + public abstract string TypeAt(int? index); +#endif /// /// Wraps an untyped enumerable in an object-based . @@ -375,15 +438,8 @@ sealed class CollectionTracker : CollectionTracker, IEnumerable public override void Dispose() => enumerator?.DisposeInternal(); - /// - /// Formats the collection when you have a mismatched index. The formatted result will be the section of the - /// collection surrounded by the mismatched item. - /// - /// The index of the mismatched item - /// How many spaces into the output value the pointed-to item begins at - /// The optional printing depth (1 indicates a top-level value) - /// The formatted collection - public string FormatIndexedMismatch( + /// + public override string FormatIndexedMismatch( int? mismatchedIndex, out int? pointerIndent, int depth = 1) @@ -414,18 +470,8 @@ sealed class CollectionTracker : CollectionTracker, IEnumerable ); } - /// - /// Formats the collection when you have a mismatched index. The formatted result will be the section of the - /// collection from to . These indices are usually - /// obtained by calling . - /// - /// The start index of the collection to print - /// The end index of the collection to print - /// The mismatched item index - /// How many spaces into the output value the pointed-to item begins at - /// The optional printing depth (1 indicates a top-level value) - /// The formatted collection - public string FormatIndexedMismatch( + /// + public override string FormatIndexedMismatch( int startIndex, int endIndex, int? mismatchedIndex, @@ -640,18 +686,11 @@ public IEnumerator GetEnumerator() GetEnumerator(); /// - protected override IEnumerator GetSafeEnumerator() => + protected internal override IEnumerator GetSafeEnumerator() => GetEnumerator(); - /// - /// Gets the extents to print when you find a mismatched index, in the form of - /// a and . If the mismatched - /// index is null, the extents will start at index 0. - /// - /// The mismatched item index - /// The start index that should be used for printing - /// The end index that should be used for printing - public void GetMismatchExtents( + /// + public override void GetMismatchExtents( int? mismatchedIndex, out int startIndex, out int endIndex) @@ -671,19 +710,11 @@ public IEnumerator GetEnumerator() startIndex = Math.Max(0, endIndex - ArgumentFormatter.MAX_ENUMERABLE_LENGTH + 1); } - /// - /// Gets the full name of the type of the element at the given index, if known. - /// Since this uses the item cache produced by enumeration, it may return null - /// when we haven't enumerated enough to see the given element, or if we enumerated - /// so much that the item has left the cache, or if the item at the given index - /// is null. It will also return null when the - /// is null. - /// - /// The item index + /// #if XUNIT_NULLABLE - public string? TypeAt(int? index) + public override string? TypeAt(int? index) #else - public string TypeAt(int? index) + public override string TypeAt(int? index) #endif { if (enumerator == null || !index.HasValue) @@ -694,7 +725,7 @@ public string TypeAt(int? index) #else T item; #endif - if (!enumerator.CurrentItems.TryGetValue(index.Value, out item)) + if (!enumerator.TryGetCurrentItemAt(index.Value, out item)) return null; return item?.GetType().FullName; @@ -709,6 +740,8 @@ public string TypeAt(int? index) sealed class Enumerator : IEnumerator { + int currentItemsLastInsertionIndex = -1; + T[] currentItemsRingBuffer = new T[ArgumentFormatter.MAX_ENUMERABLE_LENGTH]; readonly IEnumerator innerEnumerator; public Enumerator(IEnumerator innerEnumerator) @@ -728,7 +761,32 @@ public Enumerator(IEnumerator innerEnumerator) public int CurrentIndex { get; private set; } = -1; - public Dictionary CurrentItems { get; } = new Dictionary(); + public Dictionary CurrentItems + { + get + { + var result = new Dictionary(); + + if (CurrentIndex > -1) + { + var itemIndex = Math.Max(0, CurrentIndex - ArgumentFormatter.MAX_ENUMERABLE_LENGTH + 1); + + var indexInRingBuffer = (currentItemsLastInsertionIndex - CurrentIndex + itemIndex) % ArgumentFormatter.MAX_ENUMERABLE_LENGTH; + if (indexInRingBuffer < 0) + indexInRingBuffer += ArgumentFormatter.MAX_ENUMERABLE_LENGTH; + + while (itemIndex <= CurrentIndex) + { + result[itemIndex] = currentItemsRingBuffer[indexInRingBuffer]; + + ++itemIndex; + indexInRingBuffer = (indexInRingBuffer + 1) % ArgumentFormatter.MAX_ENUMERABLE_LENGTH; + } + } + + return result; + } + } public List StartItems { get; } = new List(); @@ -753,19 +811,42 @@ public bool MoveNext() if (CurrentIndex <= ArgumentFormatter.MAX_ENUMERABLE_LENGTH) StartItems.Add(current); - // Keep the most recent MAX_ENUMERABLE_LENGTH in the dictionary, + // Keep a ring buffer filled with the most recent MAX_ENUMERABLE_LENGTH items // so we can print out the items when we've found a bad index - CurrentItems[CurrentIndex] = current; - - if (CurrentIndex >= ArgumentFormatter.MAX_ENUMERABLE_LENGTH) - CurrentItems.Remove(CurrentIndex - ArgumentFormatter.MAX_ENUMERABLE_LENGTH); + currentItemsLastInsertionIndex = (currentItemsLastInsertionIndex + 1) % ArgumentFormatter.MAX_ENUMERABLE_LENGTH; + currentItemsRingBuffer[currentItemsLastInsertionIndex] = current; return true; } public void Reset() { - throw new InvalidOperationException("This enumerator does not support resetting"); + innerEnumerator.Reset(); + + CurrentIndex = -1; + currentItemsLastInsertionIndex = -1; + StartItems.Clear(); + } + + public bool TryGetCurrentItemAt( + int index, +#if XUNIT_NULLABLE + [MaybeNullWhen(false)] out T item) +#else + out T item) +#endif + { + item = default(T); + + if (index < 0 || index <= CurrentIndex - ArgumentFormatter.MAX_ENUMERABLE_LENGTH || index > CurrentIndex) + return false; + + var indexInRingBuffer = (currentItemsLastInsertionIndex - CurrentIndex + index) % ArgumentFormatter.MAX_ENUMERABLE_LENGTH; + if (indexInRingBuffer < 0) + indexInRingBuffer += ArgumentFormatter.MAX_ENUMERABLE_LENGTH; + + item = currentItemsRingBuffer[indexInRingBuffer]; + return true; } } } diff --git a/Sdk/CollectionTrackerExtensions.cs b/Sdk/CollectionTrackerExtensions.cs index f356571d795..cc67d1cfe63 100644 --- a/Sdk/CollectionTrackerExtensions.cs +++ b/Sdk/CollectionTrackerExtensions.cs @@ -2,11 +2,17 @@ #nullable enable #else // In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8601 #pragma warning disable CS8603 +#pragma warning disable CS8604 #endif +using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; +using System.Reflection; #if XUNIT_NULLABLE using System.Diagnostics.CodeAnalysis; @@ -25,18 +31,23 @@ namespace Xunit.Sdk static class CollectionTrackerExtensions { #if XUNIT_NULLABLE - internal static IEnumerable? AsNonStringEnumerable(this object? value) => + static readonly MethodInfo? asTrackerOpenGeneric = typeof(CollectionTrackerExtensions).GetRuntimeMethods().FirstOrDefault(m => m.Name == nameof(AsTracker) && m.IsGenericMethod); #else - internal static IEnumerable AsNonStringEnumerable(this object value) => + static readonly MethodInfo asTrackerOpenGeneric = typeof(CollectionTrackerExtensions).GetRuntimeMethods().FirstOrDefault(m => m.Name == nameof(AsTracker) && m.IsGenericMethod); #endif - value == null || value is string ? null : value as IEnumerable; + static readonly ConcurrentDictionary cacheOfAsTrackerByType = new ConcurrentDictionary(); #if XUNIT_NULLABLE - internal static CollectionTracker? AsNonStringTracker(this object? value) => + internal static CollectionTracker? AsNonStringTracker(this object? value) #else - internal static CollectionTracker AsNonStringTracker(this object value) => + internal static CollectionTracker AsNonStringTracker(this object value) #endif - AsTracker(AsNonStringEnumerable(value)); + { + if (value == null || value is string) + return null; + + return AsTracker(value as IEnumerable); + } /// /// Wraps the given enumerable in an instance of . @@ -44,13 +55,35 @@ static class CollectionTrackerExtensions /// The enumerable to be wrapped #if XUNIT_NULLABLE [return: NotNullIfNotNull("enumerable")] - public static CollectionTracker? AsTracker(this IEnumerable? enumerable) => + public static CollectionTracker? AsTracker(this IEnumerable? enumerable) #else - public static CollectionTracker AsTracker(this IEnumerable enumerable) => + public static CollectionTracker AsTracker(this IEnumerable enumerable) +#endif + { + if (enumerable == null) + return null; + + var result = enumerable as CollectionTracker; + if (result != null) + return result; + + // CollectionTracker.Wrap for the non-T enumerable uses the CastIterator, which has terrible + // performance during iteration. We do our best to try to get a T and dynamically invoke the + // generic version of AsTracker as we can. + var iEnumerableOfT = enumerable.GetType().GetTypeInfo().ImplementedInterfaces.FirstOrDefault(i => i.IsConstructedGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + if (iEnumerableOfT == null) + return CollectionTracker.Wrap(enumerable); + + var enumerableType = iEnumerableOfT.GenericTypeArguments[0]; +#if XUNIT_NULLABLE + var method = cacheOfAsTrackerByType.GetOrAdd(enumerableType, t => asTrackerOpenGeneric!.MakeGenericMethod(enumerableType)); +#else + var method = cacheOfAsTrackerByType.GetOrAdd(enumerableType, t => asTrackerOpenGeneric.MakeGenericMethod(enumerableType)); #endif - enumerable == null - ? null - : enumerable as CollectionTracker ?? CollectionTracker.Wrap(enumerable); + + result = method.Invoke(null, new object[] { enumerable }) as CollectionTracker; + return result ?? CollectionTracker.Wrap(enumerable); + } /// /// Wraps the given enumerable in an instance of . @@ -66,5 +99,15 @@ static class CollectionTrackerExtensions enumerable == null ? null : enumerable as CollectionTracker ?? CollectionTracker.Wrap(enumerable); + + /// + /// Enumerates the elements inside the collection tracker. + /// + public static IEnumerator GetEnumerator(this CollectionTracker tracker) + { + Assert.GuardArgumentNotNull(nameof(tracker), tracker); + + return tracker.GetSafeEnumerator(); + } } } diff --git a/Sdk/Exceptions/AllException.cs b/Sdk/Exceptions/AllException.cs index 3175bafcd60..2bd1faa91d5 100644 --- a/Sdk/Exceptions/AllException.cs +++ b/Sdk/Exceptions/AllException.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading.Tasks; namespace Xunit.Sdk { @@ -23,25 +24,15 @@ partial class AllException : XunitException base(message) { } -#if XUNIT_VALUETASK - /// - /// Creates a new instance of the class to be thrown when one or - /// more items failed during , - /// , - /// , - /// or . - /// - /// The total number of items in the collection - /// The list of failures (as index, value, and exception) -#else /// /// Creates a new instance of the class to be thrown when one or /// more items failed during - /// or . + /// or , + /// , + /// or . /// /// The total number of items in the collection /// The list of failures (as index, value, and exception) -#endif public static AllException ForFailures( int totalItems, IReadOnlyList> errors) @@ -54,16 +45,26 @@ partial class AllException : XunitException var wrapSpaces = Environment.NewLine + new string(' ', maxWrapIndent); var message = - $"Assert.All() Failure: {errors.Count} out of {totalItems} items in the collection did not pass." + Environment.NewLine + - string.Join( + string.Format( + CultureInfo.CurrentCulture, + "Assert.All() Failure: {0} out of {1} items in the collection did not pass.{2}{3}", + errors.Count, + totalItems, Environment.NewLine, - errors.Select(error => - { - var indexString = $"[{error.Item1}]:".PadRight(maxItemIndexLength); - - return $"{indexString}Item: {error.Item2.Replace(Environment.NewLine, wrapSpaces)}" + Environment.NewLine + - $"{indexSpaces}Error: {error.Item3.Message.Replace(Environment.NewLine, wrapSpaces)}"; - }) + string.Join( + Environment.NewLine, + errors.Select(error => + string.Format( + CultureInfo.CurrentCulture, + "{0}Item: {1}{2}{3}Error: {4}", + string.Format(CultureInfo.CurrentCulture, "[{0}]:", error.Item1).PadRight(maxItemIndexLength), + error.Item2.Replace(Environment.NewLine, wrapSpaces), + Environment.NewLine, + indexSpaces, + error.Item3.Message.Replace(Environment.NewLine, wrapSpaces) + ) + ) + ) ); return new AllException(message); diff --git a/Sdk/Exceptions/CollectionException.cs b/Sdk/Exceptions/CollectionException.cs index e55700c9e82..01903912dba 100644 --- a/Sdk/Exceptions/CollectionException.cs +++ b/Sdk/Exceptions/CollectionException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; using System.Linq; namespace Xunit.Sdk @@ -51,9 +52,9 @@ static string FormatInnerException(Exception innerException) var message = "Assert.Collection() Failure: Item comparison failure"; if (failurePointerIndent.HasValue) - message += $"{Environment.NewLine} {new string(' ', failurePointerIndent.Value)}↓ (pos {indexFailurePoint})"; + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); - message += $"{Environment.NewLine}Collection: {formattedCollection}{Environment.NewLine}Error: {FormatInnerException(exception)}"; + message += string.Format(CultureInfo.CurrentCulture, "{0}Collection: {1}{2}Error: {3}", Environment.NewLine, formattedCollection, Environment.NewLine, FormatInnerException(exception)); return new CollectionException(message); } @@ -70,10 +71,16 @@ static string FormatInnerException(Exception innerException) int actualCount, string formattedCollection) => new CollectionException( - "Assert.Collection() Failure: Mismatched item count" + Environment.NewLine + - "Collection: " + Assert.GuardArgumentNotNull(nameof(formattedCollection), formattedCollection) + Environment.NewLine + - "Expected count: " + expectedCount + Environment.NewLine + - "Actual count: " + actualCount + string.Format( + CultureInfo.CurrentCulture, + "Assert.Collection() Failure: Mismatched item count{0}Collection: {1}{2}Expected count: {3}{4}Actual count: {5}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(formattedCollection), formattedCollection), + Environment.NewLine, + expectedCount, + Environment.NewLine, + actualCount + ) ); } } diff --git a/Sdk/Exceptions/ContainsException.cs b/Sdk/Exceptions/ContainsException.cs index 26fc542120d..5b5f8438626 100644 --- a/Sdk/Exceptions/ContainsException.cs +++ b/Sdk/Exceptions/ContainsException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; using Xunit.Internal; namespace Xunit.Sdk @@ -28,8 +29,12 @@ partial class ContainsException : XunitException /// The collection public static ContainsException ForCollectionFilterNotMatched(string collection) => new ContainsException( - "Assert.Contains() Failure: Filter not matched in collection" + Environment.NewLine + - "Collection: " + Assert.GuardArgumentNotNull(nameof(collection), collection) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Filter not matched in collection{0}Collection: {1}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(collection), collection) + ) ); /// @@ -42,9 +47,14 @@ partial class ContainsException : XunitException string item, string collection) => new ContainsException( - "Assert.Contains() Failure: Item not found in collection" + Environment.NewLine + - "Collection: " + Assert.GuardArgumentNotNull(nameof(collection), collection) + Environment.NewLine + - "Not found: " + Assert.GuardArgumentNotNull(nameof(item), item) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Item not found in collection{0}Collection: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(collection), collection), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(item), item) + ) ); /// @@ -57,9 +67,14 @@ partial class ContainsException : XunitException string expectedKey, string keys) => new ContainsException( - "Assert.Contains() Failure: Key not found in dictionary" + Environment.NewLine + - "Keys: " + Assert.GuardArgumentNotNull(nameof(keys), keys) + Environment.NewLine + - "Not found: " + Assert.GuardArgumentNotNull(nameof(expectedKey), expectedKey) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Key not found in dictionary{0}Keys: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(keys), keys), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedKey), expectedKey) + ) ); /// @@ -72,9 +87,14 @@ partial class ContainsException : XunitException string item, string set) => new ContainsException( - "Assert.Contains() Failure: Item not found in set" + Environment.NewLine + - "Set: " + Assert.GuardArgumentNotNull(nameof(set), set) + Environment.NewLine + - "Not found: " + Assert.GuardArgumentNotNull(nameof(item), item) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Item not found in set{0}Set: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(set), set), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(item), item) + ) ); /// @@ -87,9 +107,14 @@ partial class ContainsException : XunitException string expectedSubMemory, string memory) => new ContainsException( - "Assert.Contains() Failure: Sub-memory not found" + Environment.NewLine + - "Memory: " + Assert.GuardArgumentNotNull(nameof(memory), memory) + Environment.NewLine + - "Not found: " + Assert.GuardArgumentNotNull(nameof(expectedSubMemory), expectedSubMemory) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Sub-memory not found{0}Memory: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(memory), memory), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedSubMemory), expectedSubMemory) + ) ); /// @@ -102,9 +127,14 @@ partial class ContainsException : XunitException string expectedSubSpan, string span) => new ContainsException( - "Assert.Contains() Failure: Sub-span not found" + Environment.NewLine + - "Span: " + Assert.GuardArgumentNotNull(nameof(span), span) + Environment.NewLine + - "Not found: " + Assert.GuardArgumentNotNull(nameof(expectedSubSpan), expectedSubSpan) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Sub-span not found{0}Span: {1}{2}Not found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(span), span), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedSubSpan), expectedSubSpan) + ) ); /// @@ -121,9 +151,14 @@ partial class ContainsException : XunitException string @string) => #endif new ContainsException( - "Assert.Contains() Failure: Sub-string not found" + Environment.NewLine + - "String: " + AssertHelper.ShortenAndEncodeString(@string) + Environment.NewLine + - "Not found: " + AssertHelper.ShortenAndEncodeString(Assert.GuardArgumentNotNull(nameof(expectedSubString), expectedSubString)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Contains() Failure: Sub-string not found{0}String: {1}{2}Not found: {3}", + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(@string), + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(Assert.GuardArgumentNotNull(nameof(expectedSubString), expectedSubString)) + ) ); } } diff --git a/Sdk/Exceptions/DistinctException.cs b/Sdk/Exceptions/DistinctException.cs index c3c4bf416bf..483043d6331 100644 --- a/Sdk/Exceptions/DistinctException.cs +++ b/Sdk/Exceptions/DistinctException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class DistinctException : XunitException string item, string collection) => new DistinctException( - "Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine + - "Collection: " + Assert.GuardArgumentNotNull(nameof(collection), collection) + Environment.NewLine + - "Item: " + Assert.GuardArgumentNotNull(nameof(item), item) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Distinct() Failure: Duplicate item found{0}Collection: {1}{2}Item: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(collection), collection), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(item), item) + ) ); } } diff --git a/Sdk/Exceptions/DoesNotContainException.cs b/Sdk/Exceptions/DoesNotContainException.cs index f07b3da2a6f..51a74652d42 100644 --- a/Sdk/Exceptions/DoesNotContainException.cs +++ b/Sdk/Exceptions/DoesNotContainException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; using Xunit.Internal; namespace Xunit.Sdk @@ -38,9 +39,9 @@ partial class DoesNotContainException : XunitException var message = "Assert.DoesNotContain() Failure: Filter matched in collection"; if (failurePointerIndent.HasValue) - message += $"{Environment.NewLine} {new string(' ', failurePointerIndent.Value)}↓ (pos {indexFailurePoint})"; + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); - message += $"{Environment.NewLine}Collection: {collection}"; + message += string.Format(CultureInfo.CurrentCulture, "{0}Collection: {1}", Environment.NewLine, collection); return new DoesNotContainException(message); } @@ -65,9 +66,9 @@ partial class DoesNotContainException : XunitException var message = "Assert.DoesNotContain() Failure: Item found in collection"; if (failurePointerIndent.HasValue) - message += $"{Environment.NewLine} {new string(' ', failurePointerIndent.Value)}↓ (pos {indexFailurePoint})"; + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); - message += $"{Environment.NewLine}Collection: {collection}{Environment.NewLine}Found: {item}"; + message += string.Format(CultureInfo.CurrentCulture, "{0}Collection: {1}{2}Found: {3}", Environment.NewLine, collection, Environment.NewLine, item); return new DoesNotContainException(message); } @@ -82,9 +83,14 @@ partial class DoesNotContainException : XunitException string expectedKey, string keys) => new DoesNotContainException( - "Assert.DoesNotContain() Failure: Key found in dictionary" + Environment.NewLine + - "Keys: " + Assert.GuardArgumentNotNull(nameof(keys), keys) + Environment.NewLine + - "Found: " + Assert.GuardArgumentNotNull(nameof(expectedKey), expectedKey) + string.Format( + CultureInfo.CurrentCulture, + "Assert.DoesNotContain() Failure: Key found in dictionary{0}Keys: {1}{2}Found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(keys), keys), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedKey), expectedKey) + ) ); /// @@ -97,9 +103,14 @@ partial class DoesNotContainException : XunitException string item, string set) => new DoesNotContainException( - "Assert.DoesNotContain() Failure: Item found in set" + Environment.NewLine + - "Set: " + Assert.GuardArgumentNotNull(nameof(set), set) + Environment.NewLine + - "Found: " + Assert.GuardArgumentNotNull(nameof(item), item) + string.Format( + CultureInfo.CurrentCulture, + "Assert.DoesNotContain() Failure: Item found in set{0}Set: {1}{2}Found: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(set), set), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(item), item) + ) ); /// @@ -122,9 +133,9 @@ partial class DoesNotContainException : XunitException var message = "Assert.DoesNotContain() Failure: Sub-memory found"; if (failurePointerIndent.HasValue) - message += $"{Environment.NewLine} {new string(' ', failurePointerIndent.Value)}↓ (pos {indexFailurePoint})"; + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); - message += $"{Environment.NewLine}Memory: {memory}{Environment.NewLine}Found: {expectedSubMemory}"; + message += string.Format(CultureInfo.CurrentCulture, "{0}Memory: {1}{2}Found: {3}", Environment.NewLine, memory, Environment.NewLine, expectedSubMemory); return new DoesNotContainException(message); } @@ -149,9 +160,9 @@ partial class DoesNotContainException : XunitException var message = "Assert.DoesNotContain() Failure: Sub-span found"; if (failurePointerIndent.HasValue) - message += $"{Environment.NewLine} {new string(' ', failurePointerIndent.Value)}↓ (pos {indexFailurePoint})"; + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint); - message += $"{Environment.NewLine}Span: {span}{Environment.NewLine}Found: {expectedSubSpan}"; + message += string.Format(CultureInfo.CurrentCulture, "{0}Span: {1}{2}Found: {3}", Environment.NewLine, span, Environment.NewLine, expectedSubSpan); return new DoesNotContainException(message); } @@ -175,10 +186,17 @@ partial class DoesNotContainException : XunitException var encodedString = AssertHelper.ShortenAndEncodeString(@string, indexFailurePoint, out failurePointerIndent); return new DoesNotContainException( - "Assert.DoesNotContain() Failure: Sub-string found" + Environment.NewLine + - " " + new string(' ', failurePointerIndent) + "↓ (pos " + indexFailurePoint + ")" + Environment.NewLine + - "String: " + encodedString + Environment.NewLine + - "Found: " + AssertHelper.ShortenAndEncodeString(expectedSubString) + string.Format( + CultureInfo.CurrentCulture, + "Assert.DoesNotContain() Failure: Sub-string found{0} {1}\u2193 (pos {2}){3}String: {4}{5}Found: {6}", + Environment.NewLine, + new string(' ', failurePointerIndent), + indexFailurePoint, + Environment.NewLine, + encodedString, + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(expectedSubString) + ) ); } } diff --git a/Sdk/Exceptions/DoesNotMatchException.cs b/Sdk/Exceptions/DoesNotMatchException.cs index b6b05a264b8..0ffdd20495f 100644 --- a/Sdk/Exceptions/DoesNotMatchException.cs +++ b/Sdk/Exceptions/DoesNotMatchException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -35,10 +36,17 @@ partial class DoesNotMatchException : XunitException int failurePointerIndent, string @string) => new DoesNotMatchException( - "Assert.DoesNotMatch() Failure: Match found" + Environment.NewLine + - " " + new string(' ', failurePointerIndent) + "↓ (pos " + indexFailurePoint + ")" + Environment.NewLine + - "String: " + Assert.GuardArgumentNotNull(nameof(@string), @string) + Environment.NewLine + - "RegEx: " + Assert.GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern) + string.Format( + CultureInfo.CurrentCulture, + "Assert.DoesNotMatch() Failure: Match found{0} {1}\u2193 (pos {2}){3}String: {4}{5}RegEx: {6}", + Environment.NewLine, + new string(' ', failurePointerIndent), + indexFailurePoint, + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(@string), @string), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern) + ) ); } } diff --git a/Sdk/Exceptions/EmptyException.cs b/Sdk/Exceptions/EmptyException.cs index 7f8f03cd031..93d132e2fec 100644 --- a/Sdk/Exceptions/EmptyException.cs +++ b/Sdk/Exceptions/EmptyException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; using Xunit.Internal; namespace Xunit.Sdk @@ -28,8 +29,12 @@ partial class EmptyException : XunitException /// The non-empty collection public static EmptyException ForNonEmptyCollection(string collection) => new EmptyException( - "Assert.Empty() Failure: Collection was not empty" + Environment.NewLine + - "Collection: " + Assert.GuardArgumentNotNull(nameof(collection), collection) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Empty() Failure: Collection was not empty{0}Collection: {1}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(collection), collection) + ) ); /// @@ -39,8 +44,12 @@ partial class EmptyException : XunitException /// The non-empty string value public static EmptyException ForNonEmptyString(string value) => new EmptyException( - "Assert.Empty() Failure: String was not empty" + Environment.NewLine + - "String: " + AssertHelper.ShortenAndEncodeString(Assert.GuardArgumentNotNull(nameof(value), value)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Empty() Failure: String was not empty{0}String: {1}", + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(Assert.GuardArgumentNotNull(nameof(value), value)) + ) ); } } diff --git a/Sdk/Exceptions/EndsWithException.cs b/Sdk/Exceptions/EndsWithException.cs index 6d0abf0beb6..1a1faea9793 100644 --- a/Sdk/Exceptions/EndsWithException.cs +++ b/Sdk/Exceptions/EndsWithException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; using Xunit.Internal; namespace Xunit.Sdk @@ -37,9 +38,14 @@ partial class EndsWithException : XunitException string actual) => #endif new EndsWithException( - "Assert.EndsWith() Failure: String end does not match" + Environment.NewLine + - "String: " + AssertHelper.ShortenAndEncodeStringEnd(actual) + Environment.NewLine + - "Expected end: " + AssertHelper.ShortenAndEncodeString(expected) + string.Format( + CultureInfo.CurrentCulture, + "Assert.EndsWith() Failure: String end does not match{0}String: {1}{2}Expected end: {3}", + Environment.NewLine, + AssertHelper.ShortenAndEncodeStringEnd(actual), + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(expected) + ) ); } } diff --git a/Sdk/Exceptions/EqualException.cs b/Sdk/Exceptions/EqualException.cs index 2f66cc4849c..d47eb305ac0 100644 --- a/Sdk/Exceptions/EqualException.cs +++ b/Sdk/Exceptions/EqualException.cs @@ -2,10 +2,12 @@ #nullable enable #else // In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 #pragma warning disable CS8625 #endif using System; +using System.Globalization; using Xunit.Internal; namespace Xunit.Sdk @@ -22,8 +24,14 @@ partial class EqualException : XunitException { static readonly string newLineAndIndent = Environment.NewLine + new string(' ', 10); // Length of "Expected: " and "Actual: " - EqualException(string message) : - base(message) + EqualException( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) : +#else + Exception innerException = null) : +#endif + base(message, innerException) { } /// @@ -51,27 +59,66 @@ partial class EqualException : XunitException int? actualPointer, #if XUNIT_NULLABLE string? actualType, + string? collectionDisplay = null) => +#else + string actualType, + string collectionDisplay = null) => +#endif + ForMismatchedCollectionsWithError(mismatchedIndex, expected, expectedPointer, expectedType, actual, actualPointer, actualType, null, collectionDisplay); + + /// + /// Creates a new instance of to be thrown when two collections + /// are not equal, and an error has occurred during comparison. + /// + /// The index at which the collections differ + /// The expected collection + /// The spacing into the expected collection where the difference occurs + /// The type of the expected collection items, when they differ in type + /// The actual collection + /// The spacing into the actual collection where the difference occurs + /// The type of the actual collection items, when they differ in type + /// The optional exception that was thrown during comparison + /// The display name for the collection type (defaults to "Collections") + public static EqualException ForMismatchedCollectionsWithError( + int? mismatchedIndex, + string expected, + int? expectedPointer, +#if XUNIT_NULLABLE + string? expectedType, +#else + string expectedType, +#endif + string actual, + int? actualPointer, +#if XUNIT_NULLABLE + string? actualType, + Exception? error, string? collectionDisplay = null) #else string actualType, + Exception error, string collectionDisplay = null) #endif { Assert.GuardArgumentNotNull(nameof(actual), actual); - var message = $"Assert.Equal() Failure: {collectionDisplay ?? "Collections"} differ"; - var expectedTypeText = expectedType != null && actualType != null && expectedType != actualType ? $", type {expectedType}" : ""; - var actualTypeText = expectedType != null && actualType != null && expectedType != actualType ? $", type {actualType}" : ""; + var message = + error == null + ? string.Format(CultureInfo.CurrentCulture, "Assert.Equal() Failure: {0} differ", collectionDisplay ?? "Collections") + : "Assert.Equal() Failure: Exception thrown during comparison"; + + var expectedTypeText = expectedType != null && actualType != null && expectedType != actualType ? string.Format(CultureInfo.CurrentCulture, ", type {0}", expectedType) : ""; + var actualTypeText = expectedType != null && actualType != null && expectedType != actualType ? string.Format(CultureInfo.CurrentCulture, ", type {0}", actualType) : ""; if (expectedPointer.HasValue && mismatchedIndex.HasValue) - message += $"{Environment.NewLine} {new string(' ', expectedPointer.Value)}↓ (pos {mismatchedIndex}{expectedTypeText})"; + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2}{3})", Environment.NewLine, new string(' ', expectedPointer.Value), mismatchedIndex, expectedTypeText); - message += $"{Environment.NewLine}Expected: {expected}{Environment.NewLine}Actual: {actual}"; + message += string.Format(CultureInfo.CurrentCulture, "{0}Expected: {1}{2}Actual: {3}", Environment.NewLine, expected, Environment.NewLine, actual); if (actualPointer.HasValue && mismatchedIndex.HasValue) - message += $"{Environment.NewLine} {new string(' ', actualPointer.Value)}↑ (pos {mismatchedIndex}{actualTypeText})"; + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2191 (pos {2}{3})", Environment.NewLine, new string(' ', actualPointer.Value), mismatchedIndex, actualTypeText); - return new EqualException(message); + return new EqualException(message, error); } /// @@ -102,14 +149,12 @@ partial class EqualException : XunitException var formattedActual = AssertHelper.ShortenAndEncodeString(actual, actualIndex, out actualPointer); if (expected != null && expectedIndex > -1 && expectedIndex < expected.Length) - message += newLineAndIndent + new string(' ', expectedPointer) + $"↓ (pos {expectedIndex})"; + message += string.Format(CultureInfo.CurrentCulture, "{0}{1}\u2193 (pos {2})", newLineAndIndent, new string(' ', expectedPointer), expectedIndex); - message += - Environment.NewLine + "Expected: " + formattedExpected + - Environment.NewLine + "Actual: " + formattedActual; + message += string.Format(CultureInfo.CurrentCulture, "{0}Expected: {1}{2}Actual: {3}", Environment.NewLine, formattedExpected, Environment.NewLine, formattedActual); if (actual != null && expectedIndex > -1 && actualIndex < actual.Length) - message += newLineAndIndent + new string(' ', actualPointer) + $"↑ (pos {actualIndex})"; + message += string.Format(CultureInfo.CurrentCulture, "{0}{1}\u2191 (pos {2})", newLineAndIndent, new string(' ', actualPointer), actualIndex); return new EqualException(message); } @@ -127,10 +172,36 @@ partial class EqualException : XunitException #if XUNIT_NULLABLE object? expected, object? actual, + string? banner = null) => +#else + object expected, + object actual, + string banner = null) => +#endif + ForMismatchedValuesWithError(expected, actual, null, banner); + + /// + /// Creates a new instance of to be thrown when two values + /// are not equal. This may be simple values (like intrinsics) or complex values (like + /// classes or structs). Used when an error has occurred during comparison. + /// + /// The expected value + /// The actual value + /// The optional exception that was thrown during comparison + /// The banner to show; if null, then the standard + /// banner of "Values differ" will be used. If is not null, + /// then the banner used will always be "Exception thrown during comparison", regardless + /// of the value passed here. + public static EqualException ForMismatchedValuesWithError( +#if XUNIT_NULLABLE + object? expected, + object? actual, + Exception? error = null, string? banner = null) #else object expected, object actual, + Exception error = null, string banner = null) #endif { @@ -144,10 +215,22 @@ partial class EqualException : XunitException var expectedText = expected as string ?? ArgumentFormatter.Format(expected); var actualText = actual as string ?? ArgumentFormatter.Format(actual); + var message = + error == null + ? string.Format(CultureInfo.CurrentCulture, "Assert.Equal() Failure: {0}", banner ?? "Values differ") + : "Assert.Equal() Failure: Exception thrown during comparison"; + return new EqualException( - "Assert.Equal() Failure: " + (banner ?? "Values differ") + Environment.NewLine + - "Expected: " + expectedText.Replace(Environment.NewLine, newLineAndIndent) + Environment.NewLine + - "Actual: " + actualText.Replace(Environment.NewLine, newLineAndIndent) + string.Format( + CultureInfo.CurrentCulture, + "{0}{1}Expected: {2}{3}Actual: {4}", + message, + Environment.NewLine, + expectedText.Replace(Environment.NewLine, newLineAndIndent), + Environment.NewLine, + actualText.Replace(Environment.NewLine, newLineAndIndent) + ), + error ); } } diff --git a/Sdk/Exceptions/EquivalentException.cs b/Sdk/Exceptions/EquivalentException.cs index ad15e17eaea..0d2188a3039 100644 --- a/Sdk/Exceptions/EquivalentException.cs +++ b/Sdk/Exceptions/EquivalentException.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace Xunit.Sdk @@ -34,7 +35,11 @@ partial class EquivalentException : XunitException static string FormatMemberNameList( IEnumerable memberNames, string prefix) => - "[" + string.Join(", ", memberNames.Select(k => $"\"{prefix}{k}\"")) + "]"; + string.Format( + CultureInfo.CurrentCulture, + "[{0}]", + string.Join(", ", memberNames.Select(k => string.Format(CultureInfo.CurrentCulture, "\"{0}{1}\"", prefix, k))) + ); /// /// Creates a new instance of which shows a message that indicates @@ -42,7 +47,13 @@ partial class EquivalentException : XunitException /// /// The name of the member that caused the circular reference public static EquivalentException ForCircularReference(string memberName) => - new EquivalentException($"Assert.Equivalent() Failure: Circular reference found in '{Assert.GuardArgumentNotNull(nameof(memberName), memberName)}'"); + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Circular reference found in '{0}'", + Assert.GuardArgumentNotNull(nameof(memberName), memberName) + ) + ); /// /// Creates a new instance of which shows a message that indicates @@ -53,7 +64,14 @@ partial class EquivalentException : XunitException public static EquivalentException ForExceededDepth( int depth, string memberName) => - new EquivalentException($"Assert.Equivalent() Failure: Exceeded the maximum depth {depth} with '{Assert.GuardArgumentNotNull(nameof(memberName), memberName)}'; check for infinite recursion or circular references"); + new EquivalentException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Exceeded the maximum depth {0} with '{1}'; check for infinite recursion or circular references", + depth, + Assert.GuardArgumentNotNull(nameof(memberName), memberName) + ) + ); /// /// Creates a new instance of which shows a message that indicates @@ -68,9 +86,14 @@ partial class EquivalentException : XunitException IEnumerable actualMemberNames, string prefix) => new EquivalentException( - "Assert.Equivalent() Failure: Mismatched member list" + Environment.NewLine + - "Expected: " + FormatMemberNameList(Assert.GuardArgumentNotNull(nameof(expectedMemberNames), expectedMemberNames), prefix) + Environment.NewLine + - "Actual: " + FormatMemberNameList(Assert.GuardArgumentNotNull(nameof(actualMemberNames), actualMemberNames), prefix) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Mismatched member list{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + FormatMemberNameList(Assert.GuardArgumentNotNull(nameof(expectedMemberNames), expectedMemberNames), prefix), + Environment.NewLine, + FormatMemberNameList(Assert.GuardArgumentNotNull(nameof(actualMemberNames), actualMemberNames), prefix) + ) ); /// @@ -98,9 +121,15 @@ partial class EquivalentException : XunitException Exception innerException = null) => #endif new EquivalentException( - "Assert.Equivalent() Failure" + (Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : $": Mismatched value on member '{memberName}'") + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(expected) + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(actual), + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure{0}{1}Expected: {2}{3}Actual: {4}", + Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : string.Format(CultureInfo.CurrentCulture, ": Mismatched value on member '{0}'", memberName), + Environment.NewLine, + ArgumentFormatter.Format(expected), + Environment.NewLine, + ArgumentFormatter.Format(actual) + ), innerException ); @@ -122,9 +151,15 @@ partial class EquivalentException : XunitException #endif string memberName) => new EquivalentException( - "Assert.Equivalent() Failure: Collection value not found" + (Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : $" in member '{memberName}'") + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(expected) + Environment.NewLine + - "In: " + ArgumentFormatter.Format(actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Collection value not found{0}{1}Expected: {2}{3}In: {4}", + Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : string.Format(CultureInfo.CurrentCulture, " in member '{0}'", memberName), + Environment.NewLine, + ArgumentFormatter.Format(expected), + Environment.NewLine, + ArgumentFormatter.Format(actual) + ) ); /// @@ -151,9 +186,16 @@ partial class EquivalentException : XunitException #endif string memberName) => new EquivalentException( - "Assert.Equivalent() Failure: Extra values found" + (Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : $" in member '{memberName}'") + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actualLeftovers), actualLeftovers)) + " left over from " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Extra values found{0}{1}Expected: {2}{3}Actual: {4} left over from {5}", + Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : string.Format(CultureInfo.CurrentCulture, " in member '{0}'", memberName), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actualLeftovers), actualLeftovers)), + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) ); /// @@ -172,9 +214,15 @@ partial class EquivalentException : XunitException Type actualType, string memberName) => new EquivalentException( - "Assert.Equivalent() Failure: Types did not match" + (Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : $" in member '{memberName}'") + Environment.NewLine + - "Expected type: " + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(expectedType), expectedType), fullTypeName: true) + Environment.NewLine + - "Actual type: " + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(actualType), actualType), fullTypeName: true) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Equivalent() Failure: Types did not match{0}{1}Expected type: {2}{3}Actual type: {4}", + Assert.GuardArgumentNotNull(nameof(memberName), memberName).Length == 0 ? string.Empty : string.Format(CultureInfo.CurrentCulture, " in member '{0}'", memberName), + Environment.NewLine, + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(expectedType), expectedType), fullTypeName: true), + Environment.NewLine, + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(actualType), actualType), fullTypeName: true) + ) ); } } diff --git a/Sdk/Exceptions/FalseException.cs b/Sdk/Exceptions/FalseException.cs index c0efa8da19c..7977e8976cf 100644 --- a/Sdk/Exceptions/FalseException.cs +++ b/Sdk/Exceptions/FalseException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -34,10 +35,13 @@ partial class FalseException : XunitException #endif bool? value) => new FalseException( - message ?? - "Assert.False() Failure" + Environment.NewLine + - "Expected: False" + Environment.NewLine + - "Actual: " + (value?.ToString() ?? "null") + message ?? string.Format( + CultureInfo.CurrentCulture, + "Assert.False() Failure{0}Expected: False{1}Actual: {2}", + Environment.NewLine, + Environment.NewLine, + value?.ToString() ?? "null" + ) ); } } diff --git a/Sdk/Exceptions/InRangeException.cs b/Sdk/Exceptions/InRangeException.cs index a4e38ed82e3..191dc53695e 100644 --- a/Sdk/Exceptions/InRangeException.cs +++ b/Sdk/Exceptions/InRangeException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -32,9 +33,15 @@ partial class InRangeException : XunitException object low, object high) => new InRangeException( - "Assert.InRange() Failure: Value not in range" + Environment.NewLine + - "Range: (" + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(low), low)) + " - " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(high), high)) + ")" + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.InRange() Failure: Value not in range{0}Range: ({1} - {2}){3}Actual: {4}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(low), low)), + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(high), high)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) ); } } diff --git a/Sdk/Exceptions/IsAssignableFromException.cs b/Sdk/Exceptions/IsAssignableFromException.cs index 28af88d40d5..b33fb6958ff 100644 --- a/Sdk/Exceptions/IsAssignableFromException.cs +++ b/Sdk/Exceptions/IsAssignableFromException.cs @@ -6,6 +6,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -37,9 +38,15 @@ partial class IsAssignableFromException : XunitException object actual) => #endif new IsAssignableFromException( - "Assert.IsAssignableFrom() Failure: Value is " + (actual == null ? "null" : "an incompatible type") + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(actual?.GetType()) + string.Format( + CultureInfo.CurrentCulture, + "Assert.IsAssignableFrom() Failure: Value is {0}{1}Expected: {2}{3}Actual: {4}", + actual == null ? "null" : "an incompatible type", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(actual?.GetType()) + ) ); } } diff --git a/Sdk/Exceptions/IsNotAssignableFromException.cs b/Sdk/Exceptions/IsNotAssignableFromException.cs index 1d59da7f1ec..b7ab4ee6877 100644 --- a/Sdk/Exceptions/IsNotAssignableFromException.cs +++ b/Sdk/Exceptions/IsNotAssignableFromException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class IsNotAssignableFromException : XunitException Type expected, object actual) => new IsNotAssignableFromException( - "Assert.IsNotAssignableFrom() Failure: Value is a compatible type" + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()) + string.Format( + CultureInfo.CurrentCulture, + "Assert.IsNotAssignableFrom() Failure: Value is a compatible type{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()) + ) ); } } diff --git a/Sdk/Exceptions/IsNotTypeException.cs b/Sdk/Exceptions/IsNotTypeException.cs index b370b33bc6f..977c1c676c5 100644 --- a/Sdk/Exceptions/IsNotTypeException.cs +++ b/Sdk/Exceptions/IsNotTypeException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -32,9 +33,14 @@ public static IsNotTypeException ForExactType(Type type) var formattedType = ArgumentFormatter.Format(type); return new IsNotTypeException( - "Assert.IsNotType() Failure: Value is the exact type" + Environment.NewLine + - "Expected: " + formattedType + Environment.NewLine + - "Actual: " + formattedType + string.Format( + CultureInfo.CurrentCulture, + "Assert.IsNotType() Failure: Value is the exact type{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + formattedType, + Environment.NewLine, + formattedType + ) ); } } diff --git a/Sdk/Exceptions/IsTypeException.cs b/Sdk/Exceptions/IsTypeException.cs index 6de679507c3..43149916ed0 100644 --- a/Sdk/Exceptions/IsTypeException.cs +++ b/Sdk/Exceptions/IsTypeException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -34,9 +35,15 @@ partial class IsTypeException : XunitException string actualTypeName) => #endif new IsTypeException( - "Assert.IsType() Failure: Value is " + (actualTypeName == null ? "null" : "not the exact type") + Environment.NewLine + - "Expected: " + Assert.GuardArgumentNotNull(nameof(expectedTypeName), expectedTypeName) + Environment.NewLine + - "Actual: " + (actualTypeName ?? "null") + string.Format( + CultureInfo.CurrentCulture, + "Assert.IsType() Failure: Value is {0}{1}Expected: {2}{3}Actual: {4}", + actualTypeName == null ? "null" : "not the exact type", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expectedTypeName), expectedTypeName), + Environment.NewLine, + actualTypeName ?? "null" + ) ); } } diff --git a/Sdk/Exceptions/MatchesException.cs b/Sdk/Exceptions/MatchesException.cs index 856e41a1223..7aa6e06a5cf 100644 --- a/Sdk/Exceptions/MatchesException.cs +++ b/Sdk/Exceptions/MatchesException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -34,9 +35,14 @@ partial class MatchesException : XunitException string actual) => #endif new MatchesException( - "Assert.Matches() Failure: Pattern not found in value" + Environment.NewLine + - "Regex: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern)) + Environment.NewLine + - "Value: " + ArgumentFormatter.Format(actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Matches() Failure: Pattern not found in value{0}Regex: {1}{2}Value: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expectedRegexPattern), expectedRegexPattern)), + Environment.NewLine, + ArgumentFormatter.Format(actual) + ) ); } } diff --git a/Sdk/Exceptions/NotEqualException.cs b/Sdk/Exceptions/NotEqualException.cs index 06dee597e30..04030cd8393 100644 --- a/Sdk/Exceptions/NotEqualException.cs +++ b/Sdk/Exceptions/NotEqualException.cs @@ -2,10 +2,12 @@ #nullable enable #else // In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE +#pragma warning disable CS8604 #pragma warning disable CS8625 #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -19,8 +21,14 @@ namespace Xunit.Sdk #endif partial class NotEqualException : XunitException { - NotEqualException(string message) : - base(message) + NotEqualException( + string message, +#if XUNIT_NULLABLE + Exception? innerException = null) : +#else + Exception innerException = null) : +#endif + base(message, innerException) { } /// @@ -34,16 +42,57 @@ partial class NotEqualException : XunitException string expected, string actual, #if XUNIT_NULLABLE + string? collectionDisplay = null) => +#else + string collectionDisplay = null) => +#endif + ForEqualCollectionsWithError(null, expected, null, actual, null, null, collectionDisplay); + + /// + /// Creates a new instance of to be thrown when two collections + /// are equal, and an error has occurred during comparison. + /// + /// The index at which the collections error occurred (should be null + /// when is null) + /// The expected collection + /// The spacing into the expected collection where the difference occurs + /// (should be null when is null) + /// The actual collection + /// The spacing into the actual collection where the difference occurs + /// (should be null when is null) + /// The optional exception that was thrown during comparison + /// The display name for the collection type (defaults to "Collections") + public static NotEqualException ForEqualCollectionsWithError( + int? mismatchedIndex, + string expected, + int? expectedPointer, + string actual, + int? actualPointer, +#if XUNIT_NULLABLE + Exception? error = null, string? collectionDisplay = null) #else + Exception error = null, string collectionDisplay = null) #endif { - return new NotEqualException( - "Assert.NotEqual() Failure: " + (collectionDisplay ?? "Collections") + " are equal" + Environment.NewLine + - "Expected: Not " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) - ); + Assert.GuardArgumentNotNull(nameof(expected), expected); + Assert.GuardArgumentNotNull(nameof(actual), actual); + + var message = + error == null + ? string.Format(CultureInfo.CurrentCulture, "Assert.NotEqual() Failure: {0} are equal", collectionDisplay ?? "Collections") + : "Assert.NotEqual() Failure: Exception thrown during comparison"; + + if (expectedPointer.HasValue && mismatchedIndex.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', expectedPointer.Value), mismatchedIndex); + + message += string.Format(CultureInfo.CurrentCulture, "{0}Expected: Not {1}{2}Actual: {3}", Environment.NewLine, expected, Environment.NewLine, actual); + + if (actualPointer.HasValue && mismatchedIndex.HasValue) + message += string.Format(CultureInfo.CurrentCulture, "{0} {1}\u2191 (pos {2})", Environment.NewLine, new string(' ', actualPointer.Value), mismatchedIndex); + + return new NotEqualException(message, error); } /// @@ -59,15 +108,51 @@ partial class NotEqualException : XunitException string expected, string actual, #if XUNIT_NULLABLE + string? banner = null) => +#else + string banner = null) => +#endif + ForEqualValuesWithError(expected, actual, null, banner); + + /// + /// Creates a new instance of to be thrown when two values + /// are equal. This may be simple values (like intrinsics) or complex values (like + /// classes or structs). Used when an error has occurred during comparison. + /// + /// The expected value + /// The actual value + /// The optional exception that was thrown during comparison + /// The banner to show; if null, then the standard + /// banner of "Values are equal" will be used. If is not null, + /// then the banner used will always be "Exception thrown during comparison", regardless + /// of the value passed here. + public static NotEqualException ForEqualValuesWithError( + string expected, + string actual, +#if XUNIT_NULLABLE + Exception? error = null, string? banner = null) #else + Exception error = null, string banner = null) #endif { + var message = + error == null + ? string.Format(CultureInfo.CurrentCulture, "Assert.NotEqual() Failure: {0}", banner ?? "Values are equal") + : "Assert.NotEqual() Failure: Exception thrown during comparison"; + return new NotEqualException( - "Assert.NotEqual() Failure: " + (banner ?? "Values are equal") + Environment.NewLine + - "Expected: Not " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) + string.Format( + CultureInfo.CurrentCulture, + "{0}{1}Expected: Not {2}{3}Actual: {4}", + message, + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ), + error ); } } diff --git a/Sdk/Exceptions/NotInRangeException.cs b/Sdk/Exceptions/NotInRangeException.cs index 8cb61e9b9e9..0800a208ff8 100644 --- a/Sdk/Exceptions/NotInRangeException.cs +++ b/Sdk/Exceptions/NotInRangeException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -32,9 +33,15 @@ partial class NotInRangeException : XunitException object low, object high) => new NotInRangeException( - "Assert.NotInRange() Failure: Value in range" + Environment.NewLine + - "Range: (" + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(low), low)) + " - " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(high), high)) + ")" + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.NotInRange() Failure: Value in range{0}Range: ({1} - {2}){3}Actual: {4}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(low), low)), + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(high), high)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) ); } } diff --git a/Sdk/Exceptions/NotNullException.cs b/Sdk/Exceptions/NotNullException.cs index 7949132ab6d..8d1b1c25896 100644 --- a/Sdk/Exceptions/NotNullException.cs +++ b/Sdk/Exceptions/NotNullException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -26,7 +27,13 @@ partial class NotNullException : XunitException /// /// The inner type of the value public static Exception ForNullStruct(Type type) => - new NotNullException($"Assert.NotNull() Failure: Value of type 'Nullable<{ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(type), type))}>' does not have a value"); + new NotNullException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.NotNull() Failure: Value of type 'Nullable<{0}>' does not have a value", + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(type), type)) + ) + ); /// /// Creates a new instance of the class to be diff --git a/Sdk/Exceptions/NotStrictEqualException.cs b/Sdk/Exceptions/NotStrictEqualException.cs index 36f28409c91..9ce294b81c4 100644 --- a/Sdk/Exceptions/NotStrictEqualException.cs +++ b/Sdk/Exceptions/NotStrictEqualException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class NotStrictEqualException : XunitException string expected, string actual) => new NotStrictEqualException( - "Assert.NotStrictEqual() Failure: Values are equal" + Environment.NewLine + - "Expected: Not " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.NotStrictEqual() Failure: Values are equal{0}Expected: Not {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) ); } } diff --git a/Sdk/Exceptions/NullException.cs b/Sdk/Exceptions/NullException.cs index 0e83e0b473a..f28bfd00d4a 100644 --- a/Sdk/Exceptions/NullException.cs +++ b/Sdk/Exceptions/NullException.cs @@ -6,6 +6,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -34,9 +35,14 @@ partial class NullException : XunitException T? actual) where T : struct => new NullException( - $"Assert.Null() Failure: Value of type 'Nullable<{ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(type), type))}>' has a value" + Environment.NewLine + - "Expected: null" + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Null() Failure: Value of type 'Nullable<{0}>' has a value{1}Expected: null{2}Actual: {3}", + ArgumentFormatter.FormatTypeName(Assert.GuardArgumentNotNull(nameof(type), type)), + Environment.NewLine, + Environment.NewLine, + ArgumentFormatter.Format(actual) + ) ); /// @@ -46,9 +52,13 @@ partial class NullException : XunitException /// The actual non-null value public static NullException ForNonNullValue(object actual) => new NullException( - "Assert.Null() Failure: Value is not null" + Environment.NewLine + - "Expected: null" + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Null() Failure: Value is not null{0}Expected: null{1}Actual: {2}", + Environment.NewLine, + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) ); } } diff --git a/Sdk/Exceptions/ProperSubsetException.cs b/Sdk/Exceptions/ProperSubsetException.cs index dba68371f6e..0602c8c8899 100644 --- a/Sdk/Exceptions/ProperSubsetException.cs +++ b/Sdk/Exceptions/ProperSubsetException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class ProperSubsetException : XunitException string expected, string actual) => new ProperSubsetException( - "Assert.ProperSubset() Failure: Value is not a proper subset" + Environment.NewLine + - "Expected: " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.ProperSubset() Failure: Value is not a proper subset{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) ); } } diff --git a/Sdk/Exceptions/ProperSupersetException.cs b/Sdk/Exceptions/ProperSupersetException.cs index 54167f69249..15546a31454 100644 --- a/Sdk/Exceptions/ProperSupersetException.cs +++ b/Sdk/Exceptions/ProperSupersetException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class ProperSupersetException : XunitException string expected, string actual) => new ProperSupersetException( - "Assert.ProperSuperset() Failure: Value is not a proper superset" + Environment.NewLine + - "Expected: " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.ProperSuperset() Failure: Value is not a proper superset{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) ); } } diff --git a/Sdk/Exceptions/PropertyChangedException.cs b/Sdk/Exceptions/PropertyChangedException.cs index 205f5303ab1..927399cfbf9 100644 --- a/Sdk/Exceptions/PropertyChangedException.cs +++ b/Sdk/Exceptions/PropertyChangedException.cs @@ -2,6 +2,8 @@ #nullable enable #endif +using System.Globalization; + namespace Xunit.Sdk { /// @@ -24,6 +26,12 @@ partial class PropertyChangedException : XunitException /// /// The name of the property that was expected to be changed. public static PropertyChangedException ForUnsetProperty(string propertyName) => - new PropertyChangedException($"Assert.PropertyChanged() failure: Property '{Assert.GuardArgumentNotNull(nameof(propertyName), propertyName)}' was not set"); + new PropertyChangedException( + string.Format( + CultureInfo.CurrentCulture, + "Assert.PropertyChanged() failure: Property '{0}' was not set", + Assert.GuardArgumentNotNull(nameof(propertyName), propertyName) + ) + ); } } diff --git a/Sdk/Exceptions/RaisesAnyException.cs b/Sdk/Exceptions/RaisesAnyException.cs index f2e7bcae249..f6345e9938e 100644 --- a/Sdk/Exceptions/RaisesAnyException.cs +++ b/Sdk/Exceptions/RaisesAnyException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -27,9 +28,13 @@ partial class RaisesAnyException : XunitException /// The type of the event args that was expected public static RaisesAnyException ForNoEvent(Type expected) => new RaisesAnyException( - "Assert.RaisesAny() Failure: No event was raised" + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Actual: No event was raised" + string.Format( + CultureInfo.CurrentCulture, + "Assert.RaisesAny() Failure: No event was raised{0}Expected: {1}{2}Actual: No event was raised", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine + ) ); } } diff --git a/Sdk/Exceptions/RaisesException.cs b/Sdk/Exceptions/RaisesException.cs index 25ad9b237d0..ab959ea082d 100644 --- a/Sdk/Exceptions/RaisesException.cs +++ b/Sdk/Exceptions/RaisesException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class RaisesException : XunitException Type expected, Type actual) => new RaisesException( - "Assert.Raises() Failure: Wrong event type was raised" + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Raises() Failure: Wrong event type was raised{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual)) + ) ); /// @@ -42,9 +48,13 @@ partial class RaisesException : XunitException /// The type of the event args that was expected public static RaisesException ForNoEvent(Type expected) => new RaisesException( - "Assert.Raises() Failure: No event was raised" + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Actual: No event was raised" + string.Format( + CultureInfo.CurrentCulture, + "Assert.Raises() Failure: No event was raised{0}Expected: {1}{2}Actual: No event was raised", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine + ) ); } } diff --git a/Sdk/Exceptions/SameException.cs b/Sdk/Exceptions/SameException.cs index 35cb78f0242..0ec5da0df28 100644 --- a/Sdk/Exceptions/SameException.cs +++ b/Sdk/Exceptions/SameException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class SameException : XunitException string expected, string actual) => new SameException( - "Assert.Same() Failure: Values are not the same instance" + Environment.NewLine + - "Expected: " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Same() Failure: Values are not the same instance{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) ); } } diff --git a/Sdk/Exceptions/SingleException.cs b/Sdk/Exceptions/SingleException.cs index 3af8f3f63e1..cc7054497e1 100644 --- a/Sdk/Exceptions/SingleException.cs +++ b/Sdk/Exceptions/SingleException.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Globalization; namespace Xunit.Sdk { @@ -41,9 +42,14 @@ partial class SingleException : XunitException return new SingleException("Assert.Single() Failure: The collection was empty"); return new SingleException( - "Assert.Single() Failure: The collection did not contain any matching items" + Environment.NewLine + - "Expected: " + expected + Environment.NewLine + - "Collection: " + collection + string.Format( + CultureInfo.CurrentCulture, + "Assert.Single() Failure: The collection did not contain any matching items{0}Expected: {1}{2}Collection: {3}", + Environment.NewLine, + expected, + Environment.NewLine, + collection + ) ); } @@ -68,15 +74,31 @@ partial class SingleException : XunitException Assert.GuardArgumentNotNull(nameof(collection), collection); Assert.GuardArgumentNotNull(nameof(matchIndices), matchIndices); - var message = $"Assert.Single() Failure: The collection contained {count} {(expected == null ? "" : "matching ")}items"; + var message = string.Format( + CultureInfo.CurrentCulture, + "Assert.Single() Failure: The collection contained {0} {1}items", + count, + expected == null ? "" : "matching " + ); if (expected == null) - message += Environment.NewLine + "Collection: " + collection; + message += string.Format( + CultureInfo.CurrentCulture, + "{0}Collection: {1}", + Environment.NewLine, + collection + ); else - message += - Environment.NewLine + "Expected: " + expected + - Environment.NewLine + "Collection: " + collection + - Environment.NewLine + "Match indices: " + string.Join(", ", matchIndices); + message += string.Format( + CultureInfo.CurrentCulture, + "{0}Expected: {1}{2}Collection: {3}{4}Match indices: {5}", + Environment.NewLine, + expected, + Environment.NewLine, + collection, + Environment.NewLine, + string.Join(", ", matchIndices) + ); return new SingleException(message); } diff --git a/Sdk/Exceptions/SkipException.cs b/Sdk/Exceptions/SkipException.cs index 5b05a094263..6befe2e5e3c 100644 --- a/Sdk/Exceptions/SkipException.cs +++ b/Sdk/Exceptions/SkipException.cs @@ -2,6 +2,8 @@ #nullable enable #endif +using System.Globalization; + namespace Xunit.Sdk { /// @@ -24,6 +26,13 @@ partial class SkipException : XunitException /// v3 and later of xUnit.net, as it requires runtime infrastructure changes. /// public static SkipException ForSkip(string message) => - new SkipException($"{DynamicSkipToken.Value}{Assert.GuardArgumentNotNull(nameof(message), message)}"); + new SkipException( + string.Format( + CultureInfo.CurrentCulture, + "{0}{1}", + DynamicSkipToken.Value, + Assert.GuardArgumentNotNull(nameof(message), message) + ) + ); } } diff --git a/Sdk/Exceptions/StartsWithException.cs b/Sdk/Exceptions/StartsWithException.cs index 8c8e8c6c592..48e19b39944 100644 --- a/Sdk/Exceptions/StartsWithException.cs +++ b/Sdk/Exceptions/StartsWithException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; using Xunit.Internal; namespace Xunit.Sdk @@ -37,9 +38,14 @@ partial class StartsWithException : XunitException string actual) => #endif new StartsWithException( - "Assert.StartsWith() Failure: String start does not match" + Environment.NewLine + - "String: " + AssertHelper.ShortenAndEncodeString(actual) + Environment.NewLine + - "Expected start: " + AssertHelper.ShortenAndEncodeString(expected) + string.Format( + CultureInfo.CurrentCulture, + "Assert.StartsWith() Failure: String start does not match{0}String: {1}{2}Expected start: {3}", + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(actual), + Environment.NewLine, + AssertHelper.ShortenAndEncodeString(expected) + ) ); } } diff --git a/Sdk/Exceptions/StrictEqualException.cs b/Sdk/Exceptions/StrictEqualException.cs index b83b64d965b..d4d77a05baa 100644 --- a/Sdk/Exceptions/StrictEqualException.cs +++ b/Sdk/Exceptions/StrictEqualException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class StrictEqualException : XunitException string expected, string actual) => new StrictEqualException( - "Assert.StrictEqual() Failure: Values differ" + Environment.NewLine + - "Expected: " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.StrictEqual() Failure: Values differ{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) ); } } diff --git a/Sdk/Exceptions/SubsetException.cs b/Sdk/Exceptions/SubsetException.cs index f5c62365f42..619d7c468ef 100644 --- a/Sdk/Exceptions/SubsetException.cs +++ b/Sdk/Exceptions/SubsetException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class SubsetException : XunitException string expected, string actual) => new SubsetException( - "Assert.Subset() Failure: Value is not a subset" + Environment.NewLine + - "Expected: " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Subset() Failure: Value is not a subset{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) ); } } diff --git a/Sdk/Exceptions/SupersetException.cs b/Sdk/Exceptions/SupersetException.cs index acf7fe36248..71b07b25937 100644 --- a/Sdk/Exceptions/SupersetException.cs +++ b/Sdk/Exceptions/SupersetException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -30,9 +31,14 @@ partial class SupersetException : XunitException string expected, string actual) => new SupersetException( - "Assert.Superset() Failure: Value is not a superset" + Environment.NewLine + - "Expected: " + Assert.GuardArgumentNotNull(nameof(expected), expected) + Environment.NewLine + - "Actual: " + Assert.GuardArgumentNotNull(nameof(actual), actual) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Superset() Failure: Value is not a superset{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(expected), expected), + Environment.NewLine, + Assert.GuardArgumentNotNull(nameof(actual), actual) + ) ); } } diff --git a/Sdk/Exceptions/ThrowsAnyException.cs b/Sdk/Exceptions/ThrowsAnyException.cs index edf41e6b9d3..4e53a4126f7 100644 --- a/Sdk/Exceptions/ThrowsAnyException.cs +++ b/Sdk/Exceptions/ThrowsAnyException.cs @@ -6,6 +6,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -39,9 +40,14 @@ partial class ThrowsAnyException : XunitException Type expected, Exception actual) => new ThrowsAnyException( - "Assert.ThrowsAny() Failure: Exception type was not compatible" + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()), + string.Format( + CultureInfo.CurrentCulture, + "Assert.ThrowsAny() Failure: Exception type was not compatible{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()) + ), actual ); @@ -52,8 +58,12 @@ partial class ThrowsAnyException : XunitException /// The expected exception type public static ThrowsAnyException ForNoException(Type expected) => new ThrowsAnyException( - "Assert.ThrowsAny() Failure: No exception was thrown" + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.ThrowsAny() Failure: No exception was thrown{0}Expected: {1}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + ) ); } } diff --git a/Sdk/Exceptions/ThrowsException.cs b/Sdk/Exceptions/ThrowsException.cs index 55b0de59fa2..52380c3b4c3 100644 --- a/Sdk/Exceptions/ThrowsException.cs +++ b/Sdk/Exceptions/ThrowsException.cs @@ -6,6 +6,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -39,9 +40,14 @@ partial class ThrowsException : XunitException Type expected, Exception actual) => new ThrowsException( - "Assert.Throws() Failure: Exception type was not an exact match" + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()), + string.Format( + CultureInfo.CurrentCulture, + "Assert.Throws() Failure: Exception type was not an exact match{0}Expected: {1}{2}Actual: {3}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(actual), actual).GetType()) + ), actual ); @@ -62,10 +68,16 @@ partial class ThrowsException : XunitException string actualParamName) => #endif new ThrowsException( - "Assert.Throws() Failure: Incorrect parameter name" + Environment.NewLine + - "Exception: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(expectedParamName) + Environment.NewLine + - "Actual: " + ArgumentFormatter.Format(actualParamName) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Throws() Failure: Incorrect parameter name{0}Exception: {1}{2}Expected: {3}{4}Actual: {5}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)), + Environment.NewLine, + ArgumentFormatter.Format(expectedParamName), + Environment.NewLine, + ArgumentFormatter.Format(actualParamName) + ) ); /// @@ -75,8 +87,12 @@ partial class ThrowsException : XunitException /// The expected exception type public static ThrowsException ForNoException(Type expected) => new ThrowsException( - "Assert.Throws() Failure: No exception was thrown" + Environment.NewLine + - "Expected: " + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + string.Format( + CultureInfo.CurrentCulture, + "Assert.Throws() Failure: No exception was thrown{0}Expected: {1}", + Environment.NewLine, + ArgumentFormatter.Format(Assert.GuardArgumentNotNull(nameof(expected), expected)) + ) ); } } diff --git a/Sdk/Exceptions/TrueException.cs b/Sdk/Exceptions/TrueException.cs index c01e3ff53fd..a59ca6c2ea5 100644 --- a/Sdk/Exceptions/TrueException.cs +++ b/Sdk/Exceptions/TrueException.cs @@ -3,6 +3,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -34,11 +35,14 @@ partial class TrueException : XunitException #endif bool? value) => new TrueException( - message != null - ? message - : "Assert.True() Failure" + Environment.NewLine + - "Expected: True" + Environment.NewLine + - "Actual: " + (value?.ToString() ?? "null") + message ?? + string.Format( + CultureInfo.CurrentCulture, + "Assert.True() Failure{0}Expected: True{1}Actual: {2}", + Environment.NewLine, + Environment.NewLine, + value?.ToString() ?? "null" + ) ); } } diff --git a/Sdk/Exceptions/XunitException.cs b/Sdk/Exceptions/XunitException.cs index c5578c9140e..8b898eeb53f 100644 --- a/Sdk/Exceptions/XunitException.cs +++ b/Sdk/Exceptions/XunitException.cs @@ -6,6 +6,7 @@ #endif using System; +using System.Globalization; namespace Xunit.Sdk { @@ -59,11 +60,11 @@ public override string ToString() if (message == null || message.Length <= 0) result = className; else - result = $"{className}: {message}"; + result = string.Format(CultureInfo.CurrentCulture, "{0}: {1}", className, message); var stackTrace = StackTrace; if (stackTrace != null) - result = $"{result}{Environment.NewLine}{stackTrace}"; + result = string.Format(CultureInfo.CurrentCulture, "{0}{1}{2}", result, Environment.NewLine, stackTrace); return result; } diff --git a/TypeAsserts.cs b/TypeAsserts.cs index 57a8f785c6f..3ad7bbd9770 100644 --- a/TypeAsserts.cs +++ b/TypeAsserts.cs @@ -7,6 +7,7 @@ #endif using System; +using System.Globalization; using System.Reflection; using Xunit.Sdk; @@ -177,8 +178,8 @@ public static T IsType(object @object) if (expectedTypeName == actualTypeName) { - expectedTypeName += $" (from {expectedType.GetTypeInfo().Assembly.GetName().FullName})"; - actualTypeName += $" (from {actualType.GetTypeInfo().Assembly.GetName().FullName})"; + expectedTypeName += string.Format(CultureInfo.CurrentCulture, " (from {0})", expectedType.GetTypeInfo().Assembly.GetName().FullName); + actualTypeName += string.Format(CultureInfo.CurrentCulture, " (from {0})", actualType.GetTypeInfo().Assembly.GetName().FullName); } throw IsTypeException.ForMismatchedType(expectedTypeName, actualTypeName);