File: ObjectSerializationTests.cs
Web Access
Project: src\src\Workspaces\CoreTest\Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj (Microsoft.CodeAnalysis.Workspaces.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
#pragma warning disable IDE0060 // Remove unused parameter
 
namespace Microsoft.CodeAnalysis.UnitTests
{
    public sealed class ObjectSerializationTests
    {
        [Fact]
        public void TestInvalidStreamVersion()
        {
            var stream = new MemoryStream();
            stream.WriteByte(0);
            stream.WriteByte(0);
 
            stream.Position = 0;
 
            var reader = ObjectReader.TryGetReader(stream);
            Assert.Null(reader);
        }
 
        private static void RoundTrip(Action<ObjectWriter> writeAction, Action<ObjectReader> readAction, bool recursive)
        {
            using var stream = new MemoryStream();
 
            using (var writer = new ObjectWriter(stream, leaveOpen: true))
            {
                writeAction(writer);
            }
 
            stream.Position = 0;
            using var reader = ObjectReader.TryGetReader(stream);
            readAction(reader);
        }
 
        private static void TestRoundTrip(Action<ObjectWriter> writeAction, Action<ObjectReader> readAction)
        {
            RoundTrip(writeAction, readAction, recursive: true);
            RoundTrip(writeAction, readAction, recursive: false);
        }
 
        private static T RoundTrip<T>(T value, Action<ObjectWriter, T> writeAction, Func<ObjectReader, T> readAction, bool recursive)
        {
            using var stream = new MemoryStream();
 
            using (var writer = new ObjectWriter(stream, leaveOpen: true))
            {
                writeAction(writer, value);
            }
 
            stream.Position = 0;
            using var reader = ObjectReader.TryGetReader(stream);
            return readAction(reader);
        }
 
        private static void TestRoundTrip<T>(T value, Action<ObjectWriter, T> writeAction, Func<ObjectReader, T> readAction, bool recursive)
        {
            var newValue = RoundTrip(value, writeAction, readAction, recursive);
            Assert.True(Equalish(value, newValue));
        }
 
        private static void TestRoundTrip<T>(T value, Action<ObjectWriter, T> writeAction, Func<ObjectReader, T> readAction)
        {
            TestRoundTrip(value, writeAction, readAction, recursive: true);
            TestRoundTrip(value, writeAction, readAction, recursive: false);
        }
 
        private static T RoundTripValue<T>(T value, bool recursive)
        {
            return RoundTrip(value,
                (w, v) =>
                {
                    if (v != null && v.GetType().IsEnum)
                    {
                        w.WriteInt64(Convert.ToInt64(v));
                    }
                    else
                    {
                        w.WriteScalarValue(v);
                    }
                },
                r => value != null && value.GetType().IsEnum
                    ? (T)Enum.ToObject(typeof(T), r.ReadInt64())
                    : (T)r.ReadScalarValue(), recursive);
        }
 
        private static void TestRoundTripValue<T>(T value, bool recursive)
        {
            var newValue = RoundTripValue(value, recursive);
            Assert.True(Equalish(value, newValue));
        }
 
        private static void TestRoundTripValue<T>(T value)
        {
            TestRoundTripValue(value, recursive: true);
            TestRoundTripValue(value, recursive: false);
        }
 
        private static bool Equalish<T>(T value1, T value2)
        {
            return object.Equals(value1, value2)
                || (value1 is Array && value2 is Array && ArrayEquals((Array)(object)value1, (Array)(object)value2));
        }
 
        private static bool ArrayEquals(Array seq1, Array seq2)
        {
            if (seq1 == null && seq2 == null)
            {
                return true;
            }
            else if (seq1 == null || seq2 == null)
            {
                return false;
            }
 
            if (seq1.Length != seq2.Length)
            {
                return false;
            }
 
            for (var i = 0; i < seq1.Length; i++)
            {
                if (!Equalish(seq1.GetValue(i), seq2.GetValue(i)))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        private class TypeWithOneMember<T> : IEquatable<TypeWithOneMember<T>>
        {
            private readonly T _member;
 
            public TypeWithOneMember(T value)
            {
                _member = value;
            }
 
            public void WriteTo(ObjectWriter writer)
            {
                if (typeof(T).IsEnum)
                {
                    writer.WriteInt64(Convert.ToInt64(_member));
                }
                else
                {
                    writer.WriteScalarValue(_member);
                }
            }
 
            public override Int32 GetHashCode()
            {
                if (_member == null)
                {
                    return 0;
                }
                else
                {
                    return _member.GetHashCode();
                }
            }
 
            public override Boolean Equals(Object obj)
            {
                return Equals(obj as TypeWithOneMember<T>);
            }
 
            public bool Equals(TypeWithOneMember<T> other)
            {
                return other != null && Equalish(_member, other._member);
            }
        }
 
        [Fact]
        public void TestValueInt32()
        {
            TestRoundTripValue(123);
        }
 
        [Fact]
        public void TestInt32TypeCodes()
        {
            Assert.Equal(ObjectWriter.TypeCode.Int32_1, ObjectWriter.TypeCode.Int32_0 + 1);
            Assert.Equal(ObjectWriter.TypeCode.Int32_2, ObjectWriter.TypeCode.Int32_0 + 2);
            Assert.Equal(ObjectWriter.TypeCode.Int32_3, ObjectWriter.TypeCode.Int32_0 + 3);
            Assert.Equal(ObjectWriter.TypeCode.Int32_4, ObjectWriter.TypeCode.Int32_0 + 4);
            Assert.Equal(ObjectWriter.TypeCode.Int32_5, ObjectWriter.TypeCode.Int32_0 + 5);
            Assert.Equal(ObjectWriter.TypeCode.Int32_6, ObjectWriter.TypeCode.Int32_0 + 6);
            Assert.Equal(ObjectWriter.TypeCode.Int32_7, ObjectWriter.TypeCode.Int32_0 + 7);
            Assert.Equal(ObjectWriter.TypeCode.Int32_8, ObjectWriter.TypeCode.Int32_0 + 8);
            Assert.Equal(ObjectWriter.TypeCode.Int32_9, ObjectWriter.TypeCode.Int32_0 + 9);
            Assert.Equal(ObjectWriter.TypeCode.Int32_10, ObjectWriter.TypeCode.Int32_0 + 10);
        }
 
        [Fact]
        public void TestUInt32TypeCodes()
        {
            Assert.Equal(ObjectWriter.TypeCode.UInt32_1, ObjectWriter.TypeCode.UInt32_0 + 1);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_2, ObjectWriter.TypeCode.UInt32_0 + 2);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_3, ObjectWriter.TypeCode.UInt32_0 + 3);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_4, ObjectWriter.TypeCode.UInt32_0 + 4);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_5, ObjectWriter.TypeCode.UInt32_0 + 5);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_6, ObjectWriter.TypeCode.UInt32_0 + 6);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_7, ObjectWriter.TypeCode.UInt32_0 + 7);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_8, ObjectWriter.TypeCode.UInt32_0 + 8);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_9, ObjectWriter.TypeCode.UInt32_0 + 9);
            Assert.Equal(ObjectWriter.TypeCode.UInt32_10, ObjectWriter.TypeCode.UInt32_0 + 10);
        }
 
        private static void TestRoundTripCompressedUint(uint value)
        {
            TestRoundTrip(value, (w, v) => ((ObjectWriter)w).WriteCompressedUInt(v), r => ((ObjectReader)r).ReadCompressedUInt());
        }
 
        [Fact]
        public void TestCompressedUInt()
        {
            TestRoundTripCompressedUint(0);
            TestRoundTripCompressedUint(0x01u);
            TestRoundTripCompressedUint(0x0123u);     // unique bytes tests order
            TestRoundTripCompressedUint(0x012345u);   // unique bytes tests order
            TestRoundTripCompressedUint(0x01234567u); // unique bytes tests order
            TestRoundTripCompressedUint(0x3Fu);       // largest value packed in one byte
            TestRoundTripCompressedUint(0x3FFFu);     // largest value packed into two bytes
            TestRoundTripCompressedUint(0x3FFFFFu);   // no three byte option yet, but test anyway
            TestRoundTripCompressedUint(0x3FFFFFFFu); // largest unit allowed in four bytes
 
            Assert.Throws<ArgumentException>(() => TestRoundTripCompressedUint(uint.MaxValue)); // max uint not allowed
            Assert.Throws<ArgumentException>(() => TestRoundTripCompressedUint(0x80000000u)); // highest bit set not allowed
            Assert.Throws<ArgumentException>(() => TestRoundTripCompressedUint(0x40000000u)); // second highest bit set not allowed
            Assert.Throws<ArgumentException>(() => TestRoundTripCompressedUint(0xC0000000u)); // both high bits set not allowed
        }
 
        [Theory, CombinatorialData]
        public void TestByteSpan([CombinatorialValues(0, 1, 2, 3, 1000, 1000000)] int size)
        {
            var data = new byte[size];
            for (var i = 0; i < data.Length; i++)
            {
                data[i] = (byte)i;
            }
 
            TestRoundTrip(w => TestWritingByteSpan(data, w), r => TestReadingByteSpan(data, r));
        }
 
        private static void TestWritingByteSpan(byte[] data, ObjectWriter writer)
        {
            writer.WriteSpan(data.AsSpan());
        }
 
        private static void TestReadingByteSpan(byte[] expected, ObjectReader reader)
        {
            Assert.True(Enumerable.SequenceEqual(expected, (byte[])reader.ReadByteArray()));
        }
 
        private static readonly DateTime _testNow = DateTime.Now;
 
        [Fact]
        public void TestPrimitiveValues()
        {
            TestRoundTripValue(true);
            TestRoundTripValue(false);
            TestRoundTripValue(Byte.MaxValue);
            TestRoundTripValue(SByte.MaxValue);
            TestRoundTripValue(Int16.MaxValue);
            TestRoundTripValue(Int32.MaxValue);
            TestRoundTripValue(Byte.MaxValue);
            TestRoundTripValue(Int16.MaxValue);
            TestRoundTripValue(Int64.MaxValue);
            TestRoundTripValue(UInt16.MaxValue);
            TestRoundTripValue(UInt32.MaxValue);
            TestRoundTripValue(UInt64.MaxValue);
            TestRoundTripValue(Decimal.MaxValue);
            TestRoundTripValue(Double.MaxValue);
            TestRoundTripValue(Single.MaxValue);
            TestRoundTripValue('X');
            TestRoundTripValue("YYY");
            TestRoundTripValue("\uD800\uDC00"); // valid surrogate pair
            TestRoundTripValue("\uDC00\uD800"); // invalid surrogate pair
            TestRoundTripValue("\uD800"); // incomplete surrogate pair
            TestRoundTripValue<object>(null);
            TestRoundTripValue(ConsoleColor.Cyan);
            TestRoundTripValue(EByte.Value);
            TestRoundTripValue(ESByte.Value);
            TestRoundTripValue(EShort.Value);
            TestRoundTripValue(EUShort.Value);
            TestRoundTripValue(EInt.Value);
            TestRoundTripValue(EUInt.Value);
            TestRoundTripValue(ELong.Value);
            TestRoundTripValue(EULong.Value);
            TestRoundTripValue(_testNow);
        }
 
        [Fact]
        public void TestInt32Values()
        {
            TestRoundTripValue<Int32>(0);
            TestRoundTripValue<Int32>(1);
            TestRoundTripValue<Int32>(2);
            TestRoundTripValue<Int32>(3);
            TestRoundTripValue<Int32>(4);
            TestRoundTripValue<Int32>(5);
            TestRoundTripValue<Int32>(6);
            TestRoundTripValue<Int32>(7);
            TestRoundTripValue<Int32>(8);
            TestRoundTripValue<Int32>(9);
            TestRoundTripValue<Int32>(10);
            TestRoundTripValue<Int32>(-1);
            TestRoundTripValue<Int32>(Int32.MinValue);
            TestRoundTripValue<Int32>(Byte.MaxValue);
            TestRoundTripValue<Int32>(UInt16.MaxValue);
            TestRoundTripValue<Int32>(Int32.MaxValue);
        }
 
        [Fact]
        public void TestUInt32Values()
        {
            TestRoundTripValue<UInt32>(0);
            TestRoundTripValue<UInt32>(1);
            TestRoundTripValue<UInt32>(2);
            TestRoundTripValue<UInt32>(3);
            TestRoundTripValue<UInt32>(4);
            TestRoundTripValue<UInt32>(5);
            TestRoundTripValue<UInt32>(6);
            TestRoundTripValue<UInt32>(7);
            TestRoundTripValue<UInt32>(8);
            TestRoundTripValue<UInt32>(9);
            TestRoundTripValue<UInt32>(10);
            TestRoundTripValue<Int32>(Byte.MaxValue);
            TestRoundTripValue<Int32>(UInt16.MaxValue);
            TestRoundTripValue<Int32>(Int32.MaxValue);
        }
 
        [Fact]
        public void TestInt64Values()
        {
            TestRoundTripValue<Int64>(0);
            TestRoundTripValue<Int64>(1);
            TestRoundTripValue<Int64>(2);
            TestRoundTripValue<Int64>(3);
            TestRoundTripValue<Int64>(4);
            TestRoundTripValue<Int64>(5);
            TestRoundTripValue<Int64>(6);
            TestRoundTripValue<Int64>(7);
            TestRoundTripValue<Int64>(8);
            TestRoundTripValue<Int64>(9);
            TestRoundTripValue<Int64>(10);
            TestRoundTripValue<Int64>(-1);
            TestRoundTripValue<Int64>(Byte.MinValue);
            TestRoundTripValue<Int64>(Byte.MaxValue);
            TestRoundTripValue<Int64>(Int16.MinValue);
            TestRoundTripValue<Int64>(Int16.MaxValue);
            TestRoundTripValue<Int64>(UInt16.MinValue);
            TestRoundTripValue<Int64>(UInt16.MaxValue);
            TestRoundTripValue<Int64>(Int32.MinValue);
            TestRoundTripValue<Int64>(Int32.MaxValue);
            TestRoundTripValue<Int64>(UInt32.MinValue);
            TestRoundTripValue<Int64>(UInt32.MaxValue);
            TestRoundTripValue<Int64>(Int64.MinValue);
            TestRoundTripValue<Int64>(Int64.MaxValue);
        }
 
        [Fact]
        public void TestUInt64Values()
        {
            TestRoundTripValue<UInt64>(0);
            TestRoundTripValue<UInt64>(1);
            TestRoundTripValue<UInt64>(2);
            TestRoundTripValue<UInt64>(3);
            TestRoundTripValue<UInt64>(4);
            TestRoundTripValue<UInt64>(5);
            TestRoundTripValue<UInt64>(6);
            TestRoundTripValue<UInt64>(7);
            TestRoundTripValue<UInt64>(8);
            TestRoundTripValue<UInt64>(9);
            TestRoundTripValue<UInt64>(10);
            TestRoundTripValue<UInt64>(Byte.MinValue);
            TestRoundTripValue<UInt64>(Byte.MaxValue);
            TestRoundTripValue<UInt64>(UInt16.MinValue);
            TestRoundTripValue<UInt64>(UInt16.MaxValue);
            TestRoundTripValue<UInt64>(Int32.MaxValue);
            TestRoundTripValue<UInt64>(UInt32.MinValue);
            TestRoundTripValue<UInt64>(UInt32.MaxValue);
            TestRoundTripValue<UInt64>(UInt64.MinValue);
            TestRoundTripValue<UInt64>(UInt64.MaxValue);
        }
 
        [Fact]
        public void TestPrimitiveAPIs()
        {
            TestRoundTrip(w => TestWritingPrimitiveAPIs(w), r => TestReadingPrimitiveAPIs(r));
        }
 
        private static void TestWritingPrimitiveAPIs(ObjectWriter writer)
        {
            writer.WriteBoolean(true);
            writer.WriteBoolean(false);
            writer.WriteByte(Byte.MaxValue);
            writer.WriteSByte(SByte.MaxValue);
            writer.WriteInt16(Int16.MaxValue);
            writer.WriteInt32(Int32.MaxValue);
            writer.WriteInt32(Byte.MaxValue);
            writer.WriteInt32(Int16.MaxValue);
            writer.WriteInt64(Int64.MaxValue);
            writer.WriteUInt16(UInt16.MaxValue);
            writer.WriteUInt32(UInt32.MaxValue);
            writer.WriteUInt64(UInt64.MaxValue);
            writer.WriteDecimal(Decimal.MaxValue);
            writer.WriteDouble(Double.MaxValue);
            writer.WriteSingle(Single.MaxValue);
            writer.WriteChar('X');
            writer.WriteString("YYY");
            writer.WriteString("\uD800\uDC00"); // valid surrogate pair
            writer.WriteString("\uDC00\uD800"); // invalid surrogate pair
            writer.WriteString("\uD800"); // incomplete surrogate pair
        }
 
        private static void TestReadingPrimitiveAPIs(ObjectReader reader)
        {
            Assert.True(reader.ReadBoolean());
            Assert.False(reader.ReadBoolean());
            Assert.Equal(Byte.MaxValue, reader.ReadByte());
            Assert.Equal(SByte.MaxValue, reader.ReadSByte());
            Assert.Equal(Int16.MaxValue, reader.ReadInt16());
            Assert.Equal(Int32.MaxValue, reader.ReadInt32());
            Assert.Equal(Byte.MaxValue, reader.ReadInt32());
            Assert.Equal(Int16.MaxValue, reader.ReadInt32());
            Assert.Equal(Int64.MaxValue, reader.ReadInt64());
            Assert.Equal(UInt16.MaxValue, reader.ReadUInt16());
            Assert.Equal(UInt32.MaxValue, reader.ReadUInt32());
            Assert.Equal(UInt64.MaxValue, reader.ReadUInt64());
            Assert.Equal(Decimal.MaxValue, reader.ReadDecimal());
            Assert.Equal(Double.MaxValue, reader.ReadDouble());
            Assert.Equal(Single.MaxValue, reader.ReadSingle());
            Assert.Equal('X', reader.ReadChar());
            Assert.Equal("YYY", reader.ReadString());
            Assert.Equal("\uD800\uDC00", reader.ReadString()); // valid surrogate pair
            Assert.Equal("\uDC00\uD800", reader.ReadString()); // invalid surrogate pair
            Assert.Equal("\uD800", reader.ReadString()); // incomplete surrogate pair
        }
 
        [Fact]
        public void TestPrimitivesValue()
        {
            TestRoundTrip(w => TestWritingPrimitiveValues(w), r => TestReadingPrimitiveValues(r));
        }
 
        private static void TestWritingPrimitiveValues(ObjectWriter writer)
        {
            writer.WriteScalarValue(true);
            writer.WriteScalarValue(false);
            writer.WriteScalarValue(Byte.MaxValue);
            writer.WriteScalarValue(SByte.MaxValue);
            writer.WriteScalarValue(Int16.MaxValue);
            writer.WriteScalarValue(Int32.MaxValue);
            writer.WriteScalarValue((Int32)Byte.MaxValue);
            writer.WriteScalarValue((Int32)Int16.MaxValue);
            writer.WriteScalarValue(Int64.MaxValue);
            writer.WriteScalarValue(UInt16.MaxValue);
            writer.WriteScalarValue(UInt32.MaxValue);
            writer.WriteScalarValue(UInt64.MaxValue);
            writer.WriteScalarValue(Decimal.MaxValue);
            writer.WriteScalarValue(Double.MaxValue);
            writer.WriteScalarValue(Single.MaxValue);
            writer.WriteScalarValue('X');
            writer.WriteScalarValue((object)"YYY");
            writer.WriteScalarValue((object)"\uD800\uDC00"); // valid surrogate pair
            writer.WriteScalarValue((object)"\uDC00\uD800"); // invalid surrogate pair
            writer.WriteScalarValue((object)"\uD800"); // incomplete surrogate pair
            writer.WriteScalarValue((object)null);
            unchecked
            {
                writer.WriteInt64((long)ConsoleColor.Cyan);
                writer.WriteInt64((long)EByte.Value);
                writer.WriteInt64((long)ESByte.Value);
                writer.WriteInt64((long)EShort.Value);
                writer.WriteInt64((long)EUShort.Value);
                writer.WriteInt64((long)EInt.Value);
                writer.WriteInt64((long)EUInt.Value);
                writer.WriteInt64((long)ELong.Value);
                writer.WriteInt64((long)EULong.Value);
            }
            writer.WriteScalarValue(_testNow);
        }
 
        private static void TestReadingPrimitiveValues(ObjectReader reader)
        {
            Assert.True((bool)reader.ReadScalarValue());
            Assert.False((bool)reader.ReadScalarValue());
            Assert.Equal(Byte.MaxValue, (Byte)reader.ReadScalarValue());
            Assert.Equal(SByte.MaxValue, (SByte)reader.ReadScalarValue());
            Assert.Equal(Int16.MaxValue, (Int16)reader.ReadScalarValue());
            Assert.Equal(Int32.MaxValue, (Int32)reader.ReadScalarValue());
            Assert.Equal(Byte.MaxValue, (Int32)reader.ReadScalarValue());
            Assert.Equal(Int16.MaxValue, (Int32)reader.ReadScalarValue());
            Assert.Equal(Int64.MaxValue, (Int64)reader.ReadScalarValue());
            Assert.Equal(UInt16.MaxValue, (UInt16)reader.ReadScalarValue());
            Assert.Equal(UInt32.MaxValue, (UInt32)reader.ReadScalarValue());
            Assert.Equal(UInt64.MaxValue, (UInt64)reader.ReadScalarValue());
            Assert.Equal(Decimal.MaxValue, (Decimal)reader.ReadScalarValue());
            Assert.Equal(Double.MaxValue, (Double)reader.ReadScalarValue());
            Assert.Equal(Single.MaxValue, (Single)reader.ReadScalarValue());
            Assert.Equal('X', (Char)reader.ReadScalarValue());
            Assert.Equal("YYY", (String)reader.ReadScalarValue());
            Assert.Equal("\uD800\uDC00", (String)reader.ReadScalarValue()); // valid surrogate pair
            Assert.Equal("\uDC00\uD800", (String)reader.ReadScalarValue()); // invalid surrogate pair
            Assert.Equal("\uD800", (String)reader.ReadScalarValue()); // incomplete surrogate pair
            Assert.Null(reader.ReadScalarValue());
 
            unchecked
            {
                Assert.Equal((long)ConsoleColor.Cyan, reader.ReadInt64());
                Assert.Equal((long)EByte.Value, reader.ReadInt64());
                Assert.Equal((long)ESByte.Value, reader.ReadInt64());
                Assert.Equal((long)EShort.Value, reader.ReadInt64());
                Assert.Equal((long)EUShort.Value, reader.ReadInt64());
                Assert.Equal((long)EInt.Value, reader.ReadInt64());
                Assert.Equal((long)EUInt.Value, reader.ReadInt64());
                Assert.Equal((long)ELong.Value, reader.ReadInt64());
                Assert.Equal((long)EULong.Value, reader.ReadInt64());
            }
 
            Assert.Equal(_testNow, (DateTime)reader.ReadScalarValue());
        }
 
        public enum EByte : byte
        {
            Value = 1
        }
 
        public enum ESByte : sbyte
        {
            Value = 2
        }
 
        public enum EShort : short
        {
            Value = 3
        }
 
        public enum EUShort : ushort
        {
            Value = 4
        }
 
        public enum EInt : int
        {
            Value = 5
        }
 
        public enum EUInt : uint
        {
            Value = 6
        }
 
        public enum ELong : long
        {
            Value = 7
        }
 
        public enum EULong : ulong
        {
            Value = 8
        }
 
        [Fact]
        public void TestRoundTripCharacters()
        {
            // round trip all possible characters as a string
            for (int i = ushort.MinValue; i <= ushort.MaxValue; i++)
            {
                TestRoundTripChar((char)i);
            }
        }
 
        private static void TestRoundTripChar(Char ch)
        {
            TestRoundTrip(ch, (w, v) => w.WriteChar(v), r => r.ReadChar());
        }
 
        [Fact]
        public void TestRoundTripGuid()
        {
            static void test(Guid guid)
            {
                TestRoundTrip(guid, (w, v) => w.WriteGuid(v), r => r.ReadGuid());
            }
 
            test(Guid.Empty);
            test(new Guid(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1));
            test(new Guid(0b10000000000000000000000000000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1));
            test(new Guid(0b10000000000000000000000000000000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0));
            for (var i = 0; i < 10; i++)
            {
                test(Guid.NewGuid());
            }
        }
 
        [Fact]
        public void TestRoundTripStringCharacters()
        {
            // round trip all possible characters as a string
            for (int i = ushort.MinValue; i <= ushort.MaxValue; i++)
            {
                TestRoundTripStringCharacter((ushort)i);
            }
 
            // round trip single string with all possible characters
            var sb = new StringBuilder();
            for (int i = ushort.MinValue; i <= ushort.MaxValue; i++)
            {
                sb.Append((char)i);
            }
 
            TestRoundTripString(sb.ToString());
        }
 
        private static void TestRoundTripString(string text)
        {
            TestRoundTrip(text, (w, v) => w.WriteString(v), r => r.ReadString());
        }
 
        private static void TestRoundTripStringCharacter(ushort code)
        {
            TestRoundTripString(new String((char)code, 1));
        }
 
        private static void TestRoundTripArray<T>(T[] values)
        {
            TestRoundTripValue(values);
        }
 
        public static IEnumerable<object[]> GetEncodingTestCases()
            => EncodingTestHelpers.GetEncodingTestCases();
 
        [Theory]
        [MemberData(nameof(GetEncodingTestCases))]
        public void Encodings(Encoding original)
        {
            using var stream = new MemoryStream();
 
            using (var writer = new ObjectWriter(stream, leaveOpen: true))
            {
                writer.WriteEncoding(original);
            }
 
            stream.Position = 0;
 
            using var reader = ObjectReader.TryGetReader(stream);
            Assert.NotNull(reader);
            var deserialized = reader.ReadEncoding();
            EncodingTestHelpers.AssertEncodingsEqual(original, deserialized);
        }
 
        [Fact]
        public void TestMultipleAssetWritingAndReader()
        {
            using var stream = new MemoryStream();
 
            const string GooString = "Goo";
            const string BarString = "Bar";
            var largeString = new string('a', 1024);
 
            // Write out some initial bytes, to demonstrate the reader not throwing, even if we don't have the right
            // validation bytes at the start.
            stream.WriteByte(1);
            stream.WriteByte(2);
 
            using (var writer = new ObjectWriter(stream, leaveOpen: true, writeValidationBytes: false))
            {
                writer.WriteValidationBytes();
                writer.WriteString(GooString);
                writer.WriteString("Bar");
                writer.WriteString(largeString);
 
                // Random data, not going through the writer.
                stream.WriteByte(3);
                stream.WriteByte(4);
 
                // We should be able to write out a new object, using strings we've already seen.
                writer.WriteValidationBytes();
                writer.WriteString(largeString);
                writer.WriteString("Bar");
                writer.WriteString(GooString);
            }
 
            stream.Position = 0;
 
            using var reader = ObjectReader.GetReader(stream, leaveOpen: true, checkValidationBytes: false);
 
            Assert.Equal(1, reader.ReadByte());
            Assert.Equal(2, reader.ReadByte());
 
            reader.CheckValidationBytes();
 
            var string1 = reader.ReadString();
            var string2 = reader.ReadString();
            var string3 = reader.ReadString();
            Assert.Equal(GooString, string1);
            Assert.Equal(BarString, string2);
            Assert.Equal(largeString, string3);
            Assert.NotSame(GooString, string1);
            Assert.NotSame(BarString, string2);
            Assert.NotSame(largeString, string3);
 
            Assert.Equal(3, stream.ReadByte());
            Assert.Equal(4, stream.ReadByte());
 
            reader.CheckValidationBytes();
            var string4 = reader.ReadString();
            var string5 = reader.ReadString();
            var string6 = reader.ReadString();
            Assert.Equal(largeString, string4);
            Assert.Equal(BarString, string5);
            Assert.Equal(GooString, string6);
            Assert.NotSame(largeString, string4);
            Assert.NotSame(BarString, string5);
            Assert.NotSame(GooString, string6);
 
            // These should be references to the same strings in the format string and should return the values already
            // returned.
            Assert.Same(string1, string6);
            Assert.Same(string2, string5);
            Assert.Same(string3, string4);
        }
 
        // keep these around for analyzing perf issues
#if false
        [Fact]
        public void TestReaderPerf()
        {
            var iterations = 10000;
 
            var recTime = TestReaderPerf(iterations, recursive: true);
            var nonTime = TestReaderPerf(iterations, recursive: false);
 
            Console.WriteLine($"Recursive Time    : {recTime.TotalMilliseconds}");
            Console.WriteLine($"Non Recursive Time: {nonTime.TotalMilliseconds}");
        }
 
        [Fact]
        public void TestNonRecursiveReaderPerf()
        {
            var iterations = 10000;
            var nonTime = TestReaderPerf(iterations, recursive: false);
        }
 
        private TimeSpan TestReaderPerf(int iterations, bool recursive)
        {
            int id = 0;
            var graph = ConstructGraph(ref id, 5, 3);
 
            var stream = new MemoryStream();
            var binder = new RecordingObjectBinder();
            var writer = new StreamObjectWriter(stream, binder: binder, recursive: recursive);
 
            writer.WriteValue(graph);
            writer.Dispose();
 
            var start = DateTime.Now;
            for (int i = 0; i < iterations; i++)
            {
                stream.Position = 0;
                var reader = new StreamObjectReader(stream, binder: binder);
                var item = reader.ReadValue();
            }
 
            return DateTime.Now - start;
        }
#endif
    }
}
 
#pragma warning restore IDE0060 // Remove unused parameter