| #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; |
| } |
| } |