From e691753245eb6f2d459c7f02aaaa511077c1e833 Mon Sep 17 00:00:00 2001 From: devsko Date: Mon, 2 Nov 2020 22:37:52 +0100 Subject: [PATCH] JSON continuation tests (#42393) * Repro #42070 * formatting * namespace * Fix * never forget the header * More tests - Test continuation at every position inside the tested object - Many member with primitive and nullable types - One more level of nested object - All combinations of class/struct for tested and nested object - tested and nested object with parametrized ctor for some properties * Addressed feedback * refactoring * Test with original repro data from #42070 * custom converter to ensure the padding is written in front of the tested object * rename * test data moved to Strings.resx * Using test data from SR * Generalize continuation tests for payloads of any length Tweak the payload and expect `JsonException` * merge * Test deserialize with Utf8JsonReader and ReadOnlySequence * Again with value typed nested object * Add tests for splitted whitespaces * Addressed feedback Added dictionary test * Validate line and position of failure in tweaked payloads more tweaks * Fixed comment Co-authored-by: Layomi Akinrinade --- .../Serialization/ContinuationTests.NullToken.cs | 146 +++++ .../tests/Serialization/ContinuationTests.cs | 665 +++++++++------------ .../tests/System.Text.Json.Tests.csproj | 1 + 3 files changed, 427 insertions(+), 385 deletions(-) create mode 100644 src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.NullToken.cs diff --git a/src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.NullToken.cs b/src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.NullToken.cs new file mode 100644 index 0000000..6acee1c --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.NullToken.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace System.Text.Json.Serialization.Tests +{ + public static partial class ContinuationTests + { + // From https://github.com/dotnet/runtime/issues/42070 + [Theory] + [MemberData(nameof(ContinuationAtNullTokenTestData))] + public static async Task ContinuationAtNullToken(string payload) + { + using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(payload))) + { + CustomerCollectionResponse response = await JsonSerializer.DeserializeAsync(stream, new JsonSerializerOptions { IgnoreNullValues = true }); + Assert.Equal(50, response.Customers.Count); + } + } + + public static IEnumerable ContinuationAtNullTokenTestData + => new[] + { + new[] { SR.CustomerSearchApi108KB }, + new[] { SR.CustomerSearchApi107KB }, + }; + + private class CustomerCollectionResponse + { + [JsonPropertyName("customers")] + public List Customers { get; set; } + } + + private class CustomerAddress + { + [JsonPropertyName("first_name")] + public string FirstName { get; set; } + + [JsonPropertyName("address1")] + public string Address1 { get; set; } + + [JsonPropertyName("phone")] + public string Phone { get; set; } + + [JsonPropertyName("city")] + public string City { get; set; } + + [JsonPropertyName("zip")] + public string Zip { get; set; } + + [JsonPropertyName("province")] + public string Province { get; set; } + + [JsonPropertyName("country")] + public string Country { get; set; } + + [JsonPropertyName("last_name")] + public string LastName { get; set; } + + [JsonPropertyName("address2")] + public string Address2 { get; set; } + + [JsonPropertyName("company")] + public string Company { get; set; } + + [JsonPropertyName("latitude")] + public float? Latitude { get; set; } + + [JsonPropertyName("longitude")] + public float? Longitude { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("country_code")] + public string CountryCode { get; set; } + + [JsonPropertyName("province_code")] + public string ProvinceCode { get; set; } + } + + private class Customer + { + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("email")] + public string Email { get; set; } + + [JsonPropertyName("accepts_marketing")] + public bool AcceptsMarketing { get; set; } + + [JsonPropertyName("created_at")] + public DateTimeOffset? CreatedAt { get; set; } + + [JsonPropertyName("updated_at")] + public DateTimeOffset? UpdatedAt { get; set; } + + [JsonPropertyName("first_name")] + public string FirstName { get; set; } + + [JsonPropertyName("last_name")] + public string LastName { get; set; } + + [JsonPropertyName("orders_count")] + public int OrdersCount { get; set; } + + [JsonPropertyName("state")] + public string State { get; set; } + + [JsonPropertyName("total_spent")] + public string TotalSpent { get; set; } + + [JsonPropertyName("last_order_id")] + public long? LastOrderId { get; set; } + + [JsonPropertyName("note")] + public string Note { get; set; } + + [JsonPropertyName("verified_email")] + public bool VerifiedEmail { get; set; } + + [JsonPropertyName("multipass_identifier")] + public string MultipassIdentifier { get; set; } + + [JsonPropertyName("tax_exempt")] + public bool TaxExempt { get; set; } + + [JsonPropertyName("tags")] + public string Tags { get; set; } + + [JsonPropertyName("last_order_name")] + public string LastOrderName { get; set; } + + [JsonPropertyName("default_address")] + public CustomerAddress DefaultAddress { get; set; } + + [JsonPropertyName("addresses")] + public IList Addresses { get; set; } + } + } +} diff --git a/src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.cs b/src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.cs index e3b9b69..9736142 100644 --- a/src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.cs +++ b/src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.cs @@ -1,517 +1,412 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; - using Xunit; namespace System.Text.Json.Serialization.Tests { - public static class ContinuationTests + public static partial class ContinuationTests { - // To hit all possible continuation positions inside the tested object, - // the outer-class padding needs to be between 5 and 116 bytes long. - - // {"S":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","C":{"A":null,"B":"Hello","C":42,"D":null,"E":3.14E+17,"F":null,"G":true,"H":null,"I":[42,17],"J":{"A":null,"B":7}}} - // |<------------------------------------------------128 byte buffer------------------------------------------------------------->| - // {"S":"xxxxx","C":{"A":"Hello","B":null,"C":42,"D":null,"E":3.14E+17,"F":null,"G":true,"H":null,"I":[42,17],"J":{"A":null,"B":7}}} - - private const int MinPaddingLength = 5; - private const int MaxPaddingLength = 116; - - private static IEnumerable ContinuationPaddingLengths - => Enumerable.Range(MinPaddingLength, MaxPaddingLength - MinPaddingLength + 1); + private static readonly (Func, Func, int)[] s_payloadTweaks = new (Func, Func, int)[] + { + (payload => payload, payload => -1, 0), + (payload => payload.Replace("null", "nullX"), payload => payload.IndexOf("nullX"), "null".Length), + (payload => payload.Replace("true", "trueX"), payload => payload.IndexOf("trueX"), "true".Length), + (payload => payload.Replace("false", "falseX"), payload => payload.IndexOf("falseX"), "false".Length), + (payload => payload.Replace("E+17", "E+-17"), payload => payload.IndexOf("E+-17"), "E+".Length) + }; + + private static IEnumerable<(ITestObject, INestedObject)> TestObjects + => new (ITestObject, INestedObject)[] + { + (new TestClass(), new NestedClass()), + (new TestClass(), new NestedValueType()), + (new TestValueType(), new NestedClass()), + (new TestValueType(), new NestedValueType()), + (new TestClass(), new NestedClassWithParamCtor(null)), + (new DictionaryTestClass(), new NestedClass()), + }; private static IEnumerable IgnoreNullValues => new[] { true, false }; - [Theory] - [MemberData(nameof(TestData))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)] - public static async Task ContinuationShouldWorkAtAnyPosition_Class_Class(int paddingLength, bool ignoreNullValues) + private static IEnumerable WriteIndented + => new[] { true, false }; + + private static IEnumerable TestData(bool enumeratePayloadTweaks) { - var stream = new MemoryStream(); + // The serialized json gets padded with leading ' ' chars. The length of the + // incrementing paddings, leads to continuations at every position of the payload. + // The complete strings (padding + payload) are then passed to the test method. + + // <------min-padding------>[{--payload--}] min-padding = buffer - payload + 1 + // <-----------2^n byte buffer-----------> + // <-------------max-padding------------>[{--payload--}] max-padding = buffer - 1 + + foreach ((ITestObject TestObject, INestedObject Nested) in TestObjects.Take(enumeratePayloadTweaks ? 1 : TestObjects.Count())) { - var obj = new Outer> + Type testObjectType = TestObject.GetType(); + TestObject.Initialize(Nested); + + foreach (bool writeIndented in WriteIndented) { - S = new string('x', paddingLength), - C = new() + string payload = JsonSerializer.Serialize(TestObject, testObjectType, new JsonSerializerOptions { WriteIndented = writeIndented }); + + foreach ((Func Tweak, Func Position, int Offset) tweak in enumeratePayloadTweaks ? s_payloadTweaks.Skip(1) : s_payloadTweaks.Take(1)) { - A = "Hello", - B = null, - C = 42, - D = null, - E = 3.14e+17f, - F = null, - G = true, - H = null, - I = new int[] {42, 17}, - J = new() + string tweaked = tweak.Tweak(payload); + + // Wrap the payload inside an array to have something to read before/after. + tweaked = '[' + tweaked + ']'; + Type arrayType = Type.GetType(testObjectType.FullName + "[]"); + + (int Line, int Col) failurePosition = GetExpectedFailure(tweaked, tweak.Position(tweaked), tweak.Offset); + + // Determine the DefaultBufferSize that is required to contain the complete json. + int bufferSize = 16; + while (tweaked.Length > bufferSize) { - A = null, - B = 7, + bufferSize *= 2; + } + int minPaddingLength = bufferSize - tweaked.Length + 1; + int maxPaddingLength = bufferSize - 1; + + foreach (int length in Enumerable.Range(minPaddingLength, maxPaddingLength - minPaddingLength + 1)) + { + (int Line, int Col) paddedFailurePosition = failurePosition; + if (failurePosition != default && failurePosition.Line == 0) + paddedFailurePosition = (failurePosition.Line, failurePosition.Col + length); + + foreach (bool ignoreNull in IgnoreNullValues) + { + yield return new object[] + { + new string(' ', length) + tweaked, + bufferSize, + arrayType, + ignoreNull, + paddedFailurePosition + }; + } } } - }; - await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter>() } }); + } } - stream.Position = 0; + static (int Line, int Col) GetExpectedFailure(string payload, int position, int offset) { - var readOptions = new JsonSerializerOptions + if (position < 0) + return default; + + position += offset; + ReadOnlySpan utf8 = Encoding.UTF8.GetBytes(payload); + utf8 = utf8.Slice(0, position); + int positionInLine = position; + int lastNewLine; + int newLineCount = 0; + while ((lastNewLine = utf8.LastIndexOf((byte)'\n')) >= 0) { - DefaultBufferSize = 128, - IgnoreNullValues = ignoreNullValues, - }; + if (newLineCount == 0) + positionInLine -= lastNewLine + 1; + newLineCount++; + utf8 = utf8.Slice(0, lastNewLine); + } - Outer> obj = await JsonSerializer.DeserializeAsync>>(stream, readOptions); - - Assert.Equal(new string('x', paddingLength), obj.S); - Assert.Equal("Hello", obj.C.A); - Assert.Null(obj.C.B); - Assert.Equal(42, obj.C.C); - Assert.Null(obj.C.D); - Assert.Equal(3.14e17f, obj.C.E); - Assert.Null(obj.C.F); - Assert.True(obj.C.G); - Assert.Null(obj.C.H); - Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v)); - Assert.NotNull(obj.C.J); - Assert.Null(obj.C.J.A); - Assert.Equal(7, obj.C.J.B); + return (newLineCount, positionInLine); } } [Theory] - [MemberData(nameof(TestData))] + [MemberData(nameof(TestData), /* enumeratePayloadTweaks: */ false)] [ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)] - public static async Task ContinuationShouldWorkAtAnyPosition_Class_ValueType(int paddingLength, bool ignoreNullValues) + public static async Task ShouldWorkAtAnyPosition_Stream( + string json, + int bufferSize, + Type type, + bool ignoreNullValues, + (int Line, int Column) expectedFailure) { - var stream = new MemoryStream(); - { - var obj = new Outer> - { - S = new string('x', paddingLength), - C = new() - { - A = "Hello", - B = null, - C = 42, - D = null, - E = 3.14e+17f, - F = null, - G = true, - H = null, - I = new int[] { 42, 17 }, - J = new() - { - A = null, - B = 7, - } - } - }; - await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter>() } }); - } - - stream.Position = 0; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); { var readOptions = new JsonSerializerOptions { - DefaultBufferSize = 128, + DefaultBufferSize = bufferSize, IgnoreNullValues = ignoreNullValues, }; - Outer> obj = await JsonSerializer.DeserializeAsync>>(stream, readOptions); - - Assert.Equal(new string('x', paddingLength), obj.S); - Assert.Equal("Hello", obj.C.A); - Assert.Null(obj.C.B); - Assert.Equal(42, obj.C.C); - Assert.Null(obj.C.D); - Assert.Equal(3.14e17f, obj.C.E); - Assert.Null(obj.C.F); - Assert.True(obj.C.G); - Assert.Null(obj.C.H); - Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v)); - Assert.Null(obj.C.J.A); - Assert.Equal(7, obj.C.J.B); + var array = (ITestObject[])await JsonSerializer.DeserializeAsync(stream, type, readOptions); + + Assert.NotNull(array); + Assert.Equal(1, array.Length); + array[0].Verify(); } + Assert.Equal(default, expectedFailure); } [Theory] - [MemberData(nameof(TestData))] + [MemberData(nameof(TestData), /* enumeratePayloadTweaks: */ true)] [ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)] - public static async Task ContinuationShouldWorkAtAnyPosition_ValueType_Class(int paddingLength, bool ignoreNullValues) + public static async Task InvalidJsonShouldFailAtAnyPosition_Stream( + string json, + int bufferSize, + Type type, + bool ignoreNullValues, + (int Line, int Column) expectedFailure) { - var stream = new MemoryStream(); + if (expectedFailure == default) { - var obj = new Outer> - { - S = new string('x', paddingLength), - C = new() - { - A = "Hello", - B = null, - C = 42, - D = null, - E = 3.14e+17f, - F = null, - G = true, - H = null, - I = new int[] { 42, 17 }, - J = new() - { - A = null, - B = 7, - } - } - }; - await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter>() } }); + // The tweak didn't find something to tweak in the payload + return; } - stream.Position = 0; + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); { var readOptions = new JsonSerializerOptions { - DefaultBufferSize = 128, + DefaultBufferSize = bufferSize, IgnoreNullValues = ignoreNullValues, }; - Outer> obj = await JsonSerializer.DeserializeAsync>>(stream, readOptions); - - Assert.Equal(new string('x', paddingLength), obj.S); - Assert.Equal("Hello", obj.C.A); - Assert.Null(obj.C.B); - Assert.Equal(42, obj.C.C); - Assert.Null(obj.C.D); - Assert.Equal(3.14e17f, obj.C.E); - Assert.Null(obj.C.F); - Assert.True(obj.C.G); - Assert.Null(obj.C.H); - Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v)); - Assert.NotNull(obj.C.J); - Assert.Null(obj.C.J.A); - Assert.Equal(7, obj.C.J.B); + JsonException ex = await Assert.ThrowsAsync(async () => await JsonSerializer.DeserializeAsync(stream, type, readOptions)); + Assert.Equal(expectedFailure.Line, ex.LineNumber); + Assert.Equal(expectedFailure.Column, ex.BytePositionInLine); } } [Theory] - [MemberData(nameof(TestData))] + [MemberData(nameof(TestData), /* enumeratePayloadTweaks: */ false)] [ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)] - public static async Task ContinuationShouldWorkAtAnyPosition_ValueType_ValueType(int paddingLength, bool ignoreNullValues) + public static void ShouldWorkAtAnyPosition_Sequence( + string json, + int bufferSize, + Type type, + bool ignoreNullValues, + (int Line, int Column) expectedFailure) { - var stream = new MemoryStream(); - { - var obj = new Outer> - { - S = new string('x', paddingLength), - C = new() - { - A = "Hello", - B = null, - C = 42, - D = null, - E = 3.14e+17f, - F = null, - G = true, - H = null, - I = new int[] { 42, 17 }, - J = new() - { - A = null, - B = 7, - } - } - }; - await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter>() } }); - } + var readOptions = new JsonSerializerOptions { IgnoreNullValues = ignoreNullValues, }; - stream.Position = 0; - { - var readOptions = new JsonSerializerOptions - { - DefaultBufferSize = 128, - IgnoreNullValues = ignoreNullValues, - }; + var chunk = new Chunk(json, bufferSize); + var sequence = new ReadOnlySequence(chunk, 0, chunk.Next, chunk.Next.Memory.Length); - Outer> obj = await JsonSerializer.DeserializeAsync>>(stream, readOptions); - - Assert.Equal(new string('x', paddingLength), obj.S); - Assert.Equal("Hello", obj.C.A); - Assert.Null(obj.C.B); - Assert.Equal(42, obj.C.C); - Assert.Null(obj.C.D); - Assert.Equal(3.14e17f, obj.C.E); - Assert.Null(obj.C.F); - Assert.True(obj.C.G); - Assert.Null(obj.C.H); - Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v)); - Assert.Null(obj.C.J.A); - Assert.Equal(7, obj.C.J.B); - } + var reader = new Utf8JsonReader(sequence); + var array = (ITestObject[])JsonSerializer.Deserialize(ref reader, type, readOptions); + + Assert.NotNull(array); + Assert.Equal(1, array.Length); + array[0].Verify(); + Assert.Equal(default, expectedFailure); } [Theory] - [MemberData(nameof(TestData))] + [MemberData(nameof(TestData), /* enumeratePayloadTweaks: */ true)] [ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)] - public static async Task ContinuationShouldWorkAtAnyPosition_ClassWithParamCtor_Class(int paddingLength, bool ignoreNullValues) + public static void InvalidJsonShouldFailAtAnyPosition_Sequence( + string json, + int bufferSize, + Type type, + bool ignoreNullValues, + (int Line, int Column) expectedFailure) { - var stream = new MemoryStream(); + if (expectedFailure == default) { - var obj = new Outer> - { - S = new string('x', paddingLength), - C = new(null, 42, null, 3.14e+17f, null, true, null, new int[] { 42, 17 }) - { - A = "Hello", - J = new(null) - { - B = 7, - }, - }, - }; - await JsonSerializer.SerializeAsync(stream, obj, new JsonSerializerOptions { Converters = { new OuterConverter>() } }); + // The tweak didn't find something to tweak in the payload + return; } - stream.Position = 0; - { - var readOptions = new JsonSerializerOptions - { - DefaultBufferSize = 128, - IgnoreNullValues = ignoreNullValues, - }; + var readOptions = new JsonSerializerOptions { IgnoreNullValues = ignoreNullValues, }; - Outer> obj = await JsonSerializer.DeserializeAsync>>(stream, readOptions); - - Assert.Equal(new string('x', paddingLength), obj.S); - Assert.Equal("Hello", obj.C.A); - Assert.Null(obj.C.B); - Assert.Equal(42, obj.C.C); - Assert.Null(obj.C.D); - Assert.Equal(3.14e17f, obj.C.E); - Assert.Null(obj.C.F); - Assert.True(obj.C.G); - Assert.Null(obj.C.H); - Assert.Collection(obj.C.I, v => Assert.Equal(42, v), v => Assert.Equal(17, v)); - Assert.Null(obj.C.J.A); - Assert.Equal(7, obj.C.J.B); - } + var chunk = new Chunk(json, bufferSize); + var sequence = new ReadOnlySequence(chunk, 0, chunk.Next, chunk.Next.Memory.Length); + + JsonException ex = Assert.Throws(() => + { + var reader = new Utf8JsonReader(sequence); + JsonSerializer.Deserialize(ref reader, type, readOptions); + }); + Assert.Equal(expectedFailure.Line, ex.LineNumber); + Assert.Equal(expectedFailure.Column, ex.BytePositionInLine); } - private static IEnumerable TestData() + private class Chunk : ReadOnlySequenceSegment { - foreach (int length in ContinuationPaddingLengths) + public Chunk(string json, int firstSegmentLength) { - foreach (bool ignore in IgnoreNullValues) + Memory bytes = Encoding.UTF8.GetBytes(json); + Memory = bytes.Slice(0, firstSegmentLength); + RunningIndex = 0; + Next = new Chunk() { - yield return new object[] { length, ignore }; - } + Memory = bytes.Slice(firstSegmentLength), + RunningIndex = firstSegmentLength, + Next = null, + }; } + private Chunk() + { } + } + + private interface ITestObject + { + void Initialize(INestedObject nested); + void Verify(); } - private class Outer + private interface INestedObject { - public string S { get; set; } - public TTest C { get; set; } + void Initialize(); + void Verify(); } - private class TestClass + private class TestClass : ITestObject where TNested : INestedObject { public string A { get; set; } public string B { get; set; } public int C { get; set; } public int? D { get; set; } public float E { get; set; } - public float? F { get; set; } public bool G { get; set; } - public bool? H { get; set; } public int[] I { get; set; } public TNested J { get; set; } - } - private class TestClassWithParamCtor : TestClass - { - public TestClassWithParamCtor(string b, int c, int? d, float e, float? f, bool g, bool? h, int[] i) - => (B, C, D, E, F, G, H, I) = (b, c, d, e, f, g, h, i); + void ITestObject.Initialize(INestedObject nested) + { + A = "Hello"; + B = null; + C = 42; + D = null; + E = 3.14e+17f; + G = true; + I = new int[] { 42, 17 }; + nested.Initialize(); + J = (TNested)nested; + } + + void ITestObject.Verify() + { + Assert.Equal("Hello", A); + Assert.Null(B); + Assert.Equal(42, C); + Assert.Null(D); + Assert.Equal(3.14e17f, E); + Assert.True(G); + Assert.NotNull(I); + Assert.True(I.SequenceEqual(new[] { 42, 17 })); + Assert.NotNull(J); + J.Verify(); + } } - private class TestValueType + private class TestValueType : ITestObject where TNested : INestedObject { public string A { get; set; } public string B { get; set; } public int C { get; set; } public int? D { get; set; } public float E { get; set; } - public float? F { get; set; } public bool G { get; set; } - public bool? H { get; set; } public int[] I { get; set; } public TNested J { get; set; } - } - private class NestedClass - { - public string A { get; set; } - public int B { get; set; } - } + void ITestObject.Initialize(INestedObject nested) + { + A = "Hello"; + B = null; + C = 42; + D = null; + E = 3.14e+17f; + G = true; + I = new int[] { 42, 17 }; + nested.Initialize(); + J = (TNested)nested; + } - private class NestedClassWithParamCtor : NestedClass - { - public NestedClassWithParamCtor(string a) - => A = a; + void ITestObject.Verify() + { + Assert.Equal("Hello", A); + Assert.Null(B); + Assert.Equal(42, C); + Assert.Null(D); + Assert.Equal(3.14e17f, E); + Assert.True(G); + Assert.NotNull(I); + Assert.True(I.SequenceEqual(new[] { 42, 17 })); + Assert.NotNull(J); + J.Verify(); + } } - private struct NestedValueType + private class NestedClass : INestedObject { public string A { get; set; } public int B { get; set; } - } - // custom converter to ensure that the padding is written in front of the tested object. - private class OuterConverter : JsonConverter> - { - public override Outer Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => throw new NotImplementedException(); + void INestedObject.Initialize() + { + A = null; + B = 7; + } - public override void Write(Utf8JsonWriter writer, Outer value, JsonSerializerOptions options) + void INestedObject.Verify() { - writer.WriteStartObject(); - writer.WriteString("S", value.S); - writer.WritePropertyName("C"); - JsonSerializer.Serialize(writer, value.C, typeof(T), options); - writer.WriteEndObject(); + Assert.Null(A); + Assert.Equal(7, B); } } - // From https://github.com/dotnet/runtime/issues/42070 - [Theory] - [InlineData("CustomerSearchApi108KB")] - [InlineData("CustomerSearchApi107KB")] - [ActiveIssue("https://github.com/dotnet/runtime/issues/42677", platforms: TestPlatforms.Windows, runtimes: TestRuntimes.Mono)] - public static async Task ContinuationAtNullToken(string resourceName) + private struct NestedValueType : INestedObject { - using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(SR.GetResourceString(resourceName)))) + public string A { get; set; } + public int B { get; set; } + + void INestedObject.Initialize() { - CustomerCollectionResponse response = await JsonSerializer.DeserializeAsync(stream, new JsonSerializerOptions { IgnoreNullValues = true }); - Assert.Equal(50, response.Customers.Count); + A = null; + B = 7; } - } - private class CustomerCollectionResponse - { - [JsonPropertyName("customers")] - public List Customers { get; set; } + void INestedObject.Verify() + { + Assert.Null(A); + Assert.Equal(7, B); + } } - private class CustomerAddress + private class NestedClassWithParamCtor : NestedClass { - [JsonPropertyName("first_name")] - public string FirstName { get; set; } - - [JsonPropertyName("address1")] - public string Address1 { get; set; } - - [JsonPropertyName("phone")] - public string Phone { get; set; } - - [JsonPropertyName("city")] - public string City { get; set; } - - [JsonPropertyName("zip")] - public string Zip { get; set; } - - [JsonPropertyName("province")] - public string Province { get; set; } - - [JsonPropertyName("country")] - public string Country { get; set; } - - [JsonPropertyName("last_name")] - public string LastName { get; set; } - - [JsonPropertyName("address2")] - public string Address2 { get; set; } - - [JsonPropertyName("company")] - public string Company { get; set; } - - [JsonPropertyName("latitude")] - public float? Latitude { get; set; } - - [JsonPropertyName("longitude")] - public float? Longitude { get; set; } - - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("country_code")] - public string CountryCode { get; set; } - - [JsonPropertyName("province_code")] - public string ProvinceCode { get; set; } + public NestedClassWithParamCtor(string a) + => A = a; } - private class Customer + private class DictionaryTestClass : ITestObject where TNested : INestedObject { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("email")] - public string Email { get; set; } - - [JsonPropertyName("accepts_marketing")] - public bool AcceptsMarketing { get; set; } - - [JsonPropertyName("created_at")] - public DateTimeOffset? CreatedAt { get; set; } - - [JsonPropertyName("updated_at")] - public DateTimeOffset? UpdatedAt { get; set; } - - [JsonPropertyName("first_name")] - public string FirstName { get; set; } + public Dictionary A { get; set; } - [JsonPropertyName("last_name")] - public string LastName { get; set; } - - [JsonPropertyName("orders_count")] - public int OrdersCount { get; set; } - - [JsonPropertyName("state")] - public string State { get; set; } - - [JsonPropertyName("total_spent")] - public string TotalSpent { get; set; } - - [JsonPropertyName("last_order_id")] - public long? LastOrderId { get; set; } - - [JsonPropertyName("note")] - public string Note { get; set; } - - [JsonPropertyName("verified_email")] - public bool VerifiedEmail { get; set; } - - [JsonPropertyName("multipass_identifier")] - public string MultipassIdentifier { get; set; } - - [JsonPropertyName("tax_exempt")] - public bool TaxExempt { get; set; } - - [JsonPropertyName("tags")] - public string Tags { get; set; } - - [JsonPropertyName("last_order_name")] - public string LastOrderName { get; set; } - - [JsonPropertyName("default_address")] - public CustomerAddress DefaultAddress { get; set; } + void ITestObject.Initialize(INestedObject nested) + { + nested.Initialize(); + A = new() { { "a", (TNested)nested }, { "b", (TNested)nested } }; + } - [JsonPropertyName("addresses")] - public IList Addresses { get; set; } + void ITestObject.Verify() + { + Assert.NotNull(A); + Assert.Collection(A, + kv => + { + Assert.Equal("a", kv.Key); + kv.Value.Verify(); + }, + kv => + { + Assert.Equal("b", kv.Key); + kv.Value.Verify(); + }); + } } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj index a2bed29..7632275 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj @@ -62,6 +62,7 @@ + -- 2.7.4