Browse Source

LegacyGeneratedCodeTest now passing

Jan Tattermusch 5 years ago
parent
commit
90d4969bd1

+ 213 - 0
csharp/src/Google.Protobuf.Test/Buffers/ArrayBufferWriter.cs

@@ -0,0 +1,213 @@
+#region Copyright notice and license
+// Protocol Buffers - Google's data interchange format
+// Copyright 2008 Google Inc.  All rights reserved.
+// https://developers.google.com/protocol-buffers/
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//     * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#endregion
+
+using System;
+using System.Buffers;
+using System.Diagnostics;
+
+namespace Google.Protobuf.Buffers
+{
+    /// <summary>
+    /// Represents a heap-based, array-backed output sink into which <typeparam name="T"/> data can be written.
+    /// 
+    /// ArrayBufferWriter is originally from corefx, and has been contributed to Protobuf
+    /// https://github.com/dotnet/runtime/blob/071da4c41aa808c949a773b92dca6f88de9d11f3/src/libraries/Common/src/System/Buffers/ArrayBufferWriter.cs
+    /// </summary>
+    internal sealed class ArrayBufferWriter<T> : IBufferWriter<T>
+    {
+        private T[] _buffer;
+        private int _index;
+
+        private const int DefaultInitialBufferSize = 256;
+
+        /// <summary>
+        /// Creates an instance of an <see cref="ArrayBufferWriter{T}"/>, in which data can be written to,
+        /// with the default initial capacity.
+        /// </summary>
+        public ArrayBufferWriter()
+        {
+            _buffer = new T[0];
+            _index = 0;
+        }
+
+        /// <summary>
+        /// Creates an instance of an <see cref="ArrayBufferWriter{T}"/>, in which data can be written to,
+        /// with an initial capacity specified.
+        /// </summary>
+        /// <param name="initialCapacity">The minimum capacity with which to initialize the underlying buffer.</param>
+        /// <exception cref="ArgumentException">
+        /// Thrown when <paramref name="initialCapacity"/> is not positive (i.e. less than or equal to 0).
+        /// </exception>
+        public ArrayBufferWriter(int initialCapacity)
+        {
+            if (initialCapacity <= 0)
+                throw new ArgumentException(nameof(initialCapacity));
+
+            _buffer = new T[initialCapacity];
+            _index = 0;
+        }
+
+        /// <summary>
+        /// Returns the data written to the underlying buffer so far, as a <see cref="ReadOnlyMemory{T}"/>.
+        /// </summary>
+        public ReadOnlyMemory<T> WrittenMemory => _buffer.AsMemory(0, _index);
+
+        /// <summary>
+        /// Returns the data written to the underlying buffer so far, as a <see cref="ReadOnlySpan{T}"/>.
+        /// </summary>
+        public ReadOnlySpan<T> WrittenSpan => _buffer.AsSpan(0, _index);
+
+        /// <summary>
+        /// Returns the amount of data written to the underlying buffer so far.
+        /// </summary>
+        public int WrittenCount => _index;
+
+        /// <summary>
+        /// Returns the total amount of space within the underlying buffer.
+        /// </summary>
+        public int Capacity => _buffer.Length;
+
+        /// <summary>
+        /// Returns the amount of space available that can still be written into without forcing the underlying buffer to grow.
+        /// </summary>
+        public int FreeCapacity => _buffer.Length - _index;
+
+        /// <summary>
+        /// Clears the data written to the underlying buffer.
+        /// </summary>
+        /// <remarks>
+        /// You must clear the <see cref="ArrayBufferWriter{T}"/> before trying to re-use it.
+        /// </remarks>
+        public void Clear()
+        {
+            Debug.Assert(_buffer.Length >= _index);
+            _buffer.AsSpan(0, _index).Clear();
+            _index = 0;
+        }
+
+        /// <summary>
+        /// Notifies <see cref="IBufferWriter{T}"/> that <paramref name="count"/> amount of data was written to the output <see cref="Span{T}"/>/<see cref="Memory{T}"/>
+        /// </summary>
+        /// <exception cref="ArgumentException">
+        /// Thrown when <paramref name="count"/> is negative.
+        /// </exception>
+        /// <exception cref="InvalidOperationException">
+        /// Thrown when attempting to advance past the end of the underlying buffer.
+        /// </exception>
+        /// <remarks>
+        /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer.
+        /// </remarks>
+        public void Advance(int count)
+        {
+            if (count < 0)
+                throw new ArgumentException(nameof(count));
+
+            if (_index > _buffer.Length - count)
+                throw new InvalidOperationException("Advanced past capacity.");
+
+            _index += count;
+        }
+
+        /// <summary>
+        /// Returns a <see cref="Memory{T}"/> to write to that is at least the requested length (specified by <paramref name="sizeHint"/>).
+        /// If no <paramref name="sizeHint"/> is provided (or it's equal to <code>0</code>), some non-empty buffer is returned.
+        /// </summary>
+        /// <exception cref="ArgumentException">
+        /// Thrown when <paramref name="sizeHint"/> is negative.
+        /// </exception>
+        /// <remarks>
+        /// This will never return an empty <see cref="Memory{T}"/>.
+        /// </remarks>
+        /// <remarks>
+        /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer.
+        /// </remarks>
+        /// <remarks>
+        /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer.
+        /// </remarks>
+        public Memory<T> GetMemory(int sizeHint = 0)
+        {
+            CheckAndResizeBuffer(sizeHint);
+            Debug.Assert(_buffer.Length > _index);
+            return _buffer.AsMemory(_index);
+        }
+
+        /// <summary>
+        /// Returns a <see cref="Span{T}"/> to write to that is at least the requested length (specified by <paramref name="sizeHint"/>).
+        /// If no <paramref name="sizeHint"/> is provided (or it's equal to <code>0</code>), some non-empty buffer is returned.
+        /// </summary>
+        /// <exception cref="ArgumentException">
+        /// Thrown when <paramref name="sizeHint"/> is negative.
+        /// </exception>
+        /// <remarks>
+        /// This will never return an empty <see cref="Span{T}"/>.
+        /// </remarks>
+        /// <remarks>
+        /// There is no guarantee that successive calls will return the same buffer or the same-sized buffer.
+        /// </remarks>
+        /// <remarks>
+        /// You must request a new buffer after calling Advance to continue writing more data and cannot write to a previously acquired buffer.
+        /// </remarks>
+        public Span<T> GetSpan(int sizeHint = 0)
+        {
+            CheckAndResizeBuffer(sizeHint);
+            Debug.Assert(_buffer.Length > _index);
+            return _buffer.AsSpan(_index);
+        }
+
+        private void CheckAndResizeBuffer(int sizeHint)
+        {
+            if (sizeHint < 0)
+                throw new ArgumentException(nameof(sizeHint));
+
+            if (sizeHint == 0)
+            {
+                sizeHint = 1;
+            }
+
+            if (sizeHint > FreeCapacity)
+            {
+                int growBy = Math.Max(sizeHint, _buffer.Length);
+
+                if (_buffer.Length == 0)
+                {
+                    growBy = Math.Max(growBy, DefaultInitialBufferSize);
+                }
+
+                int newSize = checked(_buffer.Length + growBy);
+
+                Array.Resize(ref _buffer, newSize);
+            }
+
+            Debug.Assert(FreeCapacity > 0 && FreeCapacity >= sizeHint);
+        }
+    }
+}

+ 82 - 23
csharp/src/Google.Protobuf.Test/LegacyGeneratedCodeTest.cs

@@ -35,7 +35,9 @@ using System.Buffers;
 using pb = global::Google.Protobuf;
 using pbr = global::Google.Protobuf.Reflection;
 using NUnit.Framework;
-
+using System.IO;
+using System;
+using Google.Protobuf.Buffers;
 
 namespace Google.Protobuf
 {
@@ -46,14 +48,14 @@ namespace Google.Protobuf
         {
             var message = new ParseContextEnabledMessageB
             {
-              A = new LegacyGeneratedCodeMessageA
-              {
-                Bb = new ParseContextEnabledMessageB { OptionalInt32 = 12345 }
-              },
-              OptionalInt32 = 6789
+                A = new LegacyGeneratedCodeMessageA
+                {
+                    Bb = new ParseContextEnabledMessageB { OptionalInt32 = 12345 }
+                },
+                OptionalInt32 = 6789
             };
             var data = message.ToByteArray();
-            
+
             // when parsing started using CodedInputStream and a message with legacy generated code
             // is encountered somewhere in the parse tree, we still need to be able to use its
             // MergeFrom(CodedInputStream) method to parse correctly.
@@ -71,11 +73,11 @@ namespace Google.Protobuf
         {
             var message = new ParseContextEnabledMessageB
             {
-              A = new LegacyGeneratedCodeMessageA
-              {
-                Bb = new ParseContextEnabledMessageB { OptionalInt32 = 12345 }
-              },
-              OptionalInt32 = 6789
+                A = new LegacyGeneratedCodeMessageA
+                {
+                    Bb = new ParseContextEnabledMessageB { OptionalInt32 = 12345 }
+                },
+                OptionalInt32 = 6789
             };
             var data = message.ToByteArray();
 
@@ -86,13 +88,65 @@ namespace Google.Protobuf
             // code up to date.
             var exception = Assert.Throws<InvalidProtocolBufferException>(() =>
             {
-              ParseContext.Initialize(new ReadOnlySequence<byte>(data), out ParseContext parseCtx);
-              var parsed = new ParseContextEnabledMessageB();
-              ParsingPrimitivesMessages.ReadRawMessage(ref parseCtx, parsed);
+                ParseContext.Initialize(new ReadOnlySequence<byte>(data), out ParseContext parseCtx);
+                var parsed = new ParseContextEnabledMessageB();
+                ParsingPrimitivesMessages.ReadRawMessage(ref parseCtx, parsed);
             });
             Assert.AreEqual($"Message {typeof(LegacyGeneratedCodeMessageA).Name} doesn't provide the generated method that enables ParseContext-based parsing. You might need to regenerate the generated protobuf code.", exception.Message);
         }
 
+        [Test]
+        public void IntermixingOfNewAndLegacyGeneratedCodeWorksWithCodedOutputStream()
+        {
+            // when serialization started using CodedOutputStream and a message with legacy generated code
+            // is encountered somewhere in the parse tree, we still need to be able to use its
+            // WriteTo(CodedOutputStream) method to serialize correctly.
+            var ms = new MemoryStream();
+            var codedOutput = new CodedOutputStream(ms);
+            var message = new ParseContextEnabledMessageB
+            {
+                A = new LegacyGeneratedCodeMessageA
+                {
+                    Bb = new ParseContextEnabledMessageB { OptionalInt32 = 12345 }
+                },
+                OptionalInt32 = 6789
+            };
+            message.WriteTo(codedOutput);
+            codedOutput.Flush();
+
+            var codedInput = new CodedInputStream(ms.ToArray());
+            var parsed = new ParseContextEnabledMessageB();
+            codedInput.ReadRawMessage(parsed);
+            Assert.IsTrue(codedInput.IsAtEnd);
+
+            Assert.AreEqual(12345, parsed.A.Bb.OptionalInt32);
+            Assert.AreEqual(6789, parsed.OptionalInt32);
+        }
+
+        [Test]
+        public void LegacyGeneratedCodeThrowsWithIBufferWriter()
+        {
+            // if serialization started using IBufferWriter and we don't have a CodedOutputStream
+            // instance at hand, we cannot fall back to the legacy WriteTo(CodedOutputStream)
+            // method and serializatin will fail. As a consequence, one can only use serialization
+            // to IBufferWriter if all the messages in the parsing tree have their generated
+            // code up to date.
+            var message = new ParseContextEnabledMessageB
+            {
+                A = new LegacyGeneratedCodeMessageA
+                {
+                    Bb = new ParseContextEnabledMessageB { OptionalInt32 = 12345 }
+                },
+                OptionalInt32 = 6789
+            };
+            var exception = Assert.Throws<InvalidProtocolBufferException>(() =>
+            {
+                WriteContext.Initialize(new ArrayBufferWriter<byte>(), out WriteContext writeCtx);
+                ((IBufferMessage)message).InternalWriteTo(ref writeCtx);
+            });
+            Assert.AreEqual($"Message {typeof(LegacyGeneratedCodeMessageA).Name} doesn't provide the generated method that enables WriteContext-based serialization. You might need to regenerate the generated protobuf code.", exception.Message);
+        }
+
         // hand-modified version of a generated message that only provides the legacy
         // MergeFrom(CodedInputStream) method and doesn't implement IBufferMessage.
         private sealed partial class LegacyGeneratedCodeMessageA : pb::IMessage {
@@ -178,18 +232,27 @@ namespace Google.Protobuf
           }
 
           public void WriteTo(pb::CodedOutputStream output) {
-            if (a_ != null) {
+            output.WriteRawMessage(this);
+          }
+
+          void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output)
+          {
+            if (a_ != null)
+            {
               output.WriteRawTag(10);
               output.WriteMessage(A);
             }
-            if (OptionalInt32 != 0) {
+            if (OptionalInt32 != 0)
+            {
               output.WriteRawTag(16);
               output.WriteInt32(OptionalInt32);
             }
-            if (_unknownFields != null) {
-              _unknownFields.WriteTo(output);
+            if (_unknownFields != null)
+            {
+              _unknownFields.WriteTo(ref output);
             }
           }
+
           public int CalculateSize() {
             int size = 0;
             if (a_ != null) {
@@ -228,10 +291,6 @@ namespace Google.Protobuf
               }
             }
           }
-
-          void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
-            // TODO: implement this, add tests!!!
-          }
         }
     }
 }