blob: 00efabff0b61bdb97469f6221096d5fa660d2439 [file] [log] [blame]
#region Copyright notice and license
// Protocol Buffers - Google's data interchange format
// Copyright 2015 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#endregion
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using NUnit.Framework;
namespace Google.Protobuf.Collections;
public class UnsafeCollectionOperationsTest
{
[Test]
public void NullFieldAsSpanValueType()
{
RepeatedField<int> field = null;
Assert.Throws<ArgumentNullException>(() => UnsafeCollectionOperations.AsSpan(field));
}
[Test]
public void NullFieldAsSpanClass()
{
RepeatedField<object> field = null;
Assert.Throws<ArgumentNullException>(() => UnsafeCollectionOperations.AsSpan(field));
}
[Test]
public void FieldAsSpanValueType()
{
var field = new RepeatedField<int>();
foreach (var length in Enumerable.Range(0, 36))
{
field.Clear();
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
for (var i = 0; i < length; i++)
{
field.Add(i);
}
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
field.Add(length + 1);
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
}
static void ValidateContentEquality(RepeatedField<int> field, Span<int> span)
{
Assert.AreEqual(field.Count, span.Length);
for (var i = 0; i < span.Length; i++)
{
Assert.AreEqual(field[i], span[i]);
}
}
}
[Test]
public void FieldAsSpanClass()
{
var field = new RepeatedField<IntAsObject>();
foreach (var length in Enumerable.Range(0, 36))
{
field.Clear();
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
for (var i = 0; i < length; i++)
{
field.Add(new IntAsObject { Value = i });
}
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
field.Add(new IntAsObject { Value = length + 1 });
ValidateContentEquality(field, UnsafeCollectionOperations.AsSpan(field));
}
static void ValidateContentEquality(
RepeatedField<IntAsObject> field,
Span<IntAsObject> span)
{
Assert.AreEqual(field.Count, span.Length);
for (var i = 0; i < span.Length; i++)
{
Assert.AreEqual(field[i].Value, span[i].Value);
}
}
}
[Test]
public void FieldAsSpanLinkBreaksOnResize()
{
var field = new RepeatedField<int>();
for (var i = 0; i < 8; i++)
{
field.Add(i);
}
var span = UnsafeCollectionOperations.AsSpan(field);
var startCapacity = field.Capacity;
var startCount = field.Count;
Assert.AreEqual(startCount, startCapacity);
Assert.AreEqual(startCount, span.Length);
for (var i = 0; i < span.Length; i++)
{
span[i]++;
Assert.AreEqual(field[i], span[i]);
field[i]++;
Assert.AreEqual(field[i], span[i]);
}
// Resize to break link between Span and RepeatedField
field.Add(11);
Assert.AreNotEqual(startCapacity, field.Capacity);
Assert.AreNotEqual(startCount, field.Count);
Assert.AreEqual(startCount, span.Length);
for (var i = 0; i < span.Length; i++)
{
span[i] += 2;
Assert.AreNotEqual(field[i], span[i]);
field[i] += 3;
Assert.AreNotEqual(field[i], span[i]);
}
}
[Test]
public void FieldSetCount()
{
RepeatedField<int> field = null;
Assert.Throws<ArgumentNullException>(() => UnsafeCollectionOperations.SetCount(field, 3));
field = new RepeatedField<int>();
Assert.Throws<ArgumentOutOfRangeException>(()
=> UnsafeCollectionOperations.SetCount(field, -1));
UnsafeCollectionOperations.SetCount(field, 5);
Assert.AreEqual(5, field.Count);
field = new RepeatedField<int> { 1, 2, 3, 4, 5 };
ref var intRef = ref MemoryMarshal.GetReference(UnsafeCollectionOperations.AsSpan(field));
// make sure that size decrease preserves content
UnsafeCollectionOperations.SetCount(field, 3);
Assert.AreEqual(3, field.Count);
Assert.Throws<ArgumentOutOfRangeException>(() => field[3] = 42);
var span = UnsafeCollectionOperations.AsSpan(field);
SequenceEqual(span, new[] { 1, 2, 3 });
Assert.True(Unsafe.AreSame(ref intRef, ref MemoryMarshal.GetReference(span)));
// make sure that size increase preserves content and doesn't clear
UnsafeCollectionOperations.SetCount(field, 5);
span = UnsafeCollectionOperations.AsSpan(field);
// .NET Framework always clears values. .NET 6+ only clears references.
var expected =
#if NET5_0_OR_GREATER
new[] { 1, 2, 3, 4, 5 };
#else
new[] { 1, 2, 3, 0, 0 };
#endif
SequenceEqual(span, expected);
Assert.True(Unsafe.AreSame(ref intRef, ref MemoryMarshal.GetReference(span)));
// make sure that reallocations preserve content
var newCount = field.Capacity * 2;
UnsafeCollectionOperations.SetCount(field, newCount);
Assert.AreEqual(newCount, field.Count);
span = UnsafeCollectionOperations.AsSpan(field);
SequenceEqual(span.Slice(0, 3), new[] { 1, 2, 3 });
Assert.True(!Unsafe.AreSame(ref intRef, ref MemoryMarshal.GetReference(span)));
RepeatedField<string> listReference = new() { "a", "b", "c", "d", "e" };
var listSpan = UnsafeCollectionOperations.AsSpan(listReference);
ref var stringRef = ref MemoryMarshal.GetReference(listSpan);
UnsafeCollectionOperations.SetCount(listReference, 3);
// verify that reference types aren't cleared
listSpan = UnsafeCollectionOperations.AsSpan(listReference);
SequenceEqual(listSpan, new[] { "a", "b", "c" });
Assert.True(Unsafe.AreSame(ref stringRef, ref MemoryMarshal.GetReference(listSpan)));
UnsafeCollectionOperations.SetCount(listReference, 5);
// verify that removed reference types are cleared
listSpan = UnsafeCollectionOperations.AsSpan(listReference);
SequenceEqual(listSpan, new[] { "a", "b", "c", null, null });
Assert.True(Unsafe.AreSame(ref stringRef, ref MemoryMarshal.GetReference(listSpan)));
}
private static void SequenceEqual<T>(Span<T> span, Span<T> expected)
{
Assert.AreEqual(expected.Length, span.Length);
for (var i = 0; i < expected.Length; i++)
{
Assert.AreEqual(expected[i], span[i]);
}
}
private class IntAsObject
{
public int Value;
}
}