From 0ceb116d749d07017df5295001c8ea024640193b Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Fri, 25 Mar 2022 23:45:00 +0000 Subject: [PATCH] Refactor HandleCollectionsAsync to an xunit Theory (#67126) * Refactor HandleCollectionsAsync to a Theory * add small buffer testing in StreamTests * redisable test for mono/wasm platforms --- .../Serialization/Stream.Collections.cs | 262 +++++++++--------- .../Serialization/StreamTests.cs | 7 +- 2 files changed, 141 insertions(+), 128 deletions(-) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.Collections.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.Collections.cs index b5fa2534860..c5548282465 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.Collections.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/Stream.Collections.cs @@ -15,137 +15,138 @@ namespace System.Text.Json.Serialization.Tests { public partial class StreamTests { - [Fact] + // Empty class functioning as witness type for TElement + public class Witness { } + + [Theory] [ActiveIssue("https://github.com/dotnet/runtime/issues/35927", typeof(PlatformDetection), nameof(PlatformDetection.IsMonoInterpreter))] [ActiveIssue("https://github.com/dotnet/runtime/issues/35927", TestPlatforms.Browser)] - public async Task HandleCollectionsAsync() + [MemberData(nameof(GetTestedCollectionData))] + public async Task HandleCollectionsAsync(TCollection collection, int bufferSize, Witness elementType) { - await RunTestAsync(); - await RunTestAsync(); - await RunTestAsync(); + _ = elementType; // only needed by Xunit to inject the right TElement type parameter + + var options = new JsonSerializerOptions { DefaultBufferSize = bufferSize }; + await PerformSerialization(collection, options); + + options = new JsonSerializerOptions(options) { ReferenceHandler = ReferenceHandler.Preserve }; + await PerformSerialization(collection, options); } - private async Task RunTestAsync() + private async Task PerformSerialization( + TCollection collection, + JsonSerializerOptions options) { - foreach ((Type, int) pair in CollectionTestData()) - { - Type type = pair.Item1; - int bufferSize = pair.Item2; - - // bufferSize * 0.9 is the threshold size from codebase, subtract 2 for [ or { characters, then create a - // string containing (threshold - 2) amount of char 'a' which when written into output buffer produces buffer - // which size equal to or very close to threshold size, then adding the string to the list, then adding a big - // object to the list which changes depth of written json and should cause buffer flush. - int thresholdSize = (int)(bufferSize * 0.9 - 2); + string expectedjson = JsonSerializer.Serialize(collection, options); + string actualJson = await Serializer.SerializeWrapper(collection, options); + JsonTestHelper.AssertJsonEqual(expectedjson, actualJson); - var options = new JsonSerializerOptions - { - DefaultBufferSize = bufferSize, - WriteIndented = true - }; + if (options.ReferenceHandler == ReferenceHandler.Preserve && + TypeHelper.NonRoundtrippableWithReferenceHandler.Contains(typeof(TCollection))) + { + return; + } - var optionsWithPreservedReferenceHandling = new JsonSerializerOptions(options) - { - ReferenceHandler = ReferenceHandler.Preserve - }; + await TestDeserialization(actualJson, options); - object obj = GetPopulatedCollection(type, thresholdSize); - await PerformSerialization(obj, type, options); - await PerformSerialization(obj, type, optionsWithPreservedReferenceHandling); - } + // Deserialize with extra whitespace + string jsonWithWhiteSpace = GetPayloadWithWhiteSpace(actualJson); + await TestDeserialization(jsonWithWhiteSpace, options); } - private async Task PerformSerialization(object obj, Type type, JsonSerializerOptions options) + private async Task TestDeserialization(string json, JsonSerializerOptions options) { - string expectedjson = JsonSerializer.Serialize(obj, options); + if (TypeHelper.NotSupportedForDeserialization.Contains(typeof(TCollection))) + { + NotSupportedException exception = await Assert.ThrowsAsync(() => Serializer.DeserializeWrapper(json, options)); + Assert.Contains(typeof(TCollection).ToString(), exception.ToString()); + return; + } - using var memoryStream = new MemoryStream(); - await Serializer.SerializeWrapper(memoryStream, obj, options); - string serialized = Encoding.UTF8.GetString(memoryStream.ToArray()); - JsonTestHelper.AssertJsonEqual(expectedjson, serialized); + TCollection deserialized = await Serializer.DeserializeWrapper(json, options); - memoryStream.Position = 0; + // Validate the integrity of the deserialized value by reserializing + // it using the non-streaming serializer and comparing the roundtripped value. + string roundtrippedJson = JsonSerializer.Serialize(deserialized, options); - if (options.ReferenceHandler == null || !GetTypesNonRoundtrippableWithReferenceHandler().Contains(type)) + // Stack elements reversed during serialization. + if (TypeHelper.StackTypes.Contains(typeof(TCollection))) { - await TestDeserialization(memoryStream, expectedjson, type, options); + deserialized = JsonSerializer.Deserialize(roundtrippedJson, options); + roundtrippedJson = JsonSerializer.Serialize(deserialized, options); + } - // Deserialize with extra whitespace - string jsonWithWhiteSpace = GetPayloadWithWhiteSpace(expectedjson); - using var memoryStreamWithWhiteSpace = new MemoryStream(Encoding.UTF8.GetBytes(jsonWithWhiteSpace)); - await TestDeserialization(memoryStreamWithWhiteSpace, expectedjson, type, options); + // TODO: https://github.com/dotnet/runtime/issues/35611. + // Can't control order of dictionary elements when serializing, so reference metadata might not match up. + if (options.ReferenceHandler == ReferenceHandler.Preserve && + TypeHelper.DictionaryTypes.Contains(typeof(TCollection))) + { + return; } + + JsonTestHelper.AssertJsonEqual(json, roundtrippedJson); } - private async Task TestDeserialization( - Stream memoryStream, - string expectedJson, - Type type, - JsonSerializerOptions options) + public static IEnumerable GetTestedCollectionData() { - try - { - object deserialized = await Serializer.DeserializeWrapper(memoryStream, type, options); - string serialized = JsonSerializer.Serialize(deserialized, options); + return new IEnumerable[] { + GetTestedCollectionsForElement(), + GetTestedCollectionsForElement(), + GetTestedCollectionsForElement(), + }.SelectMany(x => x); - // Stack elements reversed during serialization. - if (StackTypes().Contains(type)) - { - deserialized = JsonSerializer.Deserialize(serialized, type, options); - serialized = JsonSerializer.Serialize(deserialized, options); - } - - // TODO: https://github.com/dotnet/runtime/issues/35611. - // Can't control order of dictionary elements when serializing, so reference metadata might not match up. - if(!(CollectionTestTypes.DictionaryTypes().Contains(type) && options.ReferenceHandler == ReferenceHandler.Preserve)) + static IEnumerable GetTestedCollectionsForElement() + { + foreach ((Type collectionType, int bufferSize) in CollectionTestData()) { - JsonTestHelper.AssertJsonEqual(expectedJson, serialized); + // bufferSize * 0.9 is the threshold size from codebase, subtract 2 for [ or { characters, then create a + // string containing (threshold - 2) amount of char 'a' which when written into output buffer produces buffer + // which size equal to or very close to threshold size, then adding the string to the list, then adding a big + // object to the list which changes depth of written json and should cause buffer flush. + int elementSize = (int)(bufferSize * 0.9 - 2); + object collection = GetPopulatedCollection(collectionType, elementSize); + yield return new object[] { collection, bufferSize, new Witness() }; } } - catch (NotSupportedException ex) - { - Assert.True(GetTypesNotSupportedForDeserialization().Contains(type)); - Assert.Contains(type.ToString(), ex.ToString()); - } } - private static object GetPopulatedCollection(Type type, int stringLength) + private static object GetPopulatedCollection(Type collectionType, int elementSize) { - if (type == typeof(TElement[])) + if (collectionType == typeof(TElement[])) { - return GetArr_TypedElements(stringLength); + return GetArr_TypedElements(elementSize); } - else if (type == typeof(ImmutableList)) + else if (collectionType == typeof(ImmutableList)) { - return ImmutableList.CreateRange(GetArr_TypedElements(stringLength)); + return ImmutableList.CreateRange(GetArr_TypedElements(elementSize)); } - else if (type == typeof(ImmutableStack)) + else if (collectionType == typeof(ImmutableStack)) { - return ImmutableStack.CreateRange(GetArr_TypedElements(stringLength)); + return ImmutableStack.CreateRange(GetArr_TypedElements(elementSize)); } - else if (type == typeof(ImmutableDictionary)) + else if (collectionType == typeof(ImmutableDictionary)) { - return ImmutableDictionary.CreateRange(GetDict_TypedElements(stringLength)); + return ImmutableDictionary.CreateRange(GetDict_TypedElements(elementSize)); } - else if (type == typeof(KeyValuePair)) + else if (collectionType == typeof(KeyValuePair)) { - TElement item = GetCollectionElement(stringLength); + TElement item = GetCollectionElement(elementSize); return new KeyValuePair(item, item); } else if ( - typeof(IDictionary).IsAssignableFrom(type) || - typeof(IReadOnlyDictionary).IsAssignableFrom(type) || - typeof(IDictionary).IsAssignableFrom(type)) + typeof(IDictionary).IsAssignableFrom(collectionType) || + typeof(IReadOnlyDictionary).IsAssignableFrom(collectionType) || + typeof(IDictionary).IsAssignableFrom(collectionType)) { - return Activator.CreateInstance(type, new object[] { GetDict_TypedElements(stringLength) }); + return Activator.CreateInstance(collectionType, new object[] { GetDict_TypedElements(elementSize) }); } - else if (typeof(IEnumerable).IsAssignableFrom(type)) + else if (typeof(IEnumerable).IsAssignableFrom(collectionType)) { - return Activator.CreateInstance(type, new object[] { GetArr_TypedElements(stringLength) }); + return Activator.CreateInstance(collectionType, new object[] { GetArr_TypedElements(elementSize) }); } else { - return Activator.CreateInstance(type, new object[] { GetArr_BoxedElements(stringLength) }); + return Activator.CreateInstance(collectionType, new object[] { GetArr_BoxedElements(elementSize) }); } } @@ -177,17 +178,18 @@ private static object GetEmptyCollection(Type type) private const int NumElements = 15; - private static TElement[] GetArr_TypedElements(int stringLength) + private static TElement[] GetArr_TypedElements(int elementSize) { Debug.Assert(NumElements > 2); var arr = new TElement[NumElements]; - TElement item = GetCollectionElement(stringLength); + Random random = new Random(Seed: elementSize); + TElement item = GetCollectionElement(elementSize, random); arr[0] = item; for (int i = 1; i < NumElements - 1; i++) { - arr[i] = GetCollectionElement(stringLength); + arr[i] = GetCollectionElement(elementSize, random); } arr[NumElements - 1] = item; @@ -195,17 +197,18 @@ private static TElement[] GetArr_TypedElements(int stringLength) return arr; } - private static object[] GetArr_BoxedElements(int stringLength) + private static object[] GetArr_BoxedElements(int elementSize) { Debug.Assert(NumElements > 2); var arr = new object[NumElements]; - TElement item = GetCollectionElement(stringLength); + Random random = new Random(Seed: elementSize); + TElement item = GetCollectionElement(elementSize, random); arr[0] = item; for (int i = 1; i < NumElements - 1; i++) { - arr[i] = GetCollectionElement(stringLength); + arr[i] = GetCollectionElement(elementSize, random); } arr[NumElements - 1] = item; @@ -213,11 +216,12 @@ private static object[] GetArr_BoxedElements(int stringLength) return arr; } - private static Dictionary GetDict_TypedElements(int stringLength) + private static Dictionary GetDict_TypedElements(int elementSize) { Debug.Assert(NumElements > 2); - TElement item = GetCollectionElement(stringLength); + Random random = new Random(Seed: elementSize); + TElement item = GetCollectionElement(elementSize, random); var dict = new Dictionary(); @@ -225,7 +229,7 @@ private static object[] GetArr_BoxedElements(int stringLength) for (int i = 1; i < NumElements - 1; i++) { - TElement newItem = GetCollectionElement(stringLength); + TElement newItem = GetCollectionElement(elementSize, random); dict[$"{newItem}{i}"] = newItem; } @@ -234,18 +238,12 @@ private static object[] GetArr_BoxedElements(int stringLength) return dict; } - private static TElement GetCollectionElement(int stringLength) + private static TElement GetCollectionElement(int elementSize, Random? random = null) { Type type = typeof(TElement); + char randomChar = (char)(random ??= new(Seed: elementSize)).Next('a', 'z'); - Random rand = new Random(); - char randomChar = (char)rand.Next('a', 'z'); - - string value = new string(randomChar, stringLength); - var kvp = new KeyValuePair(value, new SimpleStruct { - One = 1, - Two = 2 - }); + string value = new string(randomChar, elementSize); if (type == typeof(string)) { @@ -253,6 +251,11 @@ private static TElement GetCollectionElement(int stringLength) } else if (type == typeof(ClassWithKVP)) { + var kvp = new KeyValuePair(value, new SimpleStruct { + One = 1, + Two = 2 + }); + return (TElement)(object)new ClassWithKVP { MyKvp = kvp }; } else @@ -263,7 +266,7 @@ private static TElement GetCollectionElement(int stringLength) throw new NotImplementedException(); } - private static IEnumerable<(Type, int)> CollectionTestData() + private static IEnumerable<(Type collectionType, int bufferSize)> CollectionTestData() { foreach (Type type in CollectionTypes()) { @@ -295,7 +298,7 @@ private static IEnumerable CollectionTypes() yield return type; } // Stack types - foreach (Type type in StackTypes()) + foreach (Type type in TypeHelper.StackTypes) { yield return type; } @@ -311,30 +314,35 @@ private static IEnumerable ObjectNotationTypes() yield return typeof(KeyValuePair); // KeyValuePairConverter } - private static HashSet StackTypes() => new HashSet + private static class TypeHelper { - typeof(ConcurrentStack), // ConcurrentStackOfTConverter - typeof(Stack), // IEnumerableWithAddMethodConverter - typeof(Stack), // StackOfTConverter - typeof(ImmutableStack) // ImmutableEnumerableOfTConverter - }; + public static HashSet DictionaryTypes { get; } = new HashSet(CollectionTestTypes.DictionaryTypes()); - private static HashSet GetTypesNotSupportedForDeserialization() => new HashSet - { - typeof(WrapperForIEnumerable), - typeof(WrapperForIReadOnlyCollectionOfT), - typeof(GenericIReadOnlyDictionaryWrapper) - }; + public static HashSet StackTypes { get; } = new HashSet + { + typeof(ConcurrentStack), // ConcurrentStackOfTConverter + typeof(Stack), // IEnumerableWithAddMethodConverter + typeof(Stack), // StackOfTConverter + typeof(ImmutableStack) // ImmutableEnumerableOfTConverter + }; - // Non-generic types cannot roundtrip when they contain a $ref written on serialization and they are the root type. - private static HashSet GetTypesNonRoundtrippableWithReferenceHandler() => new HashSet - { - typeof(Hashtable), - typeof(Queue), - typeof(Stack), - typeof(WrapperForIList), - typeof(WrapperForIEnumerable) - }; + public static HashSet NotSupportedForDeserialization { get; } = new HashSet + { + typeof(WrapperForIEnumerable), + typeof(WrapperForIReadOnlyCollectionOfT), + typeof(GenericIReadOnlyDictionaryWrapper) + }; + + // Non-generic types cannot roundtrip when they contain a $ref written on serialization and they are the root type. + public static HashSet NonRoundtrippableWithReferenceHandler { get; } = new HashSet + { + typeof(Hashtable), + typeof(Queue), + typeof(Stack), + typeof(WrapperForIList), + typeof(WrapperForIEnumerable) + }; + } private class ClassWithKVP { @@ -382,7 +390,7 @@ public void SerializeEmptyCollection() Assert.Equal("[]", JsonSerializer.Serialize(GetEmptyCollection(type))); } - foreach (Type type in StackTypes()) + foreach (Type type in TypeHelper.StackTypes) { Assert.Equal("[]", JsonSerializer.Serialize(GetEmptyCollection(type))); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/StreamTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/StreamTests.cs index 98f01b0f9bf..90da21670a2 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/StreamTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/StreamTests.cs @@ -8,9 +8,14 @@ public sealed class StreamTests_Async : StreamTests public StreamTests_Async() : base(JsonSerializerWrapper.AsyncStreamSerializer) { } } + public sealed class StreamTests_AsyncWithSmallBuffer : StreamTests + { + public StreamTests_AsyncWithSmallBuffer() : base(JsonSerializerWrapper.AsyncStreamSerializerWithSmallBuffer) { } + } + public sealed class StreamTests_Sync : StreamTests { - public StreamTests_Sync() : base(StreamingJsonSerializerWrapper.SyncStreamSerializer) { } + public StreamTests_Sync() : base(JsonSerializerWrapper.SyncStreamSerializer) { } } public abstract partial class StreamTests -- GitLab