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; #if !UNITY_PORTABLE_TEST_RUNNER using System.Text.RegularExpressions; #endif 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] 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; Assert.AreEqual(2, array.Length); Assert.AreEqual(42, array[0]); Assert.AreEqual(2, array[1]); list.Dispose(); } [Test] 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; list.Add(1000); ExpectedLength(ref list, 2); Assert.Throws( () => { array[0] = 1; }); list.Dispose(); } [Test] public void NativeArrayFromNativeListInvalidatesOnCapacityChange() { var list = new NativeList(Allocator.Persistent); list.Add(42); NativeArray array = list; 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] public void NativeArrayFromNativeListInvalidatesOnDispose() { var list = new NativeList(Allocator.Persistent); list.Add(42); NativeArray array = list; 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; 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_Unmanaged() { var list = new NativeList(4, Allocator.Persistent); var ar = new NativeArray(new float[] { 0, 1, 2, 3, 4, 5, 6, 7 }, Allocator.Persistent); list.CopyFrom(ar); ExpectedLength(ref list, 8); for (int i = 0; i < list.Length; ++i) { Assert.AreEqual(i, list[i]); } ar.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(); } #if ENABLE_UNITY_COLLECTIONS_CHECKS [Test] public void SetCapacityLessThanLength() { var list = new NativeList(Allocator.Persistent); list.Resize(10, NativeArrayOptions.UninitializedMemory); Assert.Throws(() => { list.Capacity = 5; }); list.Dispose(); } [Test] public void DisposingNativeListDerivedArrayDoesNotThrow() { var list = new NativeList(Allocator.Persistent); list.Add(1); NativeArray array = list; Assert.DoesNotThrow(() => { array.Dispose(); }); list.Dispose(); } #endif [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); Assert.Throws( () => { container.Contains(0); }); 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(); } struct NativeQueueAddJob : IJob { NativeQueue queue; public NativeQueueAddJob(NativeQueue queue) { this.queue = queue; } public void Execute() { queue.Enqueue(1); } } [Test] public void NativeQueue_DisposeJobWithMissingDependencyThrows() { var queue = new NativeQueue(Allocator.Persistent); var deps = new NativeQueueAddJob(queue).Schedule(); Assert.Throws(() => { queue.Dispose(default); }); deps.Complete(); queue.Dispose(); } [Test] public void NativeQueue_DisposeJobCantBeScheduled() { var queue = new NativeQueue(Allocator.Persistent); var deps = queue.Dispose(default); Assert.Throws(() => { new NativeQueueAddJob(queue).Schedule(deps); }); deps.Complete(); } // These tests require: // - JobsDebugger support for static safety IDs (added in 2020.1) // - Asserting throws #if !UNITY_DOTSRUNTIME [Test,DotsRuntimeIgnore] 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,DotsRuntimeIgnore] 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,DotsRuntimeIgnore] 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")); } #endif [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.Throws(() => list.InsertRangeWithBeginEnd(-1, 8)); Assert.Throws(() => list.InsertRangeWithBeginEnd(0, 8)); Assert.Throws(() => list.InsertRangeWithBeginEnd(3, 1)); Assert.DoesNotThrow(() => list.InsertRangeWithBeginEnd(1, 3)); list[1] = 1; list[2] = 2; for (var i = 0; i < 5; ++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); Assert.Throws(() => list.SetCapacity(1)); 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(); } } }