A game about forced loneliness, made by TACStudios
1using System;
2using NUnit.Framework;
3using Unity.Burst;
4using Unity.Collections;
5using Unity.Collections.LowLevel.Unsafe;
6using Unity.Collections.NotBurstCompatible;
7using Unity.Collections.Tests;
8using Unity.Jobs;
9using UnityEngine;
10using UnityEngine.TestTools;
11using System.Text.RegularExpressions;
12
13internal class NativeParallelMultiHashMapTests : CollectionsTestFixture
14{
15 [Test]
16 [TestRequiresCollectionChecks]
17 public void NativeParallelMultiHashMap_UseAfterFree_UsesCustomOwnerTypeName()
18 {
19 var container = new NativeParallelMultiHashMap<int, int>(10, CommonRwdAllocator.Handle);
20 container.Add(0, 123);
21 container.Dispose();
22 Assert.That(() => container.ContainsKey(0),
23 Throws.Exception.TypeOf<ObjectDisposedException>()
24 .With.Message.Contains($"The {container.GetType()} has been deallocated"));
25 }
26
27 [BurstCompile(CompileSynchronously = true)]
28 struct NativeParallelMultiHashMap_CreateAndUseAfterFreeBurst : IJob
29 {
30 public void Execute()
31 {
32 var container = new NativeParallelMultiHashMap<int, int>(10, Allocator.Temp);
33 container.Add(0, 17);
34 container.Dispose();
35 container.Add(1, 42);
36 }
37 }
38
39 [Test]
40 [TestRequiresCollectionChecks]
41 public void NativeParallelMultiHashMap_CreateAndUseAfterFreeInBurstJob_UsesCustomOwnerTypeName()
42 {
43 // Make sure this isn't the first container of this type ever created, so that valid static safety data exists
44 var container = new NativeParallelMultiHashMap<int, int>(10, CommonRwdAllocator.Handle);
45 container.Dispose();
46
47 var job = new NativeParallelMultiHashMap_CreateAndUseAfterFreeBurst
48 {
49 };
50
51 // Two things:
52 // 1. This exception is logged, not thrown; thus, we use LogAssert to detect it.
53 // 2. Calling write operation after container.Dispose() emits an unintuitive error message. For now, all this test cares about is whether it contains the
54 // expected type name.
55 job.Run();
56 LogAssert.Expect(LogType.Exception,
57 new Regex($"InvalidOperationException: The {Regex.Escape(container.GetType().ToString())} has been declared as \\[ReadOnly\\] in the job, but you are writing to it"));
58 }
59
60 [Test]
61 public void NativeParallelMultiHashMap_IsEmpty()
62 {
63 var container = new NativeParallelMultiHashMap<int, int>(0, Allocator.Persistent);
64 Assert.IsTrue(container.IsEmpty);
65
66 container.Add(0, 0);
67 Assert.IsFalse(container.IsEmpty);
68 Assert.AreEqual(1, container.Capacity);
69 ExpectedCount(ref container, 1);
70
71 container.Remove(0, 0);
72 Assert.IsTrue(container.IsEmpty);
73
74 container.Add(0, 0);
75 container.Clear();
76 Assert.IsTrue(container.IsEmpty);
77
78 container.Dispose();
79 }
80
81 [Test]
82 public void NativeParallelMultiHashMap_CountValuesForKey()
83 {
84 var hashMap = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp);
85 hashMap.Add(5, 7);
86 hashMap.Add(6, 9);
87 hashMap.Add(6, 10);
88
89 Assert.AreEqual(1, hashMap.CountValuesForKey(5));
90 Assert.AreEqual(2, hashMap.CountValuesForKey(6));
91 Assert.AreEqual(0, hashMap.CountValuesForKey(7));
92
93 hashMap.Dispose();
94 }
95
96 [Test]
97 public void NativeParallelMultiHashMap_RemoveKeyAndValue()
98 {
99 var hashMap = new NativeParallelMultiHashMap<int, long>(1, Allocator.Temp);
100 hashMap.Add(10, 0);
101 hashMap.Add(10, 1);
102 hashMap.Add(10, 2);
103
104 hashMap.Add(20, 2);
105 hashMap.Add(20, 2);
106 hashMap.Add(20, 1);
107 hashMap.Add(20, 2);
108 hashMap.Add(20, 1);
109
110 hashMap.Remove(10, 1L);
111 ExpectValues(hashMap, 10, new[] { 0L, 2L });
112 ExpectValues(hashMap, 20, new[] { 1L, 1L, 2L, 2L, 2L });
113
114 hashMap.Remove(20, 2L);
115 ExpectValues(hashMap, 10, new[] { 0L, 2L });
116 ExpectValues(hashMap, 20, new[] { 1L, 1L });
117
118 hashMap.Remove(20, 1L);
119 ExpectValues(hashMap, 10, new[] { 0L, 2L });
120 ExpectValues(hashMap, 20, new long[0]);
121
122 hashMap.Dispose();
123 }
124
125 [Test]
126 public void NativeParallelMultiHashMap_ValueIterator()
127 {
128 var hashMap = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp);
129 hashMap.Add(5, 0);
130 hashMap.Add(5, 1);
131 hashMap.Add(5, 2);
132
133 var list = new NativeList<int>(CommonRwdAllocator.Handle);
134
135 GCAllocRecorder.ValidateNoGCAllocs(() =>
136 {
137 list.Clear();
138 foreach (var value in hashMap.GetValuesForKey(5))
139 list.Add(value);
140 });
141
142 list.Sort();
143 Assert.AreEqual(list.ToArrayNBC(), new int[] { 0, 1, 2 });
144
145 foreach (var value in hashMap.GetValuesForKey(6))
146 Assert.Fail();
147
148 list.Dispose();
149 hashMap.Dispose();
150 }
151
152 [Test]
153 public void NativeParallelMultiHashMap_RemoveKeyValueDoesntDeallocate()
154 {
155 var hashMap = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp) { { 5, 1 } };
156
157 hashMap.Remove(5, 5);
158 GCAllocRecorder.ValidateNoGCAllocs(() =>
159 {
160 hashMap.Remove(5, 1);
161 });
162 Assert.IsTrue(hashMap.IsEmpty);
163
164 hashMap.Dispose();
165 }
166
167 static void ExpectedCount<TKey, TValue>(ref NativeParallelMultiHashMap<TKey, TValue> container, int expected)
168 where TKey : unmanaged, IEquatable<TKey>
169 where TValue : unmanaged
170 {
171 Assert.AreEqual(expected == 0, container.IsEmpty);
172 Assert.AreEqual(expected, container.Count());
173 }
174
175 [Test]
176 public void NativeParallelMultiHashMap_RemoveOnEmptyMap_DoesNotThrow()
177 {
178 var hashMap = new NativeParallelMultiHashMap<int, int>(0, Allocator.Temp);
179
180 Assert.DoesNotThrow(() => hashMap.Remove(0));
181 Assert.DoesNotThrow(() => hashMap.Remove(-425196));
182 Assert.DoesNotThrow(() => hashMap.Remove(0, 0));
183 Assert.DoesNotThrow(() => hashMap.Remove(-425196, 0));
184
185 hashMap.Dispose();
186 }
187
188 [Test]
189 public void NativeParallelMultiHashMap_RemoveFromMultiHashMap()
190 {
191 var hashMap = new NativeParallelMultiHashMap<int, int>(16, Allocator.Temp);
192 int iSquared;
193 // Make sure inserting values work
194 for (int i = 0; i < 8; ++i)
195 hashMap.Add(i, i * i);
196 for (int i = 0; i < 8; ++i)
197 hashMap.Add(i, i);
198 Assert.AreEqual(16, hashMap.Capacity, "HashMap grew larger than expected");
199 // Make sure reading the inserted values work
200 for (int i = 0; i < 8; ++i)
201 {
202 NativeParallelMultiHashMapIterator<int> it;
203 Assert.IsTrue(hashMap.TryGetFirstValue(i, out iSquared, out it), "Failed get value from hash table");
204 Assert.AreEqual(iSquared, i, "Got the wrong value from the hash table");
205 Assert.IsTrue(hashMap.TryGetNextValue(out iSquared, ref it), "Failed get value from hash table");
206 Assert.AreEqual(iSquared, i * i, "Got the wrong value from the hash table");
207 }
208 for (int rm = 0; rm < 8; ++rm)
209 {
210 Assert.AreEqual(2, hashMap.Remove(rm));
211 NativeParallelMultiHashMapIterator<int> it;
212 Assert.IsFalse(hashMap.TryGetFirstValue(rm, out iSquared, out it), "Failed to remove value from hash table");
213 for (int i = rm + 1; i < 8; ++i)
214 {
215 Assert.IsTrue(hashMap.TryGetFirstValue(i, out iSquared, out it), "Failed get value from hash table");
216 Assert.AreEqual(iSquared, i, "Got the wrong value from the hash table");
217 Assert.IsTrue(hashMap.TryGetNextValue(out iSquared, ref it), "Failed get value from hash table");
218 Assert.AreEqual(iSquared, i * i, "Got the wrong value from the hash table");
219 }
220 }
221 // Make sure entries were freed
222 for (int i = 0; i < 8; ++i)
223 hashMap.Add(i, i * i);
224 for (int i = 0; i < 8; ++i)
225 hashMap.Add(i, i);
226 Assert.AreEqual(16, hashMap.Capacity, "HashMap grew larger than expected");
227 hashMap.Dispose();
228 }
229
230 void ExpectValues(NativeParallelMultiHashMap<int, long> hashMap, int key, long[] expectedValues)
231 {
232 var list = new NativeList<long>(CommonRwdAllocator.Handle);
233 foreach (var value in hashMap.GetValuesForKey(key))
234 list.Add(value);
235
236 list.Sort();
237 Assert.AreEqual(list.ToArrayNBC(), expectedValues);
238 list.Dispose();
239 }
240
241 [Test]
242 public void NativeParallelMultiHashMap_GetKeys()
243 {
244 var container = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp);
245 for (int i = 0; i < 30; ++i)
246 {
247 container.Add(i, 2 * i);
248 container.Add(i, 3 * i);
249 }
250 var keys = container.GetKeyArray(Allocator.Temp);
251 var (unique, uniqueLength) = container.GetUniqueKeyArray(Allocator.Temp);
252 Assert.AreEqual(30, uniqueLength);
253
254 Assert.AreEqual(60, keys.Length);
255 keys.Sort();
256 for (int i = 0; i < 30; ++i)
257 {
258 Assert.AreEqual(i, keys[i * 2 + 0]);
259 Assert.AreEqual(i, keys[i * 2 + 1]);
260 Assert.AreEqual(i, unique[i]);
261 }
262 }
263
264 [Test]
265 public void NativeParallelMultiHashMap_GetUniqueKeysEmpty()
266 {
267 var hashMap = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp);
268 var keys = hashMap.GetUniqueKeyArray(Allocator.Temp);
269
270 Assert.AreEqual(0, keys.Item1.Length);
271 Assert.AreEqual(0, keys.Item2);
272 }
273
274 [Test]
275 public void NativeParallelMultiHashMap_GetUniqueKeys()
276 {
277 var hashMap = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp);
278 for (int i = 0; i < 30; ++i)
279 {
280 hashMap.Add(i, 2 * i);
281 hashMap.Add(i, 3 * i);
282 }
283 var keys = hashMap.GetUniqueKeyArray(Allocator.Temp);
284 hashMap.Dispose();
285 Assert.AreEqual(30, keys.Item2);
286 for (int i = 0; i < 30; ++i)
287 {
288 Assert.AreEqual(i, keys.Item1[i]);
289 }
290 keys.Item1.Dispose();
291 }
292
293 [Test]
294 public void NativeParallelMultiHashMap_GetValues()
295 {
296 var hashMap = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp);
297 for (int i = 0; i < 30; ++i)
298 {
299 hashMap.Add(i, 30 + i);
300 hashMap.Add(i, 60 + i);
301 }
302 var values = hashMap.GetValueArray(Allocator.Temp);
303 hashMap.Dispose();
304
305 Assert.AreEqual(60, values.Length);
306 values.Sort();
307 for (int i = 0; i < 60; ++i)
308 {
309 Assert.AreEqual(30 + i, values[i]);
310 }
311 values.Dispose();
312 }
313
314 [Test]
315 public void NativeParallelMultiHashMap_ForEach_FixedStringInHashMap()
316 {
317 using (var stringList = new NativeList<FixedString32Bytes>(10, Allocator.Persistent) { "Hello", ",", "World", "!" })
318 {
319 var container = new NativeParallelMultiHashMap<FixedString128Bytes, float>(50, Allocator.Temp);
320 var seen = new NativeArray<int>(stringList.Length, Allocator.Temp);
321 foreach (var str in stringList)
322 {
323 container.Add(str, 0);
324 }
325
326 foreach (var pair in container)
327 {
328 int index = stringList.IndexOf(pair.Key);
329 Assert.AreEqual(stringList[index], pair.Key.ToString());
330 seen[index] = seen[index] + 1;
331 }
332
333 for (int i = 0; i < stringList.Length; i++)
334 {
335 Assert.AreEqual(1, seen[i], $"Incorrect value count {stringList[i]}");
336 }
337 }
338 }
339
340 [Test]
341 public void NativeParallelMultiHashMap_ForEach([Values(10, 1000)]int n)
342 {
343 var seenKeys = new NativeArray<int>(n, Allocator.Temp);
344 var seenValues = new NativeArray<int>(n * 2, Allocator.Temp);
345 using (var container = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp))
346 {
347 for (int i = 0; i < n; ++i)
348 {
349 container.Add(i, i);
350 container.Add(i, i + n);
351 }
352
353 var count = 0;
354 foreach (var kv in container)
355 {
356 if (kv.Value < n)
357 {
358 Assert.AreEqual(kv.Key, kv.Value);
359 }
360 else
361 {
362 Assert.AreEqual(kv.Key + n, kv.Value);
363 }
364
365 seenKeys[kv.Key] = seenKeys[kv.Key] + 1;
366 seenValues[kv.Value] = seenValues[kv.Value] + 1;
367
368 ++count;
369 }
370
371 Assert.AreEqual(container.Count(), count);
372 for (int i = 0; i < n; i++)
373 {
374 Assert.AreEqual(2, seenKeys[i], $"Incorrect key count {i}");
375 Assert.AreEqual(1, seenValues[i], $"Incorrect value count {i}");
376 Assert.AreEqual(1, seenValues[i + n], $"Incorrect value count {i + n}");
377 }
378 }
379 }
380
381 struct NativeParallelMultiHashMap_ForEach_Job : IJob
382 {
383 [ReadOnly]
384 public NativeParallelMultiHashMap<int, int> Input;
385
386 [ReadOnly]
387 public int Num;
388
389 public void Execute()
390 {
391 var seenKeys = new NativeArray<int>(Num, Allocator.Temp);
392 var seenValues = new NativeArray<int>(Num * 2, Allocator.Temp);
393
394 var count = 0;
395 foreach (var kv in Input)
396 {
397 if (kv.Value < Num)
398 {
399 Assert.AreEqual(kv.Key, kv.Value);
400 }
401 else
402 {
403 Assert.AreEqual(kv.Key + Num, kv.Value);
404 }
405
406 seenKeys[kv.Key] = seenKeys[kv.Key] + 1;
407 seenValues[kv.Value] = seenValues[kv.Value] + 1;
408
409 ++count;
410 }
411
412 Assert.AreEqual(Input.Count(), count);
413 for (int i = 0; i < Num; i++)
414 {
415 Assert.AreEqual(2, seenKeys[i], $"Incorrect key count {i}");
416 Assert.AreEqual(1, seenValues[i], $"Incorrect value count {i}");
417 Assert.AreEqual(1, seenValues[i + Num], $"Incorrect value count {i + Num}");
418 }
419
420 seenKeys.Dispose();
421 seenValues.Dispose();
422 }
423 }
424
425 [Test]
426 public void NativeParallelMultiHashMap_ForEach_From_Job([Values(10, 1000)] int n)
427 {
428 using (var container = new NativeParallelMultiHashMap<int, int>(1, CommonRwdAllocator.Handle))
429 {
430 for (int i = 0; i < n; ++i)
431 {
432 container.Add(i, i);
433 container.Add(i, i + n);
434 }
435
436 new NativeParallelMultiHashMap_ForEach_Job
437 {
438 Input = container,
439 Num = n,
440
441 }.Run();
442 }
443 }
444
445 [Test]
446 [TestRequiresCollectionChecks]
447 public void NativeParallelMultiHashMap_ForEach_Throws_When_Modified()
448 {
449 using (var container = new NativeParallelMultiHashMap<int, int>(32, CommonRwdAllocator.Handle))
450 {
451 for (int i = 0; i < 30; ++i)
452 {
453 container.Add(i, 30 + i);
454 container.Add(i, 60 + i);
455 }
456
457 Assert.Throws<ObjectDisposedException>(() =>
458 {
459 foreach (var kv in container)
460 {
461 container.Add(10, 10);
462 }
463 });
464
465 Assert.Throws<ObjectDisposedException>(() =>
466 {
467 foreach (var kv in container)
468 {
469 container.Remove(1);
470 }
471 });
472 }
473 }
474
475 struct NativeParallelMultiHashMap_ForEachIterator : IJob
476 {
477 [ReadOnly]
478 public NativeParallelMultiHashMap<int, int>.KeyValueEnumerator Iter;
479
480 public void Execute()
481 {
482 while (Iter.MoveNext())
483 {
484 }
485 }
486 }
487
488 [Test]
489 [TestRequiresCollectionChecks]
490 public void NativeParallelMultiHashMap_ForEach_Throws_Job_Iterator()
491 {
492 using (var container = new NativeParallelMultiHashMap<int, int>(32, CommonRwdAllocator.Handle))
493 {
494 var jobHandle = new NativeParallelMultiHashMap_ForEachIterator
495 {
496 Iter = container.GetEnumerator()
497
498 }.Schedule();
499
500 Assert.Throws<InvalidOperationException>(() => { container.Add(1, 1); });
501
502 jobHandle.Complete();
503 }
504 }
505
506 struct ParallelWriteToMultiHashMapJob : IJobParallelFor
507 {
508 [WriteOnly]
509 public NativeParallelMultiHashMap<int, int>.ParallelWriter Writer;
510
511 public void Execute(int index)
512 {
513 Writer.Add(index, 0);
514 }
515 }
516
517 [Test]
518 [TestRequiresCollectionChecks]
519 public void NativeParallelMultiHashMap_ForEach_Throws_When_Modified_From_Job()
520 {
521 using (var container = new NativeParallelMultiHashMap<int, int>(32, CommonRwdAllocator.Handle))
522 {
523 var iter = container.GetEnumerator();
524
525 var jobHandle = new ParallelWriteToMultiHashMapJob
526 {
527 Writer = container.AsParallelWriter()
528
529 }.Schedule(1, 2);
530
531 Assert.Throws<ObjectDisposedException>(() =>
532 {
533 while (iter.MoveNext())
534 {
535 }
536 });
537
538 jobHandle.Complete();
539 }
540 }
541
542 [Test]
543 public void NativeParallelMultiHashMap_GetKeysAndValues()
544 {
545 var container = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp);
546 for (int i = 0; i < 30; ++i)
547 {
548 container.Add(i, 30 + i);
549 container.Add(i, 60 + i);
550 }
551 var keysValues = container.GetKeyValueArrays(Allocator.Temp);
552 container.Dispose();
553
554 Assert.AreEqual(60, keysValues.Keys.Length);
555 Assert.AreEqual(60, keysValues.Values.Length);
556
557 // ensure keys and matching values are aligned (though unordered)
558 for (int i = 0; i < 30; ++i)
559 {
560 var k0 = keysValues.Keys[i * 2 + 0];
561 var k1 = keysValues.Keys[i * 2 + 1];
562 var v0 = keysValues.Values[i * 2 + 0];
563 var v1 = keysValues.Values[i * 2 + 1];
564
565 if (v0 > v1)
566 (v0, v1) = (v1, v0);
567
568 Assert.AreEqual(k0, k1);
569 Assert.AreEqual(30 + k0, v0);
570 Assert.AreEqual(60 + k0, v1);
571 }
572
573 keysValues.Keys.Sort();
574 for (int i = 0; i < 30; ++i)
575 {
576 Assert.AreEqual(i, keysValues.Keys[i * 2 + 0]);
577 Assert.AreEqual(i, keysValues.Keys[i * 2 + 1]);
578 }
579
580 keysValues.Values.Sort();
581 for (int i = 0; i < 60; ++i)
582 {
583 Assert.AreEqual(30 + i, keysValues.Values[i]);
584 }
585
586 keysValues.Dispose();
587 }
588
589 [Test]
590 public void NativeParallelMultiHashMap_ContainsKeyMultiHashMap()
591 {
592 var container = new NativeParallelMultiHashMap<int, int>(1, Allocator.Temp);
593 container.Add(5, 7);
594
595 container.Add(6, 9);
596 container.Add(6, 10);
597
598 Assert.IsTrue(container.ContainsKey(5));
599 Assert.IsTrue(container.ContainsKey(6));
600 Assert.IsFalse(container.ContainsKey(4));
601
602 container.Dispose();
603 }
604
605 [Test]
606 public void NativeParallelMultiHashMap_CustomAllocatorTest()
607 {
608 AllocatorManager.Initialize();
609 var allocatorHelper = new AllocatorHelper<CustomAllocatorTests.CountingAllocator>(AllocatorManager.Persistent);
610 ref var allocator = ref allocatorHelper.Allocator;
611 allocator.Initialize();
612
613 using (var container = new NativeParallelMultiHashMap<int, int>(1, allocator.Handle))
614 {
615 }
616
617 Assert.IsTrue(allocator.WasUsed);
618 allocator.Dispose();
619 allocatorHelper.Dispose();
620 AllocatorManager.Shutdown();
621 }
622
623 [BurstCompile]
624 struct BurstedCustomAllocatorJob : IJob
625 {
626 [NativeDisableUnsafePtrRestriction]
627 public unsafe CustomAllocatorTests.CountingAllocator* Allocator;
628
629 public void Execute()
630 {
631 unsafe
632 {
633 using (var container = new NativeParallelMultiHashMap<int, int>(1, Allocator->Handle))
634 {
635 }
636 }
637 }
638 }
639
640 [Test]
641 public unsafe void NativeParallelMultiHashMap_BurstedCustomAllocatorTest()
642 {
643 AllocatorManager.Initialize();
644 var allocatorHelper = new AllocatorHelper<CustomAllocatorTests.CountingAllocator>(AllocatorManager.Persistent);
645 ref var allocator = ref allocatorHelper.Allocator;
646 allocator.Initialize();
647
648 var allocatorPtr = (CustomAllocatorTests.CountingAllocator*)UnsafeUtility.AddressOf<CustomAllocatorTests.CountingAllocator>(ref allocator);
649 unsafe
650 {
651 var handle = new BurstedCustomAllocatorJob {Allocator = allocatorPtr}.Schedule();
652 handle.Complete();
653 }
654
655 Assert.IsTrue(allocator.WasUsed);
656 allocator.Dispose();
657 allocatorHelper.Dispose();
658 AllocatorManager.Shutdown();
659 }
660
661 public struct NestedHashMap
662 {
663 public NativeParallelMultiHashMap<int, int> map;
664 }
665
666 [Test]
667 public void NativeParallelMultiHashMap_Nested()
668 {
669 var mapInner = new NativeParallelMultiHashMap<int, int>(16, CommonRwdAllocator.Handle);
670 NestedHashMap mapStruct = new NestedHashMap { map = mapInner };
671
672 var mapNestedStruct = new NativeParallelMultiHashMap<int, NestedHashMap>(16, CommonRwdAllocator.Handle);
673 var mapNested = new NativeParallelMultiHashMap<int, NativeParallelMultiHashMap<int, int>>(16, CommonRwdAllocator.Handle);
674
675 mapNested.Add(14, mapInner);
676 mapNestedStruct.Add(17, mapStruct);
677
678 mapNested.Dispose();
679 mapNestedStruct.Dispose();
680 mapInner.Dispose();
681 }
682}