Skip to content

Commit

Permalink
LegacyGeneratedCodeTest now passing
Browse files Browse the repository at this point in the history
  • Loading branch information
jtattermusch committed Jun 2, 2020
1 parent 361c933 commit 90d4969
Show file tree
Hide file tree
Showing 2 changed files with 295 additions and 23 deletions.
213 changes: 213 additions & 0 deletions 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);
}
}
}
105 changes: 82 additions & 23 deletions csharp/src/Google.Protobuf.Test/LegacyGeneratedCodeTest.cs
Expand Up @@ -35,7 +35,9 @@
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
{
Expand All @@ -46,14 +48,14 @@ public void IntermixingOfNewAndLegacyGeneratedCodeWorksWithCodedInputStream()
{
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.
Expand All @@ -71,11 +73,11 @@ public void LegacyGeneratedCodeThrowsWithReadOnlySequence()
{
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();

Expand All @@ -86,13 +88,65 @@ public void LegacyGeneratedCodeThrowsWithReadOnlySequence()
// 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 {
Expand Down Expand Up @@ -178,18 +232,27 @@ private sealed partial class ParseContextEnabledMessageB : pb::IBufferMessage {
}

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) {
Expand Down Expand Up @@ -228,10 +291,6 @@ private sealed partial class ParseContextEnabledMessageB : pb::IBufferMessage {
}
}
}

void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {
// TODO: implement this, add tests!!!
}
}
}
}

0 comments on commit 90d4969

Please sign in to comment.