Skip to content

Commit

Permalink
xunit/xunit#2800: Record exceptions from Assert.(Not)Equal comparer
Browse files Browse the repository at this point in the history
  • Loading branch information
bradwilson committed Oct 25, 2023
1 parent ba25254 commit a92673b
Show file tree
Hide file tree
Showing 3 changed files with 275 additions and 53 deletions.
132 changes: 102 additions & 30 deletions EqualityAsserts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ partial class Assert

var expectedTracker = expected.AsNonStringTracker();
var actualTracker = actual.AsNonStringTracker();
var exception = default(Exception);

try
{
Expand All @@ -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
{
Expand Down Expand Up @@ -164,8 +174,15 @@ partial class Assert

if (itemComparer != null)
{
if (CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer<T>.DefaultInnerComparer, out mismatchedIndex))
return;
try
{
if (CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer<T>.DefaultInnerComparer, out mismatchedIndex))
return;
}
catch (Exception ex)
{
exception = ex;
}

var expectedStartIdx = -1;
var expectedEndIdx = -1;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -568,6 +592,7 @@ partial class Assert

var expectedTracker = expected.AsNonStringTracker();
var actualTracker = actual.AsNonStringTracker();
var exception = default(Exception);

try
{
Expand All @@ -578,26 +603,35 @@ partial class Assert

if (!haveCollections)
{
if (comparer.Equals(expected, actual))
try
{
if (!comparer.Equals(expected, actual))
return;
}
catch (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.ForEqualCollections(formattedExpected, formattedActual, "Strings");
else
throw NotEqualException.ForEqualValues(formattedExpected, formattedActual);
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);
Expand All @@ -610,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<T>.DefaultInnerComparer, out mismatchedIndex))
return;
try
{
if (!CollectionTracker.AreCollectionsEqual(expectedTracker, actualTracker, itemComparer, itemComparer == AssertEqualityComparer<T>.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);

expectedPointer = null;
formattedExpected = expectedTracker?.FormatIndexedMismatch(expectedStartIdx, expectedEndIdx, mismatchedIndex, out expectedPointer) ?? ArgumentFormatter.Format(expected);

formattedExpected = expectedTracker?.FormatStart() ?? "null";
formattedActual = actualTracker?.FormatStart() ?? "null";
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);
Expand Down Expand Up @@ -659,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
Expand Down
92 changes: 85 additions & 7 deletions Sdk/Exceptions/EqualException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#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

Expand All @@ -23,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)
{ }

/// <summary>
Expand Down Expand Up @@ -52,15 +59,54 @@ 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);

/// <summary>
/// Creates a new instance of <see cref="EqualException"/> to be thrown when two collections
/// are not equal, and an error has occurred during comparison.
/// </summary>
/// <param name="mismatchedIndex">The index at which the collections differ</param>
/// <param name="expected">The expected collection</param>
/// <param name="expectedPointer">The spacing into the expected collection where the difference occurs</param>
/// <param name="expectedType">The type of the expected collection items, when they differ in type</param>
/// <param name="actual">The actual collection</param>
/// <param name="actualPointer">The spacing into the actual collection where the difference occurs</param>
/// <param name="actualType">The type of the actual collection items, when they differ in type</param>
/// <param name="error">The optional exception that was thrown during comparison</param>
/// <param name="collectionDisplay">The display name for the collection type (defaults to "Collections")</param>
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 = string.Format(CultureInfo.CurrentCulture, "Assert.Equal() Failure: {0} differ", collectionDisplay ?? "Collections");
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) : "";

Expand All @@ -72,7 +118,7 @@ partial class EqualException : XunitException
if (actualPointer.HasValue && mismatchedIndex.HasValue)
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);
}

/// <summary>
Expand Down Expand Up @@ -126,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);

/// <summary>
/// Creates a new instance of <see cref="EqualException"/> 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.
/// </summary>
/// <param name="expected">The expected value</param>
/// <param name="actual">The actual value</param>
/// <param name="error">The optional exception that was thrown during comparison</param>
/// <param name="banner">The banner to show; if <c>null</c>, then the standard
/// banner of "Values differ" will be used. If <paramref name="error"/> is not <c>null</c>,
/// then the banner used will always be "Exception thrown during comparison", regardless
/// of the value passed here.</param>
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
{
Expand All @@ -143,16 +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(
string.Format(
CultureInfo.CurrentCulture,
"Assert.Equal() Failure: {0}{1}Expected: {2}{3}Actual: {4}",
banner ?? "Values differ",
"{0}{1}Expected: {2}{3}Actual: {4}",
message,
Environment.NewLine,
expectedText.Replace(Environment.NewLine, newLineAndIndent),
Environment.NewLine,
actualText.Replace(Environment.NewLine, newLineAndIndent)
)
),
error
);
}
}
Expand Down

0 comments on commit a92673b

Please sign in to comment.