JSON continuation tests (#42393)
authordevsko <devsko@users.noreply.github.com>
Mon, 2 Nov 2020 21:37:52 +0000 (22:37 +0100)
committerGitHub <noreply@github.com>
Mon, 2 Nov 2020 21:37:52 +0000 (13:37 -0800)
* 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 <laakinri@microsoft.com>
src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.NullToken.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Serialization/ContinuationTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj

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 (file)
index 0000000..6acee1c
--- /dev/null
@@ -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<CustomerCollectionResponse>(stream, new JsonSerializerOptions { IgnoreNullValues = true });
+                Assert.Equal(50, response.Customers.Count);
+            }
+        }
+
+        public static IEnumerable<object[]> ContinuationAtNullTokenTestData
+            => new[]
+            {
+                new[] { SR.CustomerSearchApi108KB },
+                new[] { SR.CustomerSearchApi107KB },
+            };
+
+        private class CustomerCollectionResponse
+        {
+            [JsonPropertyName("customers")]
+            public List<Customer> 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<CustomerAddress> Addresses { get; set; }
+        }
+    }
+}
index e3b9b69..9736142 100644 (file)
 // 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<int> ContinuationPaddingLengths
-            => Enumerable.Range(MinPaddingLength, MaxPaddingLength - MinPaddingLength + 1);
+        private static readonly (Func<string, string>, Func<string, int>, int)[] s_payloadTweaks = new (Func<string, string>, Func<string, int>, 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<NestedClass>(), new NestedClass()),
+                (new TestClass<NestedValueType>(), new NestedValueType()),
+                (new TestValueType<NestedClass>(), new NestedClass()),
+                (new TestValueType<NestedValueType>(), new NestedValueType()),
+                (new TestClass<NestedClassWithParamCtor>(), new NestedClassWithParamCtor(null)),
+                (new DictionaryTestClass<NestedClass>(), new NestedClass()),
+            };
 
         private static IEnumerable<bool> 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<bool> WriteIndented
+            => new[] { true, false };
+
+        private static IEnumerable<object[]> 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<TestClass<NestedClass>>
+                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<string, string> Tweak, Func<string, int> 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<TestClass<NestedClass>>() } });
+                }
             }
 
-            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<byte> 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<TestClass<NestedClass>> obj = await JsonSerializer.DeserializeAsync<Outer<TestClass<NestedClass>>>(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<TestClass<NestedValueType>>
-                {
-                    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<TestClass<NestedClass>>() } });
-            }
-
-            stream.Position = 0;
+            var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
             {
                 var readOptions = new JsonSerializerOptions
                 {
-                    DefaultBufferSize = 128,
+                    DefaultBufferSize = bufferSize,
                     IgnoreNullValues = ignoreNullValues,
                 };
 
-                Outer<TestClass<NestedValueType>> obj = await JsonSerializer.DeserializeAsync<Outer<TestClass<NestedValueType>>>(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<TestValueType<NestedClass>>
-                {
-                    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<TestClass<NestedClass>>() } });
+                // 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<TestValueType<NestedClass>> obj = await JsonSerializer.DeserializeAsync<Outer<TestValueType<NestedClass>>>(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<JsonException>(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<TestValueType<NestedValueType>>
-                {
-                    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<TestClass<NestedClass>>() } });
-            }
+            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<byte>(chunk, 0, chunk.Next, chunk.Next.Memory.Length);
 
-                Outer<TestValueType<NestedValueType>> obj = await JsonSerializer.DeserializeAsync<Outer<TestValueType<NestedValueType>>>(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<TestClassWithParamCtor<NestedClassWithParamCtor>>
-                {
-                    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<TestClass<NestedClass>>() } });
+                // 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<TestClassWithParamCtor<NestedClassWithParamCtor>> obj = await JsonSerializer.DeserializeAsync<Outer<TestClassWithParamCtor<NestedClassWithParamCtor>>>(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<byte>(chunk, 0, chunk.Next, chunk.Next.Memory.Length);
+
+            JsonException ex = Assert.Throws<JsonException>(() =>
+            {
+                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<object[]> TestData()
+        private class Chunk : ReadOnlySequenceSegment<byte>
         {
-            foreach (int length in ContinuationPaddingLengths)
+            public Chunk(string json, int firstSegmentLength)
             {
-                foreach (bool ignore in IgnoreNullValues)
+                Memory<byte> 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<TTest>
+        private interface INestedObject
         {
-            public string S { get; set; }
-            public TTest C { get; set; }
+            void Initialize();
+            void Verify();
         }
 
-        private class TestClass<TNested>
+        private class TestClass<TNested> : 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<TNested> : TestClass<TNested>
-        {
-            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<TNested>
+        private class TestValueType<TNested> : 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<T> : JsonConverter<Outer<T>>
-        {
-            public override Outer<T> 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<T> 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<CustomerCollectionResponse>(stream, new JsonSerializerOptions { IgnoreNullValues = true });
-                Assert.Equal(50, response.Customers.Count);
+                A = null;
+                B = 7;
             }
-        }
 
-        private class CustomerCollectionResponse
-        {
-            [JsonPropertyName("customers")]
-            public List<Customer> 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<TNested> : 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<string, TNested> 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<CustomerAddress> 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();
+                    });
+            }
         }
     }
 }
index a2bed29..7632275 100644 (file)
@@ -62,6 +62,7 @@
     <Compile Include="Serialization\ConstructorTests\ConstructorTests.ParameterMatching.cs" />
     <Compile Include="Serialization\ConstructorTests\ConstructorTests.Stream.cs" />
     <Compile Include="Serialization\ContinuationTests.cs" />
+    <Compile Include="Serialization\ContinuationTests.NullToken.cs" />
     <Compile Include="Serialization\CustomConverterTests\CustomConverterTests.Array.cs" />
     <Compile Include="Serialization\CustomConverterTests\CustomConverterTests.Attribute.cs" />
     <Compile Include="Serialization\CustomConverterTests\CustomConverterTests.BadConverters.cs" />