From e941fdf8db0a58b3cf3faaaccb813b6b73ca5481 Mon Sep 17 00:00:00 2001 From: mlemanczyk Date: Sat, 9 Sep 2023 18:19:28 +0200 Subject: [PATCH] Unit tests extended. Various fixes. README.md updated. --- README.md | 15 +- Recyclable.Collections/RecyclableList.cs | 11 +- .../RecyclableLongList.Enumerator.cs | 8 +- .../RecyclableLongList.Helpers.cs | 26 +- Recyclable.Collections/RecyclableLongList.cs | 135 ++++-- .../RecyclableListTests.cs | 383 +++++++++++++---- .../RecyclableLongListTestData.cs | 5 +- .../RecyclableLongListTests.cs | 398 +++++++++++++++--- 8 files changed, 779 insertions(+), 202 deletions(-) diff --git a/README.md b/README.md index 6915b30..22f280a 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,12 @@ 1. ✅ `InsertAt` to allow inserting items anywhere 1. ✅ `RemoveAt` to allow removing item at any index 1. ✅ Release 0.0.4 + 1. ✅ Extend unit tests + 1. ✅ `.Add` / `.AddRange` must accept `null` values + 1. ✅ `.AddRange` must accept empty collections + 1. ✅ `.Remove` / `.RemoveAt` / `.Clear` / `.Dispose` must clear reference when reference type + 1. ✅ `.Clear` / `.Dispose` must return arrays to pools + 1. ✅ `int` / `long` variants of `.Count` / `.LongCount`, `.IndexOf` / `.LongIndexOf`, `.Insert`, `.RemoveAt`, `this[]` 1. 👉 Add `.ToRecyclableList` / `.ToRecyclableLongList` variants for all supported collection types 1. 🅿️ `RecyclableList` 1. 🅿️ `RecyclableLongList` @@ -187,9 +193,9 @@ 1. 🅿️ Multi-threading benchmarks 1. 🅿️ Release 0.0.13 1. 🅿️ Extend unit tests - 1. 🅿️ `.Add` / `.AddRange` must allow `null` values - 1. 🅿️ `.Remove` / `.RemoveAt` / `.Clear` must clear reference when reference type 1. 🅿️ `RecyclableLongListExtensions.CopyTo` + 1. 🅿️ Pools + 1. 🅿️ Multi-threading 1. 🅿️ Cleanup 1. 🅿️ Replace `LastBlockWithData` property with `_lastBlockWithData` field 1. 🅿️ Replace properties with field refs, where possible @@ -198,13 +204,16 @@ 1. 🅿️ `MathUtils` 1. 🅿️ Resolve TODOs 1. 🅿️ Remove obsolete methods - 1. 🅿️ `RecyclableLongListHelpers.Enumerate` to benchmarks + 1. 🅿️ Add`RecyclableLongListHelpers.Enumerate` to benchmarks + 1. 🅿️ Review all classes & add `volatile`, where needed + 1. 🅿️ Remove `expectedItemsCount` parameter from `RecyclableLongList`'s constructor 1. 🅿️ Release 0.0.14 1. 🅿️ Optimize 1. 🅿️ `ListExtensions` 1. 🅿️ `MathUtils` 1. 🅿️ `SystemRandomNumberGenerator` 1. 🅿️ `RecyclableLongListExtensions` + 1. 🅿️ Memory release in pools to utilize GC events in case of memory pressure 1. 🅿️ Release 0.0.15 1. ✅ Review and remove warnings & hints 1. ✅ Warnings diff --git a/Recyclable.Collections/RecyclableList.cs b/Recyclable.Collections/RecyclableList.cs index 68aee3b..c419a2e 100644 --- a/Recyclable.Collections/RecyclableList.cs +++ b/Recyclable.Collections/RecyclableList.cs @@ -272,11 +272,6 @@ namespace Recyclable.Collections int oldCount = _count; long sourceItemsCount = items._longCount; - if (sourceItemsCount > int.MaxValue) - { - ThrowHelper.ThrowArgumentOutOfRangeException(nameof(items), $"The number of items exceeds the maximum capacity of {nameof(RecyclableList)}, equal {int.MaxValue}, equal {int.MaxValue}. Please consider using {nameof(RecyclableLongList)}, instead"); - } - if (oldCount + sourceItemsCount > int.MaxValue) { ThrowHelper.ThrowArgumentOutOfRangeException(nameof(items), $"The total number of items in source and target table exceeds the maximum capacity of {nameof(RecyclableList)}, equal {int.MaxValue}. Please consider using {nameof(RecyclableLongList)}, instead"); @@ -337,7 +332,7 @@ namespace Recyclable.Collections { AddRange(sourceIList); } - else if (items.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity)) + else if (items.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity) && requiredAdditionalCapacity != 0) { AddRangeWithKnownCount(items, _count, requiredAdditionalCapacity); _version++; @@ -388,7 +383,7 @@ namespace Recyclable.Collections if (index < oldCount) { - new Span(_memoryBlock, index, oldCount) + new Span(_memoryBlock, index, oldCount -= index) .CopyTo(new Span(_memoryBlock, index + 1, oldCount)); } @@ -468,11 +463,11 @@ namespace Recyclable.Collections if (_capacity >= RecyclableDefaults.MinPooledArrayLength) { - _capacity = 0; // If anything, it has been already cleared by .Clear(), Remove() or RemoveAt() methods, as the list was modified / disposed. RecyclableArrayPool.ReturnShared(_memoryBlock, false); } + _capacity = 0; GC.SuppressFinalize(this); } } diff --git a/Recyclable.Collections/RecyclableLongList.Enumerator.cs b/Recyclable.Collections/RecyclableLongList.Enumerator.cs index e02b82e..6fb02cc 100644 --- a/Recyclable.Collections/RecyclableLongList.Enumerator.cs +++ b/Recyclable.Collections/RecyclableLongList.Enumerator.cs @@ -26,7 +26,7 @@ namespace Recyclable.Collections { var list = _list; - if (_currentBlockIndex < list._lastBlockWithData) + if (_currentBlockIndex <= list._lastBlockWithData && _currentItemIndex <= (_currentBlockIndex == list._lastBlockWithData && list._nextItemIndex > 0 ? list._nextItemIndex - 1: list._blockSizeMinus1)) { _current = list._memoryBlocks[_currentBlockIndex][_currentItemIndex]; if (_currentItemIndex + 1 < list._blockSize) @@ -42,12 +42,6 @@ namespace Recyclable.Collections return true; } // Move to the next item if we're within block boundaries, otherwise report end of collection. - else if (list._longCount > 0 && _currentItemIndex < (list._nextItemIndex > 0 ? list._nextItemIndex : list._blockSize)) - { - _current = list._memoryBlocks[_currentBlockIndex][_currentItemIndex]; - _currentItemIndex++; - return true; - } return false; } diff --git a/Recyclable.Collections/RecyclableLongList.Helpers.cs b/Recyclable.Collections/RecyclableLongList.Helpers.cs index 5077712..6012fd6 100644 --- a/Recyclable.Collections/RecyclableLongList.Helpers.cs +++ b/Recyclable.Collections/RecyclableLongList.Helpers.cs @@ -11,24 +11,21 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] public static void CopyFollowingItems(RecyclableLongList list, long destinationItemIndex) { - T[] currentBlock; - T[][] memoryBlocks = list._memoryBlocks; int blockSizeMinus1 = list._blockSizeMinus1, targetItemIndex = (int)(destinationItemIndex & blockSizeMinus1), - targetBlockIndex = (int)(destinationItemIndex >> list._blockSizePow2BitShift) + (targetItemIndex != 0 ? 1 : 0), + targetBlockIndex = (int)(destinationItemIndex >> list._blockSizePow2BitShift) + (targetItemIndex + 1 == 0 ? 1 : 0), lastTakenBlockIndex = list._lastBlockWithData - 1; // Each iteration should // 1. copy the remaining items in the current block (those after the deleted item) // 2. copy the 1st item from the next block. We deleted 1 item which leaves 1 item gap in the block. // We need to fill it. - currentBlock = memoryBlocks[targetBlockIndex]; - if (targetItemIndex < blockSizeMinus1) - { - new Span(currentBlock, targetItemIndex + 1, blockSizeMinus1 - targetItemIndex) - .CopyTo(new Span(currentBlock, targetItemIndex, blockSizeMinus1 - targetItemIndex)); - } + T[][] memoryBlocks = list._memoryBlocks; + T[] currentBlock = memoryBlocks[targetBlockIndex]; + new Span(currentBlock, targetItemIndex + 1, blockSizeMinus1 - targetItemIndex) + .CopyTo(new Span(currentBlock, targetItemIndex, blockSizeMinus1 - targetItemIndex)); + while (targetBlockIndex < lastTakenBlockIndex) { new Span(currentBlock)[blockSizeMinus1] = (currentBlock = memoryBlocks[++targetBlockIndex])[0]; @@ -36,7 +33,7 @@ namespace Recyclable.Collections .CopyTo(new Span(currentBlock)); } - if (lastTakenBlockIndex >= 0) + if (targetBlockIndex == lastTakenBlockIndex) { new Span(currentBlock)[blockSizeMinus1] = (currentBlock = memoryBlocks[targetBlockIndex + 1])[0]; new Span(currentBlock, 1, list._nextItemIndex != 0 ? list._nextItemIndex - 1 : blockSizeMinus1) @@ -51,7 +48,7 @@ namespace Recyclable.Collections T[][] memoryBlocks = list._memoryBlocks; int blockSizeMinus1 = list._blockSizeMinus1, targetItemIndex = (int)(startingItemIndex & blockSizeMinus1), - targetBlockIndex = (int)(startingItemIndex >> list._blockSizePow2BitShift) + (targetItemIndex != 0 ? 1 : 0), + targetBlockIndex = (int)(startingItemIndex >> list._blockSizePow2BitShift) + (targetItemIndex + 1 == 0 ? 1 : 0), lastTakenBlockIndex = list._lastBlockWithData; if (list._nextItemIndex == 0) @@ -67,11 +64,8 @@ namespace Recyclable.Collections new Span(currentBlock)[0] = memoryBlocks[currentBlockIndex][blockSizeMinus1]; } - if (lastTakenBlockIndex >= 0) - { - new Span(currentBlock = memoryBlocks[targetBlockIndex], targetItemIndex, blockSizeMinus1 - targetItemIndex) - .CopyTo(new Span(currentBlock, targetItemIndex + 1, blockSizeMinus1 - targetItemIndex)); - } + new Span(currentBlock = memoryBlocks[targetBlockIndex], targetItemIndex, blockSizeMinus1 - targetItemIndex) + .CopyTo(new Span(currentBlock, targetItemIndex + 1, blockSizeMinus1 - targetItemIndex)); new Span(memoryBlocks[targetBlockIndex])[targetItemIndex] = item; } diff --git a/Recyclable.Collections/RecyclableLongList.cs b/Recyclable.Collections/RecyclableLongList.cs index f161fc7..86affde 100644 --- a/Recyclable.Collections/RecyclableLongList.cs +++ b/Recyclable.Collections/RecyclableLongList.cs @@ -341,17 +341,14 @@ namespace Recyclable.Collections public RecyclableLongList(IEnumerable source, int minBlockSize = RecyclableDefaults.BlockSize, long? expectedItemsCount = default) { - minBlockSize = checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize)); - + Helpers.SetupBlockArrayPooling(this, minBlockSize = checked((int)BitOperations.RoundUpToPowerOf2((uint)minBlockSize))); if (expectedItemsCount > 0) { - Helpers.SetupBlockArrayPooling(this, minBlockSize); _capacity = Helpers.Resize(this, minBlockSize, _blockSizePow2BitShift, checked((int)BitOperations.RoundUpToPowerOf2((uint)expectedItemsCount.Value))); _blockSizeMinus1 = minBlockSize - 1; } else { - Helpers.SetupBlockArrayPooling(this, minBlockSize); _blockSizeMinus1 = _blockSize - 1; _memoryBlocks = _emptyMemoryBlocksArray; } @@ -486,14 +483,22 @@ namespace Recyclable.Collections if (itemsSpan.Length > 0) { itemsSpan.CopyTo(targetBlockArraySpan); - _nextItemBlockIndex = targetBlockIndex; + if (_nextItemBlockIndex != targetBlockIndex) + { + _nextItemIndex = itemsSpan.Length; + _nextItemBlockIndex = targetBlockIndex; + } + else + { + _nextItemIndex += itemsSpan.Length; + } } else { _nextItemBlockIndex = targetBlockIndex + 1; + _nextItemIndex = 0; } - _nextItemIndex = itemsSpan.Length; _longCount = targetCapacity; _lastBlockWithData = targetBlockIndex; _version++; @@ -532,22 +537,38 @@ namespace Recyclable.Collections targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex], 0, blockSize); } - _nextItemIndex = itemsSpan.Length; - itemsSpan.CopyTo(targetBlockArraySpan); + if (itemsSpan.Length > 0) + { + itemsSpan.CopyTo(targetBlockArraySpan); + if (_nextItemBlockIndex != targetBlockIndex) + { + _nextItemIndex = itemsSpan.Length; + _nextItemBlockIndex = targetBlockIndex; + } + else + { + _nextItemIndex += itemsSpan.Length; + } + } + else + { + _nextItemBlockIndex = targetBlockIndex; + _nextItemIndex = 0; + } + _longCount = targetCapacity; - _nextItemBlockIndex = targetBlockIndex; _lastBlockWithData = targetBlockIndex - (itemsSpan.Length > 0 ? 0 : 1); _version++; } - public void AddRange(ReadOnlySpan itemsSpan) + public void AddRange(ReadOnlySpan items) { - if (itemsSpan.Length == 0) + if (items.Length == 0) { return; } - long targetCapacity = _longCount + itemsSpan.Length; + long targetCapacity = _longCount + items.Length; if (_capacity < targetCapacity) { _capacity = Helpers.Resize(this, _blockSize, _blockSizePow2BitShift, checked((long)BitOperations.RoundUpToPowerOf2((ulong)targetCapacity))); @@ -559,11 +580,11 @@ namespace Recyclable.Collections Span memoryBlocksSpan = new(_memoryBlocks, 0, memoryBlockCount); Span targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex], _nextItemIndex, blockSize - _nextItemIndex); - while (itemsSpan.Length >= targetBlockArraySpan.Length) + while (items.Length >= targetBlockArraySpan.Length) { - itemsSpan[..targetBlockArraySpan.Length].CopyTo(targetBlockArraySpan); - itemsSpan = itemsSpan[targetBlockArraySpan.Length..]; - if (itemsSpan.Length == 0) + items[..targetBlockArraySpan.Length].CopyTo(targetBlockArraySpan); + items = items[targetBlockArraySpan.Length..]; + if (items.Length == 0) { break; } @@ -572,17 +593,25 @@ namespace Recyclable.Collections targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex], 0, blockSize); } - if (itemsSpan.Length > 0) + if (items.Length > 0) { - itemsSpan.CopyTo(targetBlockArraySpan); - _nextItemBlockIndex = targetBlockIndex; + items.CopyTo(targetBlockArraySpan); + if (_nextItemBlockIndex != targetBlockIndex) + { + _nextItemIndex = items.Length; + _nextItemBlockIndex = targetBlockIndex; + } + else + { + _nextItemIndex += items.Length; + } } else { _nextItemBlockIndex = targetBlockIndex + 1; + _nextItemIndex = 0; } - _nextItemIndex = itemsSpan.Length; _longCount = targetCapacity; _lastBlockWithData = targetBlockIndex; _version++; @@ -749,10 +778,26 @@ namespace Recyclable.Collections targetBlockArraySpan = new(memoryBlocksSpan[targetBlockIndex], 0, blockSize); } - itemsSpan.CopyTo(targetBlockArraySpan); + if (itemsSpan.Length > 0) + { + itemsSpan.CopyTo(targetBlockArraySpan); + if (_nextItemBlockIndex != targetBlockIndex) + { + _nextItemIndex = itemsSpan.Length; + _nextItemBlockIndex = targetBlockIndex; + } + else + { + _nextItemIndex += itemsSpan.Length; + } + } + else + { + _nextItemIndex = 0; + } + _longCount = targetCapacity; _nextItemBlockIndex = targetBlockIndex; - _nextItemIndex = itemsSpan.Length; _lastBlockWithData = targetBlockIndex - (itemsSpan.Length > 0 ? 0 : 1); } finally @@ -766,36 +811,36 @@ namespace Recyclable.Collections } } - public void AddRange(IEnumerable source) + public void AddRange(IEnumerable items) { - if (source is RecyclableLongList sourceRecyclableLongList) + if (items is RecyclableLongList sourceRecyclableLongList) { AddRange(sourceRecyclableLongList); } - else if (source is RecyclableList sourceRecyclableList) + else if (items is RecyclableList sourceRecyclableList) { AddRange(sourceRecyclableList); } - else if (source is T[] sourceArray) + else if (items is T[] sourceArray) { AddRange(sourceArray); } - else if (source is List sourceList) + else if (items is List sourceList) { AddRange(sourceList); } - else if (source is IList sourceIList) + else if (items is IList sourceIList) { AddRange(sourceIList); } - else if (source.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity)) + else if (items.TryGetNonEnumeratedCount(out var requiredAdditionalCapacity)) { - AddRangeWithKnownCount(source, requiredAdditionalCapacity); + AddRangeWithKnownCount(items, requiredAdditionalCapacity); _version++; } else { - AddRangeEnumerated(source); + AddRangeEnumerated(items); _version++; } } @@ -807,16 +852,24 @@ namespace Recyclable.Collections return; } + T[][] memoryBlocks = _memoryBlocks; + int memoryBlocksCount = _reservedBlockCount; + int toRemoveIdx = 0; + if (_blockSize >= RecyclableDefaults.MinPooledArrayLength) { - T[][] memoryBlocks = _memoryBlocks; - int memoryBlocksCount = _reservedBlockCount; - int toRemoveIdx = 0; while (toRemoveIdx < memoryBlocksCount) { RecyclableArrayPool.ReturnShared(memoryBlocks[toRemoveIdx++], _needsClearing); } } + else if (_needsClearing) + { + while (toRemoveIdx < memoryBlocksCount) + { + Array.Clear(memoryBlocks[toRemoveIdx++]); + } + } // TODO: Do we want to initialize with empty arrays or null refs? Null refs may be faster. Array.Fill(_memoryBlocks, _emptyBlockArray, 0, _reservedBlockCount); @@ -954,17 +1007,19 @@ namespace Recyclable.Collections new Span(_memoryBlocks[_nextItemBlockIndex])[_nextItemIndex] = item; } - if (_nextItemIndex < _blockSizeMinus1) + if (++_nextItemIndex == _blockSize) { - if (++_nextItemIndex == 1) + if (_nextItemBlockIndex > _lastBlockWithData) { _lastBlockWithData++; } - } - else - { - _nextItemIndex = 0; + _nextItemBlockIndex++; + _nextItemIndex = 0; + } + else if (_nextItemIndex == 1 && _nextItemBlockIndex > _lastBlockWithData) + { + _lastBlockWithData++; } _version++; diff --git a/Recyclable.CollectionsTests/RecyclableListTests.cs b/Recyclable.CollectionsTests/RecyclableListTests.cs index 9104730..0fada0a 100644 --- a/Recyclable.CollectionsTests/RecyclableListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableListTests.cs @@ -8,15 +8,32 @@ namespace Recyclable.CollectionsTests { public class RecyclableListTests { + [Fact] + public void AddRangeShouldAcceptNulls() + { + // Prepare + using var list = new RecyclableList(); + + // Act + list.AddRange(new long?[] { null, default }); + + // Validate + _ = list.Should().HaveCount(2).And.AllSatisfy(x => x.Should().BeNull()); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void AddRangeShouldAddItemsInCorrectOrder(string testCase, IEnumerable testData, long itemsCount) + public void AddRangeShouldAddItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount) { // Prepare using var list = new RecyclableList(); // Act - if (testData is long[] testDataArray) + if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + list.AddRange(new ReadOnlySpan((long[])testData)); + } + else if (testData is long[] testDataArray) { list.AddRange(testDataArray); } @@ -46,20 +63,24 @@ namespace Recyclable.CollectionsTests } // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount, "when capacity == 0, then we should allocate as much memory as needed, only"); - _ = list.Count.Should().Be((int)itemsCount); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount); _ = list.Should().Equal(testData); } [Theory] [MemberData(nameof(RecyclableLongListTestData.EmptySourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void AddRangeShouldDoNothingWhenSourceIsEmpty(string testCase, IEnumerable testData, long itemsCount) + public void AddRangeShouldDoNothingWhenSourceIsEmpty(string testCase, IEnumerable testData, int itemsCount) { // Prepare using var list = new RecyclableList(); // Act - if (testData is long[] testDataArray) + if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + list.AddRange(new ReadOnlySpan((long[])testData)); + } + else if (testData is long[] testDataArray) { list.AddRange(testDataArray); } @@ -90,19 +111,25 @@ namespace Recyclable.CollectionsTests // Validate _ = list.Capacity.Should().Be(RecyclableDefaults.InitialCapacity); - _ = list.Count.Should().Be(0); + _ = list.Count.Should().Be(itemsCount); _ = list.Should().BeEmpty(); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void AddRangeShouldNotOverrideItems(string testCase, IEnumerable testData, long itemsCount) + public void AddRangeShouldNotOverrideItems(string testCase, IEnumerable testData, int itemsCount) { // Prepare using var list = new RecyclableList(); // Act - if (testData is long[] testDataArray) + if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + var readOnlySpan = new ReadOnlySpan((long[])testData); + list.AddRange(readOnlySpan); + list.AddRange(readOnlySpan); + } + else if (testData is long[] testDataArray) { list.AddRange(testDataArray); list.AddRange(testDataArray); @@ -138,23 +165,18 @@ namespace Recyclable.CollectionsTests } // Validate - var expectedData = testData.Concat(testData).ToArray(); - - if (itemsCount > 0) - { - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount * 2); - } - - _ = list.Count.Should().Be(expectedData.Length); + var expectedData = testData.Concat(testData); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); + _ = list.Count.Should().Be(itemsCount * 2); _ = list.Should().Equal(expectedData); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void AddShouldAcceptDuplicates(string testCase, IEnumerable testData, long itemsCount) + public void AddShouldAcceptDuplicates(string testCase, IEnumerable testData, int itemsCount) { // Prepare - testData = testData.Concat(testData).ToArray(); + testData = testData.Concat(testData); // Act using var list = new RecyclableList(); @@ -164,13 +186,31 @@ namespace Recyclable.CollectionsTests } // Validate - _ = list.Count.Should().Be((int)(itemsCount * 2)); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); + _ = list.Count.Should().Be(itemsCount * 2); _ = list.Should().Equal(testData); } + [Fact] + public void AddShouldAcceptNulls() + { + // Prepare +#pragma warning disable IDE0028 + using var list = new RecyclableList(); + + // Act + list.Add(null); + list.Add(default); +#pragma warning restore IDE0028 + + // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(RecyclableDefaults.InitialCapacity); + _ = list.Should().AllSatisfy(x => x.Should().BeNull()); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void AddShouldAddItems(string testCase, IEnumerable testData, long itemsCount) + public void AddShouldAddItems(string testCase, IEnumerable testData, int itemsCount) { // Prepare using var list = new RecyclableList(); @@ -183,14 +223,14 @@ namespace Recyclable.CollectionsTests } // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount); - _ = list.Count.Should().Be((int)itemsCount); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount); _ = list.Should().Equal(testData); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void AddShouldAddItemWhenAfterAddRange(string testCase, IEnumerable testData, long itemsCount) + public void AddShouldAddItemWhenAfterAddRange(string testCase, IEnumerable testData, int itemsCount) { // Act using var list = new RecyclableList(); @@ -201,34 +241,56 @@ namespace Recyclable.CollectionsTests } // Validate - _ = list.Count.Should().Be((int)(itemsCount * 2)); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); + _ = list.Count.Should().Be(itemsCount * 2); _ = list.Should().Equal(testData.Concat(testData)); } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceRefDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ClearShouldClearItemsWhenReferenceType(string testCase, IEnumerable testData, int itemsCount) + { + if (itemsCount == 0) + { + /// Clearing empty list is tested in . + return; + } + + // Arrange + using var list = new RecyclableList(testData, itemsCount); + _ = list.AsArray[..itemsCount].Should().AllSatisfy(x => x.Should().NotBeNull()); + + // Act + list.Clear(); + + // Assert + _ = list.AsArray[..itemsCount].Should().AllSatisfy(x => x.Should().BeNull()); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void ClearShouldRemoveAllItems(string testCase, IEnumerable testData, long itemsCount) + public void ClearShouldRemoveAllItems(string testCase, IEnumerable testData, int itemsCount) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); - _ = list.Count.Should().Be((int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); + _ = list.Count.Should().Be(itemsCount); // Act list.Clear(); list.Clear(); // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.Count.Should().Be(0); _ = list.Should().BeEmpty(); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void ConsecutiveDisposeShouldSucceed(string testCase, IEnumerable testData, long itemsCount) + public void ConsecutiveDisposeShouldSucceed(string testCase, IEnumerable testData, int itemsCount) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); list.Dispose(); // Act @@ -237,41 +299,45 @@ namespace Recyclable.CollectionsTests [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void ConstructorShouldAcceptDuplicates(string testCase, IEnumerable testData, long itemsCount) + public void ConstructorShouldAcceptDuplicates(string testCase, IEnumerable testData, int itemsCount) { + // Prepare + testData = testData.Concat(testData); + // Act - using var list = new RecyclableList(testData.Concat(testData), (int)itemsCount * 2); + using var list = new RecyclableList(testData, itemsCount * 2); // Validate - _ = list.Count.Should().Be((int)itemsCount * 2); - _ = list.Should().Equal(testData.Concat(testData)); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); + _ = list.Count.Should().Be(itemsCount * 2); + _ = list.Should().Equal(testData); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void ConstructorSourceShouldInitializeList(string testCase, IEnumerable testData, long itemsCount) + public void ConstructorSourceShouldInitializeList(string testCase, IEnumerable testData, int itemsCount) { // Act - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount, "when we succeed in taking count without enumerating, then we allocate as much memory as needed, only"); - _ = list.Count.Should().Be((int)itemsCount); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount); _ = list.Should().Equal(testData); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void ContainsShouldFindAllItems(string testCase, IEnumerable testData, long itemsCount, in long[] itemIndexes) + public void ContainsShouldFindAllItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); // Act & Validate foreach (var itemIndex in itemIndexes) { - _ = list.Contains(list[(int)itemIndex]).Should().BeTrue(); + _ = list.Contains(list[(int)itemIndex]).Should().BeTrue($"we searched for {list[(int)itemIndex]}"); } } @@ -288,10 +354,10 @@ namespace Recyclable.CollectionsTests [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void ContainsShouldNotFindNonExistingItems(string testCase, IEnumerable testData, long itemsCount, in long[] itemIndexes) + public void ContainsShouldNotFindNonExistingItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); // Act & Validate @@ -303,10 +369,10 @@ namespace Recyclable.CollectionsTests [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void CopyToShouldCopyAllItemsInTheCorrectOrder(string testCase, IEnumerable testData, long itemsCount) + public void CopyToShouldCopyAllItemsInTheCorrectOrder(string testCase, IEnumerable testData, int itemsCount) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); long[] copiedItems = new long[itemsCount]; // Act @@ -317,14 +383,72 @@ namespace Recyclable.CollectionsTests } [Theory] - [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void EnumerateShouldYieldAllItemsInCorrectOrder(string testCase, IEnumerable testData, long itemsCount) + [MemberData(nameof(RecyclableLongListTestData.SourceRefDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void DisposeShouldClearItemsWhenReferenceType(string testCase, IEnumerable testData, int itemsCount) { - // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + if (itemsCount == 0) + { + /// Disposing empty list is tested in . + return; + } + + // Arrange + using var list = new RecyclableList(testData, itemsCount); + var listArray = list.AsArray; + _ = list.AsArray[..itemsCount].Should().NotBeNull(); + _ = listArray[..itemsCount].Should().AllSatisfy(x => x.Should().NotBeNull()); // Act - var yieldedItems = new List((int)itemsCount); + list.Dispose(); + + // Assert + _ = listArray[..itemsCount].Should().AllSatisfy(x => x.Should().BeNull()); + } + + [Fact] + public void DisposeShouldReturnPooledArrayOnlyOnce() + { + // Arrange + var testData = RecyclableLongListTestData.CreateTestData(RecyclableDefaults.MinPooledArrayLength); + RecyclableList firstInstance = new(testData); + var firstArray = firstInstance.AsArray; + + // Act + firstInstance.Dispose(); + + // Assert + using RecyclableList sameInstance = new(testData); + _ = sameInstance.AsArray.Should().BeSameAs(firstArray); + + using RecyclableList anotherInstance = new(testData); + _ = anotherInstance.AsArray.Should().NotBeSameAs(firstArray); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void DisposeShouldSucceed(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + + // Act + list.Dispose(); + + // Validate + _ = list.Capacity.Should().Be(0); + _ = list.Count.Should().Be(0); + _ = list.Should().BeEmpty(); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void EnumerateShouldYieldAllItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + + // Act + var yieldedItems = new List(itemsCount); using var enumerator = list.GetEnumerator(); while (enumerator.MoveNext()) { @@ -332,10 +456,23 @@ namespace Recyclable.CollectionsTests } // Validate - _ = yieldedItems.Count.Should().Be((int)itemsCount); _ = yieldedItems.Should().Equal(testData); } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void GetByIndexShouldReturnCorrectItem(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableLongList(testData, itemsCount); + + // Act & Validate + foreach (var item in testData) + { + _ = list[item - 1].Should().Be(item); + } + } + [Fact] public void IndexOfShouldNotFindAnythingWhenListEmpty() { @@ -349,10 +486,10 @@ namespace Recyclable.CollectionsTests [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void IndexOfShouldNotFindNonExistingItems(string testCase, IEnumerable testData, long itemsCount, in long[] itemIndexes) + public void IndexOfShouldNotFindNonExistingItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); _ = testData.Any().Should().BeTrue("we need items on the list that we can look for"); // Act & Validate @@ -364,10 +501,10 @@ namespace Recyclable.CollectionsTests [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void IndexOfShouldReturnCorrectIndexes(string testCase, IEnumerable testData, long itemsCount, in long[] itemIndexes) + public void IndexOfShouldReturnCorrectIndexes(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); // Act & Validate foreach (var itemIndex in itemIndexes) @@ -378,17 +515,21 @@ namespace Recyclable.CollectionsTests [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void InitializeSortClearAddRangeRemoveShouldSucceed(string testCase, IEnumerable testData, long itemsCount, in long[] itemIndexes) + public void InitializeSortClearAddRangeRemoveShouldSucceed(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { foreach (var itemIndex in itemIndexes) { + // Prepare + var testDataList = testData.ToList(); + var item = testDataList[(int)itemIndex]; + // Act - using RecyclableList list = new(testData, (int)itemsCount); + using RecyclableList list = new(testData); QuickSortExtensions.QuickSort(list); // Validate - _ = list.Count.Should().Be((int)itemsCount); - _ = list.Should().Equal(testData); + _ = list.Count.Should().Be(itemsCount); + _ = list.Should().Equal(testDataList); // Act list.Clear(); @@ -401,22 +542,29 @@ namespace Recyclable.CollectionsTests list.AddRange(testData); // Validate - _ = list.Count.Should().Be((int)itemsCount); - _ = list.Should().Equal(testData); + _ = list.Count.Should().Be(itemsCount); + _ = list.Should().Equal(testDataList); + + // Prepare + testDataList.RemoveAt((int)itemIndex); // Act - _ = list.Remove(list[(int)itemIndex]).Should().BeTrue(); + _ = list.Remove(item).Should().BeTrue(); + _ = list.IndexOf(item).Should().Be(RecyclableDefaults.ItemNotFoundIndex); + _ = list.Contains(item).Should().BeFalse(); + _ = list.Count.Should().Be(itemsCount - 1); + _ = list.Should().Equal(testDataList); } } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void InsertShouldMoveItems(string testCase, IEnumerable testData, long itemsCount, in long[] itemIndexes) + public void InsertShouldMoveItems(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { foreach (var itemIndex in itemIndexes) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); var item = list[(int)itemIndex]; var expectedItems = list.ToList(); expectedItems.Insert((int)itemIndex, item); @@ -425,22 +573,40 @@ namespace Recyclable.CollectionsTests list.Insert((int)itemIndex, item); // Validate - _ = list.Count.Should().Be((int)itemsCount + 1); - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount); _ = list[(int)itemIndex].Should().Be(item); _ = list[(int)itemIndex + 1].Should().Be(item); + _ = list.Count.Should().Be(itemsCount + 1); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.Should().Equal(expectedItems); } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceRefDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveAtShouldClearItemWhenReferenceType(string testCase, IEnumerable testData, int itemsCount, in long[] indexesToRemove) + { + // Arrange + foreach (int indexToRemove in indexesToRemove) + { + using var list = new RecyclableList(testData, itemsCount); + _ = list[^1].Should().NotBeNull(); + + // Act + list.RemoveAt(indexToRemove); + + // Assert + _ = list[list.Count].Should().BeNull(); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void RemoveAtShouldSucceed(string testCase, IEnumerable testData, long itemsCount, in long[] itemIndexes) + public void RemoveAtShouldRemoveTheCorrectItem(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { foreach (var itemIndex in itemIndexes) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); var expectedItems = testData.ToList(); expectedItems.RemoveAt((int)itemIndex); @@ -448,42 +614,103 @@ namespace Recyclable.CollectionsTests list.RemoveAt((int)itemIndex); // Validate - _ = list.Count.Should().Be((int)(itemsCount - 1)); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount - 1); _ = list.Should().Equal(expectedItems); - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceRefDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveShouldClearItemWhenReferenceType(string testCase, IEnumerable testData, int itemsCount, in long[] indexesToRemove) + { + var expectedItems = testData.ToArray(); + + foreach (int indexToRemove in indexesToRemove) + { + // Arrange + var itemToRemove = expectedItems[indexToRemove]; + using var list = new RecyclableList(testData, itemsCount); + _ = list[^1].Should().NotBeNull(); + + // Act + list.Remove(itemToRemove); + + // Assert + _ = list[list.Count].Should().BeNull(); } } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void RemoveShouldRemoveTheCorrectItem(string testCase, IEnumerable testData, long itemsCount, in long[] itemIndexes) + public void RemoveShouldRemoveTheCorrectItem(string testCase, IEnumerable testData, int itemsCount, in long[] itemIndexes) { foreach (var itemIndex in itemIndexes) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); - var item = list[(int)itemIndex]; + using var list = new RecyclableList(testData, itemsCount); var expectedItems = testData.ToList(); + var item = list[(int)itemIndex]; expectedItems.RemoveAt((int)itemIndex); // Act _ = list.Remove(item).Should().BeTrue(); // Validate - _ = list.Count.Should().Be((int)(itemsCount - 1)); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.Count.Should().Be(itemsCount - 1); _ = list.Should().Equal(expectedItems); } } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] - public void VersionedEnumerateShouldYieldAllItemsInCorrectOrder(string testCase, IEnumerable testData, long itemsCount) + public void SetByIndexShouldReturnCorrectItem(string testCase, IEnumerable testData, int itemsCount) { // Prepare - using var list = new RecyclableList(testData, (int)itemsCount); + using var list = new RecyclableList(testData, itemsCount); + var expectedData = testData.ToList(); + + // Act & Validate + foreach (long item in testData) + { + // Act + list[(int)item - 1] = item + 1; + + // Validate + expectedData[(int)item - 1] = item + 1; + list[(int)item - 1].Should().Be(item + 1); + list.Should().Equal(expectedData); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void VersionedEnumerateShouldRaiseExceptionWhenCollectionIsModified(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); // Act - var yieldedItems = new List((int)itemsCount); + Assert.Throws(() => + { + list.Add(-1); + foreach (var item in (IRecyclableVersionedList)list) + { + list.Add(item); + } + }); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataVariants), MemberType = typeof(RecyclableLongListTestData))] + public void VersionedEnumerateShouldYieldAllItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount) + { + // Prepare + using var list = new RecyclableList(testData, itemsCount); + + // Act + var yieldedItems = new List(itemsCount); foreach (var item in (IRecyclableVersionedList)list) { yieldedItems.Add(item); diff --git a/Recyclable.CollectionsTests/RecyclableLongListTestData.cs b/Recyclable.CollectionsTests/RecyclableLongListTestData.cs index 665c589..e1da8e3 100644 --- a/Recyclable.CollectionsTests/RecyclableLongListTestData.cs +++ b/Recyclable.CollectionsTests/RecyclableLongListTestData.cs @@ -30,7 +30,7 @@ namespace Recyclable.CollectionsTests public static readonly IEnumerable ItemsCountVariants = new long[] { //RecyclableDefaults.MinPooledArrayLength - 5, RecyclableDefaults.MinPooledArrayLength - 1, RecyclableDefaults.MinPooledArrayLength, RecyclableDefaults.MinPooledArrayLength + 1, RecyclableDefaults.MinPooledArrayLength + 5, 127, 128, 129, RecyclableDefaults.MinItemsCountForParallelization - 0, 1, 2, 3, 7, 10, 16, RecyclableDefaults.MinPooledArrayLength - 5, RecyclableDefaults.MinPooledArrayLength - 1, RecyclableDefaults.MinPooledArrayLength, RecyclableDefaults.MinPooledArrayLength + 1, RecyclableDefaults.MinPooledArrayLength + 5, RecyclableDefaults.MinPooledArrayLength * 100, RecyclableDefaults.MinItemsCountForParallelization + 0, 1, 2, 3, 7, 10, 16, RecyclableDefaults.MinPooledArrayLength - 5, RecyclableDefaults.MinPooledArrayLength - 1, RecyclableDefaults.MinPooledArrayLength, RecyclableDefaults.MinPooledArrayLength + 1, RecyclableDefaults.MinPooledArrayLength + 5, RecyclableDefaults.MinPooledArrayLength * 100 }; public static readonly IEnumerable BlockSizeVariants = new int[] @@ -83,9 +83,10 @@ namespace Recyclable.CollectionsTests switch (refType) { case false: - IEnumerable testData = CreateTestData(itemsCount).ToArray(); + long[] testData = CreateTestData(itemsCount).ToArray(); yield return new object[] { $"int[{itemsCount}]", testData, itemsCount }; + yield return new object[] { $"ReadOnlySpan[{itemsCount}]", testData, itemsCount }; //GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true, true); yield return new object[] { $"List(itemsCount: {itemsCount}", testData.ToList(), itemsCount }; diff --git a/Recyclable.CollectionsTests/RecyclableLongListTests.cs b/Recyclable.CollectionsTests/RecyclableLongListTests.cs index 24c796b..fd83ed9 100644 --- a/Recyclable.CollectionsTests/RecyclableLongListTests.cs +++ b/Recyclable.CollectionsTests/RecyclableLongListTests.cs @@ -12,12 +12,7 @@ namespace Recyclable.CollectionsTests private static long CalculateExpectedCapacity(string testCase, long itemsCount, int targetBlockSize) { var expectedCapacity = (long)BitOperations.RoundUpToPowerOf2((uint)itemsCount + 1); - if (testCase.Contains("non-enumerated")) - { - expectedCapacity = (long)BitOperations.RoundUpToPowerOf2((ulong)itemsCount + RecyclableDefaults.BlockSize); - expectedCapacity = expectedCapacity >= RecyclableDefaults.InitialCapacity ? expectedCapacity : RecyclableDefaults.InitialCapacity; - } - else if (targetBlockSize > itemsCount) + if (targetBlockSize > itemsCount) { expectedCapacity = ((itemsCount / targetBlockSize) + (itemsCount % targetBlockSize > 0 ? 1 : 0)) * targetBlockSize; } @@ -25,6 +20,19 @@ namespace Recyclable.CollectionsTests return expectedCapacity; } + [Fact] + public void AddRangeShouldAcceptNulls() + { + // Prepare + using var list = new RecyclableLongList(); + + // Act + list.AddRange(new long?[] { null, default }); + + // Validate + _ = list.Should().HaveCount(2).And.AllSatisfy(x => x.Should().BeNull()); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] public void AddRangeShouldAddItemsInCorrectOrder(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) @@ -33,7 +41,11 @@ namespace Recyclable.CollectionsTests using var list = new RecyclableLongList(minBlockSize: targetBlockSize); // Act - if (testData is long[] testDataArray) + if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + list.AddRange(new ReadOnlySpan((long[])testData)); + } + else if (testData is long[] testDataArray) { list.AddRange(testDataArray); } @@ -63,9 +75,9 @@ namespace Recyclable.CollectionsTests } // Validate - int expectedItemsCount = testData.Count(); - _ = list.Capacity.Should().BeGreaterThanOrEqualTo(expectedItemsCount, "when capacity == 0, then we should allocate as much memory as needed, only"); - _ = list.LastBlockWithData.Should().Be((int)(itemsCount / list.BlockSize) - (itemsCount % list.BlockSize != 0 ? 0 : 1)); + long expectedItemsCount = itemsCount; + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(expectedItemsCount); + _ = list.LastBlockWithData.Should().Be((int)(itemsCount / targetBlockSize) - (itemsCount % targetBlockSize != 0 ? 0 : 1)); _ = list.LongCount.Should().Be(expectedItemsCount); _ = list.Should().Equal(testData); } @@ -78,7 +90,11 @@ namespace Recyclable.CollectionsTests using var list = new RecyclableLongList(); // Act - if (testData is long[] testDataArray) + if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + list.AddRange(new ReadOnlySpan((long[])testData)); + } + else if (testData is long[] testDataArray) { list.AddRange(testDataArray); } @@ -110,7 +126,7 @@ namespace Recyclable.CollectionsTests // Validate _ = list.Capacity.Should().Be(0); _ = list.LastBlockWithData.Should().Be(-1); - _ = list.LongCount.Should().Be(0); + _ = list.LongCount.Should().Be(itemsCount); _ = list.Should().BeEmpty(); } @@ -122,7 +138,13 @@ namespace Recyclable.CollectionsTests using var list = new RecyclableLongList(minBlockSize: targetBlockSize); // Act - if (testData is long[] testDataArray) + if (testCase.Contains("ReadOnlySpan[", StringComparison.OrdinalIgnoreCase)) + { + var readOnlySpan = new ReadOnlySpan((long[])testData); + list.AddRange(readOnlySpan); + list.AddRange(readOnlySpan); + } + else if (testData is long[] testDataArray) { list.AddRange(testDataArray); list.AddRange(testDataArray); @@ -158,14 +180,9 @@ namespace Recyclable.CollectionsTests } // Validate - var expectedData = testData.Concat(testData).ToArray(); - - if (itemsCount > 0) - { - _ = list.Capacity.Should().BeGreaterThanOrEqualTo(targetBlockSize); - } - - _ = list.LongCount.Should().Be(expectedData.Length); + var expectedData = testData.Concat(testData); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); + _ = list.LongCount.Should().Be(itemsCount * 2); _ = list.LastBlockWithData.Should().Be((int)(itemsCount * 2 / list.BlockSize) - (itemsCount * 2 % list.BlockSize != 0 ? 0 : 1)); _ = list.Should().Equal(expectedData); } @@ -175,7 +192,7 @@ namespace Recyclable.CollectionsTests public void AddShouldAcceptDuplicates(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) { // Prepare - testData = testData.Concat(testData).ToArray(); + testData = testData.Concat(testData); // Act using var list = new RecyclableLongList(minBlockSize: targetBlockSize); @@ -185,11 +202,29 @@ namespace Recyclable.CollectionsTests } // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); _ = list.LongCount.Should().Be(itemsCount * 2); _ = list.LastBlockWithData.Should().Be((int)(itemsCount * 2 / list.BlockSize) - (itemsCount * 2 % list.BlockSize != 0 ? 0 : 1)); _ = list.Should().Equal(testData); } + [Fact] + public void AddShouldAcceptNulls() + { + // Prepare +#pragma warning disable IDE0028 + using var list = new RecyclableLongList(); + + // Act + list.Add(null); + list.Add(default); +#pragma warning restore IDE0028 + + // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(RecyclableDefaults.BlockSize); + _ = list.Should().AllSatisfy(x => x.Should().BeNull()); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] public void AddShouldAddItems(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) @@ -201,9 +236,11 @@ namespace Recyclable.CollectionsTests foreach (var index in testData) { list.Add(index); + _ = list.LongCount.Should().Be(index); } // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.LongCount.Should().Be(itemsCount, "we added so many items"); _ = list.LastBlockWithData.Should().Be((int)(itemsCount / list.BlockSize) - (itemsCount % list.BlockSize == 0 ? 1 : 0)); _ = list.Should().Equal(testData); @@ -223,11 +260,38 @@ namespace Recyclable.CollectionsTests } // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); _ = list.LongCount.Should().Be(itemsCount * 2); _ = list.LastBlockWithData.Should().Be((int)(itemsCount * 2 / list.BlockSize) - (itemsCount * 2 % list.BlockSize != 0 ? 0 : 1)); _ = list.Should().Equal(testData.Concat(testData)); } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceRefDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] + public void ClearShouldClearItemsWhenReferenceType(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) + { + if (itemsCount == 0) + { + /// Clearing empty list is tested in . + return; + } + + // Arrange + using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); + var blocks = list.AsArray.ToArray(); + _ = blocks.Should().AllBeOfType(); + var beforeItems = blocks.SelectMany(x => x.Take(targetBlockSize)).Take((int)itemsCount).ToArray(); + _ = beforeItems.Should().AllSatisfy(x => x.Should().NotBeNull()); + + // Act + list.Clear(); + + // Assert + var afterItems = blocks.SelectMany(x => x.Take(targetBlockSize)).Take((int)itemsCount).ToArray(); + _ = afterItems.Should().AllSatisfy(x => x.Should().BeNull()); + _ = list.AsArray.Should().AllBeOfType().And.AllSatisfy(x => x.Should().BeEmpty()); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] public void ClearShouldRemoveAllItems(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) @@ -241,10 +305,31 @@ namespace Recyclable.CollectionsTests list.Clear(); // Validate + _ = list.Capacity.Should().Be(0); + _ = list.LastBlockWithData.Should().Be(-1); _ = list.LongCount.Should().Be(0); _ = list.Should().BeEmpty(); } + [Fact] + public void ClearShouldReturnPooledArraysOnlyOnce() + { + // Arrange + var testData = RecyclableLongListTestData.CreateTestData(RecyclableDefaults.MinPooledArrayLength * 10); + using RecyclableLongList firstInstances = new(testData, RecyclableDefaults.MinPooledArrayLength); + var firstArrays = firstInstances.AsArray.ToArray(); + + // Act + firstInstances.Clear(); + + // Assert + using RecyclableLongList sameInstances = new(testData, RecyclableDefaults.MinPooledArrayLength); + _ = sameInstances.AsArray.Should().Equal(firstArrays.Reverse()); + + using RecyclableLongList otherInstances = new(testData, RecyclableDefaults.MinPooledArrayLength); + _ = otherInstances.AsArray.Should().NotIntersectWith(firstArrays); + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] public void ConsecutiveDisposeShouldSucceed(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) @@ -262,12 +347,14 @@ namespace Recyclable.CollectionsTests public void ConstructorShouldAcceptDuplicates(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) { // Prepare - testData = testData.Concat(testData).ToArray(); + testData = testData.Concat(testData); // Act using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount * 2); + _ = list.LastBlockWithData.Should().Be((int)(itemsCount * 2 / targetBlockSize) - (itemsCount * 2 % targetBlockSize > 0 ? 0 : 1)); _ = list.LongCount.Should().Be(itemsCount * 2); _ = list.Should().Equal(testData); } @@ -280,7 +367,8 @@ namespace Recyclable.CollectionsTests using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize); // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount, "when we succeed in taking count without enumerating, then we allocate as much memory as needed, only"); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.LastBlockWithData.Should().Be((int)(itemsCount / targetBlockSize) - (itemsCount % targetBlockSize > 0 ? 0 : 1)); _ = list.LongCount.Should().Be(itemsCount); _ = list.Should().Equal(testData); } @@ -334,11 +422,57 @@ namespace Recyclable.CollectionsTests { // Prepare using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); + long[] copiedItems = new long[itemsCount]; // Act - var actual = new long[itemsCount]; - list.CopyTo(actual, 0); - _ = actual.Should().Equal(testData); + list.CopyTo(copiedItems, 0); + + // Validate + _ = copiedItems.Should().Equal(testData); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceRefDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] + public void DisposeShouldClearItemsWhenReferenceType(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) + { + if (itemsCount == 0) + { + /// Disposing empty list is tested in . + return; + } + + // Arrange + using var list = new RecyclableLongList(testData, targetBlockSize, itemsCount); + var blocks = list.AsArray.ToArray(); + _ = blocks.Should().AllBeOfType(); + + _ = blocks.SelectMany(x => x.Take(targetBlockSize)).Take((int)itemsCount).Should().AllSatisfy(x => x.Should().NotBeNull()); + + // Act + list.Dispose(); + + // Assert + _ = blocks.SelectMany(x => x.Take(targetBlockSize)).Take((int)itemsCount).Should().AllSatisfy(x => x.Should().BeNull()); + _ = list.AsArray.Should().AllBeOfType().And.AllSatisfy(x => x.Should().BeEmpty()); + } + + [Fact] + public void DisposeShouldReturnPooledArrayOnlyOnce() + { + // Arrange + var testData = RecyclableLongListTestData.CreateTestData(RecyclableDefaults.MinPooledArrayLength * 10); + using RecyclableLongList firstInstances = new(testData, RecyclableDefaults.MinPooledArrayLength); + var firstArrays = firstInstances.AsArray.ToArray(); + + // Act + firstInstances.Dispose(); + + // Assert + using RecyclableLongList sameInstances = new(testData, RecyclableDefaults.MinPooledArrayLength); + _ = sameInstances.AsArray.Should().BeEquivalentTo(firstArrays); + + using RecyclableLongList otherInstances = new(testData, RecyclableDefaults.MinPooledArrayLength); + _ = otherInstances.AsArray.Should().NotIntersectWith(firstArrays); } [Theory] @@ -352,27 +486,29 @@ namespace Recyclable.CollectionsTests list.Dispose(); // Validate - _ = list.LongCount.Should().Be(0L); + _ = list.Capacity.Should().Be(0); + _ = list.LastBlockWithData.Should().Be(-1); + _ = list.LongCount.Should().Be(0); _ = list.Should().BeEmpty(); } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] - public void EnumerateShouldYieldAllItemsInCorrectOrder(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) + public void EnumerateShouldYieldAllItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount, int targetBlockSize) { // Prepare using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); // Act - var actual = new List((int)itemsCount); + var yieldedItems = new List(itemsCount); using var enumerator = list.GetEnumerator(); while (enumerator.MoveNext()) { - actual.Add(enumerator.Current); + yieldedItems.Add(enumerator.Current); } // Validate - _ = actual.Should().Equal(testData); + _ = yieldedItems.Should().Equal(testData); } [Theory] @@ -383,7 +519,21 @@ namespace Recyclable.CollectionsTests using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); // Act & Validate - foreach (var item in testData) + foreach (long item in testData) + { + _ = list[(int)item - 1].Should().Be(item); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] + public void GetByIndexShouldReturnCorrectItemWhenLong(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) + { + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); + + // Act & Validate + foreach (long item in testData) { _ = list[item - 1].Should().Be(item); } @@ -436,7 +586,7 @@ namespace Recyclable.CollectionsTests foreach (var itemIndex in itemIndexes) { // Prepare - using var testDataList = testData.ToRecyclableList(); + var testDataList = testData.ToList(); var item = testDataList[(int)itemIndex]; // Act @@ -477,23 +627,52 @@ namespace Recyclable.CollectionsTests [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void InsertShouldMoveItems(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] itemIndexes) { + foreach (var itemIndex in itemIndexes) { // Prepare using var list = new RecyclableLongList(testData, targetBlockSize, itemsCount); long expectedCapacity = CalculateExpectedCapacity(testCase, itemsCount, targetBlockSize); + var expectedItems = testData.ToList(); - var item = list[itemIndex]; - var expectedItems = list.ToList(); - expectedItems.Insert((int)itemIndex, item); + var existingItem = expectedItems[(int)itemIndex]; + var newItem = existingItem + 1; + expectedItems.Insert((int)itemIndex, newItem); // Act - list.Insert(itemIndex, item); + list.Insert((int)itemIndex, newItem); // Validate + _ = list[itemIndex].Should().Be(newItem); + _ = list[itemIndex + 1].Should().Be(existingItem); + _ = list.Capacity.Should().Be(expectedCapacity); + _ = list.Count.Should().Be((int)itemsCount + 1); + _ = list.Should().Equal(expectedItems); + } + } - _ = list[itemIndex].Should().Be(item); - _ = list[itemIndex + 1].Should().Be(item); + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void InsertShouldMoveItemsWhenLong(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] itemIndexes) + { + + foreach (var itemIndex in itemIndexes) + { + // Prepare + using var list = new RecyclableLongList(testData, targetBlockSize, itemsCount); + long expectedCapacity = CalculateExpectedCapacity(testCase, itemsCount, targetBlockSize); + var expectedItems = testData.ToList(); + + var existingItem = expectedItems[(int)itemIndex]; + var newItem = existingItem + 1; + expectedItems.Insert((int)itemIndex, newItem); + + // Act + list.Insert(itemIndex, newItem); + + // Validate + _ = list[itemIndex].Should().Be(newItem); + _ = list[itemIndex + 1].Should().Be(existingItem); _ = list.Capacity.Should().Be(expectedCapacity); _ = list.Count.Should().Be((int)itemsCount + 1); _ = list.Should().Equal(expectedItems); @@ -540,9 +719,49 @@ namespace Recyclable.CollectionsTests } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceRefDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveAtShouldClearItemWhenReferenceType(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] indexesToRemove) + { + foreach (var indexToRemove in indexesToRemove) + { + // Arrange + using var list = new RecyclableLongList(testData, targetBlockSize, itemsCount); + _ = list[^1].Should().NotBeNull(); + + // Act + list.RemoveAt(indexToRemove); + + // Assert + _ = list[list.LongCount].Should().BeNull(); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] - public void RemoveAtShouldSucceed(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] itemIndexes) + public void RemoveAtShouldRemoveTheCorrectItem(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] itemIndexes) + { + foreach (var itemIndex in itemIndexes) + { + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize); + var expectedItems = list.ToList(); + expectedItems.RemoveAt((int)itemIndex); + + // Act + list.RemoveAt((int)itemIndex); + + // Validate + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); + _ = list.LastBlockWithData.Should().Be((int)(list.LongCount >> list.BlockSizePow2BitShift) - ((list.LongCount & list.BlockSizeMinus1) != 0 ? 0 : 1)); + _ = list.LongCount.Should().Be(itemsCount - 1); + _ = list.Should().Equal(expectedItems); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveAtShouldRemoveTheCorrectItemWhenLong(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] itemIndexes) { foreach (var itemIndex in itemIndexes) { @@ -555,13 +774,34 @@ namespace Recyclable.CollectionsTests list.RemoveAt(itemIndex); // Validate - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.LastBlockWithData.Should().Be((int)(list.LongCount >> list.BlockSizePow2BitShift) - ((list.LongCount & list.BlockSizeMinus1) != 0 ? 0 : 1)); _ = list.LongCount.Should().Be(itemsCount - 1); _ = list.Should().Equal(expectedItems); } } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceRefDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] + public void RemoveShouldClearItemWhenReferenceType(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] indexesToRemove) + { + var expectedItems = testData.ToArray(); + + foreach (var indexToRemove in indexesToRemove) + { + // Arrange + var itemToRemove = expectedItems[(int)indexToRemove]; + using var list = new RecyclableLongList(testData, targetBlockSize, itemsCount); + _ = list[^1].Should().NotBeNull(); + + // Act + list.Remove(itemToRemove); + + // Assert + _ = list[list.LongCount].Should().BeNull(); + } + } + [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeWithItemIndexVariants), MemberType = typeof(RecyclableLongListTestData))] public void RemoveShouldRemoveTheCorrectItem(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize, in long[] itemIndexes) @@ -578,29 +818,91 @@ namespace Recyclable.CollectionsTests _ = list.Remove(item).Should().BeTrue(); // Validate - _ = list.LongCount.Should().Be(itemsCount - 1); + _ = list.Capacity.Should().BeGreaterThanOrEqualTo(itemsCount); _ = list.LastBlockWithData.Should().Be((int)(list.LongCount >> list.BlockSizePow2BitShift) - ((list.LongCount & list.BlockSizeMinus1) != 0 ? 0 : 1)); + _ = list.LongCount.Should().Be(itemsCount - 1); _ = list.Should().Equal(expectedItems); - _ = list.Capacity.Should().BeGreaterThanOrEqualTo((int)itemsCount); } } [Theory] [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] - public void VersionedEnumerateShouldYieldAllItemsInCorrectOrder(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) + public void SetByIndexShouldReturnCorrectItem(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) + { + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); + var expectedData = testData.ToList(); + + // Act & Validate + foreach (long item in testData) + { + // Act + list[(int)item - 1] = item + 1; + + // Validate + expectedData[(int)item - 1] = item + 1; + list[item - 1].Should().Be(item + 1); + list.Should().Equal(expectedData); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] + public void SetByIndexShouldReturnCorrectItemWhenLong(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) + { + // Prepare + using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); + var expectedData = testData.ToList(); + + // Act & Validate + foreach (long item in testData) + { + // Act + list[item - 1] = item + 1; + + // Validate + expectedData[(int)item - 1] = item + 1; + list[item - 1].Should().Be(item + 1); + list.Should().Equal(expectedData); + } + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] + public void VersionedEnumerateShouldRaiseExceptionWhenCollectionIsModified(string testCase, IEnumerable testData, long itemsCount, int targetBlockSize) + { + // Prepare + using var list = new RecyclableLongList(testData, targetBlockSize, itemsCount) + { + -1 + }; + + // Act + _ = Assert.Throws(() => + { + foreach (var item in (IRecyclableVersionedLongList)list) + { + list.Add(item); + } + }); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.SourceDataWithBlockSizeVariants), MemberType = typeof(RecyclableLongListTestData))] + public void VersionedEnumerateShouldYieldAllItemsInCorrectOrder(string testCase, IEnumerable testData, int itemsCount, int targetBlockSize) { // Prepare using var list = new RecyclableLongList(testData, minBlockSize: targetBlockSize, itemsCount); // Act - using var actual = new RecyclableList((int)itemsCount); + var yieldedItems = new List(itemsCount); foreach (var item in (IRecyclableVersionedLongList)list) { - actual.Add(item); + yieldedItems.Add(item); } // Validate - _ = actual.Should().Equal(testData); + _ = yieldedItems.Should().Equal(testData); } } } \ No newline at end of file