File: Sdk\Exceptions\AssertActualExpectedException.cs
Web Access
Project: src\src\Microsoft.DotNet.XUnitAssert\src\Microsoft.DotNet.XUnitAssert.csproj (xunit.assert)
#if XUNIT_NULLABLE
#nullable enable
#endif

using System;
using System.Collections;
using System.Linq;
using System.Reflection;

#if XUNIT_NULLABLE
using System.Diagnostics.CodeAnalysis;
#endif

namespace Xunit.Sdk
{
	/// <summary>
	/// Base class for exceptions that have actual and expected values
	/// </summary>
#if XUNIT_VISIBILITY_INTERNAL
	internal
#else
	public
#endif
	class AssertActualExpectedException : XunitException
	{
		/// <summary>
		/// Creates a new instance of the <see href="AssertActualExpectedException"/> class.
		/// </summary>
		/// <param name="expected">The expected value</param>
		/// <param name="actual">The actual value</param>
		/// <param name="userMessage">The user message to be shown</param>
		/// <param name="expectedTitle">The title to use for the expected value (defaults to "Expected")</param>
		/// <param name="actualTitle">The title to use for the actual value (defaults to "Actual")</param>
		public AssertActualExpectedException(
#if XUNIT_NULLABLE
			object? expected,
			object? actual,
			string? userMessage,
			string? expectedTitle = null,
			string? actualTitle = null) :
#else
			object expected,
			object actual,
			string userMessage,
			string expectedTitle = null,
			string actualTitle = null) :
#endif
				this(expected, actual, userMessage, expectedTitle, actualTitle, null)
		{ }

		/// <summary>
		/// Creates a new instance of the <see href="AssertActualExpectedException"/> class.
		/// </summary>
		/// <param name="expected">The expected value</param>
		/// <param name="actual">The actual value</param>
		/// <param name="userMessage">The user message to be shown</param>
		/// <param name="expectedTitle">The title to use for the expected value (defaults to "Expected")</param>
		/// <param name="actualTitle">The title to use for the actual value (defaults to "Actual")</param>
		/// <param name="innerException">The inner exception.</param>
		public AssertActualExpectedException(
#if XUNIT_NULLABLE
			object? expected,
			object? actual,
			string? userMessage,
			string? expectedTitle,
			string? actualTitle,
			Exception? innerException) :
#else
			object expected,
			object actual,
			string userMessage,
			string expectedTitle,
			string actualTitle,
			Exception innerException) :
#endif
				base(userMessage, innerException)
		{
			Actual = ConvertToString(actual);
			ActualTitle = actualTitle ?? "Actual";
			Expected = ConvertToString(expected);
			ExpectedTitle = expectedTitle ?? "Expected";

			if (actual != null &&
				expected != null &&
				Actual == Expected &&
				actual.GetType() != expected.GetType())
			{
				Actual += $" ({actual.GetType().FullName})";
				Expected += $" ({expected.GetType().FullName})";
			}
		}

		/// <summary>
		/// Gets the actual value.
		/// </summary>
#if XUNIT_NULLABLE
		public string? Actual { get; }
#else
		public string Actual { get; }
#endif

		/// <summary>
		/// Gets the title used for the actual value.
		/// </summary>
		public string ActualTitle { get; }

		/// <summary>
		/// Gets the expected value.
		/// </summary>
#if XUNIT_NULLABLE
		public string? Expected { get; }
#else
		public string Expected { get; }
#endif

		/// <summary>
		/// Gets the title used for the expected value.
		/// </summary>
		public string ExpectedTitle { get; }

		/// <summary>
		/// Gets a message that describes the current exception. Includes the expected and actual values.
		/// </summary>
		/// <returns>The error message that explains the reason for the exception, or an empty string("").</returns>
		/// <filterpriority>1</filterpriority>
		public override string Message
		{
			get
			{
				var titleLength = Math.Max(ExpectedTitle.Length, ActualTitle.Length) + 2;  // + the colon and space
				var formattedExpectedTitle = (ExpectedTitle + ":").PadRight(titleLength);
				var formattedActualTitle = (ActualTitle + ":").PadRight(titleLength);
				var indentedNewLine = Environment.NewLine + " ".PadRight(titleLength);

				return $"{base.Message}{Environment.NewLine}{formattedExpectedTitle}{Expected?.Replace(Environment.NewLine, indentedNewLine) ?? "(null)"}{Environment.NewLine}{formattedActualTitle}{Actual?.Replace(Environment.NewLine, indentedNewLine) ?? "(null)"}";
			}
		}

		static string ConvertToSimpleTypeName(TypeInfo typeInfo)
		{
			if (!typeInfo.IsGenericType)
				return typeInfo.Name;

			var simpleNames = typeInfo.GenericTypeArguments.Select(type => ConvertToSimpleTypeName(type.GetTypeInfo()));
			var backTickIdx = typeInfo.Name.IndexOf('`');
			if (backTickIdx < 0)
				backTickIdx = typeInfo.Name.Length;  // F# doesn't use backticks for generic type names

			return $"{typeInfo.Name.Substring(0, backTickIdx)}<{string.Join(", ", simpleNames)}>";
		}

#if XUNIT_NULLABLE
		[return: NotNullIfNotNull("value")]
		static string? ConvertToString(object? value)
#else
		static string ConvertToString(object value)
#endif
		{
			if (value == null)
				return null;

			var stringValue = value as string;
			if (stringValue != null)
				return stringValue;

			var formattedValue = ArgumentFormatter.Format(value);
			if (value is IEnumerable)
				formattedValue = $"{ConvertToSimpleTypeName(value.GetType().GetTypeInfo())} {formattedValue}";

			return formattedValue;
		}
	}
}