File: Asserts\Sdk\ArgumentFormatterTests.cs
Web Access
Project: src\src\Microsoft.DotNet.XUnitAssert\tests\Microsoft.DotNet.XUnitAssert.Tests.csproj (Microsoft.DotNet.XUnitAssert.Tests)
using System.Collections;
using Xunit;
using Xunit.Sdk;
 
public class ArgumentFormatterTests
{
	public class SimpleValues
	{
		[CulturedFactDefault]
		public static void NullValue()
		{
			Assert.Equal("null", ArgumentFormatter.Format(null));
		}
 
		// NOTE: It's important that this stays as MemberData
		// instead of InlineData. String constants in attributes
		// are stored as UTF-8 as IL, so since "\uD800" cannot
		// be converted to valid UTF-8, it ends up getting corrupted
		// and being stored as two characters instead. Thus, this test
		// will fail:
 
		// [Theory]
		// [InlineData("\uD800", 1)]
		// public void HasLength(string s, int length)
		// {
		//     Assert.Equal(length, s.Length);
		// }
 
		// While this one will pass:
 
		// public static IEnumerable<object[]> HasLength_TestData()
		// {
		//     yield return new object[] { "\uD800", 1 };
		// }
 
		// [Theory]
		// [MemberData(nameof(HasLength_TestData))]
		// public void HasLength(string s, int length)
		// {
		//     Assert.Equal(length, s.Length);
		// }
 
		// For more information, see the following links:
		// - http://stackoverflow.com/q/36104766/4077294
		// - http://codeblog.jonskeet.uk/2014/11/07/when-is-a-string-not-a-string/
		public static IEnumerable<TheoryDataRow<string, string>> StringValue_TestData =
		[
			new("\uD800", @"""\xd800"""),
			new("\uDC00", @"""\xdc00"""),
			new("\uDC00\uD800", @"""\xdc00\xd800"""),
			new("\uFFFD", "\"\uFFFD\""),
		];
 
		[Theory]
		[InlineData("Hello, world!", "\"Hello, world!\"")]
		[InlineData(@"""", @"""\""""")] // quotes should be escaped
		[InlineData("\uD800\uDFFF", "\"\uD800\uDFFF\"")] // valid surrogates should print normally
		[InlineData("\uFFFE", @"""\xfffe""")] // same for U+FFFE
		[InlineData("\uFFFF", @"""\xffff""")] // and U+FFFF, which are non-characters
		[InlineData("\u001F", @"""\x1f""")] // non-escaped C0 controls should be 2 digits
											// Other escape sequences
		[InlineData("\0", @"""\0""")] // null
		[InlineData("\r", @"""\r""")] // carriage return
		[InlineData("\n", @"""\n""")] // line feed
		[InlineData("\a", @"""\a""")] // alert
		[InlineData("\b", @"""\b""")] // backspace
		[InlineData("\\", @"""\\""")] // backslash
		[InlineData("\v", @"""\v""")] // vertical tab
		[InlineData("\t", @"""\t""")] // tab
		[InlineData("\f", @"""\f""")] // formfeed
		[InlineData("----|----1----|----2----|----3----|----4----|----5-", "\"----|----1----|----2----|----3----|----4----|----5\"$$ELLIPSIS$$")] // truncation
		[MemberData(nameof(StringValue_TestData), DisableDiscoveryEnumeration = true)]
		public static void StringValue(string value, string expected)
		{
			Assert.Equal(expected.Replace("$$ELLIPSIS$$", ArgumentFormatter.Ellipsis), ArgumentFormatter.Format(value));
		}
 
		public static IEnumerable<TheoryDataRow<char, string>> CharValue_TestData =
		[
			new('\uD800', "0xd800"),
			new('\uDC00', "0xdc00"),
			new('\uFFFD', "'\uFFFD'"),
			new('\uFFFE', "0xfffe"),
		];
 
		[Theory]
 
		// Printable
		[InlineData(' ', "' '")]
		[InlineData('a', "'a'")]
		[InlineData('1', "'1'")]
		[InlineData('!', "'!'")]
 
		// Escape sequences
		[InlineData('\t', @"'\t'")] // tab
		[InlineData('\n', @"'\n'")] // newline
		[InlineData('\'', @"'\''")] // single quote
		[InlineData('\v', @"'\v'")] // vertical tab
		[InlineData('\a', @"'\a'")] // alert
		[InlineData('\\', @"'\\'")] // backslash
		[InlineData('\b', @"'\b'")] // backspace
		[InlineData('\r', @"'\r'")] // carriage return
		[InlineData('\f', @"'\f'")] // formfeed
 
		// Non-ASCII
		[InlineData('©', "'©'")]
		[InlineData('╬', "'╬'")]
		[InlineData('ئ', "'ئ'")]
 
		// Unprintable
		[InlineData(char.MinValue, @"'\0'")]
		[InlineData(char.MaxValue, "0xffff")]
		[MemberData(nameof(CharValue_TestData), DisableDiscoveryEnumeration = true)]
		public static void CharacterValue(char value, string expected)
		{
			Assert.Equal(expected, ArgumentFormatter.Format(value));
		}
 
		[CulturedFactDefault]
		public static void FloatValue()
		{
			var floatPI = (float)Math.PI;
 
			Assert.Equal(floatPI.ToString("G9"), ArgumentFormatter.Format(floatPI));
		}
 
		[CulturedFactDefault]
		public static void DoubleValue()
		{
			Assert.Equal(Math.PI.ToString("G17"), ArgumentFormatter.Format(Math.PI));
		}
 
		[CulturedFactDefault]
		public static void DecimalValue()
		{
			Assert.Equal(123.45M.ToString(), ArgumentFormatter.Format(123.45M));
		}
 
		[CulturedFactDefault]
		public static void DateTimeValue()
		{
			var now = DateTime.UtcNow;
 
			Assert.Equal(now.ToString("o"), ArgumentFormatter.Format(now));
		}
 
		[CulturedFactDefault]
		public static void DateTimeOffsetValue()
		{
			var now = DateTimeOffset.UtcNow;
 
			Assert.Equal(now.ToString("o"), ArgumentFormatter.Format(now));
		}
 
		[Fact]
		public static async Task TaskValue()
		{
			var task = Task.Run(() => { }, TestContext.Current.CancellationToken);
			await task;
 
			Assert.Equal("Task { Status = RanToCompletion }", ArgumentFormatter.Format(task));
		}
 
		[Fact]
		public static void TaskGenericValue()
		{
			var taskCompletionSource = new TaskCompletionSource<int>();
			taskCompletionSource.SetException(new DivideByZeroException());
 
			Assert.Equal("Task<int> { Status = Faulted }", ArgumentFormatter.Format(taskCompletionSource.Task));
		}
 
		public static IEnumerable<TheoryDataRow<Type, string>> TypeValueData =
		[
			new(typeof(string), "typeof(string)"),
			new(typeof(int[]), "typeof(int[])"),
#if !XUNIT_AOT  // MakeArrayType is not available in Native AOT
			new(typeof(int).MakeArrayType(1), "typeof(int[*])"),
			new(typeof(int).MakeArrayType(2), "typeof(int[,])"),
			new(typeof(int).MakeArrayType(3), "typeof(int[,,])"),
#endif
			new(typeof(DateTime[,]), "typeof(System.DateTime[,])"),
			new(typeof(decimal[][,]), "typeof(decimal[][,])"),
			new(typeof(IEnumerable<>), "typeof(System.Collections.Generic.IEnumerable<>)"),
			new(typeof(IEnumerable<int>), "typeof(System.Collections.Generic.IEnumerable<int>)"),
			new(typeof(IDictionary<,>), "typeof(System.Collections.Generic.IDictionary<,>)"),
			new(typeof(IDictionary<string, DateTime>), "typeof(System.Collections.Generic.IDictionary<string, DateTime>)"),
			new(typeof(IDictionary<string[,], DateTime[,][]>), "typeof(System.Collections.Generic.IDictionary<string[,], DateTime[,][]>)"),
			new(typeof(bool?), "typeof(bool?)"),
			new(typeof(bool?[]), "typeof(bool?[])"),
			new(typeof(nint), "typeof(nint)"),
			new(typeof(IntPtr), "typeof(nint)"),
			new(typeof(nuint), "typeof(nuint)"),
			new(typeof(UIntPtr), "typeof(nuint)"),
		];
 
		[Theory]
		[MemberData(nameof(TypeValueData), DisableDiscoveryEnumeration = true)]
		public static void TypeValue(Type type, string expected)
		{
			Assert.Equal(expected, ArgumentFormatter.Format(type));
		}
	}
 
	public class Enums
	{
		public enum NonFlagsEnum
		{
			Value0 = 0,
			Value1 = 1
		}
 
		[CulturedTheoryDefault]
#pragma warning disable xUnit1010 // The value is not convertible to the method parameter type
		[InlineData(0, "Value0")]
		[InlineData(1, "Value1")]
		[InlineData(42, "42")]
#pragma warning restore xUnit1010 // The value is not convertible to the method parameter type
		public static void NonFlags(NonFlagsEnum enumValue, string expected)
		{
			var actual = ArgumentFormatter.Format(enumValue);
 
			Assert.Equal(expected, actual);
		}
 
		[Flags]
		public enum FlagsEnum
		{
			Nothing = 0,
			Value1 = 1,
			Value2 = 2,
		}
 
		[CulturedTheoryDefault]
#pragma warning disable xUnit1010 // The value is not convertible to the method parameter type
		[InlineData(0, "Nothing")]
		[InlineData(1, "Value1")]
		[InlineData(3, "Value1 | Value2")]
		// This is expected, not "Value1 | Value2 | 4"
		[InlineData(7, "7")]
#pragma warning restore xUnit1010 // The value is not convertible to the method parameter type
		public static void Flags(FlagsEnum enumValue, string expected)
		{
			var actual = ArgumentFormatter.Format(enumValue);
 
			Assert.Equal(expected, actual);
		}
	}
 
	public class KeyValuePair
	{
		[CulturedFactDefault]
		public static void KeyValuePairValue()
		{
			var kvp = new KeyValuePair<object, List<object>>(42, [21.12M, "2600"]);
			var expected = $"[42] = [{21.12M}, \"2600\"]";
 
			Assert.Equal(expected, ArgumentFormatter.Format(kvp));
		}
	}
 
	public class Enumerables
	{
#pragma warning disable xUnit1047 // Avoid using TheoryDataRow arguments that might not be serializable
 
		// Both tracked and untracked should be the same
		public static TheoryData<IEnumerable<object>> Collections =
		[
			new([1, 2.3M, "Hello, world!"]),
			new(CollectionTracker<object>.Wrap([1, 2.3M, "Hello, world!"])),
		];
 
#pragma warning restore xUnit1047
 
		[CulturedTheoryDefault]
		[MemberData(nameof(Collections), DisableDiscoveryEnumeration = true)]
		public static void EnumerableValue(IEnumerable collection)
		{
			var expected = $"[1, {2.3M}, \"Hello, world!\"]";
 
			Assert.Equal(expected, ArgumentFormatter.Format(collection));
		}
 
		[CulturedFactDefault]
		public static void DictionaryValue()
		{
			var value = new Dictionary<object, List<object>>
			{
				{ 42, new() { 21.12M, "2600" } },
				{ "123", new() { } },
			};
#if XUNIT_AOT
			var expected = "[[42, System.Collections.Generic.List`1[System.Object]], [123, System.Collections.Generic.List`1[System.Object]]]";
#else
			var expected = $"[[42] = [{21.12M}, \"2600\"], [\"123\"] = []]";
#endif
 
			Assert.Equal(expected, ArgumentFormatter.Format(value));
		}
 
#pragma warning disable xUnit1047 // Avoid using TheoryDataRow arguments that might not be serializable
 
		public static TheoryData<IEnumerable<object>> LongCollections =
		[
			new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
			new(CollectionTracker<object>.Wrap([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])),
		];
 
#pragma warning restore xUnit1047
 
		[CulturedTheoryDefault]
		[MemberData(nameof(LongCollections), DisableDiscoveryEnumeration = true)]
		public static void OnlyFirstFewValuesOfEnumerableAreRendered(IEnumerable collection)
		{
			Assert.Equal($"[0, 1, 2, 3, 4, {ArgumentFormatter.Ellipsis}]", ArgumentFormatter.Format(collection));
		}
 
		[CulturedFactDefault]
		public static void EnumerablesAreRenderedWithMaximumDepthToPreventInfiniteRecursion()
		{
			var looping = new object[2];
			looping[0] = 42;
			looping[1] = looping;
 
			Assert.Equal($"[42, [42, [42, [{ArgumentFormatter.Ellipsis}]]]]", ArgumentFormatter.Format(looping));
		}
 
		[Fact]
		public static void GroupingIsRenderedAsCollectionsOfKeysLinkedToCollectionsOfValues()
		{
#if XUNIT_AOT
			var expected = $"[{ArgumentFormatter.Ellipsis}]";
#else
			var expected = "[True] = [0, 2, 4, 6, 8]";
#endif
 
			var grouping = Enumerable.Range(0, 10).GroupBy(i => i % 2 == 0).FirstOrDefault(g => g.Key == true);
 
			Assert.Equal(expected, ArgumentFormatter.Format(grouping));
		}
 
		[Fact]
		public static void GroupingsAreRenderedAsCollectionsOfKeysLinkedToCollectionsOfValues()
		{
#if XUNIT_AOT
			var expected = $"[{ArgumentFormatter.Ellipsis}]";
#else
			var expected = "[[True] = [0, 2, 4, 6, 8], [False] = [1, 3, 5, 7, 9]]";
#endif
 
			var grouping = Enumerable.Range(0, 10).GroupBy(i => i % 2 == 0);
 
			Assert.Equal(expected, ArgumentFormatter.Format(grouping));
		}
	}
 
	public class ComplexTypes
	{
		[CulturedFactDefault]
		public static void Empty()
		{
#if XUNIT_AOT
			var expected = $"Object {{ {ArgumentFormatter.Ellipsis} }}";
#else
			var expected = "Object { }";
#endif
 
			var result = ArgumentFormatter.Format(new object());
 
			Assert.Equal(expected, result);
		}
 
#if !XUNIT_AOT  // Native AOT cannot render properties and fields of object

		[CulturedFactDefault]
		public static void ReturnsValuesInAlphabeticalOrder()
		{
			var expected = $"MyComplexType {{ MyPublicField = 42, MyPublicProperty = {21.12M} }}";
 
			var result = ArgumentFormatter.Format(new MyComplexType());
 
			Assert.Equal(expected, result);
		}
 
		public class MyComplexType
		{
#pragma warning disable CS0414
			readonly string MyPrivateField = "Hello, world";
#pragma warning restore CS0414

			public static int MyPublicStaticField = 2112;
 
			public decimal MyPublicProperty { get; private set; }
 
			public int MyPublicField = 42;
 
			public MyComplexType()
			{
				MyPublicProperty = 21.12M;
			}
		}
 
		[CulturedFactDefault]
		public static void ComplexTypeInsideComplexType()
		{
			var expected = $"MyComplexTypeWrapper {{ c = 'A', s = \"Hello, world!\", t = MyComplexType {{ MyPublicField = 42, MyPublicProperty = {21.12M} }} }}";
 
			var result = ArgumentFormatter.Format(new MyComplexTypeWrapper());
 
			Assert.Equal(expected, result);
		}
 
		public class MyComplexTypeWrapper
		{
			public MyComplexType t = new();
			public char c = 'A';
			public string s = "Hello, world!";
		}
 
		[CulturedFactDefault]
		public static void WithThrowingPropertyGetter()
		{
			var result = ArgumentFormatter.Format(new ThrowingGetter());
 
			Assert.Equal("ThrowingGetter { MyThrowingProperty = (throws NotImplementedException) }", result);
		}
 
		public class ThrowingGetter
		{
			public string MyThrowingProperty { get { throw new NotImplementedException(); } }
		}
 
		[CulturedFactDefault]
		public static void LimitsOutputToFirstFewValues()
		{
			var expected = $@"Big {{ MyField1 = 42, MyField2 = ""Hello, world!"", MyProp1 = {21.12}, MyProp2 = typeof({typeof(Big).FullName}), MyProp3 = 2014-04-17T07:45:23.0000000+00:00, {ArgumentFormatter.Ellipsis} }}";
 
			var result = ArgumentFormatter.Format(new Big());
 
			Assert.Equal(expected, result);
		}
 
		public class Big
		{
			public string MyField2 = "Hello, world!";
 
			public decimal MyProp1 { get; set; }
 
			public object MyProp4 { get; set; }
 
			public object MyProp3 { get; set; }
 
			public int MyField1 = 42;
 
			public Type MyProp2 { get; set; }
 
			public Big()
			{
				MyProp1 = 21.12M;
				MyProp2 = typeof(Big);
				MyProp3 = new DateTimeOffset(2014, 04, 17, 07, 45, 23, TimeSpan.Zero);
				MyProp4 = "Should not be shown";
			}
		}
 
		[CulturedFactDefault]
		public static void TypesAreRenderedWithMaximumDepthToPreventInfiniteRecursion()
		{
			var expected = $"Looping {{ Me = Looping {{ Me = Looping {{ {ArgumentFormatter.Ellipsis} }} }} }}";
 
			var result = ArgumentFormatter.Format(new Looping());
 
			Assert.Equal(expected, result);
		}
 
		public class Looping
		{
			public Looping Me;
 
			public Looping() => Me = this;
		}
 
		[CulturedFactDefault]
		public static void RecursionViaCollectionsIsPreventedWithMaximumDepth()
		{
			var expected = $"LoopingChild {{ Parent = LoopingParent {{ ProjectsById = [{ArgumentFormatter.Ellipsis}] }} }}";
 
			var result = ArgumentFormatter.Format(new LoopingChild(new LoopingParent()));
 
			Assert.Equal(expected, result);
		}
 
		public class LoopingParent
		{
			public Dictionary<string, LoopingChild> ProjectsById { get; } = [];
		}
 
		public class LoopingChild
		{
			public LoopingParent Parent;
 
			public LoopingChild(LoopingParent parent)
			{
				parent.ProjectsById[Guid.NewGuid().ToString()] = this;
				Parent = parent;
			}
		}
 
#endif  // !XUNIT_AOT
 
		[Fact]
		public static void WhenCustomTypeImplementsToString_UsesToString()
		{
			var result = ArgumentFormatter.Format(new TypeWithToString());
 
			Assert.Equal("This is what you should show", result);
		}
 
		public class TypeWithToString
		{
			public override string ToString() => "This is what you should show";
		}
	}
 
	public class TypeNames
	{
		public static TheoryData<Type, string> ArgumentFormatterFormatTypeNamesData = new()
		{
			{ typeof(int), "typeof(int)" },
			{ typeof(long), "typeof(long)" },
			{ typeof(string), "typeof(string)" },
			{ typeof(List<int>), "typeof(System.Collections.Generic.List<int>)" },
			{ typeof(Dictionary<int, string>), "typeof(System.Collections.Generic.Dictionary<int, string>)" },
			{ typeof(List<>), "typeof(System.Collections.Generic.List<>)" },
			{ typeof(Dictionary<,>), "typeof(System.Collections.Generic.Dictionary<,>)" }
		};
 
		[Theory]
		[MemberData(nameof(ArgumentFormatterFormatTypeNamesData), DisableDiscoveryEnumeration = true)]
		public void ArgumentFormatterFormatTypeNames(Type type, string expectedResult)
		{
			Assert.Equal(expectedResult, ArgumentFormatter.Format(type));
		}
 
		[Fact]
		public void ArgumentFormatterFormatTypeNameGenericTypeParameter()
		{
			var genericTypeParameters = typeof(List<>).GetGenericArguments();
			var parameterType = genericTypeParameters.First();
 
			Assert.Equal("typeof(T)", ArgumentFormatter.Format(parameterType));
		}
 
		[Fact]
		public void ArgumentFormatterFormatTypeNameGenericTypeParameters()
		{
			var genericTypeParameters = typeof(Dictionary<,>).GetGenericArguments();
			var parameterTKey = genericTypeParameters.First();
 
			Assert.Equal("typeof(TKey)", ArgumentFormatter.Format(parameterTKey));
 
			var parameterTValue = genericTypeParameters.Last();
			Assert.Equal("typeof(TValue)", ArgumentFormatter.Format(parameterTValue));
		}
	}
}