public void Write_WithPartialData()
        {
            MemoryStream innerStream       = new MemoryStream();
            Stream       transcodingStream = Encoding.CreateTranscodingStream(
                innerStream,
                innerStreamEncoding: CustomAsciiEncoding /* performs custom substitution */,
                outerStreamEncoding: Encoding.UTF8 /* performs U+FFFD substition */,
                leaveOpen: true);

            // First, write some incomplete data

            transcodingStream.Write(new byte[] { 0x78, 0x79, 0x7A, 0xC3 }); // [C3] shouldn't be flushed yet
            Assert.Equal("xyz", ErrorCheckingAsciiEncoding.GetString(innerStream.ToArray()));

            // Flushing should have no effect

            transcodingStream.Flush();
            Assert.Equal("xyz", ErrorCheckingAsciiEncoding.GetString(innerStream.ToArray()));

            // Provide the second byte of the multi-byte sequence

            transcodingStream.WriteByte(0xA0); // [C3 A0] = U+00E0
            Assert.Equal("xyz[00E0]", ErrorCheckingAsciiEncoding.GetString(innerStream.ToArray()));

            // Provide an incomplete sequence, then close the stream.
            // Closing the stream should flush the underlying buffers and write the replacement char.

            transcodingStream.Write(new byte[] { 0xE0, 0xBF });                                     // first 2 bytes of incomplete 3-byte sequence
            Assert.Equal("xyz[00E0]", ErrorCheckingAsciiEncoding.GetString(innerStream.ToArray())); // wasn't flushed yet

            transcodingStream.Close();
            Assert.Equal("xyz[00E0][FFFD]", ErrorCheckingAsciiEncoding.GetString(innerStream.ToArray()));
        }
        private void RunReadTest(Func <Stream, MemoryStream, int> callback)
        {
            MemoryStream sink = new MemoryStream();

            MemoryStream innerStream       = new MemoryStream();
            Stream       transcodingStream = Encoding.CreateTranscodingStream(innerStream,
                                                                              innerStreamEncoding: Encoding.UTF8,
                                                                              outerStreamEncoding: CustomAsciiEncoding);

            // Test with a small string, then test with a large string

            RunOneTestIteration(128);
            RunOneTestIteration(10 * 1024 * 1024);

            Assert.Equal(-1, transcodingStream.ReadByte()); // should've reached EOF

            // Now put some invalid data into the inner stream as EOF.

            innerStream.SetLength(0); // reset
            innerStream.WriteByte(0xC0);
            innerStream.Position = 0;

            sink.SetLength(0); // reset
            int numBytesReadJustNow;

            do
            {
                numBytesReadJustNow = callback(transcodingStream, sink);
                Assert.True(numBytesReadJustNow >= 0);
            } while (numBytesReadJustNow > 0);

            Assert.Equal("[FFFD]", ErrorCheckingAsciiEncoding.GetString(sink.ToArray()));
            Assert.Equal(-1, transcodingStream.ReadByte()); // should've reached EOF

            void RunOneTestIteration(int stringLength)
            {
                sink.SetLength(0); // reset

                string expectedStringContents = GetVeryLongAsciiString(stringLength);

                innerStream.SetLength(0); // reset
                innerStream.Write(Encoding.UTF8.GetBytes(expectedStringContents));
                innerStream.Position = 0;

                int numBytesReadJustNow;

                do
                {
                    numBytesReadJustNow = callback(transcodingStream, sink);
                    Assert.True(numBytesReadJustNow >= 0);
                } while (numBytesReadJustNow > 0);

                Assert.Equal(expectedStringContents, ErrorCheckingAsciiEncoding.GetString(sink.ToArray()));
            }
        }
        public async Task WriteAsync_WithPartialData()
        {
            MemoryStream      sink = new MemoryStream();
            CancellationToken expectedCancellationToken = new CancellationTokenSource().Token;

            var innerStreamMock = new Mock <Stream>(MockBehavior.Strict);

            innerStreamMock.Setup(o => o.CanWrite).Returns(true);
            innerStreamMock.Setup(o => o.WriteAsync(It.IsAny <ReadOnlyMemory <byte> >(), expectedCancellationToken))
            .Returns <ReadOnlyMemory <byte>, CancellationToken>(sink.WriteAsync);

            Stream transcodingStream = Encoding.CreateTranscodingStream(
                innerStreamMock.Object,
                innerStreamEncoding: CustomAsciiEncoding /* performs custom substitution */,
                outerStreamEncoding: Encoding.UTF8 /* performs U+FFFD substition */,
                leaveOpen: true);

            // First, write some incomplete data

            await transcodingStream.WriteAsync(new byte[] { 0x78, 0x79, 0x7A, 0xC3 }, expectedCancellationToken); // [C3] shouldn't be flushed yet

            Assert.Equal("xyz", ErrorCheckingAsciiEncoding.GetString(sink.ToArray()));

            // Provide the second byte of the multi-byte sequence

            await transcodingStream.WriteAsync(new byte[] { 0xA0 }, expectedCancellationToken); // [C3 A0] = U+00E0

            Assert.Equal("xyz[00E0]", ErrorCheckingAsciiEncoding.GetString(sink.ToArray()));

            // Provide an incomplete sequence, then close the stream.
            // Closing the stream should flush the underlying buffers and write the replacement char.

            await transcodingStream.WriteAsync(new byte[] { 0xE0, 0xBF }, expectedCancellationToken); // first 2 bytes of incomplete 3-byte sequence

            Assert.Equal("xyz[00E0]", ErrorCheckingAsciiEncoding.GetString(sink.ToArray()));          // wasn't flushed yet

            // The call to DisposeAsync() will call innerStream.WriteAsync without a CancellationToken.

            innerStreamMock.Setup(o => o.WriteAsync(It.IsAny <ReadOnlyMemory <byte> >(), CancellationToken.None))
            .Returns <ReadOnlyMemory <byte>, CancellationToken>(sink.WriteAsync);

            await transcodingStream.DisposeAsync();

            Assert.Equal("xyz[00E0][FFFD]", ErrorCheckingAsciiEncoding.GetString(sink.ToArray()));
        }
        private async Task RunReadTestAsync(Func <Stream, CancellationToken, MemoryStream, ValueTask <int> > callback, bool suppressExpectedCancellationTokenAsserts = false)
        {
            CancellationToken expectedCancellationToken = new CancellationTokenSource().Token;
            MemoryStream      sink        = new MemoryStream();
            MemoryStream      innerStream = new MemoryStream();

            var delegatingInnerStreamMock = new Mock <Stream>(MockBehavior.Strict);

            delegatingInnerStreamMock.Setup(o => o.CanRead).Returns(true);

            if (suppressExpectedCancellationTokenAsserts)
            {
                delegatingInnerStreamMock.Setup(o => o.ReadAsync(It.IsAny <Memory <byte> >(), It.IsAny <CancellationToken>()))
                .Returns <Memory <byte>, CancellationToken>(innerStream.ReadAsync);
            }
            else
            {
                delegatingInnerStreamMock.Setup(o => o.ReadAsync(It.IsAny <Memory <byte> >(), expectedCancellationToken))
                .Returns <Memory <byte>, CancellationToken>(innerStream.ReadAsync);
            }

            Stream transcodingStream = Encoding.CreateTranscodingStream(
                innerStream: delegatingInnerStreamMock.Object,
                innerStreamEncoding: Encoding.UTF8,
                outerStreamEncoding: CustomAsciiEncoding);

            // Test with a small string, then test with a large string

            await RunOneTestIteration(128);
            await RunOneTestIteration(10 * 1024 * 1024);

            Assert.Equal(-1, await transcodingStream.ReadByteAsync(expectedCancellationToken)); // should've reached EOF

            // Now put some invalid data into the inner stream as EOF.

            innerStream.SetLength(0); // reset
            innerStream.WriteByte(0xC0);
            innerStream.Position = 0;

            sink.SetLength(0); // reset
            int numBytesReadJustNow;

            do
            {
                numBytesReadJustNow = await callback(transcodingStream, expectedCancellationToken, sink);

                Assert.True(numBytesReadJustNow >= 0);
            } while (numBytesReadJustNow > 0);

            Assert.Equal("[FFFD]", ErrorCheckingAsciiEncoding.GetString(sink.ToArray()));
            Assert.Equal(-1, await transcodingStream.ReadByteAsync(expectedCancellationToken)); // should've reached EOF

            async Task RunOneTestIteration(int stringLength)
            {
                sink.SetLength(0); // reset

                string expectedStringContents = GetVeryLongAsciiString(stringLength);

                innerStream.SetLength(0); // reset
                innerStream.Write(Encoding.UTF8.GetBytes(expectedStringContents));
                innerStream.Position = 0;

                int numBytesReadJustNow;

                do
                {
                    numBytesReadJustNow = await callback(transcodingStream, expectedCancellationToken, sink);

                    Assert.True(numBytesReadJustNow >= 0);
                } while (numBytesReadJustNow > 0);

                Assert.Equal(expectedStringContents, ErrorCheckingAsciiEncoding.GetString(sink.ToArray()));
            }
        }