using NUnit.Framework; using System; using Unity.Burst; using Unity.Collections; using Unity.Collections.NotBurstCompatible; using Unity.Collections.LowLevel.Unsafe; using Unity.Collections.Tests; using Unity.Jobs; using UnityEngine; using UnityEngine.TestTools; using System.Text.RegularExpressions; internal class NativeListTests : CollectionsTestFixture { static void ExpectedLength(ref NativeList container, int expected) where T : unmanaged { Assert.AreEqual(expected == 0, container.IsEmpty); Assert.AreEqual(expected, container.Length); } [Test] [TestRequiresCollectionChecks] public void NullListThrow() { var list = new NativeList(); Assert.Throws(() => list[0] = 5); Assert.Throws( () => list.Add(1)); } [Test] public void NativeList_Allocate_Deallocate_Read_Write() { var list = new NativeList(Allocator.Persistent); list.Add(1); list.Add(2); ExpectedLength(ref list, 2); Assert.AreEqual(1, list[0]); Assert.AreEqual(2, list[1]); list.Dispose(); } [Test] public void NativeArrayFromNativeList() { var list = new NativeList(Allocator.Persistent); list.Add(42); list.Add(2); NativeArray array = list.AsArray(); Assert.AreEqual(2, array.Length); Assert.AreEqual(42, array[0]); Assert.AreEqual(2, array[1]); list.Dispose(); } [Test] [TestRequiresCollectionChecks] public void NativeArrayFromNativeListInvalidatesOnAdd() { var list = new NativeList(Allocator.Persistent); // This test checks that adding an element without reallocation invalidates the native array // (length changes) list.Capacity = 2; list.Add(42); NativeArray array = list.AsArray(); list.Add(1000); ExpectedLength(ref list, 2); Assert.Throws( () => { array[0] = 1; }); list.Dispose(); } [Test] [TestRequiresCollectionChecks] public void NativeArrayFromNativeListInvalidatesOnCapacityChange() { var list = new NativeList(Allocator.Persistent); list.Add(42); NativeArray array = list.AsArray(); ExpectedLength(ref list, 1); list.Capacity = 10; //Assert.AreEqual(1, array.Length); - temporarily commenting out updated assert checks to ensure editor version promotion succeeds Assert.Throws( () => { array[0] = 1; }); list.Dispose(); } [Test] [TestRequiresCollectionChecks] public void NativeArrayFromNativeListInvalidatesOnDispose() { var list = new NativeList(Allocator.Persistent); list.Add(42); NativeArray array = list.AsArray(); list.Dispose(); Assert.Throws( () => { array[0] = 1; }); Assert.Throws( () => { list[0] = 1; }); } [Test] public void NativeArrayFromNativeListMayDeallocate() { var list = new NativeList(Allocator.Persistent); list.Add(42); NativeArray array = list.AsArray(); Assert.DoesNotThrow(() => { array.Dispose(); }); list.Dispose(); } [Test] public void CopiedNativeListIsKeptInSync() { var list = new NativeList(Allocator.Persistent); var listCpy = list; list.Add(42); Assert.AreEqual(42, listCpy[0]); Assert.AreEqual(42, list[0]); Assert.AreEqual(1, listCpy.Length); ExpectedLength(ref list, 1); list.Dispose(); } [Test] public void NativeList_CopyFrom_Managed() { var list = new NativeList(4, Allocator.Persistent); var ar = new float[] { 0, 1, 2, 3, 4, 5, 6, 7 }; list.CopyFromNBC(ar); ExpectedLength(ref list, 8); for (int i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } list.Dispose(); } [Test] public void NativeList_CopyFrom_OtherContainers() { var list = new NativeList(4, Allocator.Persistent); { var container = new NativeArray(new int[] { 0, 1, 2, 3, 4, 5, 6, 7 }, Allocator.Persistent); list.CopyFrom(container); ExpectedLength(ref list, 8); for (int i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } container.Dispose(); } list.Add(123); { var container = new NativeList(32, Allocator.Persistent) { 0, 1, 2, 3, 4, 5, 6, 7 }; list.CopyFrom(container); ExpectedLength(ref list, 8); for (int i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } container.Dispose(); } list.Add(345); { var container = new UnsafeList(32, Allocator.Persistent) { 0, 1, 2, 3, 4, 5, 6, 7 }; list.CopyFrom(container); ExpectedLength(ref list, 8); for (int i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } container.Dispose(); } list.Add(789); { var container = new NativeHashSet(32, Allocator.Persistent) { 0, 1, 2, 3, 4, 5, 6, 7 }; using (var array = container.ToNativeArray(Allocator.TempJob)) { list.CopyFrom(array); } ExpectedLength(ref list, 8); for (int i = 0; i < list.Length; ++i) { list.Contains(i); } container.Dispose(); } list.Add(123); { var container = new UnsafeHashSet(32, Allocator.Persistent) { 0, 1, 2, 3, 4, 5, 6, 7 }; using (var array = container.ToNativeArray(Allocator.TempJob)) { list.CopyFrom(array); } ExpectedLength(ref list, 8); for (int i = 0; i < list.Length; ++i) { list.Contains(i); } container.Dispose(); } list.Dispose(); } [BurstCompile(CompileSynchronously = true)] struct TempListInJob : IJob { public NativeArray Output; public void Execute() { var list = new NativeList(Allocator.Temp); list.Add(17); Output[0] = list[0]; list.Dispose(); } } [Test] [Ignore("Unstable on CI, DOTS-1965")] public void TempListInBurstJob() { var job = new TempListInJob() { Output = CollectionHelper.CreateNativeArray(1, CommonRwdAllocator.Handle) }; job.Schedule().Complete(); Assert.AreEqual(17, job.Output[0]); job.Output.Dispose(); } [Test] public void SetCapacityLessThanLength() { var list = new NativeList(Allocator.Persistent); list.Resize(10, NativeArrayOptions.UninitializedMemory); #if ENABLE_UNITY_COLLECTIONS_CHECKS Assert.Throws(() => { list.Capacity = 5; }); #endif list.Dispose(); } [Test] public void DisposingNativeListDerivedArrayDoesNotThrow() { var list = new NativeList(Allocator.Persistent); list.Add(1); NativeArray array = list.AsArray(); Assert.DoesNotThrow(() => { array.Dispose(); }); list.Dispose(); } [Test] public void NativeList_DisposeJob() { var container = new NativeList(Allocator.Persistent); Assert.True(container.IsCreated); Assert.DoesNotThrow(() => { container.Add(0); }); Assert.DoesNotThrow(() => { container.Contains(0); }); var disposeJob = container.Dispose(default); Assert.False(container.IsCreated); #if ENABLE_UNITY_COLLECTIONS_CHECKS Assert.Throws( () => { container.Contains(0); }); #endif disposeJob.Complete(); } [Test] public void ForEachWorks() { var container = new NativeList(Allocator.Persistent); container.Add(10); container.Add(20); int sum = 0; int count = 0; GCAllocRecorder.ValidateNoGCAllocs(() => { sum = 0; count = 0; foreach (var p in container) { sum += p; count++; } }); Assert.AreEqual(30, sum); Assert.AreEqual(2, count); container.Dispose(); } [Test] [TestRequiresCollectionChecks] public void NativeList_UseAfterFree_UsesCustomOwnerTypeName() { var list = new NativeList(10, CommonRwdAllocator.Handle); list.Add(17); list.Dispose(); Assert.That(() => list[0], Throws.Exception.TypeOf() .With.Message.Contains($"The {list.GetType()} has been deallocated")); } [Test] [TestRequiresCollectionChecks] public void AtomicSafetyHandle_AllocatorTemp_UniqueStaticSafetyIds() { // All collections that use Allocator.Temp share the same core AtomicSafetyHandle. // This test verifies that containers can proceed to assign unique static safety IDs to each // AtomicSafetyHandle value, which will not be shared by other containers using Allocator.Temp. var listInt = new NativeList(10, Allocator.Temp); var listFloat = new NativeList(10, Allocator.Temp); listInt.Add(17); listInt.Dispose(); Assert.That(() => listInt[0], Throws.Exception.TypeOf() .With.Message.Contains($"The {listInt.GetType()} has been deallocated")); listFloat.Add(1.0f); listFloat.Dispose(); Assert.That(() => listFloat[0], Throws.Exception.TypeOf() .With.Message.Contains($"The {listFloat.GetType()} has been deallocated")); } [BurstCompile(CompileSynchronously = true)] struct NativeListCreateAndUseAfterFreeBurst : IJob { public void Execute() { var list = new NativeList(10, Allocator.Temp); list.Add(17); list.Dispose(); list.Add(42); } } [Test] [TestRequiresCollectionChecks] public void NativeList_CreateAndUseAfterFreeInBurstJob_UsesCustomOwnerTypeName() { // Make sure this isn't the first container of this type ever created, so that valid static safety data exists var list = new NativeList(10, CommonRwdAllocator.Handle); list.Dispose(); var job = new NativeListCreateAndUseAfterFreeBurst { }; // Two things: // 1. This exception is logged, not thrown; thus, we use LogAssert to detect it. // 2. Calling write operation after container.Dispose() emits an unintuitive error message. For now, all this test cares about is whether it contains the // expected type name. job.Run(); LogAssert.Expect(LogType.Exception, new Regex($"InvalidOperationException: The {Regex.Escape(list.GetType().ToString())} has been declared as \\[ReadOnly\\] in the job, but you are writing to it")); } [Test] public unsafe void NativeList_IndexOf() { using (var list = new NativeList(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 NativeList_InsertRangeWithBeginEnd() { var list = new NativeList(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 NativeList_InsertRange() { var list = new NativeList(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 NativeList_CustomAllocatorTest() { AllocatorManager.Initialize(); var allocatorHelper = new AllocatorHelper(AllocatorManager.Persistent); ref var allocator = ref allocatorHelper.Allocator; allocator.Initialize(); using (var container = new NativeList(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 NativeList(1, Allocator->Handle)) { } } } } [Test] public unsafe void NativeList_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(); } [Test] public unsafe void NativeList_SetCapacity() { using (var list = new NativeList(1, Allocator.Persistent)) { 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 NativeList_TrimExcess() { using (var list = new NativeList(32, Allocator.Persistent)) { 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(); } } public struct NestedContainer { public NativeList data; } [Test] public void NativeList_Nested() { var inner = new NativeList(CommonRwdAllocator.Handle); NestedContainer nestedStruct = new NestedContainer { data = inner }; var containerNestedStruct = new NativeList(CommonRwdAllocator.Handle); var containerNested = new NativeList>(CommonRwdAllocator.Handle); containerNested.Add(inner); containerNestedStruct.Add(nestedStruct); containerNested.Dispose(); containerNestedStruct.Dispose(); inner.Dispose(); } [Test] public void NativeList_AddReplicate() { using (var list = new NativeList(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); } } }