using NUnit.Framework; using System; using Unity.Burst; using Unity.Collections; using Unity.Collections.Tests; using Unity.Collections.LowLevel.Unsafe; using Unity.Jobs; internal class UnsafeListTests : CollectionsTestCommonBase { [Test] public void UnsafeListT_Init() { var container = new UnsafeList(0, Allocator.Persistent, NativeArrayOptions.ClearMemory); Assert.True(container.IsCreated); Assert.True(container.IsEmpty); Assert.DoesNotThrow(() => container.Dispose()); } [Test] public unsafe void UnsafeListT_Init_ClearMemory() { var list = new UnsafeList(10, Allocator.Persistent, NativeArrayOptions.ClearMemory); for (var i = 0; i < list.Length; ++i) { Assert.AreEqual(0, UnsafeUtility.ReadArrayElement(list.Ptr, i)); } list.Dispose(); } [Test] public unsafe void UnsafeListT_Allocate_Deallocate_Read_Write() { var list = new UnsafeList(0, Allocator.Persistent); Assert.True(list.IsCreated); Assert.True(list.IsEmpty); list.Add(1); list.Add(2); Assert.AreEqual(2, list.Length); Assert.AreEqual(1, UnsafeUtility.ReadArrayElement(list.Ptr, 0)); Assert.AreEqual(2, UnsafeUtility.ReadArrayElement(list.Ptr, 1)); list.Dispose(); } [Test] public unsafe void UnsafeListT_Resize_ClearMemory() { var list = new UnsafeList(5, Allocator.Persistent, NativeArrayOptions.ClearMemory); list.SetCapacity(32); var capacity = list.Capacity; list.Resize(5, NativeArrayOptions.UninitializedMemory); Assert.AreEqual(capacity, list.Capacity); // list capacity should not change on resize for (var i = 0; i < 5; ++i) { UnsafeUtility.WriteArrayElement(list.Ptr, i, i); } list.Resize(10, NativeArrayOptions.ClearMemory); Assert.AreEqual(capacity, list.Capacity); // list capacity should not change on resize for (var i = 0; i < 5; ++i) { Assert.AreEqual(i, UnsafeUtility.ReadArrayElement(list.Ptr, i)); } for (var i = 5; i < list.Length; ++i) { Assert.AreEqual(0, UnsafeUtility.ReadArrayElement(list.Ptr, i)); } list.Dispose(); } [Test] public unsafe void UnsafeListT_Resize_Zero() { var list = new UnsafeList(5, Allocator.Persistent, NativeArrayOptions.ClearMemory); var capacity = list.Capacity; list.Add(1); list.Resize(0); Assert.AreEqual(0, list.Length); Assert.AreEqual(capacity, list.Capacity); // list capacity should not change on resize list.Add(2); list.Clear(); Assert.AreEqual(0, list.Length); Assert.AreEqual(capacity, list.Capacity); // list capacity should not change on resize list.Dispose(); } [Test] public unsafe void UnsafeListT_SetCapacity() { using (var list = new UnsafeList(1, Allocator.Persistent, NativeArrayOptions.ClearMemory)) { list.Add(1); Assert.DoesNotThrow(() => list.SetCapacity(128)); list.Add(1); Assert.AreEqual(2, list.Length); #if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG Assert.Throws(() => list.SetCapacity(1)); #endif list.RemoveAtSwapBack(0); Assert.AreEqual(1, list.Length); Assert.DoesNotThrow(() => list.SetCapacity(1)); list.TrimExcess(); Assert.AreEqual(1, list.Capacity); } } [Test] public unsafe void UnsafeListT_TrimExcess() { using (var list = new UnsafeList(32, Allocator.Persistent, NativeArrayOptions.ClearMemory)) { list.Add(1); list.TrimExcess(); Assert.AreEqual(1, list.Length); Assert.AreEqual(1, list.Capacity); list.RemoveAtSwapBack(0); Assert.AreEqual(list.Length, 0); list.TrimExcess(); Assert.AreEqual(list.Capacity, 0); list.Add(1); Assert.AreEqual(list.Length, 1); Assert.AreNotEqual(list.Capacity, 0); list.Clear(); } } [Test] public unsafe void UnsafeListT_DisposeJob() { var list = new UnsafeList(5, Allocator.Persistent, NativeArrayOptions.ClearMemory); var disposeJob = list.Dispose(default); Assert.IsTrue(list.Ptr == null); disposeJob.Complete(); } unsafe void Expected(ref UnsafeList list, int expectedLength, int[] expected) { Assert.AreEqual(0 == expectedLength, list.IsEmpty); Assert.AreEqual(list.Length, expectedLength); for (var i = 0; i < list.Length; ++i) { var value = UnsafeUtility.ReadArrayElement(list.Ptr, i); Assert.AreEqual(expected[i], value); } } [Test] public void UnsafeListT_AddReplicate() { using (var list = new UnsafeList(32, Allocator.Persistent)) { list.AddReplicate(value: 42, count: 10); Assert.AreEqual(10, list.Length); foreach (var item in list) Assert.AreEqual(42, item); list.AddReplicate(value: 42, count: 100); Assert.AreEqual(110, list.Length); foreach (var item in list) Assert.AreEqual(42, item); } } [Test] public unsafe void UnsafeListT_AddNoResize() { var container = new UnsafeList(1, Allocator.Persistent, NativeArrayOptions.ClearMemory); // List's capacity is always cache-line aligned, number of items fills up whole cache-line. int[] range = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; #if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG Assert.Throws(() => { fixed (int* r = range) container.AddRangeNoResize(r, 17); }); #endif container.SetCapacity(17); Assert.DoesNotThrow(() => { fixed (int* r = range) container.AddRangeNoResize(r, 17); }); container.Length = 16; container.TrimExcess(); #if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG Assert.Throws(() => { container.AddNoResize(16); }); #endif container.Dispose(); } [Test] public unsafe void UnsafeListT_AddNoResize_Read() { var container = new UnsafeList(4, Allocator.Persistent, NativeArrayOptions.ClearMemory); container.AddNoResize(4); container.AddNoResize(6); container.AddNoResize(4); container.AddNoResize(9); Expected(ref container, 4, new int[] { 4, 6, 4, 9 }); container.Dispose(); } [Test] public unsafe void UnsafeListT_RemoveAtSwapBack() { var list = new UnsafeList(10, Allocator.Persistent, NativeArrayOptions.ClearMemory); int[] range = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // test removing from the end fixed (int* r = range) list.AddRange(r, 10); list.RemoveAtSwapBack(list.Length - 1); Expected(ref list, 9, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }); list.Clear(); // test removing from the end fixed (int* r = range) list.AddRange(r, 10); list.RemoveAtSwapBack(5); Expected(ref list, 9, new int[] { 0, 1, 2, 3, 4, 9, 6, 7, 8 }); list.Clear(); list.Dispose(); } [Test] public unsafe void UnsafeListT_RemoveRangeSwapBackBE() { var list = new UnsafeList(10, Allocator.Persistent, NativeArrayOptions.ClearMemory); int[] range = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // test removing from the end fixed (int* r = range) list.AddRange(r, 10); list.RemoveRangeSwapBack(6, 3); Expected(ref list, 7, new int[] { 0, 1, 2, 3, 4, 5, 9 }); list.Clear(); // test removing all but one fixed (int* r = range) list.AddRange(r, 10); list.RemoveRangeSwapBack(0, 9); Expected(ref list, 1, new int[] { 9 }); list.Clear(); // test removing from the front fixed (int* r = range) list.AddRange(r, 10); list.RemoveRangeSwapBack(0, 3); Expected(ref list, 7, new int[] { 7, 8, 9, 3, 4, 5, 6 }); list.Clear(); // test removing from the middle fixed (int* r = range) list.AddRange(r, 10); list.RemoveRangeSwapBack(0, 3); Expected(ref list, 7, new int[] { 7, 8, 9, 3, 4, 5, 6 }); list.Clear(); // test removing whole range fixed (int* r = range) list.AddRange(r, 10); list.RemoveRangeSwapBack(0, 10); Expected(ref list, 0, new int[] { 0 }); list.Clear(); list.Dispose(); } [Test] public unsafe void UnsafeListT_RemoveAt() { var list = new UnsafeList(10, Allocator.Persistent, NativeArrayOptions.ClearMemory); int[] range = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // test removing from the end fixed (int* r = range) list.AddRange(r, 10); list.RemoveAt(list.Length - 1); Expected(ref list, 9, new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 }); list.Clear(); // test removing from the end fixed (int* r = range) list.AddRange(r, 10); list.RemoveAt(5); Expected(ref list, 9, new int[] { 0, 1, 2, 3, 4, 6, 7, 8, 9 }); list.Clear(); list.Dispose(); } [Test] public unsafe void UnsafeListT_RemoveRange() { var list = new UnsafeList(10, Allocator.Persistent, NativeArrayOptions.ClearMemory); int[] range = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; // test removing from the end fixed (int* r = range) list.AddRange(r, 10); list.RemoveRange(6, 3); Expected(ref list, 7, new int[] { 0, 1, 2, 3, 4, 5, 9 }); list.Clear(); // test removing all but one fixed (int* r = range) list.AddRange(r, 10); list.RemoveRange(0, 9); Expected(ref list, 1, new int[] { 9 }); list.Clear(); // test removing from the front fixed (int* r = range) list.AddRange(r, 10); list.RemoveRange(0, 3); Expected(ref list, 7, new int[] { 3, 4, 5, 6, 7, 8, 9 }); list.Clear(); // test removing from the middle fixed (int* r = range) list.AddRange(r, 10); list.RemoveRange(0, 3); Expected(ref list, 7, new int[] { 3, 4, 5, 6, 7, 8, 9 }); list.Clear(); // test removing whole range fixed (int* r = range) list.AddRange(r, 10); list.RemoveRange(0, 10); Expected(ref list, 0, new int[] { 0 }); list.Clear(); // Test removing at the end of the list with a zero length. // This simply must not throw. list.RemoveRange(list.Length, 0); list.Dispose(); } [Test] [TestRequiresDotsDebugOrCollectionChecks] public unsafe void UnsafeListT_Remove_Throws() { var list = new UnsafeList(10, Allocator.Persistent, NativeArrayOptions.ClearMemory); Assert.Throws(() => { list.RemoveAt(0); }); Assert.AreEqual(0, list.Length); Assert.Throws(() => { list.RemoveAtSwapBack(0); }); Assert.AreEqual(0, list.Length); int[] range = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; fixed (int* r = range) list.AddRange(r, 10); Assert.Throws(() => { list.RemoveAt(100); }); Assert.AreEqual(10, list.Length); Assert.Throws(() => { list.RemoveAtSwapBack(100); }); Assert.AreEqual(10, list.Length); Assert.Throws(() => { list.RemoveRange(0, 100); }); Assert.AreEqual(10, list.Length); Assert.Throws(() => { list.RemoveRangeSwapBack(0, 100); }); Assert.AreEqual(10, list.Length); Assert.Throws(() => { list.RemoveRange(100, -1); }); Assert.AreEqual(10, list.Length); Assert.Throws(() => { list.RemoveRangeSwapBack(100, -1); }); Assert.AreEqual(10, list.Length); list.Dispose(); } [Test] public unsafe void UnsafeListT_PtrLength() { var list = new UnsafeList(10, Allocator.Persistent, NativeArrayOptions.ClearMemory); int[] range = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; fixed (int* r = range) list.AddRange(r, 10); var listView = new UnsafeList(list.Ptr + 4, 2); Expected(ref listView, 2, new int[] { 4, 5 }); listView.Dispose(); list.Dispose(); } // Burst error BC1071: Unsupported assert type // [BurstCompile(CompileSynchronously = true)] struct UnsafeListAsReadOnly : IJob { public UnsafeList.ReadOnly list; public void Execute() { Assert.True(list.Contains(123)); } } [Test] public void UnsafeListT_AsReadOnly() { var list = new UnsafeList(10, Allocator.Persistent, NativeArrayOptions.ClearMemory); list.Add(123); var job = new UnsafeListAsReadOnly { list = list.AsReadOnly(), }; list.Dispose(job.Schedule()).Complete(); } [BurstCompile(CompileSynchronously = true)] struct UnsafeListParallelWriter : IJobParallelFor { public UnsafeList.ParallelWriter list; public void Execute(int index) { list.AddNoResize(index); } } [Test] public void UnsafeListT_ParallelWriter() { var list = new UnsafeList(256, Allocator.Persistent, NativeArrayOptions.ClearMemory); var job = new UnsafeListParallelWriter { list = list.AsParallelWriter(), }; job.Schedule(list.Capacity, 1).Complete(); Assert.AreEqual(list.Length, list.Capacity); list.Sort(); for (int i = 0; i < list.Length; i++) { unsafe { var value = UnsafeUtility.ReadArrayElement(list.Ptr, i); Assert.AreEqual(i, value); } } list.Dispose(); } [BurstCompile(CompileSynchronously = true)] struct UnsafeListTestParallelWriter : IJob { [WriteOnly] public UnsafeList.ParallelWriter writer; public unsafe void Execute() { var range = stackalloc int[2] { 7, 3 }; writer.AddNoResize(range[0]); writer.AddRangeNoResize(range, 1); } } [Test] public void UnsafeListT_ParallelWriter_NoPtrCaching() { UnsafeList list; { list = new UnsafeList(2, Allocator.Persistent); var writer = list.AsParallelWriter(); list.SetCapacity(100); var writerJob = new UnsafeListTestParallelWriter { writer = writer }.Schedule(); writerJob.Complete(); } Assert.AreEqual(2, list.Length); Assert.AreEqual(7, list[0]); Assert.AreEqual(7, list[1]); list.Dispose(); } [Test] public unsafe void UnsafeListT_IndexOf() { using (var list = new UnsafeList(10, Allocator.Persistent) { 123, 789 }) { bool r0 = false, r1 = false, r2 = false; GCAllocRecorder.ValidateNoGCAllocs(() => { r0 = -1 != list.IndexOf(456); r1 = list.Contains(123); r2 = list.Contains(789); }); Assert.False(r0); Assert.True(r1); Assert.True(r2); } } [Test] public void UnsafeListT_InsertRangeWithBeginEnd() { var list = new UnsafeList(3, Allocator.Persistent); list.Add(0); list.Add(3); list.Add(4); Assert.AreEqual(3, list.Length); #if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG Assert.Throws(() => list.InsertRangeWithBeginEnd(-1, 8)); Assert.Throws(() => list.InsertRangeWithBeginEnd(3, 1)); #endif Assert.DoesNotThrow(() => list.InsertRangeWithBeginEnd(1, 3)); Assert.AreEqual(5, list.Length); list[1] = 1; list[2] = 2; for (var i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } Assert.DoesNotThrow(() => list.InsertRangeWithBeginEnd(5, 8)); Assert.AreEqual(8, list.Length); list[5] = 5; list[6] = 6; list[7] = 7; for (var i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } list.Dispose(); } [Test] public void UnsafeListT_InsertRange() { var list = new UnsafeList(3, Allocator.Persistent); list.Add(0); list.Add(3); list.Add(4); Assert.AreEqual(3, list.Length); #if ENABLE_UNITY_COLLECTIONS_CHECKS || UNITY_DOTS_DEBUG Assert.Throws(() => list.InsertRange(-1, 8)); Assert.Throws(() => list.InsertRange(3, -1)); #endif Assert.DoesNotThrow(() => list.InsertRange(1, 0)); Assert.AreEqual(3, list.Length); Assert.DoesNotThrow(() => list.InsertRange(1, 2)); Assert.AreEqual(5, list.Length); list[1] = 1; list[2] = 2; for (var i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } Assert.DoesNotThrow(() => list.InsertRange(5, 3)); Assert.AreEqual(8, list.Length); list[5] = 5; list[6] = 6; list[7] = 7; for (var i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } list.Dispose(); } [Test] public void UnsafeListT_ForEach([Values(10, 1000)] int n) { var seen = new NativeArray(n, Allocator.Temp); using (var container = new UnsafeList(32, CommonRwdAllocator.Handle)) { for (int i = 0; i < n; i++) { container.Add(i); } var count = 0; unsafe { UnsafeList* test = &container; foreach (var item in *test) { Assert.True(test->Contains(item)); seen[item] = seen[item] + 1; ++count; } } Assert.AreEqual(container.Length, count); for (int i = 0; i < n; i++) { Assert.AreEqual(1, seen[i], $"Incorrect item count {i}"); } } } [Test] public void UnsafeListT_CustomAllocatorTest() { AllocatorManager.Initialize(); var allocatorHelper = new AllocatorHelper(AllocatorManager.Persistent); ref var allocator = ref allocatorHelper.Allocator; allocator.Initialize(); using (var container = new UnsafeList(1, allocator.Handle)) { } Assert.IsTrue(allocator.WasUsed); allocator.Dispose(); allocatorHelper.Dispose(); AllocatorManager.Shutdown(); } [BurstCompile] struct BurstedCustomAllocatorJob : IJob { [NativeDisableUnsafePtrRestriction] public unsafe CustomAllocatorTests.CountingAllocator* Allocator; public void Execute() { unsafe { using (var container = new UnsafeList(1, Allocator->Handle)) { } } } } [Test] public unsafe void UnsafeListT_BurstedCustomAllocatorTest() { AllocatorManager.Initialize(); var allocatorHelper = new AllocatorHelper(AllocatorManager.Persistent); ref var allocator = ref allocatorHelper.Allocator; allocator.Initialize(); var allocatorPtr = (CustomAllocatorTests.CountingAllocator*)UnsafeUtility.AddressOf(ref allocator); unsafe { var handle = new BurstedCustomAllocatorJob { Allocator = allocatorPtr }.Schedule(); handle.Complete(); } Assert.IsTrue(allocator.WasUsed); allocator.Dispose(); allocatorHelper.Dispose(); AllocatorManager.Shutdown(); } void IIndexableTest(T container) where T : unmanaged, IIndexable { var length = container.Length; Assert.Throws(() => container.ElementAt(-1)); Assert.Throws(() => container.ElementAt(container.Length)); Assert.DoesNotThrow(() => { for (int i = 0, len = container.Length; i < len; ++i) { container.ElementAt(i) = 4; } }); for (int i = 0, len = container.Length; i < len; ++i) { Assert.AreEqual(4, container.ElementAt(i)); } } void INativeListTest(T container) where T : unmanaged, INativeList { var length = container.Length; Assert.Throws(() => container[-1] = 1); Assert.Throws(() => container[container.Length] = 1); Assert.DoesNotThrow(() => { for (int i = 0, len = container.Length; i < len; ++i) { container[i] = 4; } }); Assert.Throws(() => container.Capacity = container.Length - 1); Assert.DoesNotThrow(() => container.Capacity = container.Length); Assert.DoesNotThrow(() => container.Capacity = container.Length + 1); for (int i = 0, len = container.Length; i < len; ++i) { Assert.AreEqual(4, container[i]); } } private unsafe void TestInterfaces(T container) where T : unmanaged, IIndexable, INativeList { container.Length = 4; Assert.DoesNotThrow(() => { for (int i = 0, len = container.Length; i < len; ++i) { container.ElementAt(i) = i; } }); IIndexableTest(container); INativeListTest(container); } private unsafe void TestInterfacesDispose(T container) where T : unmanaged, IIndexable, INativeList, IDisposable { TestInterfaces(container); container.Dispose(); } [Test] [TestRequiresDotsDebugOrCollectionChecks] public unsafe void UnsafeListT_TestInterfaces() => TestInterfacesDispose(new UnsafeList(1, CommonRwdAllocator.Handle)); [Test] [TestRequiresDotsDebugOrCollectionChecks] public unsafe void NativeList_TestInterfaces() => TestInterfacesDispose(new NativeList(1, CommonRwdAllocator.Handle)); [Test] [TestRequiresDotsDebugOrCollectionChecks] public unsafe void FixedList32Bytes_TestInterfaces() => TestInterfaces(new FixedList32Bytes()); [Test] [TestRequiresDotsDebugOrCollectionChecks] public unsafe void FixedList64Bytes_TestInterfaces() => TestInterfaces(new FixedList64Bytes()); [Test] [TestRequiresDotsDebugOrCollectionChecks] public unsafe void FixedList128Bytes_TestInterfaces() => TestInterfaces(new FixedList128Bytes()); [Test] [TestRequiresDotsDebugOrCollectionChecks] public unsafe void FixedList512Bytes_TestInterfaces() => TestInterfaces(new FixedList512Bytes()); [Test] [TestRequiresDotsDebugOrCollectionChecks] public unsafe void FixedList4096Bytes_TestInterfaces() => TestInterfaces(new FixedList4096Bytes()); }