File: Sdk\ArgumentFormatter.cs
Web Access
Project: src\src\Microsoft.DotNet.XUnitAssert\src\Microsoft.DotNet.XUnitAssert.csproj (xunit.assert)
#pragma warning disable CA1031 // Do not catch general exception types
#pragma warning disable CA1707 // Identifiers should not contain underscores
#pragma warning disable CA1810 // Initialize reference type static fields inline
#pragma warning disable CA1825 // Avoid zero-length array allocations
#pragma warning disable CA2263 // Prefer generic overload when type is known
#pragma warning disable IDE0018 // Inline variable declaration
#pragma warning disable IDE0019 // Use pattern matching
#pragma warning disable IDE0038 // Use pattern matching
#pragma warning disable IDE0040 // Add accessibility modifiers
#pragma warning disable IDE0045 // Convert to conditional expression
#pragma warning disable IDE0046 // Convert to conditional expression
#pragma warning disable IDE0057 // Use range operator
#pragma warning disable IDE0058 // Expression value is never used
#pragma warning disable IDE0078 // Use pattern matching
#pragma warning disable IDE0090 // Use 'new(...)'
#pragma warning disable IDE0161 // Convert to file-scoped namespace
#pragma warning disable IDE0300 // Simplify collection initialization

#if XUNIT_NULLABLE
#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 CS8602
#pragma warning disable CS8603
#pragma warning disable CS8604
#pragma warning disable CS8605
#pragma warning disable CS8618
#pragma warning disable CS8625
#endif

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

#if XUNIT_ARGUMENTFORMATTER_PRIVATE
namespace Xunit.Internal
#else
namespace Xunit.Sdk
#endif
{
	/// <summary>
	/// Formats value for display in assertion messages and data-driven test display names.
	/// </summary>
#if XUNIT_VISIBILITY_INTERNAL || XUNIT_ARGUMENTFORMATTER_PRIVATE
	internal
#else
	public
#endif
	static class ArgumentFormatter
	{
		internal static readonly string EllipsisInBrackets = "[" + new string((char)0x00B7, 3) + "]";

		/// <summary>
		/// Gets the maximum printing depth, in terms of objects before truncation.
		/// </summary>
		public const int MAX_DEPTH = 3;

		/// <summary>
		/// Gets the maximum number of values printed for collections before truncation.
		/// </summary>
		public const int MAX_ENUMERABLE_LENGTH = 5;

		/// <summary>
		/// Gets the maximum number of items (properties or fields) printed in an object before truncation.
		/// </summary>
		public const int MAX_OBJECT_ITEM_COUNT = 5;

		/// <summary>
		/// Gets the maximum strength length before truncation.
		/// </summary>
		public const int MAX_STRING_LENGTH = 50;

		static readonly object[] EmptyObjects = new object[0];
		static readonly Type[] EmptyTypes = new Type[0];

#if !XUNIT_AOT
#if XUNIT_NULLABLE
		static readonly PropertyInfo? tupleIndexer;
		static readonly Type? tupleInterfaceType;
		static readonly PropertyInfo? tupleLength;
#else
		static readonly PropertyInfo tupleIndexer;
		static readonly Type tupleInterfaceType;
		static readonly PropertyInfo tupleLength;
#endif
#endif

		// List of intrinsic types => C# type names
		static readonly Dictionary<TypeInfo, string> TypeMappings = new Dictionary<TypeInfo, string>
		{
			{ typeof(bool).GetTypeInfo(), "bool" },
			{ typeof(byte).GetTypeInfo(), "byte" },
			{ typeof(sbyte).GetTypeInfo(), "sbyte" },
			{ typeof(char).GetTypeInfo(), "char" },
			{ typeof(decimal).GetTypeInfo(), "decimal" },
			{ typeof(double).GetTypeInfo(), "double" },
			{ typeof(float).GetTypeInfo(), "float" },
			{ typeof(int).GetTypeInfo(), "int" },
			{ typeof(uint).GetTypeInfo(), "uint" },
			{ typeof(long).GetTypeInfo(), "long" },
			{ typeof(ulong).GetTypeInfo(), "ulong" },
			{ typeof(object).GetTypeInfo(), "object" },
			{ typeof(short).GetTypeInfo(), "short" },
			{ typeof(ushort).GetTypeInfo(), "ushort" },
			{ typeof(string).GetTypeInfo(), "string" },
			{ typeof(IntPtr).GetTypeInfo(), "nint" },
			{ typeof(UIntPtr).GetTypeInfo(), "nuint" },
		};

#if !XUNIT_AOT
		static ArgumentFormatter()
		{
			tupleInterfaceType = Type.GetType("System.Runtime.CompilerServices.ITuple");

			if (tupleInterfaceType != null)
			{
				tupleIndexer = tupleInterfaceType.GetRuntimeProperty("Item");
				tupleLength = tupleInterfaceType.GetRuntimeProperty("Length");
			}

			if (tupleIndexer == null || tupleLength == null)
				tupleInterfaceType = null;
		}
#endif

		/// <summary>
		/// Gets the ellipsis value (three middle dots, aka U+00B7).
		/// </summary>
		public static string Ellipsis { get; } = new string((char)0x00B7, 3);

		/// <summary>
		/// Escapes a string for printing, attempting to most closely model the value on how you would
		/// enter the value in a C# string literal. That means control codes that are normally backslash
		/// escaped (like "\n" for newline) are represented like that; all other control codes for ASCII
		/// values under 32 are printed as "\xnn".
		/// </summary>
		/// <param name="s">The string value to be escaped</param>
		public static string EscapeString(string s)
		{
#if NET6_0_OR_GREATER
			ArgumentNullException.ThrowIfNull(s);
#else
			if (s == null)
				throw new ArgumentNullException(nameof(s));
#endif

			var builder = new StringBuilder(s.Length);
			for (var i = 0; i < s.Length; i++)
			{
				var ch = s[i];
#if XUNIT_NULLABLE
				string? escapeSequence;
#else
				string escapeSequence;
#endif
				if (TryGetEscapeSequence(ch, out escapeSequence))
					builder.Append(escapeSequence);
				else if (ch < 32) // C0 control char
					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
					builder.Append(ch);
					builder.Append(s[++i]);
				}
				// Check for stray surrogates/other invalid chars
				else if (char.IsSurrogate(ch) || ch == '\uFFFE' || ch == '\uFFFF')
				{
					builder.AppendFormat(CultureInfo.CurrentCulture, @"\x{0}", (+ch).ToString("x4", CultureInfo.CurrentCulture));
				}
				else
					builder.Append(ch); // Append the char like normal
			}
			return builder.ToString();
		}

#if XUNIT_NULLABLE
		public static string Format(Type? value)
#else
		public static string Format(Type value)
#endif
		{
			if (value is null)
				return "null";

			return string.Format(CultureInfo.CurrentCulture, "typeof({0})", FormatTypeName(value, fullTypeName: true));
		}

		/// <summary>
		/// Formats a value for display.
		/// </summary>
		/// <param name="value">The value to be formatted</param>
		/// <param name="depth">The optional printing depth (1 indicates a top-level value)</param>
		[DynamicDependency("ToString", typeof(object))]
		[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072", Justification = "We can't easily annotate callers of this type to require them to preserve the ToString method as we need to use the runtime type. We also can't preserve all of the properties and fields for the complex type printing, but any members that are trimmed aren't used and thus don't contribute to the asserts.")]
		public static string Format<
			[DynamicallyAccessedMembers(
				DynamicallyAccessedMemberTypes.PublicFields |
				DynamicallyAccessedMemberTypes.NonPublicFields |
				DynamicallyAccessedMemberTypes.PublicProperties |
				DynamicallyAccessedMemberTypes.NonPublicProperties |
				DynamicallyAccessedMemberTypes.PublicMethods)] T>(T value, int depth = 1)
		{
			if (value == null)
				return "null";

			var valueAsType = value as Type;
			if (valueAsType != null)
				return string.Format(CultureInfo.CurrentCulture, "typeof({0})", FormatTypeName(valueAsType, fullTypeName: true));

			try
			{
				if (value.GetType().GetTypeInfo().IsEnum)
					return FormatEnumValue(value);

				if (value is char c)
					return FormatCharValue(c);

				if (value is float)
					return FormatFloatValue(value);

				if (value is double)
					return FormatDoubleValue(value);

				if (value is DateTime || value is DateTimeOffset)
					return FormatDateTimeValue(value);

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

#if !XUNIT_ARGUMENTFORMATTER_PRIVATE
				var tracker = value as CollectionTracker;
				if (tracker != null)
					return tracker.FormatStart(depth);
#endif

				var enumerable = value as IEnumerable;
				if (enumerable != null)
					return FormatEnumerableValue(enumerable, depth);

				var type = value.GetType();
				var typeInfo = type.GetTypeInfo();

				if (value is ITuple tuple)
					return FormatTupleValue(tuple, depth);

				if (typeInfo.IsValueType)
					return FormatValueTypeValue(value, typeInfo);

				var task = value as Task;
				if (task != null)
				{
					var typeParameters = typeInfo.GenericTypeArguments;
					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?

				var isAnonymousType = typeInfo.IsAnonymousType();
				if (!isAnonymousType)
				{
					var toString = type.GetRuntimeMethod("ToString", EmptyTypes);

					if (toString != null && toString.DeclaringType != typeof(object))
#if XUNIT_NULLABLE
						return ((string?)toString.Invoke(value, EmptyObjects)) ?? "null";
#else
						return ((string)toString.Invoke(value, EmptyObjects)) ?? "null";
#endif
				}

				return FormatComplexValue(value, depth, type, isAnonymousType);
			}
			catch (Exception ex)
			{
				// 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 string.Format(CultureInfo.CurrentCulture, "{0} was thrown formatting an object of type \"{1}\"", ex.GetType().Name, value.GetType());
			}
		}

		static string FormatCharValue(char value)
		{
			if (value == '\'')
				return @"'\''";

			// Take care of all of the escape sequences
#if XUNIT_NULLABLE
			string? escapeSequence;
#else
			string escapeSequence;
#endif
			if (TryGetEscapeSequence(value, out escapeSequence))
				return string.Format(CultureInfo.CurrentCulture, "'{0}'", escapeSequence);

			if (char.IsLetterOrDigit(value) || char.IsPunctuation(value) || char.IsSymbol(value) || value == ' ')
				return string.Format(CultureInfo.CurrentCulture, "'{0}'", value);

			// Fallback to hex
			return string.Format(CultureInfo.CurrentCulture, "0x{0:x4}", (int)value);
		}

		static string FormatComplexValue(
			object value,
			int depth,
			[DynamicallyAccessedMembers(
				DynamicallyAccessedMemberTypes.PublicFields |
				DynamicallyAccessedMemberTypes.NonPublicFields |
				DynamicallyAccessedMemberTypes.PublicProperties |
				DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type,
			bool isAnonymousType)
		{
			var typeName = isAnonymousType ? "" : type.Name + " ";

			if (depth == MAX_DEPTH)
				return string.Format(CultureInfo.CurrentCulture, "{0}{{ {1} }}", typeName, Ellipsis);

			var fields =
				type
					.GetRuntimeFields()
					.Where(f => f.IsPublic && !f.IsStatic)
					.Select(f => new { name = f.Name, value = WrapAndGetFormattedValue(() => f.GetValue(value), depth) });

			var properties =
				type
					.GetRuntimeProperties()
					.Where(p => p.GetMethod != null && p.GetMethod.IsPublic && !p.GetMethod.IsStatic)
					.Select(p => new { name = p.Name, value = WrapAndGetFormattedValue(() => p.GetValue(value), depth) });

			var parameters =
				fields
					.Concat(properties)
					.OrderBy(p => p.name)
					.Take(MAX_OBJECT_ITEM_COUNT + 1)
					.ToList();

			if (parameters.Count == 0)
				return string.Format(CultureInfo.CurrentCulture, "{0}{{ }}", typeName);

			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 string.Format(CultureInfo.CurrentCulture, "{0}{{ {1} }}", typeName, formattedParameters);
		}

		static string FormatDateTimeValue(object value) =>
			string.Format(CultureInfo.CurrentCulture, "{0:o}", value);

		static string FormatDoubleValue(object value) =>
			string.Format(CultureInfo.CurrentCulture, "{0:G17}", value);

		static string FormatEnumValue(object value) =>
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
			value.ToString()?.Replace(", ", " | ", StringComparison.Ordinal) ?? "null";
#else
			value.ToString()?.Replace(", ", " | ") ?? "null";
#endif

		static string FormatEnumerableValue(
			IEnumerable enumerable,
			int depth)
		{
			if (depth == MAX_DEPTH)
				return EllipsisInBrackets;

			var result = new StringBuilder();

#if !XUNIT_AOT
			var groupingTypes = GetGroupingTypes(enumerable);
			if (groupingTypes != null)
			{
				var groupingInterface = typeof(IGrouping<,>).MakeGenericType(groupingTypes);
				var key = groupingInterface.GetRuntimeProperty("Key")?.GetValue(enumerable);
				result.AppendFormat(CultureInfo.CurrentCulture, "[{0}] = ", key?.ToString() ?? "null");
			}
			else if (!SafeToMultiEnumerate(enumerable))
#else
			if (!SafeToMultiEnumerate(enumerable))
#endif
				return EllipsisInBrackets;

			// This should only be used on values that are known to be re-enumerable
			// safely, like collections that implement IDictionary or IList.
			var idx = 0;
			var enumerator = enumerable.GetEnumerator();

			result.Append('[');

			while (enumerator.MoveNext())
			{
				if (idx != 0)
					result.Append(", ");

				if (idx == MAX_ENUMERABLE_LENGTH)
				{
					result.Append(Ellipsis);
					break;
				}

				var current = enumerator.Current;
				var nextDepth = current is IEnumerable ? depth + 1 : depth;

				result.Append(Format(current, nextDepth));

				++idx;
			}

			result.Append(']');
			return result.ToString();
		}

		static string FormatFloatValue(object value) =>
			string.Format(CultureInfo.CurrentCulture, "{0:G9}", value);

		static string FormatStringValue(string value)
		{
#if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
			value = EscapeString(value).Replace(@"""", @"\""", StringComparison.Ordinal); // escape double quotes
#else
			value = EscapeString(value).Replace(@"""", @"\"""); // escape double quotes
#endif

			if (value.Length > MAX_STRING_LENGTH)
			{
				var displayed = value.Substring(0, MAX_STRING_LENGTH);
				return string.Format(CultureInfo.CurrentCulture, "\"{0}\"{1}", displayed, Ellipsis);
			}

			return string.Format(CultureInfo.CurrentCulture, "\"{0}\"", value);
		}

		static string FormatTupleValue(
			ITuple tupleParameter,
			int depth)
		{
			var result = new StringBuilder("Tuple (");
			var length = tupleParameter.Length;

			for (var idx = 0; idx < length; ++idx)
			{
				if (idx != 0)
					result.Append(", ");

				var value = tupleParameter[idx];
				result.Append(Format(value, depth + 1));
			}

			result.Append(')');

			return result.ToString();
		}

		/// <summary>
		/// Formats a type. This maps built-in C# types to their C# native name (e.g., printing "int" instead
		/// of "Int32" or "System.Int32").
		/// </summary>
		/// <param name="type">The type to get the formatted name of</param>
		/// <param name="fullTypeName">Set to <c>true</c> to include the namespace; set to <c>false</c> for just the simple type name</param>
		public static string FormatTypeName(
			Type type,
			bool fullTypeName = false)
		{
			var typeInfo = type.GetTypeInfo();
			var arraySuffix = "";

			// Deconstruct and re-construct array
			while (typeInfo.IsArray)
			{
				if (typeInfo.IsSZArrayType())
					arraySuffix += "[]";
				else
				{
					var rank = typeInfo.GetArrayRank();
					if (rank == 1)
						arraySuffix += "[*]";
					else
						arraySuffix += string.Format(CultureInfo.CurrentCulture, "[{0}]", new string(',', rank - 1));
				}

#if XUNIT_NULLABLE
				typeInfo = typeInfo.GetElementType()!.GetTypeInfo();
#else
				typeInfo = typeInfo.GetElementType().GetTypeInfo();
#endif
			}

			// Map C# built-in type names
#if XUNIT_NULLABLE
			string? result;
#else
			string result;
#endif
			var shortTypeInfo = typeInfo.IsGenericType ? typeInfo.GetGenericTypeDefinition().GetTypeInfo() : typeInfo;
			if (!TypeMappings.TryGetValue(shortTypeInfo, out result))
				result = fullTypeName ? typeInfo.FullName : typeInfo.Name;

			if (result == null)
				return typeInfo.Name;

#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
			var tickIdx = result.IndexOf('`', StringComparison.Ordinal);
#else
			var tickIdx = result.IndexOf('`');
#endif
			if (tickIdx > 0)
				result = result.Substring(0, tickIdx);

			if (typeInfo.IsGenericTypeDefinition)
				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 = string.Format(CultureInfo.CurrentCulture, "{0}<{1}>", result, string.Join(", ", typeInfo.GenericTypeArguments.Select(t => FormatTypeName(t))));
			}

			return result + arraySuffix;
		}

		[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(KeyValuePair<,>))]
		[DynamicDependency("ToString", typeof(object))]
		[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070", Justification = "We can't easily annotate callers of this type to require them to preserve properties for the one type we need or the ToString method as we need to use the runtime type")]
		static string FormatValueTypeValue(
			object value,
			TypeInfo typeInfo)
		{
			if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
			{
				var k = typeInfo.GetProperty("Key")?.GetValue(value, null);
				var v = typeInfo.GetProperty("Value")?.GetValue(value, null);

				return string.Format(CultureInfo.CurrentCulture, "[{0}] = {1}", Format(k), Format(v));
			}

			return Convert.ToString(value, CultureInfo.CurrentCulture) ?? "null";
		}

#if !XUNIT_AOT
#if XUNIT_NULLABLE
		internal static Type[]? GetGroupingTypes(object? obj)
#else
		internal static Type[] GetGroupingTypes(object obj)
#endif
		{
			if (obj == null)
				return null;

			return
				(from @interface in obj.GetType().GetTypeInfo().ImplementedInterfaces
				 where @interface.GetTypeInfo().IsGenericType
				 let genericTypeDefinition = @interface.GetGenericTypeDefinition()
				 where genericTypeDefinition == typeof(IGrouping<,>)
				 select @interface.GetTypeInfo()).FirstOrDefault()?.GenericTypeArguments;
		}
#endif

		[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(ISet<>))]
		[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075", Justification = "We can't easily annotate callers of this type to require them to preserve interfaces, so just preserve the one interface that's checked for.")]
#if XUNIT_NULLABLE
		internal static Type? GetSetElementType(object? obj)
#else
		internal static Type GetSetElementType(object obj)
#endif
		{
			if (obj == null)
				return null;

			return
				(from @interface in obj.GetType().GetTypeInfo().ImplementedInterfaces
				 where @interface.GetTypeInfo().IsGenericType
				 let genericTypeDefinition = @interface.GetGenericTypeDefinition()
				 where genericTypeDefinition == typeof(ISet<>)
				 select @interface.GetTypeInfo()).FirstOrDefault()?.GenericTypeArguments[0];
		}

		static bool IsAnonymousType(this TypeInfo typeInfo)
		{
			// There isn't a sanctioned way to do this, so we look for compiler-generated types that
			// include "AnonymousType" in their names.
			if (typeInfo.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) == null)
				return false;

#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
			return typeInfo.Name.Contains("AnonymousType", StringComparison.Ordinal);
#else
			return typeInfo.Name.Contains("AnonymousType");
#endif
		}


		static bool IsEnumerableOfGrouping(IEnumerable collection)
		{
#if !XUNIT_AOT
			var genericEnumerableType =
				(from @interface in collection.GetType().GetTypeInfo().ImplementedInterfaces
				 where @interface.GetTypeInfo().IsGenericType
				 let genericTypeDefinition = @interface.GetGenericTypeDefinition()
				 where genericTypeDefinition == typeof(IEnumerable<>)
				 select @interface.GetTypeInfo()).FirstOrDefault()?.GenericTypeArguments[0];

			if (genericEnumerableType == null)
				return false;

			return
				genericEnumerableType
					.GetTypeInfo()
					.ImplementedInterfaces
					.Concat(new[] { genericEnumerableType })
					.Any(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(IGrouping<,>));
#else
			return false;
#endif
		}

		static bool IsSZArrayType(this TypeInfo typeInfo) =>
#if NETCOREAPP2_0_OR_GREATER
			typeInfo.IsSZArray;
#elif XUNIT_NULLABLE
			typeInfo == typeInfo.GetElementType()!.MakeArrayType().GetTypeInfo();
#else
			typeInfo == typeInfo.GetElementType().MakeArrayType().GetTypeInfo();
#endif

		static bool SafeToMultiEnumerate(IEnumerable collection) =>
			collection is Array ||
			collection is BitArray ||
			collection is IList ||
			collection is IDictionary ||
			GetSetElementType(collection) != null ||
			IsEnumerableOfGrouping(collection);

		static bool TryGetEscapeSequence(
			char ch,
#if XUNIT_NULLABLE
			out string? value)
#else
			out string value)
#endif
		{
			value = null;

			if (ch == '\t') // tab
				value = @"\t";
			if (ch == '\n') // newline
				value = @"\n";
			if (ch == '\v') // vertical tab
				value = @"\v";
			if (ch == '\a') // alert
				value = @"\a";
			if (ch == '\r') // carriage return
				value = @"\r";
			if (ch == '\f') // formfeed
				value = @"\f";
			if (ch == '\b') // backspace
				value = @"\b";
			if (ch == '\0') // null char
				value = @"\0";
			if (ch == '\\') // backslash
				value = @"\\";

			return value != null;
		}

#if XUNIT_NULLABLE
		internal static Exception? UnwrapException(Exception? ex)
#else
		internal static Exception UnwrapException(Exception ex)
#endif
		{
			if (ex == null)
				return null;

			while (true)
			{
				var tiex = ex as TargetInvocationException;
				if (tiex == null || tiex.InnerException == null)
					return ex;

				ex = tiex.InnerException;
			}
		}

		static string WrapAndGetFormattedValue(
#if XUNIT_NULLABLE
			Func<object?> getter,
#else
			Func<object> getter,
#endif
			int depth)
		{
			try
			{
				return Format(getter(), depth + 1);
			}
			catch (Exception ex)
			{
				return string.Format(CultureInfo.CurrentCulture, "(throws {0})", UnwrapException(ex)?.GetType().Name);
			}
		}
	}
}